Punch list §25 item 3.8 complete. Added after starting item 4.1
surfaced the need to thread a vm_id into stadium_admit()'s new quota
parameter; Captain Bob ruled UUID/GUID rather than keeping the
narrower uint32_t.
New VMUuid type (vm_uuid.h/vm_uuid.c): two uint64_t halves, RFC-4122-
shaped for logging. Not real randomness -- checked directly against
QEMU 10.2.1's actual CPU feature set: amd64 RDRAND and riscv64 Zkr are
both real, available features here; aarch64 has no RNG property on any
CPU model including "max" (verified exhaustively via QMP
query-cpu-model-expansion). Captain Bob ruled a uniform fallback
across all three ISAs rather than a per-architecture split.
Fallback is a deterministic PRNG (splitmix64) seeded from the Mama
capsule's content hash, pre-filling a 16-entry FIFO pool at boot and
refilling with another batch of the same stream when exhausted --
exactly the shape requested. Same capsule booted twice produces the
same id sequence, preserving the dict_hash reproducibility this
session has relied on throughout.
Hera keeps a fixed, reserved all-zero id, not drawn from the pool --
capsule_birth.c uses vm_id == 0 as a load-bearing sentinel in three
places (KILL protection x2, fleet heat-fanout parent-chain
terminator), found by reading before writing any code.
Two real sentinel-collision bugs caught before shipping, same class as
STADIUM_CONTAINS_NONE: vm_uuid_none() (all-ones, not all-zero) for
"not yet assigned"/"no VM" placeholders; confirmed item 3.7's quota
table already used an in_use boolean rather than a vm_id sentinel, so
no second collision was actually possible there -- the dead,
never-referenced STADIUM_QUOTA_SLOT_EMPTY macro was removed.
Blast radius larger than first scoped, flagged mid-work rather than
silently absorbed: capsule_vm_physics.c/.h (the fleet heat-transfer
layer item 2.1 modified earlier this session) has its own vm_id-keyed
node table and walks parent_vm_id chains through the same identity
space, so it needed the same change, plus its callers in
mama_forth_words.c and sk_vm_bootstrap.c.
One live FORTH word contract changed, by explicit ruling: CAPSULE-BIRTH
was ( capsule-id -- vm-id ), a single cell -- can't hold 128 bits.
Captain Bob picked pushing two cells ("there is doubles support in the
FORTH std word set anyway"): ( capsule-id -- vm-id-hi vm-id-lo ).
MAMA-VM-ID changed the same way: ( -- 0 0 ).
Verified: full (not standalone-file) kernel rebuild to catch cross-file
breakage given the size of this change -- it surfaced the
capsule_vm_physics.c blast radius a narrower check would have missed.
Three-architecture boot (amd64, aarch64, riscv64), all reaching ok>
with identical dict_hash=0x3d4e1daf289da94f matching the item-3.7
baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Elevates §17.3 from LEANING to DECIDED, settled on paper before item
4.1's code per Captain Bob's request. The core claim (replace
round-robin eviction with Stadium density ranking) already argued for
itself in this section; what was missing was the hosted/kernel split
§25.5's "never two live heat mechanisms at once" implied but never
resolved.
Grounded in code, not policy: dictionary_management.c calls
hotwords_cache_lookup()/evict_*() unconditionally in the word-lookup
path (not gated by ENABLE_HOTWORDS_CACHE at the call sites), and that
file is vendored shared source required to work in both hosted and
kernel builds. The Stadium is kernel-only by construction of
everything built through item 3.7. ENABLE_HOTWORDS_CACHE already
defaults off in both Makefiles today (Kconfig.physics, verified
against both Makefiles directly).
Resolution: kernel builds retire the old cache's effect once item 4.1
lands (Stadium takes over, old call sites bypassed under
__STARKERNEL__ regardless of the Kconfig setting); hosted builds are
unchanged, no Stadium is built for them. Item 4.1 decides the exact
bypass mechanism, not invented here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.7 complete. Added to §25.4 after starting item
4.1 surfaced it as an unbuilt prerequisite -- 3.6's earlier "Phase 3
core complete" claim is corrected in this same commit.
StadiumVMQuota table (size STADIUM_MAX_VM_COUNT, linearly searched by
vm_id -- capsule_birth.c's vm_id is monotonic and never reused, so it
cannot index a table directly, and a 4-entry scan costs nothing). New
per-cell stadium_owner byte array records which quota a cell belongs
to, needed so eviction returns a freed cell to the correct VM's list
and so eviction search stays scoped to the evicting VM's own residents
(quota isolation).
Free-list linkage reuses each cell's `link` field as a next-free
pointer while unresident -- link is documented only as generic "index
into the Stadium, not a pointer," so this is a repurposing, not a
header change. Does not answer the separate, still-open question of
which field carries a multi-cell patron's first continuation-cell
index; item 3.5's mass != 1 refusal stands exactly as it was.
Boot-time: every cell chained into one list in ascending index order,
granted whole to vm_id 0 (Hera), the only VM that exists. Ascending
order preserves item 3.6's "Hera is patron zero" invariant once real
birth-wiring lands.
stadium_admit()'s signature changed to take vm_id -- a change to code
shipped in item 3.5, amended there. Pops the calling VM's free-list
head first (O(1)); only falls back to a same-VM-scoped eviction search
if empty.
Caught a real bug before the boot run: the header zero-fill on
eviction (and the initial free-list build) both left contains == 0,
but 0 is Hera's valid index -- the same collision item 3.1's
STADIUM_CONTAINS_NONE fix addressed, recurring at a new site. Fixed by
explicitly setting contains = STADIUM_CONTAINS_NONE at both free-list
sites.
Explicitly out of scope, reported not invented: granting quota to any
VM other than Hera is capacity arbitration (item 1.3 left "how much
moves per transfer" open). stadium_owner is set once at boot and never
rewritten, so quota_slot_for_vm() refuses every vm_id != 0 permanently
until item 4.2 adds the grant path and owner-array writes.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-3.6 baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.6 complete. Phase 3 (§25.4) core is now done:
items 3.1-3.6 all closed.
stadium_evict() now panics via sk_hal_panic() if a resident cell 0
(Hera, patron zero by construction of §6's boot order) is ever
selected for eviction. Placement is deliberate: the check runs before
the pin/contains refusal checks, not after -- if it ran after, a
wrongly-cleared pin would let the ordinary refusal path quietly return
-1 instead of ever reaching the panic, defeating the point of a check
that's supposed to be independent of pin holding.
Per §20.5 #3's explicit wording, not implemented as a filter:
stadium_admit()'s least-dense search is unchanged, still relying on
the general pin skip from item 3.5. Adding a second filter there would
have done exactly what that section warns against ("filtering hides
the bug, asserting reports it").
The panic path is, and will remain, unexercised by the acceptance
mechanism: sk_hal_panic() halts the machine, and triggering it
deliberately is incompatible with the three-arch boot being this
project's sole acceptance test. Correctness rests on the placement
argument, not a test -- same honesty precedent as items 3.4 and 3.5's
other unexercised paths.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-3.5 baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.5 complete.
stadium_admit(candidate) places into an unused cell if one exists (no
comparison needed), otherwise finds the least-dense resident -- skipping
pinned and contains-gated patrons, which are never eviction candidates
-- and evicts it only if the candidate is strictly denser, per §19.3.
stadium_evict(cell_index) dispatches the departing patron's behaviour
before clearing its slot, per §17.2.
Caught a real bug before it ran: the first draft used contains == 0 to
mean "holds nothing," but cell index 0 is a valid index (Hera, item
3.6). Fixed with a proper sentinel, STADIUM_CONTAINS_NONE (UINT32_MAX).
A second-pass review found mass was not accounted for: both functions
handled exactly one cell regardless of the candidate's stated mass,
which leaks cells on eviction of any mass > 1 patron and breaks
capacity conservation. Fixed by refusing any candidate with mass != 1
-- multi-cell patrons need the per-VM free lists item 3.2 already
deferred (§22.3), not built here.
Documented, not fixed: the discriminator bitmap can't distinguish free
from continuation cells, so the free-cell scan reads continuation-cell
payload bytes under the header layout -- latent since nothing creates
continuation cells yet, and the mass != 1 refusal keeps it provably
latent. Superseded by the free list when it exists.
Unexercised at runtime: nothing calls either function yet (no real
patron kind is wired to the Stadium). No self-test added -- filling
~74,000+ cells to reach the eviction-on-full branch was judged
impractical, following item 2.2's own precedent for its unexercised
fleet-full path.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-3.4 baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.4 complete.
stadium_density(cell_index) reads a header's heat and mass and returns
heat / mass -- a division on demand from fields already stored in the
cell, matching §19.3's "read, not computed by a scheduler" literally.
Stays valid Q48.16 without a special fixed-point routine, since heat
is already Q48.16 and mass is a plain integer divisor.
mass == 0 and an out-of-range cell_index both return 0 rather than
dividing by zero -- an empty or never-admitted slot has no footprint
to be dense within.
Deliberately not built here, per the item's own wording: finding the
densest or least-dense resident (§19.3's admission/eviction
comparison) is item 3.5's scope, not this one's. Nothing calls
stadium_density() yet either.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-3.3 baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.3 complete.
StadiumBehaviour (stadium.h) enumerates exactly the four tags §18.3
already names -- MIGRATE, DELIVER, EXPIRE, COOL -- mapped from §17.1's
patron table: blocks->MIGRATE, messages->DELIVER, ACLs->EXPIRE, words
and VMs both->COOL. Nothing invented; the tag set and mapping were
already in the document.
stadium_dispatch(cell_index, behaviour) dispatches on the tag only,
never asks what kind of patron departed. Handlers are stubs -- the
real actions belong to subsystems not yet migrated onto the Stadium
(Phase 4). Nothing calls stadium_dispatch() yet; item 3.5 is its first
consumer.
The switch is exhaustive with no default case, making §13's "closed
enumeration, fixed at build time" a compiler-enforced property under
this project's -Wall -Werror rather than just prose. Verified live:
temporarily deleted the COOL case, rebuild failed with
error: enumeration value 'STADIUM_BEHAVIOUR_COOL' not handled in
switch [-Werror=switch], restored it, confirmed clean again.
The header's behaviour field stays uint8_t, not the enum type itself,
since C does not guarantee an enum's underlying type and that field's
offset is load-bearing for item 3.1's validated 64-byte layout.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-3.2 baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.2 complete.
stadium_boot_init() (src/starkernel/vm/stadium.c) sizes the global
cell array at boot from a real memory-budget query rather than a
hardcoded count: pmm_get_stats().free_bytes at the point of
allocation, times the new STADIUM_MEMORY_PERCENT Kconfig symbol
(default 1%), rounded down to whole 64-byte cells. Matches §17.6's
position (b) literally. Also allocates the header/continuation
discriminator bitmap item 3.1 declared but did not allocate. Both are
kmalloc'd and explicitly zero-filled (kmalloc does not zero).
Called from kernel_main.c immediately before sk_vm_bootstrap_parity(),
i.e. before any VM exists (§6). Failure is soft -- logs and continues,
does not halt boot -- matching the existing precedent one line below
it (VM bootstrap parity failure does the same).
Added a "Stadium: N cells (M KB)" boot console line at the allocation
site so the acceptance logs are evidence the array was actually
allocated, not just that the kernel still boots -- the same blind spot
item 3.1's uncompiled-header gap exposed.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-3.1 baseline, and the Stadium boot line confirmed present in all
three serial logs (amd64: 74234 cells/4639 KB, aarch64: 161329
cells/10083 KB, riscv64: 76122 cells/4757 KB).
Not built here, reported per §25.0 rule 3: per-VM free lists (§22.3)
-- granted when Hera assigns quota, not this item's scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.1 re-closed after reopening.
Items 1.1 and 1.4's resolutions both explicitly named this item as
where their Kconfig symbols would be implemented, but 3.1's own stated
scope never mentioned them, so the first close missed both:
- STADIUM_CONTAINS_DEPTH_MAX (default 5) -- item 1.1's contains-chain
depth cap. No consumer yet; reap-gating enforcement is item 3.5.
- STADIUM_CAPACITY_TICK (default 1000) -- item 1.4's capacity
arbitration cadence in virtual ticks. No consumer yet; capacity
arbitration itself is not on the punch list.
Both added following STADIUM_MAX_VM_COUNT's exact pattern:
Kconfig.kernel entry, Makefile.starkernel kconfig_int +
VM_FEATURE_FLAG_VARS forwarding, starforth_config.h fallback default.
stadium.h now includes starforth_config.h and carries two more
C99-portable compile-time checks proving both symbols are defined and
sane, same discipline as the byte-count checks. Declaration only --
not inventing the consuming logic to close this out early.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f, re-run after
the reopening.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25.7 addition, not a punch list item.
Captain Bob flagged the existing "fleet heat leaks on every multi-VM
touch" entry (§20.2/§25.7) as worth a pinned reminder: it's currently
invisible because Tripod is pruned to Hera alone (item 0.1), so
others_total is always 0 and the leaky division path never executes.
It becomes reachable, and measurable, only once Phase 4 restores
Hermes/Artemis -- so a clean item-5.1 DoE re-run before that point
would be a false negative on VM-CONSERVED?, not evidence the
conservation law holds. Still reported-not-scheduled on purpose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 3.1 complete.
Defines StadiumPatronHeader and StadiumContinuationCell in new
include/starkernel/vm/stadium.h, unioned as StadiumCell per §3's
closed two-valued union. src/starkernel/vm/stadium.c added to
Makefile.starkernel's LOADER_EXTRA_SRCS/KERNEL_EXTRA_SRCS so the
header's compile-time size checks are actually compiled, not merely
included by something that never builds.
Discriminator ruled an external side bitmap (Captain Bob), not a
header field -- amended into §3 and §23.3 before this code was
written. Item 3.1 declares the bitmap's purpose/indexing in a comment
only; allocating it is item 3.2's scope.
Both cell shapes counted for real at exactly 64 bytes with zero
compiler-inserted padding (three C99-portable negative-array-size
assertions -- no _Static_assert, this project targets C99). Header
matches §23.3's original 32+32 split unchanged, since the
discriminator moving outside the cell left nothing to compete for that
space. Continuation cell matches item 1.12's 4+60 figure unchanged for
the same reason.
Verified the size assertion is actually live: broke it to 63,
confirmed the build failed with the expected negative-array-size
error, restored it, confirmed a clean compile.
Verified: three-architecture boot (amd64, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-2.2 baseline. Confirmed stadium.o present in both obj/loader/vm
and obj/kernel/vm post-build on amd64, closing the gap the item-2.2 WIP
exposed (an uncompiled header proves nothing).
Left open, not fabricated: §23.4 #2 ("does a typical message fit in
one cell") is unanswerable today -- no message patron struct exists
anywhere in this tree yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25.7 addition, not a punch list item.
Captain Bob flagged mid-item-3.1 that the physics-flavored vocabulary
(heat, mass, density, patron, Stadium) needs a glossary making explicit
these are named analogies, not physical claims -- and that the growing
set of Kconfig build knobs needs the same terminology discipline so
code, Kconfig help text, and this document don't drift apart. Captured
per §25.7's rule: not fixed, not assigned, becomes an item only if
Captain Bob says so.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires the birth-refusal check into capsule_birth_baby(): calls
vm_registry_live_count() (added in the prior WIP commit) between
capsule validation and vm_registry_alloc(), returning the new
CAPSULE_RUN_ERR_FLEET_FULL and logging via
capsule_parity_log_birth_failed() before any EMBRYO registry slot is
consumed.
Also fixes a gap in that WIP commit: STADIUM_MAX_VM_COUNT was only
ever defined via a Kconfig .config-driven -D flag, with no fallback
default the way every sibling knob in starforth_config.h has -- a
build with no .config present (this one) failed with the macro
undeclared. Added STARFORTH_CONFIG_STADIUM_MAX_VM_COUNT_DEFAULT (4,
matching Kconfig.kernel) following the existing HEARTBEAT_INFERENCE_FREQUENCY
pattern exactly.
All three architectures boot clean to ok> with dict_hash=0x3d4e1daf289da94f,
matching the item-0.10/2.1 baseline. FABRIC.md item 2.2 checked off.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Partial work toward FABRIC.md punch list item 2.2. Adds the
STADIUM_MAX_VM_COUNT Kconfig symbol (default 4, per item 1.5) wired
through Makefile.starkernel, a new CAPSULE_RUN_ERR_FLEET_FULL result
code, and a vm_registry_live_count() helper in capsule_birth.c that
counts LIVE VMs only (distinct from the existing monotonic
vm_registry_count, which never decrements on death).
NOT YET DONE: nothing calls vm_registry_live_count() yet -- the actual
birth-refusal check is not wired into capsule_birth_baby(). Not built,
not boot-tested. FABRIC.md's item 2.2 checkbox is deliberately left
unchecked; this commit exists only to save in-progress work before a
pause, not to claim the item complete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 2.1 complete.
vm_physics_touch() no longer takes a wall-clock timestamp -- it reads
fleet_heartbeat_tick_count internally, which is execution-paced
(vm_runtime.c:143), not wall-clock. VMPhysics.last_active_ns ->
last_active_tick, VMFleetTouchSample.elapsed_us -> elapsed_ticks, and a
new explicit `touched` flag replaces the old `> 0` sentinel (tick 0 is
a legitimate value a first touch can land on, unlike wall-clock ns).
Verified: three-architecture boot (amd64 x2, aarch64, riscv64), all
reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the
item-0.10 baseline. No new compiler warnings in the touched files.
Honestly flagged, not fixed: with Tripod pruned to Hera alone (item
0.1), vm_physics_touch()'s fan-out has no other live VM to pull heat
from, so the fleet-heat-sum acceptance criterion is trivially satisfied
rather than genuinely stress-tested -- a real check needs Phase 4's
multi-VM fleet. fleet_transfer_slope_q48's seed (65536/3) was
calibrated for elapsed microseconds and has not been re-fit for elapsed
ticks; left as-is rather than guessed, deferred to item 5.1's DoE work.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 items 1.8, 1.9, 1.10, 1.12 complete.
1.8 -- block content hash is computed once, at the migration boundary,
as part of the MIGRATE code field; audit retention of the old hash is
an Artemis-layer question, out of scope for the Stadium (§5).
1.9 -- confirmed factually (not decided): vm_dict_resolve_in_bucket()
keeps both old and new definitions resident with newest-wins shadowing,
no GC on redefinition.
1.10 -- closed as no. Identity elision either reintroduces the
per-kind branch §18.3 forbids, or breaks lookups (name, LBN) that
words and blocks already depend on outside the Stadium. Stays a fixed,
always-present 8-byte field for every kind.
1.12 -- linked, not contiguous. Forced by §22.3's already-decided
disjoint per-VM free list, which gives no adjacency guarantee.
4-byte next-index, 60 usable bytes per continuation cell, 18
continuation cells for a 1024-byte block. Completes §23.3's sizing
table and unblocks item 3.1.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 1.7 complete.
Bounded rather than permanently prohibited, consistent with item 1.1's
treatment of containment depth. Default 2 matches §21's already-decided
two-level structure; enforced by explicit refusal at VM-birth time if a
birth would exceed the configured depth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 1.6 complete.
Confirms §20.4's proposal as decided: mass = the quota Hera allocated,
not a fixed one-cell footprint. Already implied by §22's elasticity
mechanism and §24.3, and already assumed by items 1.2-1.5 -- this
closes the formal "proposal" flag to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 1.5 complete.
Bound defaults to 4 (Tripod's known topology), Kconfig-tunable,
explicitly a placeholder pending a later DoE campaign for an idealized
default. Fixed for the machine's lifetime once set at build. Corrects
the document's own "coldest-VM-reaped is consistent with §19.3" claim,
which doesn't survive §20.2's cold-start birth rule -- birth is
refused at the bound instead, and reaping stays Hera's deliberate act.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 1.4 complete.
Capacity-tick gets its own named constant, defaulted to 1000 virtual
ticks -- grounded in the existing HEARTBEAT_INFERENCE_FREQUENCY
precedent at capsule_vm_physics.c:434-441 rather than an invented
number, well past §12 Q5's order-of-magnitude minimum.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 1.3 complete.
Cadence (capacity-tick, a coarser multiple of the virtual tick) carries
the "sustained density" requirement; the decision itself is a pure
least-dense/densest comparison, no tuned threshold. Transfer amount per
event is reported as unresolved, not invented -- out of this item's scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 1.2 complete.
Floor = max(mass of pinned patrons, one message-sized cell); both terms
derived rather than a tuned constant, closing the zero-pinned-patrons
reachability deadlock the first term alone would leave open.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 1.1 complete.
Adds the ninth wire `contains` to §3, resolves §8's open exclusivity
question as containment with a gated reap rule, bounds chain depth at
5 (Kconfig-tunable), and notes the distinction from item 1.7's separate
VM-tree-recursion question.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Includes system diagnostics and comprehensive test logs for stack and arithmetic words in the LithosAnanke kernel.
Signed-off-by: Robert Allan James <robert.allan.james@gmail.com>
Adds a one-time boot diagnostic in kernel_main.c, right before sk_repl()
is entered: bounded wait for 3 real heartbeat ticks, then prints tick
count, TIME-TRUST, and variance. Needed because printing immediately
after apic_timer_start() (as first tried) measured 1 tick on amd64 and
0 on riscv64 -- not evidence the heartbeat doesn't work, just that
almost no wall time elapses between arming the timer and that point in
boot; report it honestly rather than let it stand as a false negative.
Verified this session (logs/20260804-001727, -001805, -001850,
-001948, -002021):
- All three architectures boot to ok>.
- Tick count non-zero: amd64 4, riscv64 3, aarch64 3.
- riscv64: trust=Q48_ONE exactly, variance=0 -- architecturally
invariant counter, as designed.
- amd64: dict_hash=0x3d4e1daf289da94f, identical to the pre-item-0.8
baseline (logs/20260803-231322) -- unchanged output, satisfying the
GAP-A1 control.
- Two consecutive amd64 boots produced the identical dict hash --
reproducible, no wall-clock leakage into patron state.
Phase 0 (Substrate) is complete.
Punch list §25 item 0.10 complete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sf_mutex_lock()'s no-op is correct only while nothing in interrupt
context mutates the structure a given lock protects. States that
explicitly, and states why a real spinlock is the wrong fix if that
constraint is ever violated: a single hart would deadlock spinning
against its own interrupt handler. The correct fix in that case is
disabling interrupts around the critical section.
Documentation only, no behaviour change.
Punch list §25 item 0.9 complete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduces src/starkernel/heartbeat.c as the shared top/bottom-half
implementation of heartbeat_init/tick/service/ticks/trust/state, replacing
the per-architecture duplicates in amd64/riscv64/aarch64 timer.c. Each
arch's timer.c now contributes only heartbeat_read_counter() (rdtsc /
rdtime / CNTPCT_EL0). Per the GAP-A1 ruling the top half stays counter+
latch only; heartbeat_service() (called every REPL idle iteration,
unconditionally per FABRIC.md's fidelity note) does the window/variance/
trust work outside interrupt context. vm_tick()'s call sites are
unchanged -- the engine still runs on the virtual tick.
Per FABRIC.md §26 (ruled 2026-08-03): wires Loop #7's execution-derived
stable/volatile signal into the physical re-arm period. vm_runtime.c's
existing Loop #7 site now calls heartbeat_set_adaptive_period_ns() with
tick_target_ns ratio-rescaled onto a 10ms kernel base (not the hosted
10us HEARTBEAT_TICK_NS -- see §26.3 for the scale mismatch). Each
architecture's re-arm function (apic_timer_rearm() on amd64/aarch64,
riscv64_timer_rearm()) now converts heartbeat_next_period_ns() to its
own raw counter units instead of a fixed constant; amd64 gained a
rearm function it didn't previously need, since periodic-mode auto-reload
never required one before this item.
Verified: all three architectures build with no new warnings and boot
cleanly to ok> with dict_hash=0x3d4e1daf289da94f, unchanged from the
pre-change baseline -- no regression. Verified NOT achieved: live re-arm
period variation under load. A temporary diagnostic (added and reverted)
confirmed Loop #7 never actually fired during a live QEMU session -- a
synthetic word-execution loop drove ~6,500 executions, past the 1000-tick
inference frequency, without tripping vm_tick_inference_engine()'s
pre-existing !vm->rolling_window.is_warm gate. That gate predates this
item and was not investigated -- out of scope. FABRIC.md's Done-when is
amended to record this honestly rather than claim it.
Punch list §25 item 0.8 complete (per amended, weaker acceptance -- see
the item's own annotation).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Loop #7 (vm_runtime.c:703-752) already computes an execution-derived adaptive
period and it is orphaned -- nothing in the kernel build ever reads
tick_target_ns. Traces the pthread-based consumer it was written for
(deliberately disabled for kernel builds, no pthread on bare metal) and
confirms via the §18.5 argument that consuming it via the physical re-arm
period does not reopen GAP-A1, since the decision to change the period stays
execution-derived and the virtual tick still owns all patron state.
Rules the scale mismatch (10us hosted base vs 10ms/100Hz kernel base) in
favor of reusing Loop #7's decision logic against the kernel-appropriate
base, and the mechanism (single-writer/single-reader shared state, no
thread/scheduler) against §21.1's existing free-locking finding.
Amends item 0.8 to include this -- no new punch-list item, this is inside
what 0.8 already builds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.7 complete. This is what finally makes items 0.5 and
0.6 provably work end to end.
apic_timer_start(): writes CNTP_TVAL_EL0 (or CNTHP_TVAL_EL2 at EL2 --
aarch64_current_el(), same EL-aware discipline as 0.4-0.6) to
s_timer_period_tsc, then CTL.ENABLE=1/IMASK=0, followed by an ISB. The ISB
is not decorative: confirmed against Linux's own arch_timer_reg_write_cp15()
(arch/arm64/include/asm/arch_timer.h) that only the *control* register write
needs synchronising before the enable/mask state is guaranteed visible to
the interrupt pipeline -- TVAL/CVAL writes do not carry the same
requirement, which is why apic_timer_rearm() omits it.
TVAL is architecturally 32-bit but MSR-to-system-register is always a
64-bit instruction form -- passing a uint32_t operand directly failed to
build (-Wasm-operand-widths). Fixed by truncating to 32 bits ourselves then
zero-extending back to 64 for the operand, which supplies explicit,
provably-correct zeros in the RES0 upper field rather than depending on
unverified hardware behaviour -- Linux's own driver never exercises this
path (it always uses the 64-bit CVAL form instead), so there was no local
source to confirm the alternative against.
apic_timer_rearm() (new): re-writes TVAL only, no ISB needed. TVAL is
relative to "now," not an absolute deadline like riscv64's SBI interface
(item 0.3), so there is no drift-correction bookkeeping -- each write means
"N ticks from this instant." Wired into aarch64_irq_handler() and called
*first*, before heartbeat_tick(), matching riscv64_timer_rearm()'s ordering
discipline exactly: the ARM Generic Timer does not auto-reload, so a return
path that skips this leaves the interrupt condition latched, which the GIC
would redeliver the instant it's EOI'd -- a real storm, the same class of
failure item 0.6's verification investigated (and that time found absent,
because nothing was armed yet).
Verified: builds clean; every generated instruction checked against
disassembly, not just reviewed by eye (both EL branches, correct TVAL/CTL
register names, single shared ISB in apic_timer_start(), no ISB in
apic_timer_rearm()). Boots to ok> with no regression, dict_hash
0x3d4e1daf289da94f unchanged.
Rate measured directly against real wall-clock time via QEMU's own -d int
trap trace (same method as riscv64's item 0.3), two independent windows:
1,090 interrupts over 11.05 s (98.679 Hz) and 4,031 over 40.88 s (98.614 Hz)
-- consistent across both, so this is a real, small, systematic bias
(~1.3-1.4% slow), not measurement noise from polling granularity, which
would have shrunk with the longer window and did not. Attributed to genuine
per-interrupt service latency: TVAL is rewritten mid-ISR, so the trampoline
save/restore, GICC_IAR read, EL branch and GICC_EOIR write all lengthen the
effective period slightly versus the nominal 10 ms, inherent to any
relative-countdown re-arm scheme. Reported as measured, not smoothed over.
The interrupt sustained continuously across both windows with no stall and
no storm, which is the primary evidence re-arm-every-tick is correct;
the small rate bias is overhead, not a defect.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.6 complete.
Ruling applied (AskUserQuestion, this session): DTB is confirmed unreachable
on this system's aarch64 firmware too (qemu-efi-aarch64 2025.11-3ubuntu7,
same finding as riscv64's item 0.3), so GICD/GICC base addresses and the
timer PPI are named QEMU-virt constants with a recorded caveat, not
DTB-discovered as the item originally asked.
Nothing here was recalled from memory. Base addresses (GICD 0x08000000,
GICC 0x08010000) and the timer PPI (30, non-secure EL1 physical) were read
out of QEMU 10.2.1's own internal devicetree via
`qemu-system-aarch64 -machine virt,dumpdtb=...`, decoded with this tree's own
fdt.c reader rather than a new tool -- correct for this exact QEMU version,
not assumed stable across others. Bonus finding from the same dump: PPI 26
for the EL2 hypervisor timer, which item 0.7 will need for its EL2 path.
Register offsets within each block (GICD_CTLR, GICC_IAR, etc.) are GICv2
architectural constants, not board-specific, and were cross-checked against
Linux's own arm-gic.h driver header rather than recalled either.
Acceptance amended before implementing (§25.0 "when an item is genuinely
wrong"): the original text required observing a delivered-and-acknowledged
timer interrupt, which cannot happen within this item's own scope --
apic_timer_start() (item 0.7) is still the no-op stub, so nothing arms the
timer. This is the same defect the earlier review's C2 fix already applied to
items 0.2 and 0.5; it was missed here. Acceptance is now: GIC initialises
without fault, the IAR/EOIR path is wired into aarch64_irq_handler() and
ready, boots with no regression -- item 0.7's tick-advance is what proves
delivery, exactly as 0.5 already defers to 0.7.
EL-aware (B3, same discipline as items 0.4/0.5): apic_init() selects PPI 30
or 26 from aarch64_current_el(), decided once and cached, not re-derived per
interrupt.
aarch64_irq_handler() now does real work: reads GICC_IAR (the GICv2
acknowledgement step), dispatches to heartbeat_tick() when the INTID matches
the timer PPI, and always completes with GICC_EOIR (INTID 1023 = spurious
handled per the GICv2 spec, not as a special case of "unrecognised"). This
mirrors exactly how riscv64's item 0.2 built full cause-dispatch logic before
its timer was armed in 0.3.
Investigated and resolved a real scare during verification: QEMU's `-d int`
trace showed 1,728 "Taking exception 5 [IRQ]" events by the time boot reached
the prompt, which looked exactly like an interrupt storm (hypothesis: EDK2
firmware leaves CNTP_CTL_EL0 enabled with a stale comparator, and enabling
the GIC path exposes it before item 0.7 reprograms the timer). A direct
one-shot probe inside aarch64_irq_handler() itself -- ground truth for
whether this code path runs at all -- fired zero times across a clean,
bounded boot. The trace events were almost certainly from EDK2 firmware's
own internal timer usage during its own boot phase, before control passes to
this kernel; the earlier conclusion was drawn from the external trace alone
without checking that distinction, and the probe (not the trace) is what
settled it. Probe code fully reverted; not part of the commit.
Verified: builds clean, boots to ok> with no regression, dict_hash
0x3d4e1daf289da94f unchanged from the item 0.1-0.5 baseline, EL banner and
IDT-installed lines still print in order, GIC init line confirms PPI 30
selected. Only aarch64-scoped files touched; amd64/riscv64 not rebuilt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.5 complete.
irq_spx now branches (one instruction, well inside the 128-byte vector slot)
to irq_spx_trampoline, a 672-byte-frame save/restore sequence that calls a
C handler and returns via ERET. The other fifteen vectors are untouched,
still routing to the existing fatal handler.
EL selection (B3) happens once, in aarch64_install_vectors(), not per
interrupt: aarch64_current_el() (item 0.4) picks VBAR_EL1 or VBAR_EL2, and
the same answer is cached in a byte flag (el2_mode_flag) that the trampoline
reads to choose ELR_EL1/SPSR_EL1 vs ELR_EL2/SPSR_EL2 -- the two forms are
genuinely different MRS/MSR encodings, not runtime-selectable operands, so
this is the cheapest correct design: decide once at install time, branch
twice (save, restore) per interrupt afterward. VBAR_EL1 was previously
written unconditionally; this closes that half of item 0.4's known gap.
EL2 is coded from the architecture reference and cannot be boot-tested in
this environment (QEMU's aarch64 virt/EDK2 combination here yields EL1) --
reported as unverified rather than asserted as tested.
FP/SIMD save is not optional (B2, carried from item 0.4's finding that the
build has no -mgeneral-regs-only): the AAPCS64 caller-saved set -- v0-v7,
v16-v31, full 128 bits each -- plus FPSR/FPCR is saved and restored around
the C handler call. v8-v15 are callee-saved by the ABI and deliberately
excluded: the handler, being ordinary compiled C, preserves those itself.
aarch64_irq_handler() (interrupts.c) is deliberately empty. Distinguishing
which interrupt fired needs the GIC's IAR, which does not exist until item
0.6; nothing unmasks or routes any source to this vector yet, so the
function is not reachable during a normal boot. Per the item's own text,
no attempt was made to manufacture an interrupt to exercise this path early
-- 0.6 (GIC) and 0.7 (timer) are what prove it took and returned one.
Verified: every hand-computed frame offset (0, 16, 32 ... 640, frame size
0x2a0=672) checked against the actual disassembly of the built kernel, not
just visually reviewed -- save and restore sequences mirror exactly, and
aarch64_install_vectors' branch on the detected EL, the flag write, and the
trampoline's read of the same flag address all confirmed consistent. Boots
clean on real QEMU output, no regression: dict_hash 0x3d4e1daf289da94f
unchanged from the item 0.1-0.4 baseline, and the item 0.4 EL banner
("AArch64: running at EL1") still prints correctly ahead of "IDT installed.".
Only aarch64-scoped files touched (isr.S, interrupts.c) -- no shared loader
or header changed, so amd64 and riscv64 are provably unaffected; not
rebuilt for this item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.4 complete.
Adds aarch64_current_el() in arch.c: reads CurrentEL[3:2] on first call,
caches the result (CurrentEL cannot change post-ExitBootServices, so every
consumer gets the same answer without repeating the MRS). Called from
arch_interrupts_init() in interrupts.c -- the earliest point with both a
working console (up since M1) and a genuine first consumer (vector
installation is the first EL-dependent operation) -- and the detected level
is printed to the boot log there.
Declared via extern-in-place in interrupts.c rather than added to the shared
arch.h: "exception level" has no amd64/riscv64 equivalent, matching the
convention already used for riscv64_timer_rearm() in item 0.3.
Verified on real QEMU output: "AArch64: running at EL1", correctly positioned
immediately before "IDT installed." in the serial log. Boots clean, dict_hash
0x3d4e1daf289da94f unchanged from the item 0.1-0.3 baseline.
Scope: this item establishes the detection and exposes it; it does not yet
change VBAR/ELR/SPSR or timer-register selection to use it. arch_interrupts_init()
still writes VBAR_EL1 unconditionally, and now says so explicitly in its own
doc comment -- if aarch64_current_el() ever reports 2 on real hardware,
exceptions taken at EL2 vector through VBAR_EL2, which nothing programs yet.
That gap is items 0.5 (vectors/saved-state) and 0.7 (CNTP vs CNTHP) to close,
per FABRIC.md's GAP-B3 finding. The boot-log EL2 case prints a note pointing
at both.
Only aarch64-scoped files touched (arch.c, interrupts.c) -- no shared loader
or header changed, so amd64 and riscv64 are provably unaffected; not rebuilt
for this item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Punch list §25 item 0.3 NOT complete.
- Added `starkernel/fdt.h` and `fdt.c` for minimal read-only devicetree parsing: sufficient for boot-time lookups such as `timebase-frequency`.
- Bootloader now captures the devicetree blob (DTB) from `EFI_DTB_TABLE_GUID` into `BootInfo::dtb`.
- RISC-V timer subsystem now uses the `time` CSR as the primary timestamp source, abandoning the hardcoded `cycle` frequency assumption.
- Timer rate is read from `timebase-frequency` in the DTB when accessible; otherwise, a fallback value is used with a RELATIVE trust level.
- Integrated the SBI TIME extension for one-shot timer deadlines, ensuring re-arming occurs after each tick to avoid missing heartbeats.
Verified: riscv64 builds clean, boots to the ok> prompt with no regression; `riscv64/timer.c` reports accurate frequencies on QEMU's default firmware.
Signed-off-by: Robert Allan James <robert.allan.james@gmail.com>
Punch list §25 item 0.2 complete.
Verified: riscv64 builds clean and boots to the ok> prompt with no regression;
dict_hash 0x3d4e1daf289da94f, unchanged from item 0.1's baseline. Disassembly
confirms the 320-byte frame, all 16 integer caller-saved registers, the FS
check, and SRET on exit; riscv64_trap_entry lands at 0x414fa8, 4-byte aligned
as stvec direct mode requires.
Not verified, and the item says so: neither new path was exercised. No timer is
armed until 0.3, so riscv64_interrupt_handler never ran, and no exception
occurred during boot, so the fatal path was not observed -- it is preserved
structurally, same branch to the same unchanged handler. This is why C2
rewrote the acceptance to no-regression rather than to having taken and
returned from a trap.
Register set is the LP64D psABI caller-saved list, not this document's summary:
integer ra/t0-t6/a0-a7 (16), FP ft0-ft11/fa0-fa7 (20) plus fcsr, and sepc +
sstatus. Callee-saved registers are the C handler's responsibility.
The FP half is conditional on sstatus.FS != Off, which the item did not
anticipate. Nothing in boot.S or kernel_entry.S programs FS, so its value is
whatever firmware leaves; touching an f-register with FS == Off raises an
illegal-instruction trap, and doing that inside the trap handler would be
unrecoverable. Omitting the FP save is not an option either -- the built
riscv64 image contains 530 FP instructions (fld, fmul.d, fcvt.lu.d among them),
confirming B2's finding against the binary rather than the build flags alone.
So the save is conditional, and sstatus is restored after the f-registers.
Dispatch: scause bit 63 routes to riscv64_interrupt_handler with scause in a0;
cause 5 (supervisor timer) calls heartbeat_tick(). Other causes are ignored
rather than fatal -- none are enabled to arrive. Everything else still falls
through to riscv64_exception_handler, unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Punch list §25 item 0.1 complete.
Verified: all three architectures build clean and boot to the ok> prompt with
Hera alone. Blocks executed are exactly 2057 -> 2049 (-> lib.4th/4050) -> 2050.
Zero occurrences of "Hermes" or "Tripod" in any of the three serial logs, and
the only parity record is MAMA_INIT -- no baby VM is born. mkcapsule --lint
passes 26 files, 0 failing, 0 violations.
Deleted blocks 2051, 2052, 2053, 2054, 2055, 2056, 2058, 2059 -- readiness
handshake, broadcast test, TRIPOD-TEST, HERMES-E2E and the fleet-DoE
scaffolding. Edited 2057 (banner), 2049 (dropped both births with their
CD-INIT calls and the common:msg.4th / process.4th loads, which are wholly
Hermes-dependent; kept lib.4th; VM-TREE and VM-CHILDREN no longer name absent
children) and 2050 (kept the BOOT-BANNER call, dropped the two calls to the
deleted words). capsules/hermes/ and capsules/artemis/ untouched on disk.
New Hera-alone parity baseline, identical across all three ISAs:
dict_hash=0x3d4e1daf289da94f capsule_hash=0xb4c4b5559146a3bd
This supersedes the pre-prune baselines in logs/ and is what item 0.10's
double-boot reproducibility check compares against.
Commit contents beyond the item's own edit, noted rather than slipped in:
capsules/BLOCK_MAP.md is regenerated by any build; the three serial logs are
this item's acceptance evidence and CLAUDE.md requires committing them; the
DoE CSVs are auto-extracted by the qemu target on every run. Leaving generated
output from this item's acceptance run for a later item to sweep up would be
worse than including it here.
Reported, not fixed: tools/mkcapsule.c emits two -Wstringop-truncation warnings
on the host build (:404, :562), pre-existing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D1 The three passages still arguing from the K-denominator justification that
§2's correction removed: §17.3's opening now cites density's need for a
volume (§19.2); §17.5's sizing argument re-grounded on mass swamping and
§23.1's abolition of by-reference payloads; §17.6(d)'s bullet no longer
cites §2 for a claim §2 explicitly disavows.
D2 §19.6 #1 and #2 struck through as resolved (#1 by §23.1 with the residue
scheduled as item 1.12; #2 by §24.3), matching §17.4's convention.
D3 §20.3's stale LEANING now points at §21's DECIDED.
§25.7.1's status updated: all fourteen findings closed. A1 ruled and applied,
B1/B2 verified against the tree and fixed into their items, B3/C1-C7/D1-D3
applied. The findings text is preserved as the record of what was found.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One batch commit for the seven mechanical item fixes from §25.7.1, each tagged
in place:
C1 Item 0.1 no longer contradicts its own Refs line. Delete set is 2051-2056 +
2058-2059; blocks 2057, 2049 and 2050 survive edited, with the per-block
edits now spelled out. The old "remove 2050-2059" wording would have
deleted the banner.
C2 Items 0.2 and 0.5 accept on no-regression. Both previously required having
taken and returned an interrupt at a stage where no interrupt source
exists; the real evidence lands with 0.3's and 0.7's tick-advance
acceptance, and both items now forbid pulling later work forward to
manufacture it.
C3 Item 0.3 carries sie.STIE and the every-tick re-arm -- the SBI timer is
one-shot and a missed re-arm stops the heartbeat forever with no error.
C4 §23.4 #4 is now schedulable as item 1.12 (continuation-cell encoding);
3.1's blocker line and the Phase 3 gate reference it.
C5 Item 1.11 is formally blocked on 4.3 instead of informally deferred.
C6 Item 0.10's "sane" sharpened to trust near Q48_ONE and variance small
relative to the new expected_delta; the amd64-control framing is noted as
valid again under the GAP-A1 ruling.
C7 The commit template no longer hardcodes a model name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
B2, verified against Makefile.starkernel: no architecture restricts FP register
use. amd64 has no -mno-sse (:124) -- works with live interrupts today, save-set
adequacy unverified. aarch64 lacks -mgeneral-regs-only (:146). riscv64 builds
-march=rv64gc -mabi=lp64d (:162) -- hard-float ABI, and kernel code genuinely
uses doubles (hotwords_stats_print). Items 0.2 and 0.5 now require saving the
ABI caller-saved FP set plus control/status registers, with the exact lists
verified against the psABI/AAPCS64 rather than this document, and explicitly
forbid "fixing" it via soft-float, which would break existing code.
B3: item 0.4's EL detection now governs everything EL-dependent -- vector base
register (today's isr.S writes VBAR_EL1 unconditionally, never consulted for
exceptions taken at EL2), saved-state pair, eret target, and timer registers.
Item 0.5's hardcoded ELR_EL1/SPSR_EL1 wording replaced accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read BootInfo (uefi.h:624-639): memory map, runtime services, ACPI,
framebuffer, stack, args -- no FDT pointer. The only FDT mentions in the whole
kernel tree are comments in riscv64/timer.c acknowledging one would be needed.
Items 0.3 and 0.6 instructed "read from the device tree" against a kernel that
cannot reach one.
Item 0.3 now carries the prerequisite explicitly: capture the DTB pointer from
the EFI configuration table into a new BootInfo field in the shared loader,
serving both 0.3 and 0.6. Item 0.6 references that field and, if the DTB is
unreachable on aarch64 EDK2, requires stop-and-report rather than falling back
to constants unilaterally.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captain Bob's ruling on §25.7.1 GAP-A1, adopting the recommended resolution.
The engine's tick is a virtual tick: a pure, deterministic function of the
execution stream, which is what exists today and why parity holds today. The
hardware heartbeat is the TIME-TRUST instrument and the idle wake source, and
drives nothing that feeds patron state. When the system idles, the REPL poll
loop pumps virtual ticks so TTLs still expire in real time, in a context where
parity was never claimed.
Applied to: §16.4 (correction appended -- the fire-on-tick-count rule was
necessary but not sufficient, since the hash measures the composition of the
instruction and tick streams, not the engine's schedule alone); §17.1 (the one
clock is the virtual tick, and the mechanisms cannot be split across clocks
because TTL expiry has instruction-stream side effects); §18.4 (L0 advances on
the virtual tick); item 0.8 (disambiguated -- the hardware tick drives
instrumentation only, vm_tick call sites unchanged); item 2.1 (transfer
restates onto the virtual tick, which is what makes its identical-sum
acceptance achievable).
Item 0.10 needed no change: with the engine execution-paced, its double-boot
check is a valid regression guard and amd64 is genuinely a control again.
Phase 0's timer bring-up stands: it makes the instrument real on three ISAs
and is the substrate SMP will need. It does not drive the engine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full re-read of the document as it stood after the first review's corrections,
looking for what would break a lower-capability model working the punch list.
Nothing in this commit fixes anything -- it records fourteen findings for triage.
The one that gates coding: GAP-A1. §16.4's inference "same tick ordinal → same
hash" is unsound, because the hash covers execution_heat, which is co-written by
the instruction stream and the tick stream, and a hardware timer makes the
interleaving of those two streams wall-clock-dependent. Firing on tick count
fixes the engine's schedule, not the composition. Blast radius: item 0.8 is
ambiguous between two different kernels, 0.10's double-boot check fails by
construction under one of them, and 2.1's corrected acceptance is still
unachievable. Recommended resolution recorded (virtual tick as a pure function
of the execution stream; hardware heartbeat as instrument and wake source only)
but explicitly not decided.
Also: three unverified prerequisites (DTB reachability for 0.3/0.6, FP-register
save vs compile flags for 0.2/0.5, EL-dependence of the vector path vs 0.4's
scope), seven punch-item defects (0.1 self-contradiction on block ranges,
0.2/0.5 unsatisfiable acceptance, 0.3's missing STIE and re-arm, §23.4 #4 not
being a schedulable item, 1.11's informal deferral, 0.10's stale control
framing, the hardcoded model attribution), and three amendment inconsistencies
(stale K-denominator language in §17.3/§17.5/§17.6d, §19.6 #1-#2 unmarked as
resolved, §20.3's stale LEANING).
Triage order is stated in the section: A1 first, B1/B2 are ten-minute reads,
C and D mechanical after that. C1-C3 minimum before any coding model starts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review item REVIEW-D4.
Item 1.1 (exclusive access, the "sitting in a car" primitive from §8) sits in
Phase 1 alongside questions with no structural effect, and it is not in that
class. A per-patron exclusivity primitive plausibly needs a held flag or holder
index -- a ninth wire in §3's table, in the header item 3.1 builds. Resolving
1.1 after 3.1 means rebuilding the cell header.
§25.4 already blocks Phase 3 on items 1.1-1.7, so the ordering was right; what
was missing was why 1.1 specifically, which is what gets an item quietly
reordered later by someone who does not know what it was holding up.
Item 3.1 now names both of its blockers explicitly: item 1.1, and §23.4 #4, the
continuation-cell encoding surfaced by REVIEW-C3. It also now requires defining
both members of §3's closed two-valued union rather than just the header.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-D3.
Item 1.3 decides what triggers a capacity transfer and said it should read the
density gradient rather than a schedule -- correct about what, silent about
when. Hera's arbitration mutates patron mass, so §18.5 binds it directly:
anything influencing patron state advances on tick count, and wall-clock time
must never be an input to a decision.
Without that stated in the item, a reasonable implementation could pace
arbitration off a wall-clock interval and reintroduce, in a new place, exactly
the defect item 2.1 exists to remove. Whatever 1.3 decides must be expressible
in ticks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-D2, following from REVIEW-A1/B1.
Item 2.2 claimed the unbounded registry "makes fleet K an identity that cannot
fail" and accepted on VM-CONSERVED? becoming able to fail. Both were wrong.
Heat is transferred rather than renormalised (§20.2), so conservation is already
a real invariant and already falsifiable -- via the dropped-remainder path at
capsule_vm_physics.c:240-244 and integer truncation at :304-305. Bounding the
population changes neither, so the item promised something it could not deliver.
The bound is still needed and now rests on the two grounds §2 states: finite
state for §13's induction and model checking, and density requiring a capacity
to be dense within, without which §19.3's admission rule has nothing to compare
against. Acceptance is now the bound existing, birth-at-bound behaving as 1.5
specifies, and the three-architecture boot unaffected.
Fixing the truncation leak is a separate and larger piece of work and stays in
§25.7 rather than being folded in here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-D1.
Item 2.1 restates vm_physics_touch on tick count and accepted on the
dictionary-hash double-boot check from item 0.10. That check cannot detect the
work: §18.5 establishes that vm_physics_touch writes node->physics, not
DictEntry.execution_heat, and so never reaches the parity hash. The dict hash
would be identical whether 2.1 succeeded, failed, or was skipped entirely.
Acceptance is now an identical fleet heat sum across two boots of the same
capsule -- fleet heat being the quantity the item actually changes. The
dict-hash check still runs, as a regression guard rather than as evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C4.
The header claimed §1-15 retained "arena" to stay quotable, but the rule was
not applied consistently -- §15 had been swept, §13 had not. A document with two
names for its central object costs every reader something on every section.
Swept all body text. "arena" now survives in four places only, each deliberate:
the src/starkernel/vm/arena.c file path; the two naming notes, which discuss the
word itself; §12's preserved question list, which the section explicitly retains
as the source several later sections quote; and the block quotes of §12 in §21
and §23.
One deviation from the review's stated rule, which said quoted text keeps the
original word. §17.3 quotes §4 and §17.6 quotes §13 -- both self-quotes within
this document, whose sources were just swept. Left unswept they would cite text
that no longer exists, so a reader following the reference finds different
words. Those two were updated to track their sources. §12's quotes were not,
because §12 itself is deliberately preserved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C3.
The stated fields sum to 28, not the ~32 claimed, and 28 + 32 inline payload is
60, not the 64-byte cell. Now stated as 28 used plus 4 reserved. The reserve is
deliberate: it keeps the header a clean half-cell, leaves room for the
header/continuation discriminator §3 now requires, and gives §8's exclusivity
primitive somewhere to live if item 1.1 resolves to a holder index.
The larger problem was the "17 cells: 1 header + 16 payload" figure for a
1024-byte block. That silently assumed continuation cells are contiguous and
carry nothing but bytes. §22.3 allocates from a per-VM free list with no
adjacency guarantee, so continuation cells would need a next-index -- 4 bytes
off each one's payload, making the same block 18 continuation cells rather than
16. The alternative, guaranteeing contiguous runs, reintroduces the
fragmentation §3 avoids.
Those are different designs with different costs, and the choice sets the mass
of every large patron. Rather than pick one, the row is marked undetermined and
recorded as §23.4 #4, which gates punch item 3.1: the cell structure cannot be
built until it is settled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C2. The heading claimed all six questions closed while the
table below marked Q5 partial and a note explained why. Five closed, one
partial.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C1. The heading read "Four patrons die four different ways"
above a table listing five. §17's preamble carries the caveat that its counts
predate §20, but a heading is where a reader anchors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>