Commit Graph
100 Commits
Author SHA1 Message Date
Robert Allan JamesandClaude Sonnet 5 f1d29e975e repl.c: unify REPL prompt to "[VM name] ok>"
Punch list §25 item 4.4a complete.
Removed the manually-built <Name>)ok>/zuse)ok> prompt suffix from
sk_repl_step()/sk_repl_run() -- console.c's console_putc() already emits a
"[VMName] " prefix at line-start, so the old code was double-printing the
name for non-Hera VMs ("[Hermes] Hermes)ok> "). Now prints only "ok> " and
lets the existing prefix supply the bracket. emergency_console/zuse_session
security semantics unchanged, display-only. Verified: all three
architectures boot live to "[Hera] ok>" (logs/20260811-073408 amd64,
logs/20260811-073448 aarch64, logs/20260811-073542 riscv64).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 07:37:13 -04:00
Robert Allan JamesandClaude Sonnet 5 838dcab5d2 FABRIC.md: lock REPL prompt format and colors
Punch list §25 item 4.4 complete.
Design-only, no code. Format [VM name] ok>; ok> reuses existing
FB_ANSI_PALETTE[14] bright cyan (0x55FFFF); [VM name] gets a fresh literal
orange (0xFFA500), since no orange exists in the ANSI palette. Verified: no
implementation, just literal values checked against the existing codebase
constants and written into FABRIC.md for 4.4a/4.4c to consume.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 07:31:15 -04:00
Robert Allan JamesandClaude Sonnet 5 a079028b40 FABRIC.md: break 4.4 REPL into 14 sub-items (4.4-4.4n), correct POST boundary
Expands the single 4.4 REPL item into a granular punch list matching the
4.3.5/4.3.6-series style: prompt format+color (4.4-4.4c), VT100 ANSI/SGR
engine retargeted onto TrueType (4.4d-4.4f), CANVAS geometry (4.4g-4.4k),
scrollback+toggle (4.4l-4.4m), and the blocked (user) prompt segment (4.4n).

Corrects the font_8x16/TTF handoff boundary: the embedded VT100 path renders
everything through and including POST (which runs after the capsule/VM
bootstrap, per CLAUDE.md), not just pre-bootstrap M1-M6 messages as
originally scoped -- TTF-TEXT takes over only once POST completes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 07:28:29 -04:00
Robert Allan JamesandClaude Sonnet 5 20801ded72 FABRIC.md/ROADMAP.md/ttf_words.c: scope console REPL (4.4), obsolete M8, flip TTF to Cartesian y
FABRIC.md: renumber Artemis-last boundary 4.4 -> 4.5, add new 4.4 REPL item
(console visual design scoped this session -- CANVAS is one shared region
above the REPL strip, 640x480 scroll box centered within it, stroke font,
ANSI color, VM-identity + ACL-identity prompt segments, ~1000-line
scrollback), linked to repl-mockup.png.

ROADMAP.md: mark M8 section OBSOLETE -- replaced by FABRIC.md 4.4, kept for
history only.

ttf_words.c: TTF-TEXT's y argument now Cartesian (origin bottom-left of the
physical framebuffer, y increasing upward) instead of raw framebuffer
coordinates, translated once via fb_height() - y. Verified via amd64
screendump (logs/20260811-070418/), text renders right-side-up with
increasing y moving up the screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 07:08:32 -04:00
Robert Allan JamesandClaude Sonnet 5 8e2c5dae5f ROADMAP.md: scope M8 REPL keyboard wiring, design decisions resolved
Not implementation -- documentation-only. M8 is tracked outside Stadium
(FABRIC.md §27.5), so this scoping lives in docs/lithosananke/ROADMAP.md
rather than as new FABRIC.md 4.3.x items.

Read the code before scoping, not assumed: the REPL already works
interactively over serial (sk_repl_run(), repl.c) with working
backspace/echo, and its on-screen text already renders via the existing
VT100 console (vt100_init()/font_8x16.c) independent of both the stroke
font and TrueType work -- neither was ever a REPL-strip prerequisite.
What's actually missing: sk_readline()'s console_getc() is
serial-UART-only, never touches KEY-EVENT (4.3.5f); KEY/?TERMINAL
(io_words.c) are dead stubs (KEY calls a getchar() hardcoded to EOF in
shim.c, ?TERMINAL always returns false).

Resolved design decisions: minimal US-QWERTY scancode->ASCII layout
with shift-state tracking (KEY-EVENT carries no modifier state today);
merge keyboard into console_getc()'s poll with serial staying a
co-equal source, not replaced (the entire acceptance/DoE harness
injects over the serial socket -- breaking that breaks make qemu,
DOE_INJECT, and every screendump technique used throughout 4.3.x); wire
KEY/?TERMINAL to the same merged source for FORTH-79 compliance;
CANVAS viewport sizing last, since it's presentation not input
plumbing. Extended scancodes, autorepeat-as-character-repeat, arrow-key
history explicitly out of scope for the "minimal" bar.

FABRIC.md's existing "not yet scoped" notes (4.3.5f's landing-point
note, 4.3.7f's closing note) updated to point here rather than
duplicating the design content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 00:52:40 -04:00
Robert Allan JamesandClaude Sonnet 5 ed8bf5460b FABRIC.md: checkpoint -- TTF rendering verified, 3 arches (4.3.7f)
Punch list §25 item 4.3.7f complete. No code changes -- ttf.c/ttf.h/
ttf_words.c unchanged since 4.3.7e; this is verification only, same
posture as 4.3.6g.

Three-arch acceptance boot clean (standard make ARCH=<arch> clean qemu,
run in order, one at a time): all three reached [Hera] ok> cleanly,
logs committed under logs/20260810-231553(amd64)/231640(aarch64)/
231739(riscv64).

One screendump per architecture, identical TTF-TEXT command sequence
(S" Hi 4.3.7e!" 200 200 28 16777215 TTF-TEXT) via monitor-socket +
serial-injection, same pattern as 4.3.7c/e. All three show the string
rendered legibly with correct mixed-case/digit/punctuation glyphs and
identical monospace spacing.

TrueType rendering (4.3.7-4.3.7f) is now complete, adjunct to the
stroke font per §27.7 decision #4. Stopping here before scoping REPL
wiring (M8), per this item's own posture matching 4.3.6g.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 00:45:11 -04:00
Robert Allan JamesandClaude Sonnet 5 715ebcc57e ttf_words.c: TTF-TEXT entry point, hmtx advance widths (item 4.3.7e)
Punch list §25 item 4.3.7e complete. New src/word_source/ttf_words.c
registers TTF-TEXT ( c-addr u x y size color -- ): lazily loads the v1
default font capsule + raster cache once, decodes UTF-8 (C
reimplementation mirroring capsules/fabric.4th's DECODE-UTF8 exactly),
looks up each glyph's cached bitmap, blits via fb_put_pixel, advances
the pen by the glyph's real hmtx advance width scaled to pixels.

Necessary plumbing: ttf_parse() now also locates hhea/hmtx, and
ttf_glyph_advance_width() reads a glyph's advance width -- required for
this item's own "proportional spacing correct" acceptance clause, no
advance-width data existed anywhere else in the parser. Verified in
tools/ttftest.c: A/a/0/space all read advance_width=600, correctly
uniform since JetBrainsMono-Regular.ttf is monospace.

(x,y) is raster pixel space (top-left origin, Y-down), deliberately not
the stroke font TEXT's Cartesian Y-up convention -- recorded explicitly
in ttf_words.h, not conflated.

Verified live, amd64, screendump: injected
S" Hi 4.3.7e!" 200 200 28 16777215 TTF-TEXT over a serial socket after
boot, no error, captured a screendump showing the string rendered
legibly with correct mixed-case/digit/punctuation glyphs and even
spacing. TTF-TEXT is this item's permanent deliverable, not a
throwaway probe. Compile-checked clean on all three architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 23:11:40 -04:00
Robert Allan JamesandClaude Sonnet 5 2db4603d04 ttf.c: glyph raster cache, fixed slots, no allocation (item 4.3.7d)
Punch list §25 item 4.3.7d complete. ttf_raster_cache_get() looks up
(font, codepoint, size_px) in a caller-owned fixed slot array, evicting
round-robin once full, rasterizing into a slot on a miss.

Verified live in tools/ttftest.c: an identical (font, 'A', 24px) call
made twice returns was_hit=0 then was_hit=1, and the slot's own hits
counter reads exactly 1 afterward -- checked programmatically. A
different-codepoint call misses again, proving the key actually
discriminates. Wall-clock timing (miss 0.040ms vs hit 0.001ms) is
printed as informational corroboration only, not the load-bearing
check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 22:31:56 -04:00
Robert Allan JamesandClaude Sonnet 5 095860251a ttf.c: rasterization -- Bezier flattening + even-odd scanline fill
Punch list §25 item 4.3.7c complete. ttf_rasterize_glyph() flattens
quadratic-Bezier contours (fixed 8-segment subdivision, matching
CIRCLE/ELLIPSE's fixed-segment precedent) and fills them into a
caller-supplied bitmap via even-odd scanline fill, no AA. A local
signed Q48.16 multiply (q48_smul) handles negative outline coordinates,
since the shared q48_mul/q48_div are unsigned-only.

Verified two ways: tools/ttftest.c's ASCII-art dump + structural checks
for 'A'/'.'/'a', all recognizable and passing; and a live amd64
screendump via a throwaway TTF-PROBE word (loaded the font capsule,
rasterized 'A', blit via fb_put_pixel), showing a clearly legible 'A'
on the CANVAS -- probe reverted immediately after capture, only the
permanent ttf.c/ttf.h rasterizer remains. Compile-checked clean on all
three architectures (hal/*.c wildcard); this item's own acceptance is
the amd64 screendump, not a three-arch boot (that's 4.3.7f).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 22:24:47 -04:00
Robert Allan JamesandClaude Sonnet 5 2ef8d26c6f capsules/fonts: JetBrainsMono-Regular.ttf as a raw-blob capsule (item 4.3.7b)
mkcapsule.c already ingests arbitrary non-.4th files as raw byte blobs
(validate_forth_blocks only applies to .4th filenames), so no hex/base64
text-encoding or tool changes are needed -- corrects the design premise
in FABRIC.md's 4.3.7b item text (see the FABRIC.md correction note this
commit carries). ttf_load_from_capsule() resolves the font capsule by
name, validates its content hash, and points ttf_font_t at the capsule
arena bytes directly, zero-copy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:50:35 -04:00
Robert Allan JamesandClaude Sonnet 5 70b6918279 ttf.c: glyph outline extraction, simple and composite (item 4.3.7a)
ttf_glyph_outline() decodes simple-glyph flag/coordinate runs and
recursively resolves composite components into a caller-supplied point/
contour-end buffer, in Q48.16. Composite scale/rotation/skew transforms
are rejected with TTF_ERR_UNSUPPORTED rather than mis-rendered, since the
shared q48_mul/q48_div are unsigned-only; translation-only composites
(the only kind the v1 glyph repertoire uses) apply cleanly via q48_add.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:45:04 -04:00
Robert Allan JamesandClaude Sonnet 5 5f6cc054d4 ttf.c/ttf.h/ttftest.c: TrueType parser core -- sfnt/head/maxp/loca/glyf/cmap (item 4.3.7)
Freestanding C module resolving a Unicode codepoint (cmap format 4) to a
glyph index and its outline header (contour count, bounding box), verified
against an independent from-scratch Python reference reader via
tools/ttftest.c. Not yet wired into the boot path or capsule system.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:41:42 -04:00
Robert Allan JamesandClaude Sonnet 5 b9b7cb2c6a FABRIC.md: checkpoint -- all 113 v1 glyphs verified, 3 arches (4.3.6g)
Punch list §25 item 4.3.6g complete. Fixed the cross-arch bug found
while first attempting this checkpoint: aarch64 and riscv64 both boot
an 800x600 ramfb device (confirmed empirically for both, not assumed),
against amd64's 1280x800 GOP framebuffer -- the original verification
grid's Y coordinates ran off the top of the smaller screens. Rebuilt
the grid to fit 800x600, safe on amd64's larger screen too.

All 113 glyphs confirmed legible and correctly positioned on all three
architectures, one screendump each, from the identical FORTH command
sequence. No code changes -- font.4th/fabric.4th are unchanged since
4.3.6f; this is verification only.

Three-arch acceptance boot clean, Stadium conservation unchanged.

Stroke font (4.3.6-4.3.6g) is now complete. Per this item's own
posture, stopping here before scoping REPL wiring (M8) -- 4.3.7
(TrueType, adjunct) is the scoped-but-not-started next work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 21:35:07 -04:00
Robert Allan JamesandClaude Sonnet 5 28731b8536 FABRIC.md: scope TrueType rendering as adjunct to stroke font (4.3.7)
Not implementation -- documentation-only. Reviewing the 4.3.6 stroke
font's full 113-glyph repertoire (mid-4.3.6g checkpoint) prompted the
decision to add TrueType rendering as an adjunct path rather than
continue refining strokes alone. Stroke font stays; nothing reverted.

Adds 4.3.7-4.3.7f (parser core, outline extraction, font data
ingestion, rasterization, glyph cache, TTF-TEXT entry point,
three-arch checkpoint) plus §27.7 documenting four resolved design
decisions: C implementation layer, Q48.16 fixed-point (not float --
confirmed real via Makefile.starkernel grep that GAP-B2's hypothesized
FP-register-clobber-on-interrupt risk is real on all three
architectures, not just hypothetical), capsule-block-encoded font
data, and TrueType as the primary text path once built.

4.3.6g itself stays open -- paused mid-verification when a real
cross-arch framebuffer-resolution difference (aarch64 800x600 ramfb
vs amd64 1280x800 GOP) surfaced; not yet re-run with corrected
coordinates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 21:10:23 -04:00
Robert Allan JamesandClaude Sonnet 5 2960989100 font.4th: TEXT entry point, UTF-8 string rendering (item 4.3.6f)
Punch list §25 item 4.3.6f complete. TEXT ( c-addr u x y size color -- )
walks a UTF-8 byte string, decoding one codepoint at a time via
DECODE-UTF8, drawing each via DRAW-GLYPH, and accumulating the cursor
X by the glyph's em-advance scaled to pixels. Uses 2>R/2R> to stash
the DECODE-UTF8 remainder off the data stack while DRAW-GLYPH's args
are pushed.

Defined in font.4th, not fabric.4th, so its compiled DRAW-GLYPH call
binds to the real definition, not fabric.4th's 4.3.6b placeholder --
same early-binding constraint as DISPATCH-GLYPH itself.

Verified live: a string mixing ASCII, Latin-1 (degree sign), and
General Punctuation (em dash) renders correctly in one TEXT call with
proportional spacing, no overlap.

Three-arch acceptance boot clean, Stadium conservation unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:46:55 -04:00
Robert Allan JamesandClaude Sonnet 5 aee09cac0a user-font-demo.4th: prove the override mechanism (item 4.3.6e)
Punch list §25 item 4.3.6e complete. Optional capsule, not wired into
the boot chain, overriding DISPATCH-UPPER's codepoint 65 (G-A) with a
marked alternate glyph while every other letter falls through to
font.4th's originals unchanged.

Corrects §27.6.5: redefining the DISPATCH-* bucket word alone is not
enough, since DISPATCH-GLYPH/DRAW-GLYPH are themselves compiled early
against font.4th's own dispatch words (same early-binding finding as
4.3.6c, one layer up). Verified both ways: the partial override (bucket
word only) left DRAW-GLYPH still drawing the original glyph; redefining
DISPATCH-GLYPH/DRAW-GLYPH too made the override take effect.

Three-arch acceptance boot clean, Stadium conservation unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:41:04 -04:00
Robert Allan JamesandClaude Sonnet 5 ef930ce04e init.4th: wire fabric.4th/font.4th into the boot chain (item 4.3.6d)
Punch list §25 item 4.3.6d complete. Both capsules now EXEC via
Hera's Mama IDENTITY capsule (init.4th block 2049), loading on every
normal boot instead of manual serial injection. Verified by booting
with zero injected commands and calling DRAW-GLYPH directly.

Three-arch acceptance boot clean, Stadium conservation unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:33:01 -04:00
Robert Allan JamesandClaude Sonnet 5 5d2e1c2184 font.4th: default system font-set capsule, 113 glyphs (item 4.3.6c)
Punch list §25 item 4.3.6c complete. Digits, upper/lowercase, ASCII
punctuation, Latin-1 Supplement, and General Punctuation, each a
stroke-drawing word in the 4.3.6 em-square convention.

Corrected mid-build: the first pass used G-LINE only, missing that
CIRCLE/ARC/ELLIPSE already existed (4.3.3b). Reworked all round shapes
to use them via new em-square-aware wrappers (G-CIRCLE/G-ARC/G-ELLIPSE/
EM-R, fabric.4th 5000-5002), bounded to quarter/half/three-quarter/
full sweeps to avoid unbounded per-glyph angle tuning. font.4th also
redefines DISPATCH-GLYPH/DRAW-GLYPH themselves, not just the six
DISPATCH-* bucket words, since fabric.4th's originals were compiled
early against the 4.3.6b placeholders (CASE early-binding, §27.6.5).

Three-arch acceptance boot clean, Stadium conservation unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:26:14 -04:00
Robert Allan JamesandClaude Sonnet 5 4427986c96 fabric: codepoint -> glyph dispatch (DISPATCH-GLYPH, TOFU)
Punch list §25 item 4.3.6b complete.
capsules/fabric.4th blocks 4921-4924: DISPATCH-GLYPH routing via
WITHIN to six bucket words (DISPATCH-DIGIT/-UPPER/-LOWER/
-ASCII-PUNCT/-LATIN1/-GENPUNCT), TOFU fallback, DRAW-GLYPH. Buckets
carry one placeholder stroke word each (G-TEST-*), not the real
113-glyph set -- that's item 4.3.6c's scope, deliberately deferred.

Also: merged two lines in block 4920 (DECODE-UTF8) to fit mkcapsule's
real 64-char x 16-line block limit once a trailing blank separator
line is counted against it -- mechanical reformat, re-verified via a
DECODE-UTF8 regression check (65/176/8212, unchanged).

Verified live on amd64: one representative codepoint per bucket plus
one out-of-range codepoint, all seven DRAW-GLYPH results matched
expected exactly (400/600/450/250/550/700/500-TOFU). Three-arch
acceptance boot clean, Stadium conservation unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 19:15:05 -04:00
Robert Allan JamesandClaude Sonnet 5 24d6b9e20e fabric: UTF-8 decoder (DECODE-UTF8)
Punch list §25 item 4.3.6a complete.
capsules/fabric.4th blocks 4918-4920: UTF8-SEQ-LEN/UTF8-CONT? verbatim
from §27.6.2, DECODE-UTF8 dispatching to four UTF8-ASSEMBLE-N helpers
(1/2/3/4-byte). Factored into helpers after the single-word draft hit
mkcapsule's real 64-char x 16-line block limit. Verified live on
amd64: a 6-byte test buffer (ASCII + deg-sign + em-dash) decoded to
65/176/8212 across three sequential calls, exact match against
hand-computed codepoints, buffer fully consumed. Three-arch acceptance
boot clean, Stadium conservation unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 19:00:36 -04:00
Robert Allan JamesandClaude Sonnet 5 82e948afe9 fabric: em-square glyph coordinate convention (EM-X/EM-Y/G-LINE)
Punch list §25 item 4.3.6 complete.
capsules/fabric.4th blocks 4916-4917: EM-UNITS, GOX/GOY/GSIZE/GCOLOR,
EM-X/EM-Y/G-LINE per §27.6.1, scaling/translating em-square strokes
into CART-PLOT screen coordinates via */. Verified live on amd64 via
a temporary probe (built, run once, reverted): two G-LINE test shapes
at GSIZE 100 and GSIZE 50, one leg each exercising a negative em-y
value chosen to hit */'s truncate-toward-zero behaviour, not a
multiple of EM-UNITS. Screendump pixel-bbox extraction matched
hand-calculated raster coordinates exactly on both shapes. Three-arch
acceptance boot clean, Stadium conservation unaffected
(resident_sum=43691 reservoir=21845 sum=65536 on all three).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 18:53:02 -04:00
Robert Allan JamesandClaude Sonnet 5 980f902c7b chore: commit stray CaptainBob.png and an uncited riscv64 boot log
CaptainBob.png: untracked repo-root image, added per Captain Bob's
direction.

logs/20260808-002843/riscv64/: a riscv64 boot log from ~00:28, minutes
after the committed 4.3.5 acceptance boots. Not cited anywhere in
FABRIC.md. It ends in the scause=0xF store-page-fault at 0x0c201000
that item 4.3.5a later formally traced to riscv64 having no
software-controlled paging -- an earlier, informal hit of the same
bug, predating 4.3.5a's own cited diagnostic logs (10:19am+).
Committed for the audit record per this repo's "logs are committed
artifacts" convention, superseded by but not duplicating 4.3.5a's
own logs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 18:35:24 -04:00
Robert Allan JamesandClaude Sonnet 5 0a03ba7871 docs: deep-derive glyph rendering design (4.3.6-4.3.6g), FABRIC.md \xc2\xa727.6.1-6
Expands the previously sparse \xc2\xa727.6 to match the derivation depth of
\xc2\xa727.3/\xc2\xa727.5.1/\xc2\xa727.5.2 -- worked line items, not just prose scoping.
Every primitive used (*/, WITHIN, CASE/OF/ENDOF, CREATE, EXECUTE, ',
LSHIFT/RSHIFT/AND/OR) was confirmed against src/word_source/*.c before
being used in a formula; no $/0x hex literal syntax exists in this tree,
so all formulas use decimal only.

Concrete design landed, in order derived:
- EM-UNITS 1000, standard Type1/OpenType em-square proportions; EM-X/
  EM-Y/G-LINE scale-and-translate into CART-PLOT space via */ (noting
  */'s "avoid overflow" rationale is vestigial at this VM's 64-bit
  cell_t width -- not a bug, just an observation)
- UTF-8 decoder: full bit-level 1/2/3/4-byte assembly formulas. Found
  mid-derivation that the 3-byte path is v1-required, not future-i18n-
  only -- smart quotes/dashes/ellipsis are Unicode General Punctuation
  (U+2000-206F), NOT Latin-1 Supplement despite the original framing
  lumping them together
- Dispatch: bucketed CASE/OF/ENDOF chains (DISPATCH-DIGIT/-UPPER/-LOWER/
  -ASCII-PUNCT/-LATIN1/-GENPUNCT) routed by WITHIN range checks, chosen
  over a flat 256-entry xt-table despite the table composing more
  cleanly with override -- explicit tradeoff, not an oversight
  - Glyph-word contract ( -- em-advance ), TOFU placeholder for misses,
    ~8-10 block budget estimate for dispatch alone
- Character list corrected and finalized: 113 glyphs (95 ASCII + 11
  Latin-1 Supplement + 7 General Punctuation), itemized
- User font override: found CASE's early binding means redefining a
  single glyph word does NOT retroactively change an already-compiled
  DISPATCH-* caller (same binding behavior 4.3.3b found for CART-PLOT).
  Resolved: override replaces a whole DISPATCH-* bucket word, not
  individual glyphs -- explicit granularity tradeoff for keeping CASE
- TEXT entry point sketched at algorithm level

DECODE-UTF8's exact byte-consumption stack mechanics and TEXT's loop
are deliberately left as algorithm-level spec, not final FORTH -- this
project's history (VARIABLE alignment, Q.TO-INT sign bug) shows exactly
this class of stack/precision bug surfaces only under live testing, not
design review; presenting untested stack-juggling code as settled would
be the "confidently wrong" mistake \xc2\xa725.0 rule 4 warns against.

Punch-list items 4.3.6-4.3.6g (\xc2\xa725.5) updated to match: concrete Done-
when criteria replacing several that were previously vague or TBD
(4.3.6b now names the six buckets; 4.3.6c names the 113-glyph list;
4.3.6e goes from "TBD" to a real, testable override-mechanism
criterion; 4.3.6f requires proportional-width verification). Still no
implementation -- scoping and derivation only, per \xc2\xa725.0 rule 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 18:14:48 -04:00
Robert Allan JamesandClaude Sonnet 5 b69dcfd050 docs: scope glyph rendering (4.3.6-4.3.6g), next after 4.3.5f
Punch list \xc2\xa725.5 additions (4.3.6-4.3.6g, unchecked) and new \xc2\xa727.6
rationale section, scoped per the 2026-08-07 sequencing note. Captures
requirements dictated this session: UTF-8-aware decoding, ASCII + Latin-1
typographic repertoire for v1, i18n-ready codepoint-indexed design, one
capsule per font-set (not per glyph), a default system font wired into
init.4th's boot chain as early as the capsule/VM bootstrap allows, with
user-font-override support -- additive to, not replacing, the existing
font_8x16.c VT100 console path. Flags two open items found while scoping
rather than deciding them unilaterally: an unused encoding field on
block_subsystem.h, and the open call on which capsule file eventually
houses the glyph data. No implementation yet -- scoping only, per \xc2\xa725.0
rule 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 16:55:01 -04:00
Robert Allan JamesandClaude Sonnet 5 40f3e3ab44 config: disable background-session worktree isolation for this repo
Background-session edits were being forced into an isolated git worktree,
defaulting to a branch off origin/master -- which has none of this
branch's FABRIC.md content -- conflicting with this repo's subversion-
style workflow (commit directly to the working branch, no side branches
without explicit permission). Sets worktree.bgIsolation: none so
background-session edits land directly in the shared checkout, consistent
with the existing CLAUDE.md policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 16:54:50 -04:00
Robert Allan JamesandClaude Sonnet 5 d0835c674b FORTH: KEY-EVENT unified keyboard interface, closes 4.3.5-4.3.5f (item 4.3.5f)
Punch list §25 item 4.3.5f complete. Keyboard-input slice done.

New KEY-EVENT ( -- keycode pressed -1 | 0 ) converges amd64's i8042,
riscv64's and aarch64's virtio-keyboard-pci behind one shape. Translation
needed is minimal: XT Set-1 make codes and Linux input keycodes are
numerically identical across the standard 84-key block (a documented
historical property of the Linux input subsystem, confirmed against this
host's own headers and this tree's own prior live observations), so amd64
needs two lines, riscv64/aarch64 need none. Verified live with a real
keypress on all three architectures: identical "-1 1 30" output everywhere.
Three-arch acceptance boot clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 12:48:05 -04:00
Robert Allan JamesandClaude Sonnet 5 a373c9124a aarch64: virtio-keyboard-pci, interrupt-driven keyboard input (item 4.3.5e)
Punch list §25 item 4.3.5e complete.

Extended virtio_input.c with a GIC-routed interrupt path alongside 4.3.5c's
PLIC one -- same capability walk, feature negotiation, eventq handling
(confirming §27.5.1's prediction that these items would share most of the
driver). aarch64_irq_handler() dispatches to virtio_input_isr() before its
EOIR write, same claim-dispatch-complete ordering riscv64 uses. Verified
with a real QEMU sendkey keypress: exact KEY_A/press match, two real
interrupts serviced, identical result to riscv64. Found (not fixed) an
unrelated pre-existing bug: BYE's cold-reset path faults on aarch64,
discovered incidentally since nobody had exercised it from a monitored
session before. Three-arch acceptance boot clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 12:29:27 -04:00
Robert Allan JamesandClaude Sonnet 5 2a2d7c5e5e aarch64: GIC SPI wiring for virtio-input (item 4.3.5d)
Punch list §25 item 4.3.5d complete.

New apic_spi_enable(intid) generalizes item 0.6's PPI-only sequence to one
explicit SPI (IPRIORITYR/ISENABLER/ITARGETSR, ICFGR read-checked not
written). Verified via a software-pended SPI (GICD_ISPENDR, no device
needed) through the existing generic IRQ dispatch, which needed no changes.
Found and fixed a real bug during verification: PSTATE.I is still set at
apic_init()'s point in boot, so the first self-test run correctly latched
but never took the interrupt. Self-test code reverted after capturing
evidence -- interrupts.c has zero net diff, only apic_spi_enable() remains,
unused until 4.3.5e. Three-arch acceptance boot clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 12:15:23 -04:00
Robert Allan JamesandClaude Sonnet 5 8251aebcf8 riscv64: virtio-keyboard-pci, interrupt-driven keyboard input (item 4.3.5c)
Punch list §25 item 4.3.5c complete.

Amended from a nonexistent MMIO transport to PCI (matching the board's
actual virtio-blk-pci precedent). New virtio-input driver: eventq with
pre-posted buffers, PLIC source computed at runtime from PCI slot/pin
(derived live from this host's QEMU riscv64 DTB), mandatory ISR-status
read, PCI interrupt-disable-bit check. New VKBD-EVENT/VKBD-DEBUG FORTH
words. Verified with a real QEMU sendkey keypress: exact KEY_A/press
match, two real interrupts serviced, zero exceptions. Three-arch
acceptance boot clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 11:51:07 -04:00
Robert Allan JamesandClaude Sonnet 5 5f4df673c1 riscv64: PLIC bring-up, external-interrupt substrate (item 4.3.5b)
Punch list §25 item 4.3.5b complete.

sie.SEIE enabled, PLIC threshold/claim/complete wired into the trap
handler. Verified with a UART-loopback synthetic interrupt (PLIC has no
software set-pending register, unlike GICv2): claim_count=1, last_irq=10,
IIR confirms genuine receive-data cause, byte matched exactly. Self-test
code run once for evidence then fully reverted, per Captain Bob's ruling;
only the permanent substrate remains, no source enabled by default.
Three-arch acceptance boot clean, zero exceptions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 11:24:47 -04:00
Robert Allan JamesandClaude Sonnet 5 36965cf812 riscv64: explicit Bare-mode satp switch (item 4.3.5a)
Punch list §25 item 4.3.5a complete.

Firmware (EDK2 RISC-V) leaves Sv57 paging active at kernel entry with an
identity mapping that has at least one confirmed hole (PLIC threshold
register). Verified live before acting: satp.MODE/PPN and __kernel_start's
address confirmed identity mapping; switched to Bare mode (csrw satp, x0 +
sfence.vma) in arch_early_init(), ahead of pmm_init()/vmm_init().
Three-arch acceptance boot clean, zero exceptions; riscv64's PLIC_THRESHOLD
write now survives (PLIC driver code itself lands separately with 4.3.5b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 11:07:29 -04:00
Robert Allan JamesandClaude Sonnet 5 06283e5364 FABRIC.md: aarch64 keyboard punch list -- GIC-SPI derivation, no landmine
Live-probed GICD_ITARGETSR before starting 4.3.5d/e; survived cleanly,
unlike riscv64's PLIC (4.3.5a). Decoded QEMU's own virt DTB for the PCI
slot->SPI interrupt-map routing table and closed-form formula. Amended
4.3.5d's circular acceptance criterion to a synthetic GICD_ISPENDR probe
(mirrors 4.3.5b's fix for the same problem), and wrote the full 4.3.5e
virtio-input execution plan: device ID 0x1052, the real virtio_input_event
struct, the mandatory ISR-status read, and the pci_enable() interrupt-
disable-bit gap.

Log: logs/20260808-093228/aarch64/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 09:56:20 -04:00
Robert Allan JamesandClaude Sonnet 5 e01c4e33e4 FABRIC.md: insert 4.3.5a -- riscv64 has no software-controlled paging
Discovered live while implementing what was 4.3.5a (PLIC bring-up,
renumbered 4.3.5b): riscv64 never activates its own page table
(load_cr3() is a no-op outside __x86_64__, arch.c's Sv39/SATP setup is
an explicit stub), so firmware's own mapping -- which doesn't cover the
PLIC's MMIO window -- is what's live for the kernel's whole lifetime.
Confirmed via a live store-page-fault, not inferred. Per Captain Bob's
ruling: stop, scope as its own item, decide the fix (Bare mode vs. real
Sv39) in a future session rather than embedded in the PLIC item.
4.3.5b-4.3.5e renumbered accordingly (4.3.5f was 4.3.5e); no items were
executed under the old numbering, so this is free to do cleanly now.

Documentation only. PLIC driver code (plic.c/plic.h, interrupts.c/
apic.c wiring, Makefile.starkernel) is written and reviewed but stays
uncommitted -- it cannot be verified until 4.3.5a unblocks it, and
riscv64 currently hangs in the fatal exception handler with it applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 00:47:31 -04:00
Robert Allan JamesandClaude Sonnet 5 6a97fa4c98 logs: add boot-run audit trail and DoE CSVs from item 4.3.5 work
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 00:18:58 -04:00
Robert Allan JamesandClaude Sonnet 5 88eb73cfe8 starkernel: item 4.3.5 -- amd64 I/O APIC + i8042 keyboard, interrupt-driven
Punch list §25 item 4.3.5 complete.

New ioapic.c/i8042.c drivers (MADT-derived I/O APIC base, no hardcoded
constants) plus a KBD-SCAN/KBD-DEBUG diagnostic word pair. Three real
bugs found and fixed en route, all blocking this item's own acceptance:
a fatal LAPIC spurious-vector crash (nothing had driven a real external
interrupt through the I/O APIC before), OVMF leaving the keyboard device
itself scanning-disabled (0xF4 fix), and isr.S's stub table only having
individually-numbered stubs through vector 32 -- everything above that,
including our IRQ1 vector 33, silently reported as vector 255 regardless
of which IDT slot actually fired. Verified live via QEMU sendkey against
KBD-SCAN: correct XT Set-1 make/break codes for two different keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 00:18:48 -04:00
Robert Allan JamesandClaude Sonnet 5 78ff335b97 FABRIC.md: note sequencing -- keyboard, glyph rendering, REPL, then 4.4
Recorded per Captain Bob's direction so the order survives between
sessions. Neither glyph rendering nor REPL is scoped yet -- each gets
Done-when criteria at its own checkpoint per 25.0 rule 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 22:59:04 -04:00
Robert Allan JamesandClaude Sonnet 5 8fffd69347 FABRIC.md: scope 4.3.5-4.3.5e -- interrupt-driven keyboard input, per-ISA split
Next after the 4.3.4 checkpoint. amd64 needs an I/O APIC (none exists;
legacy PIC is permanently masked), riscv64 needs a PLIC (external
interrupts were never enabled in Phase 0), aarch64 needs GIC SPI
routing (0.6 scoped PPI-only). Documentation only, no code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 22:57:43 -04:00
Robert Allan James 7024e413f1 logs: add boot-run audit trail from multi-ISA framebuffer bring-up
Three-arch acceptance run (amd64/aarch64/riscv64) after adding -device
ramfb to aarch64/riscv64, plus the interactive GOP-verification and cube
sessions on both.
2026-08-07 20:13:22 -04:00
Robert Allan James e58381c121 fb/: untrack and gitignore -- throwaway local verification images
Reversing the earlier decision to track fb/ in git (made when first
setting up the directory for item 4.3.2). Captain Bob: these are
disposable screenshots for eyeballing framebuffer output during Console
work, never meant to be committed. Removed from git tracking (git rm
--cached) and added to .gitignore; files that still exist locally are
untouched, deletions already made locally are left as-is.

Also fixes item 4.3.4's amd64-only blind spot found in the process:
aarch64/riscv64 had no framebuffer device at all (GOP: protocol not
found) -- the "all three architectures boot clean" checks run all session
were REPL/dict_hash parity, a different thing from GOP presence, and
conflating the two was an error. Added -device ramfb (EDK2's
firmware-only GOP framebuffer) to both architectures' qemu targets in
Makefile.starkernel. Both now report GOP: linear framebuffer found at
800x600; cube rendering verified correct on both (screenshots not
committed, per the untrack above -- verified visually this session).
Standard three-arch acceptance boot re-run afterward, all clean,
dict_hash identical and unchanged from before this fix.
2026-08-07 20:13:17 -04:00
Robert Allan James c16eaf431f logs: add boot-run audit trail and DoE CSVs from item 4.3.4 work
Three-arch acceptance run (amd64/aarch64/riscv64) plus the interactive
CUBE verification session.
2026-08-07 15:29:10 -04:00
Robert Allan James fa300ef4c6 starkernel: item 4.3.4 -- checkpoint, draw a cube (no new bugs)
Adds VERT/EDGE/CUBE to capsules/fabric.4th (blocks 4913-4915). VERT
( n -- x y z ) reads bits 0/1/2 of a corner index as the X/Y/Z sign
(+-CS from center), so all 8 cube corners come from one word. EDGE
resolves both corners via VERT and calls LINE; CUBE is 12 EDGE calls
(4 bottom, 4 top, 4 vertical).

First item in the 4.3.3.x sequence with no new bug found -- a small signal
that Q.TO-INT, the VARIABLE alignment fix, and the LINE-STUCK? cap were
the real gaps rather than something still lurking in LINE/PROJECT/CART-Y.

Verified live on amd64: a centered, half-size-100 cube renders correctly
-- front/back face squares, back face offset diagonally up-right by
exactly the 45-degree cavalier projection's depth term, all 12 edges
connecting at the right corners.

All three architectures boot clean to ok> with the DoE completing;
dict_hash identical across all three and unchanged from 4.3.3a/4.3.3b.

FABRIC.md item 4.3.4 marked done. This is the checkpoint -- 4.3.x
groundwork stops here for review per this item's own acceptance criterion.
2026-08-07 15:29:04 -04:00
Robert Allan James a3fe0702eb logs: add boot-run audit trail and DoE CSVs from item 4.3.3b work
Three-arch acceptance run (amd64/aarch64/riscv64) plus the interactive
sessions used to test LINE/CIRCLE/ARC/ELLIPSE, the block-span test, and
verify the Q.TO-INT fix.
2026-08-07 15:21:13 -04:00
Robert Allan James cb4326c712 starkernel: item 4.3.3b -- geometry drawing wordset, fixed Q.TO-INT sign bug
Adds LINE (Bresenham in raster space, endpoints projected once each --
valid because the cavalier projection is linear), CIRCLE/ELLIPSE
(36-segment polygon approximation), and ARC (18 segments over a caller
radian range) to capsules/fabric.4th (blocks 4903-4912). TO-RASTER
factored out of CART-PLOT (same behavior) so LINE can reuse the
projection+flip for both endpoints.

Found mid-implementation: colon definitions cannot span block boundaries
in this capsule loader -- verified with a throwaway test capsule, the
continuation lands in a [CAPSULE][DEFER] path that never resolves. LINE's
body is split across LINE-SETUP/LINE-DONE?/LINE-STUCK?/LINE-STEP, each
self-contained within its block, rather than one long definition.

A fourth real bug, serious this time: CIRCLE's first live test rendered
only one quadrant, then hung the VM for several minutes on a follow-up
call. Root cause: q48_to_u64() (include/q48_16.h and
include/starkernel/q48_16.h, backing Q.TO-INT) did an unsigned logical
shift, corrupting any negative Q48.16 value into a huge garbage integer
instead of sign-extending -- inevitable once Q.SIN/Q.COS leave the first
quadrant. That garbage became a bogus LINE target with no bound on
LINE-STEP's Bresenham loop. Fixed q48_to_u64 to shift through a signed
int64_t intermediate (bit-identical for the non-negative case). Also added
LINE-STUCK? (LSTEPS vs FB-WIDTH+FB-HEIGHT, the true worst case for an
on-screen line) as a defense-in-depth cap against any future bad target.

Verified live on amd64 after both fixes: -65536 Q.TO-INT . now prints -1;
LINE/CIRCLE/ARC/ELLIPSE all complete without hanging or erroring, and a
combined screendump shows all four rendering correctly and distinctly.

All three architectures boot clean to ok> with the DoE completing;
dict_hash identical across all three and unchanged from 4.3.3a (expected
-- fabric.4th isn't loaded at boot, and the Q.TO-INT fix doesn't change
dictionary structure).

FABRIC.md item 4.3.3b marked done with full acceptance evidence.
2026-08-07 15:20:58 -04:00
Robert Allan James 01822585f6 logs: add boot-run audit trail and DoE CSVs from item 4.3.3a work
Three-arch acceptance run (amd64/aarch64/riscv64) plus the interactive
Q.SIN/Q.COS verification session.
2026-08-07 13:39:09 -04:00
Robert Allan James 36389e9d4a starkernel: item 4.3.3a -- Q48.16 trigonometry (Q.SIN/Q.COS)
Adds q48_reduce_angle() (range-reduce a signed Q48.16 angle into
[-PI_Q48, PI_Q48] via one integer division plus a bounded fix-up loop) and
q48_sin_approx/q48_cos_approx (Taylor series, terms n=3,5,7,9,11 for sin
and n=2,4,6,8,10 for cos, early exit below 10). Q.SIN/Q.COS registered as
FORTH words in q48_words.c, same pattern as Q.LOG/Q.EXP/Q.SQRT.

Found mid-implementation: this codebase has two independent Q48.16
implementations -- src/word_source/q48_16_words.c (hosted/vendored) and
src/starkernel/math/q48_16.c (kernel-only; the kernel build does not
compile the former at all). The hosted build linked fine after the first
pass; the kernel build failed with undefined references until the same
two functions were added to both .c files and both q48_16.h headers
(include/q48_16.h and include/starkernel/q48_16.h). Not fixed at the root
-- Q.LOG/Q.EXP/Q.SQRT already had this same four-file duplication,
unremarked until now -- just navigated correctly for this item.

Verified live on amd64 via serial injection: sin/cos at 0, +-pi/2, pi, and
3pi (range-reduction across multiple turns) all match expected values
within Taylor-series truncation error (<0.2%).

All three architectures boot clean to ok> with the DoE completing;
dict_hash identical across all three (0x291a660b05fa7b52).

FABRIC.md item 4.3.3a marked done with full acceptance evidence.
2026-08-07 13:39:05 -04:00
Robert Allan James fd1c8ce386 fb/: split into per-ISA subdirectories (amd64/aarch64/riscv64)
Screenshots stay tracked in git, same as before -- just organized so
verification images are easy to find per architecture as Console work
progresses across all three ISAs. scripts/qemu_screenshot.sh (amd64-only)
now writes into fb/amd64/.
2026-08-07 13:23:26 -04:00
Robert Allan James 938f5e87f9 logs: add boot-run audit trail and DoE CSVs from item 4.3.3 work
Three-arch acceptance run (amd64/aarch64/riscv64) plus the interactive
fabric.4th verification sessions used to test CART-PLOT and find the
VARIABLE alignment bug.
2026-08-07 13:21:02 -04:00
Robert Allan James ef9806977a starkernel: item 4.3.3 -- Cartesian coordinate machinery, found and fixed a VARIABLE alignment bug
Adds Module 28 (framebuffer_words.c/.h): PLOT ( x y color -- ), FB-WIDTH,
FB-HEIGHT -- raw hardware-boundary C primitives, kernel-only, no-op on
hosted builds, same pattern as every other module.

Adds capsules/fabric.4th (blocks 4900-4902, mkcapsule --lint clean):
COS45/Z->DELTA/PROJECT/CART-Y/CART-PLOT -- the 45-degree cavalier
orthographic projection and Y-flip, in FORTH per the compose-in-FORTH-first
rule (this is policy, not hardware access).

Found and fixed a second real bug while live-testing CART-PLOT over the
serial socket: defining_word_variable() (defining_words.c) captured
vm->here as a VARIABLE's address with no alignment call first, while
vm_load_cell/vm_store_cell require 8-byte-aligned addresses. This capsule's
VARIABLE ZD landed misaligned (945) purely by chance of what preceded it;
other capsules' variables happened to land aligned by luck, not guarantee.
Real deviation from FORTH-83/ANS, which specifies VARIABLE reserves an
aligned cell. Fixed with vm_align(vm) before capturing addr -- ALIGN
already existed as a word but VARIABLE wasn't calling it.

Verified end-to-end on amd64 via manual serial injection + QEMU screendump:
plotted 4 marker points (origin, +100 X, +100 Y, +50 Z) and confirmed all
landed at hand-calculated raster coordinates, including the diagonal
up-right shift for the Z-axis point -- the projection math is correct, not
just non-crashing. fb/fabric-test-cart-plot.png. 4.3.1's corner diagnostic
still renders correctly in the same shot, confirming no regression.

All three architectures (amd64/aarch64/riscv64) boot clean to ok> with the
DoE completing; dict_hash identical across all three
(0xc7f9adf885e306d2), confirming parity is unaffected.

FABRIC.md item 4.3.3 marked done with full acceptance evidence.
2026-08-07 13:20:52 -04:00
Robert Allan James fbf0625317 FABRIC.md: item 4.3.3 -- settle 45deg cavalier, split off 4.3.3a/4.3.3b
Scoping discussion for 4.3.3 (Cartesian coordinate machinery) surfaced a
larger ask: a full geometry drawing wordset (circles, arcs, ellipses,
radian-based trig), which doesn't exist anywhere in this codebase yet.
Split per the one-task-per-checkbox rule rather than overloading 4.3.3:

- 4.3.3 stays scoped to single-point plumbing: PLOT/FB-WIDTH/FB-HEIGHT (C,
  raw hardware boundary) plus PROJECT/CART-Y/CART-PLOT (FORTH, capsules/
  fabric.4th, blocks 4900+), per compose-in-FORTH-first.
- 4.3.3a (new): Q.SIN/Q.COS in q48_16.c, Taylor series, same pattern as the
  file's existing Q.LOG/Q.EXP/Q.SQRT.
- 4.3.3b (new): LINE/CIRCLE/ARC/ELLIPSE in FORTH, built on 4.3.3 + 4.3.3a.

Also settled: projection angle is true 45 degree cavalier (not 2:1
isometric); "1080p" is a sizing sanity check only, not a hardcoded
constraint (Q48.16's 48 integer bits comfortably cover it).
2026-08-07 11:48:50 -04:00
Robert Allan James ab96ac0970 starkernel: item 4.3.1 -- framebuffer orientation test, found and fixed a real color-swap bug
Adds fb_draw_orientation_test() (framebuffer.c/.h): fills the four raster
corners RED/GREEN/BLUE/YELLOW via fb_fill_rect. Wired into kernel_main.c
calling fb_init() directly -- console_fb_init()/vt100_init() removed from
the boot path, since vt100.c/console.c are superseded by the Console
drawing-fabric redesign (FABRIC.md ss27) and should not be exercised even
incidentally.

The diagnostic caught a real, pre-existing bug on its first run: framebuffer.c's
pack_pixel() had its FB_PIXEL_RGBX32/FB_PIXEL_BGRX32 branches swapped relative
to UEFI GOP's own byte-order naming convention, producing a clean R<->B channel
swap (G unaffected). Spatial placement was already correct -- no flip/rotation.
Fixed by swapping pack_pixel's two return bodies to match framebuffer.h's
already-correct doc comments; kernel_main.c's GOP-format switch needed no change.

Also item 4.3.2 -- QEMU screenshot capability. scripts/qemu_screenshot.sh
already existed (monitor socket + socat + HMP screendump), just unwired and
unused this session. Redirected its PNG output to a new top-level fb/
directory (tracked in git, not logs/, not a gitignored temp dir) and added a
python3+PIL fallback for PPM->PNG conversion since imagemagick isn't
installed here. Left as a standalone script for now, not wired into a
Makefile target.

FABRIC.md items 4.3.1 and 4.3.2 marked done with acceptance evidence.
2026-08-07 11:38:33 -04:00
Robert Allan James f3acfb9b47 FABRIC.md: mark 4.3.x as an open-ended, appendable sublist
Visual cue between 4.3.4 and 4.4 so it's clear at a glance where the
growing 4.3.x Console slice ends and 4.4 begins, without reusing the
document's --- convention (reserved for ## section breaks).
2026-08-07 10:35:17 -04:00
Robert Allan James d4282eb87b FABRIC.md: split 4.3.1 into 4.3.1-4.3.4, one task per checkbox
The prior commit bundled four separate tasks (test pattern, screenshot
capability, coordinate machinery, cube checkpoint) into a single numbered
list inside one checkbox, violating the punch list's own convention of one
task per box. Split into four checkboxes and updated §27 to match.
2026-08-07 10:33:13 -04:00
Robert Allan James f3967fa596 FABRIC.md: item 4.3.1 -- console drawing-fabric groundwork spec (new §27)
Documents the actual hardware-boundary state (framebuffer already wired via
console_fb_init, raw pixel primitives already exist), the missing QEMU
screenshot capability, and the coordinate-system decisions made in
discussion: bottom-left origin, Y-up, Z as depth-into-screen, fixed
orthographic projection as a placeholder (angle still open). Acceptance is
a cube on screen; fonts/scrolling/message-protocol are later 4.3.x items.
2026-08-07 10:31:04 -04:00
Robert Allan James 8bf9480db5 FABRIC.md: item 4.3 -- console discussion held, scope 4.3.1 (framebuffer + coordinate fabric)
CONSOLE.md superseded as a rough prior draft, not edited further. Console
design now lives in FABRIC.md going forward. 4.3.1 is the first slice:
framebuffer sanity check, QEMU screenshot capability, Cartesian/Z coordinate
machinery, and a cube as the first real exercise of the projection math.
2026-08-07 10:27:46 -04:00
Robert Allan JamesandClaude Sonnet 5 7ba7a126c9 logs: add boot-run audit trail and DoE CSVs from item 4.2 work
Serial logs and DoE run CSVs from this session's acceptance-test boots
(the amd64 GOT-indirect-addressing fix and item 4.2's Hermes-native-
on-the-Stadium work) -- committed per the standing convention that
these are audit artifacts, not scratch output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 01:50:45 -04:00
Robert Allan JamesandClaude Sonnet 5 5a28458b21 starkernel: item 4.2 -- Hermes native on the Stadium (complete)
Migrates Hermes's message/channel lifecycle onto the Stadium's unified
heat/capacity economy: MSG-ALLOC/FREE-NODE and CH-ALLOC/FREE-NODE now
route entirely through stadium_admit()/stadium_evict(), replacing the
old local free-list + independent heat-field mechanism. Eight
kernel-only STADIUM-* FORTH primitives (ADMIT, EVICT, RES@, RES-PULL,
RES-PUSH, HEAT@, HEAT!, WORD-HEAT), VM.stadium_vm_id threaded through
all three vm_core.c dispatch sites (replacing item 4.1's hardcoded
vm_uuid_hera()), and the stadium_owner[idx] fix so evict-credit lands
in the VM that actually admitted a patron, not whoever owned cell 0.

This session's own contribution, on top of that pre-existing
implementation: found and fixed two bugs blocking the item's own K≡1.0
conservation self-check (HERMES-K was reading 0, not 65536):

- Q.SLOT admission-heat fix (capsules/hermes/init.4th): MSG-SEND/
  CH-ACCEPT admitted with Q.1 (the entire fleet-wide "1.0" unit) per
  item, a leftover from before the Stadium migration when each
  message/channel had its own unconstrained heat field. Instantly
  drained the shared, finite reservoir.

- Reservoir floor for word-execution admission (stadium_words.c):
  stadium_word_dispatch() (item 4.1) pulls STADIUM_WORD_HEAT_QUANTUM on
  every word dispatch, not just first admission -- exhausts a VM's
  entire reservoir in ~32 dispatches, starving any application-level
  economy sharing that VM's reservoir before it gets a chance to pull
  anything. word_dispatch_pull() now clamps word-execution's own pulls
  to leave a Q48_ONE/3 floor (same fair-share figure COMMON-CH's own
  floor already uses); application-level pulls are unaffected.

- STADIUM-WORD-HEAT primitive + stadium_words_resident_heat(): the
  floor deliberately leaves word-execution residents holding real
  heat, invisible to HERMES-K's original formula (MSG+CH+reservoir,
  no term for word patrons). Adding this term closes K to exactly
  65536 on all three architectures.

Also rules on two open scope questions in FABRIC.md: MBR-ALLOC/
MBR-FREE-NODE stay off the Stadium (membership records have no heat
field, never did -- the acceptance bullet's inclusion of them was a
completeness gesture predating a check of the actual layout), and
records the effort number (12 implementation files, +759/-120 lines).

Verified: all three architectures boot clean, full self-test passes,
Stadium conservation closes exactly (resident_sum + reservoir =
Q48_ONE) at both the C/Stadium level and the FORTH-level HERMES-K
check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 01:49:23 -04:00
Robert Allan JamesandClaude Sonnet 5 0a7f144367 amd64: fix GOT-indirect addressing bug in dictionary fast-path lookup
vm_find_word() and dict_find_word_heat_aware() reference the same extern
globals (sf_fc_list/sf_fc_count/sf_fc_cap) but GCC compiled cross-TU
references to them with GOT-indirect addressing (R_X86_64_REX_GOTPCRELX)
under -fPIC. This freestanding, statically-linked UEFI PE image has no
dynamic linker to populate a GOT, so those reads silently returned NULL
instead of the array's real address -- amd64-only, and exquisitely
sensitive to unrelated code-size changes since the choice between direct
and GOT-indirect addressing is a per-call-site GCC heuristic.

Fix: -fno-pic -fno-pie for amd64 only (ARCH_CFLAGS, overriding
COMMON_CFLAGS's -fPIC, which riscv64's -shared loader link still needs).
Also removes -DPLATFORM_TIME_NO_INLINE, a prior one-off workaround for
the identical bug applied to sf_monotonic_ns() specifically, now
redundant. Adds R_X86_64_PC32/R_X86_64_PLT32 handling to
elf_apply_relocations() as a robustness fix for the non-monolithic
split-build path (dead code for the current monolithic boot, where OVMF's
own PE loader relocates the image, not this loader).

Verified: all three architectures boot clean and pass the full item-4.2
Hermes self-test, including MSG-DELIVER-ALL, which previously triggered
the corruption on amd64 only. Write-up in FABRIC.md under item 4.2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 21:18:06 -04:00
Robert Allan JamesandClaude Sonnet 5 56ad128e2e word_source: add LSHIFT/RSHIFT bitwise-shift primitives
No shift primitive existed anywhere in the vendored VM word set. Adds
both as FORTH-83-extension words next to INVERT, guarded against stack
underflow and out-of-range shift counts (u >= 64).

Their absence was masking a real bug: capsules/hermes/init.4th's
CH-MINT-ID (item 4.2) calls LSHIFT to pack a 64-bit channel ID, which
was silently tripping the capsule loader's forward-reference retry
logic and splicing CH-REQUEST's body into CH-MINT-ID's definition.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 14:50:05 -04:00
Robert Allan JamesandClaude Sonnet 5 11cd6c8574 FABRIC.md: item 4.2 -- fourth ruling, two more STADIUM-* primitives
Punch list §25 item 4.2 acceptance amended (scoping only, no code).
Found while reading Hermes's actual implementation (capsules/hermes/init.4th):
MSG-COOL-ALL/CH-COOL-ALL mutate each live node's own heat field every
HERMES-TICK, and MSG-TOTAL-HEAT/CH-TOTAL-HEAT sum it -- none of the original
five primitives expose a resident cell's heat at all. Ruled: STADIUM-HEAT@
(read) and STADIUM-HEAT! (write, reconciling the reservoir delta atomically
in C -- same shape as stadium_word_dispatch()'s own cooling code, so
conservation is never left to FORTH to get right). Cooling cadence stays in
Hermes's own HERMES-TICK loop, not a new C-side per-tick sweep -- matches
HERMES.md's language constraint. Primitive surface is now seven, not five;
Done-when updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 15:59:05 -04:00
Robert Allan JamesandClaude Sonnet 5 2981ada2a5 starkernel: item 4.1a -- quota granting, Hermes's one-time birth grant
Punch list §25 item 4.1a complete.
New prerequisite item, found while scoping 4.2: no quota-granting mechanism
existed at all. Adds stadium_grant_quota(new_vm_id, from_vm_id) -- a
one-time initial grant at birth, distinct from item 1.3's still-unbuilt
recurring capacity-transfer arbitration. Splits the donor's free list evenly
by cell count, reassigns stadium_owner[] for every moved cell, and grants
the new VM a fresh Q48_ONE reservoir (not a split of the donor's -- per-VM
conservation, same pattern as Hera's own boot grant). Wired into every baby
VM's birth in capsule_birth.c.

Verified via a boot-time self-test in kernel_main.c using a synthetic
identity (not the real UUID pool, not a real capsule birth -- item 0.1's
Hera-alone pruning stays intact). All three architectures booted to ok> with
identical output: grant OK, Hera reservoir=0 (already fully committed to
resident words, correctly unchanged), test-vm reservoir=65536 (fresh
Q48_ONE). dict_hash identical across all three and unchanged from item 4.1's
baseline (0x3d4e1daf289da94f) -- confirms no dictionary word was added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 15:12:30 -04:00
Robert Allan JamesandClaude Sonnet 5 edfc246579 FABRIC.md: new prerequisite item 4.1a -- quota granting for Hermes's birth
Punch list §25 new item inserted before 4.2 (scoping only, no code).
Found while scoping 4.2: no quota-granting mechanism exists at all --
quota_slot_for_vm() refuses every non-Hera vm_id by design, and both
stadium_admit()'s doc and item 3.2's DONE note defer per-VM free lists to
"when Hera assigns a VM its quota," which nothing builds. Distinguished from
item 1.3's still-open recurring capacity-transfer arbitration: this is
Hermes's one-time initial grant at birth, same shape as Hera's own
whole-pool grant at stadium_boot_init(), not an instance of the open
recurring loop. Ruled: reservoir is not split (Hermes gets her own fresh
Q48_ONE, per-VM conservation per stadium.c's own invariant comment); cell
count is an even split of Hera's current free list, touching no resident
(including pinned cell 0). 4.2 is marked blocked on this item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 14:49:36 -04:00
Robert Allan JamesandClaude Sonnet 5 4b26be90c3 FABRIC.md: item 4.2 -- third ruling, VMUuid on VM struct (Option A)
Punch list §25 item 4.2 acceptance amended (scoping only, no code).
Found while scoping the C/FORTH primitive layer: all three
stadium_word_dispatch() call sites in vm_core.c hardcode vm_uuid_hera(),
already flagged inline as this item's job, but fixing it needs a running
VM* to know its own identity -- VMUuid exists only on VMRegistryEntry, never
on VM itself. Ruled: add a VMUuid field to VM under __STARKERNEL__ (Option
A), set once at birth from the same VMRegistryEntry.vm_id already assigned,
not threaded through the call chain (Option B). Done-when extended with a
Hermes-word-heat-lands-in-Hermes's-own-reservoir check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 14:28:24 -04:00
Robert Allan JamesandClaude Sonnet 5 658459b564 FABRIC.md: rule and restate acceptance for item 4.2
Punch list §25 item 4.2 complete (scoping only, no code).
Two rulings taken before implementation starts: the stadium_owner[idx]
fix (§25.7) is explicitly folded into 4.2's scope rather than split out,
since a second VM with a quota is what makes the bug live; the C/FORTH
boundary is a small new kernel-only FORTH primitive layer
(STADIUM-ADMIT/EVICT/RES@/RES-PULL/RES-PUSH), registered like BIRTH/RUN/USE,
the only new C this item may add per HERMES.md's language constraint.
Surfaces two open questions for implementation time rather than resolving
them by assumption: the STADIUM_BEHAVIOUR_DELIVER/EXPIRE mapping for
messages/channels, and whether this dissolves HERMES.md's G8 deferral.
States Done-when acceptance, including the explicit effort-number report
§10 calls for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 14:11:56 -04:00
Robert Allan JamesandClaude Sonnet 5 3d0b9351bd starkernel: item 4.1 -- hot words onto the Stadium, density-ranked eviction
Punch list §25 item 4.1 complete.
Replaces the round-robin hotwords cache with Stadium density-ranked
admission/eviction on the kernel side, via the §17.7 reservoir mechanism and a
kernel-side word_id -> cell_index map (no DictEntry change, dict_hash
untouched). Adds stadium_birth_hera() to close the cell-0 panic hazard,
STADIUM_WORD_HEAT_QUANTUM/STADIUM_WORD_COOL_RATE_Q48 Kconfig knobs (flagged
untuned), and a stadium_word_forget() FORGET coherence hook to close a
recycled-word_id aliasing gap.

Verified: all five hotwords_cache_* call sites in dictionary_management.c
bypassed under __STARKERNEL__; word dispatch feeds the Stadium at all three
vm_core.c physics_execution_heat_increment() sites; hosted make unaffected;
all three architectures booted to ok> with matching dict_hash
(0x3d4e1daf289da94f) and matching conservation stats (promotions=354
evictions=0, resident_sum=65536 reservoir=0 sum=65536).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 13:37:10 -04:00
Robert Allan JamesandClaude Sonnet 5 bd92c57834 FABRIC.md: unblock and restate acceptance for item 4.1
Records two decisions and restates item 4.1's Done-when criteria now that
§17.7 is DECIDED: word identity maps to a Stadium cell via a kernel-side
word_id -> cell_index table in stadium.c, not a new DictEntry field, keeping
the shared vendored struct and dict_hash untouched; acceptance no longer
leans on the old cache's HotwordsStats (being retired kernel-side, not
extended) but on new Stadium-side counters, the hosted build staying
unaffected, and a three-arch boot with dict_hash matching the pre-4.1
baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 11:29:12 -04:00
Robert Allan JamesandClaude Sonnet 5 c0bbff21c9 FABRIC.md: correct §17.7's stadium_admit() cost claim -- free list already O(1)
3d73b8e described stadium_admit()'s O(N) scans as the typical cost, sourced
from item 3.5's commit note written before item 3.7 landed the per-VM free
lists. Reading stadium.c:312-381 directly: the free-list pop is the primary
path (O(1), no scan), and the fallback scan only triggers once a VM's own
floor is full, scoped to that VM's residents only, never the global array.
Option B is better justified than originally written, not just corrected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 11:18:22 -04:00
Robert Allan JamesandClaude Sonnet 5 3d73b8e12a FABRIC.md: close §17.7 -- reservoir-based O(1) word heat transfer, DECIDED
Resolves the last open question: vm_physics_touch()'s O(n) proportional
fan-out (justified there only by rare fleet touches) isn't viable at word
dispatch frequency. Instead each VM's inner Stadium gets one reservoir
scalar; touch/cool/evict are all O(1) two-party transfers against it,
mirroring Hera's structural role at the fleet level rather than
peer-to-peer redistribution. Admission is the starter grant itself
(Option B): execution_heat stays fully inert in kernel builds, matching
the one-governor rule already committed, at the cost of running
stadium_admit()'s existing O(N) scan on every cold-word dispatch until
item 3.5's free list lands -- accepted debt, not a new gap. Corrects
abb1d92's invariant wording: residents sum to less than Q48_ONE, with
the reservoir holding the remainder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:53:47 -04:00
Robert Allan JamesandClaude Sonnet 5 abb1d92190 FABRIC.md: resolve four of five §17.7 questions -- Stadium heat already conserved
The Stadium cell's heat field (stadium.h:64, item 3.1) was already declared
Q48.16 conserved share of 1.0, citing §19.1, before this section was reopened.
L0 already has a real conservation mechanism (items 3.4/3.5 consume it); it
has just never been fed. execution_heat and Stadium heat are two different
fields with two different jobs -- item 4.1 feeds the second, not converts the
first. Resolves the promotion-threshold, dict_hash-baseline, parity, and
new-loop questions on that basis; states an explicit one-governor-per-build
rule mirroring §17.3. The per-dispatch transfer rule and its cost remain
open and still block item 4.1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:35:05 -04:00
Robert Allan JamesandClaude Sonnet 5 6d3d6e5ea6 FABRIC.md: draft §17.7 -- word-level heat conservation, LEANING, blocks item 4.1
Captain Bob rejected the simple q48_from_u64() scale for converting
execution_heat into the Stadium's heat field, wanting real conservation
for word heat mirroring §19.1's fleet invariant. That conflicts with the
existing absolute promotion threshold (execution_heat > 50) and moves
dict_hash, both load-bearing. Paper only -- item 4.1 does not resume
until this section reads DECIDED.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:32:27 -04:00
Robert Allan JamesandClaude Sonnet 5 9b305a5be7 starkernel: item 3.8 -- VM identifiers as UUID/GUID
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>
2026-08-04 19:50:34 -04:00
Robert Allan JamesandClaude Sonnet 5 ec2c97ef70 FABRIC.md: resolve §17.3 -- words onto the Stadium, hosted/kernel split
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>
2026-08-04 18:13:54 -04:00
Robert Allan JamesandClaude Sonnet 5 e55111c2c5 starkernel: item 3.7 -- per-VM free lists (Phase 3 core complete, for real)
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>
2026-08-04 18:09:34 -04:00
Robert Allan JamesandClaude Sonnet 5 72487e7fff starkernel: item 3.6 -- Hera as patron zero, pinned (Phase 3 core complete)
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>
2026-08-04 17:36:23 -04:00
Robert Allan JamesandClaude Sonnet 5 f8a50561b0 starkernel: item 3.5 -- admission and eviction
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>
2026-08-04 17:28:38 -04:00
Robert Allan JamesandClaude Sonnet 5 0b47c256fc starkernel: item 3.4 -- density ranking
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>
2026-08-04 17:16:46 -04:00
Robert Allan JamesandClaude Sonnet 5 378d688898 starkernel: item 3.3 -- behaviour enumeration and dispatch
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>
2026-08-04 17:06:14 -04:00
Robert Allan JamesandClaude Sonnet 5 eb0fd4fffa starkernel: item 3.2 -- Stadium boot-time allocation
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>
2026-08-04 16:48:13 -04:00
Robert Allan JamesandClaude Sonnet 5 1b2f0677de starkernel: item 3.1 reopened -- two Kconfig symbols items 1.1/1.4 deferred here
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>
2026-08-04 15:15:59 -04:00
Robert Allan JamesandClaude Sonnet 5 6da9373e74 FABRIC.md: note-to-self on the fleet heat truncation leak before 5.1
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>
2026-08-04 14:59:08 -04:00
Robert Allan JamesandClaude Sonnet 5 d55ec3241b starkernel: item 3.1 -- the Stadium cell and header
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>
2026-08-04 14:55:30 -04:00
Robert Allan JamesandClaude Sonnet 5 05c9eba360 FABRIC.md: log taxonomy/lexicon as reported-not-scheduled
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>
2026-08-04 14:38:35 -04:00
Robert Allan JamesandClaude Sonnet 5 b0416794de starkernel: item 2.2 complete -- bound the VM registry
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>
2026-08-04 13:54:22 -04:00
Robert Allan JamesandClaude Sonnet 5 5572e5e429 WIP: item 2.2 -- bound the VM registry (not complete, do not check off)
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>
2026-08-04 13:30:06 -04:00
Robert Allan JamesandClaude Sonnet 5 542d7dbf0d starkernel: restate VM fleet heat transfer on the virtual tick
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>
2026-08-04 13:15:50 -04:00
Robert Allan JamesandClaude Sonnet 5 f2eb877691 FABRIC.md: resolve items 1.8, 1.9, 1.10, 1.12
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>
2026-08-04 10:42:33 -04:00
Robert Allan JamesandClaude Sonnet 5 4fd9420ecf FABRIC.md: resolve item 1.7 -- VM-Stadium nesting depth is Kconfig-tunable, default 2
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>
2026-08-04 10:35:36 -04:00
Robert Allan JamesandClaude Sonnet 5 015333e8b8 FABRIC.md: resolve item 1.6 -- a VM's mass is its allocated share
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>
2026-08-04 10:21:15 -04:00
Robert Allan JamesandClaude Sonnet 5 729bfd8bda FABRIC.md: resolve item 1.5 -- outer VM bound is 4, tunable, birth refused at cap
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>
2026-08-04 09:32:08 -04:00
Robert Allan JamesandClaude Sonnet 5 69f71035be FABRIC.md: resolve item 1.4 -- capacity-tick ratio is 1000:1
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>
2026-08-04 09:17:08 -04:00
Robert Allan JamesandClaude Sonnet 5 e83b404f66 FABRIC.md: resolve item 1.3 -- capacity-transfer trigger is comparison, not threshold
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>
2026-08-04 08:16:04 -04:00
Robert Allan JamesandClaude Sonnet 5 9f8bd2a13d FABRIC.md: resolve item 1.2 -- resting floor is derived, not tuned
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>
2026-08-04 08:07:23 -04:00
Robert Allan JamesandClaude Sonnet 5 f81a53cbf6 FABRIC.md: resolve item 1.1 -- containment, not a lock
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>
2026-08-04 08:02:52 -04:00
Robert Allan James daf8f82f3b Add boot log from RISC-V QEMU run (20260804-001614)
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>
2026-08-04 07:28:30 -04:00
Robert Allan JamesandClaude Sonnet 5 024f8a84b5 starkernel: Phase 0 acceptance -- three-arch boot + reproducibility (item 0.10)
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>
2026-08-04 00:21:23 -04:00
Robert Allan JamesandClaude Sonnet 5 bfc0cb9bfd starkernel: document the mutex stub's concurrency constraint (item 0.9)
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>
2026-08-04 00:12:56 -04:00
Robert Allan JamesandClaude Sonnet 5 3699be964d starkernel: converge the tick path and wire the adaptive heartbeat (item 0.8)
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>
2026-08-04 00:01:48 -04:00
Robert Allan JamesandClaude Sonnet 5 da4cb14702 FABRIC.md §26: rule the physical heartbeat must be adaptive, not fixed-rate
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>
2026-08-03 23:04:09 -04:00
Robert Allan JamesandClaude Sonnet 5 43aa0a2a36 aarch64: arm the ARM Generic Timer -- CNTP/CNTHP TVAL+CTL, per-tick re-arm
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>
2026-08-03 22:14:52 -04:00
Robert Allan JamesandClaude Sonnet 5 cabb0e8bd4 aarch64: minimal GICv2 driver -- distributor, CPU interface, timer PPI
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>
2026-08-03 19:48:56 -04:00
Robert Allan JamesandClaude Sonnet 5 8d8f3aaae2 aarch64: split irq_spx into a real save/dispatch/restore/ERET trampoline
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>
2026-08-03 18:47:29 -04:00