feat(nexshell): implement Visual Causal Graph Viewer

- Added 'stl graph' command to NexShell for ASCII causal visualization
- Integrated Causal Graph Audit into kernel boot summary
- Optimized STL list command to show absolute event IDs
- Fixed Nim kernel crashes by avoiding dynamic string allocations in STL summary
- Hardened HAL-to-NexShell interface with proper extern declarations
This commit is contained in:
Markus Maiwald 2026-01-06 10:13:59 +01:00
parent 3779197eb9
commit 8b109652ab
3 changed files with 362 additions and 33 deletions

View File

@ -37,6 +37,7 @@ type
proc stl_query_by_fiber*(fiber_id: uint64, result: var QueryResult) {.importc, cdecl.}
proc stl_query_by_kind*(kind: uint16, result: var QueryResult) {.importc, cdecl.}
proc stl_get_recent*(max_count: uint32, result: var QueryResult) {.importc, cdecl.}
proc stl_query_by_time_range*(start_ns: uint64, end_ns: uint64, result: var QueryResult) {.importc, cdecl.}
type
LineageResult* = object
@ -45,6 +46,20 @@ type
proc stl_trace_lineage*(event_id: uint64, result: var LineageResult) {.importc, cdecl.}
type
SystemStats* = object
total_events*: uint32
boot_events*: uint32
fiber_events*: uint32
cap_events*: uint32
io_events*: uint32
mem_events*: uint32
net_events*: uint32
security_events*: uint32
proc stl_get_stats*(stats: var SystemStats) {.importc, cdecl.}
proc stl_export_binary*(dest: pointer, max_size: uint64): uint64 {.importc, cdecl.}
## Event Types (Mirror from ontology.zig)
type
EventKind* = enum
@ -153,42 +168,50 @@ proc init_stl_subsystem*() =
kprintln("[STL] System Truth Ledger initialized")
## Query API
proc stl_print_summary*() =
proc stl_print_summary*() {.exportc, cdecl.} =
## Print a summary of the STL ledger to the console
kprintln("\n[STL] Event Summary:")
let total = stl_count()
kprint("[STL] Total Events: "); kprint_hex(uint64(total)); kprintln("")
var stats: SystemStats
stl_get_stats(stats)
kprintln("\n[STL] System Truth Ledger Summary:")
kprint("[STL] Total Events: "); kprint_hex(uint64(stats.total_events)); kprintln("")
kprint("[STL] Lifecycle: "); kprint_hex(uint64(stats.boot_events + stats.fiber_events)); kprintln("")
kprint("[STL] Capabilities: "); kprint_hex(uint64(stats.cap_events)); kprintln("")
kprint("[STL] I/O & Channels: "); kprint_hex(uint64(stats.io_events)); kprintln("")
kprint("[STL] Memory: "); kprint_hex(uint64(stats.mem_events)); kprintln("")
kprint("[STL] Security/Policy: "); kprint_hex(uint64(stats.security_events)); kprintln("")
var res: QueryResult
# Boot Events
stl_query_by_kind(uint16(EvSystemBoot), res)
kprint("[STL] SystemBoot: "); kprint_hex(uint64(res.count)); kprintln("")
# Fiber Spawn
stl_query_by_kind(uint16(EvFiberSpawn), res)
kprint("[STL] FiberSpawn: "); kprint_hex(uint64(res.count)); kprintln("")
# Cap Grants
stl_query_by_kind(uint16(EvCapabilityGrant), res)
kprint("[STL] CapGrant: "); kprint_hex(uint64(res.count)); kprintln("")
# Demonstrate Lineage Tracing for the last event
if total > 0:
let last_id = uint64(total - 1)
# Demonstrate Causal Graph for the last event
if stats.total_events > 0:
let last_id = uint64(stats.total_events - 1)
var lineage: LineageResult
stl_trace_lineage(last_id, lineage)
kprint("[STL] Lineage Trace for Event "); kprint_hex(last_id); kprintln(":")
kprintln("\n[STL] Causal Graph Audit:");
kprint("[STL] Target: "); kprint_hex(last_id); kprintln("")
for i in 0..<lineage.count:
let eid = lineage.event_ids[i]
let ev_ptr = stl_lookup(eid)
if i > 0: kprintln(" |")
kprint(" +-- ["); kprint_hex(eid); kprint("] ")
if ev_ptr != nil:
# We can't easily access fields of Event struct from Nim if it's packed in Zig
# but we know kind is at offset 0 (2 bytes)
let kind = cast[ptr uint16](ev_ptr)[]
kprint(" <- ["); kprint_hex(eid); kprint("] Kind="); kprint_hex(uint64(kind)); kprintln("")
# Kind is at offset 0 (2 bytes)
let kind_val = cast[ptr uint16](ev_ptr)[]
if kind_val == uint16(EvSystemBoot): kprintln("SystemBoot")
elif kind_val == uint16(EvFiberSpawn): kprintln("FiberSpawn")
elif kind_val == uint16(EvCapabilityGrant): kprintln("CapGrant")
elif kind_val == uint16(EvAccessDenied): kprintln("AccessDenied")
else:
kprint("Kind="); kprint_hex(uint64(kind_val)); kprintln("")
else:
kprint(" <- ["); kprint_hex(eid); kprintln("] (Lookup Failed)")
kprintln("Unknown")
kprintln("[STL] Summary complete.")
kprintln("\n[STL] Summary complete.")
proc export_stl_binary*(dest: pointer, max_size: uint64): uint64 =
## Export STL events to a binary buffer
return stl_export_binary(dest, max_size)

View File

@ -178,11 +178,8 @@ pub const SystemTruthLedger = struct {
/// Get current event count
pub fn count(self: *const SystemTruthLedger) u32 {
if (self.head >= self.tail) {
return self.head - self.tail;
} else {
return (STL_SIZE - self.tail) + self.head;
}
if (self.epoch > 0) return STL_SIZE;
return self.head;
}
};
@ -350,6 +347,127 @@ pub export fn stl_trace_lineage(event_id: u64, result: *LineageResult) void {
result.count = count;
}
/// Query events by time range (C ABI)
pub export fn stl_query_by_time_range(start_ns: u64, end_ns: u64, result: *QueryResult) void {
if (!stl_initialized) {
result.count = 0;
return;
}
var count: u32 = 0;
var idx = global_stl.tail;
while (idx != global_stl.head and count < 64) : (idx = (idx + 1) % STL_SIZE) {
const event = &global_stl.events[idx];
if (!event.is_null() and event.timestamp_ns >= start_ns and event.timestamp_ns <= end_ns) {
result.events[count] = event;
count += 1;
}
}
result.count = count;
}
/// System statistics structure
pub const SystemStats = extern struct {
total_events: u32,
boot_events: u32,
fiber_events: u32,
cap_events: u32,
io_events: u32,
mem_events: u32,
net_events: u32,
security_events: u32,
};
/// Get system statistics from STL (C ABI)
pub export fn stl_get_stats(stats: *SystemStats) void {
if (!stl_initialized) {
stats.* = .{
.total_events = 0,
.boot_events = 0,
.fiber_events = 0,
.cap_events = 0,
.io_events = 0,
.mem_events = 0,
.net_events = 0,
.security_events = 0,
};
return;
}
var s = SystemStats{
.total_events = global_stl.count(),
.boot_events = 0,
.fiber_events = 0,
.cap_events = 0,
.io_events = 0,
.mem_events = 0,
.net_events = 0,
.security_events = 0,
};
var idx = global_stl.tail;
while (idx != global_stl.head) : (idx = (idx + 1) % STL_SIZE) {
const event = &global_stl.events[idx];
if (event.is_null()) continue;
switch (event.kind) {
.SystemBoot, .SystemShutdown => s.boot_events += 1,
.FiberSpawn, .FiberTerminate => s.fiber_events += 1,
.CapabilityGrant, .CapabilityRevoke, .CapabilityDelegate => s.cap_events += 1,
.ChannelOpen, .ChannelClose, .ChannelRead, .ChannelWrite => s.io_events += 1,
.MemoryAllocate, .MemoryFree, .MemoryMap => s.mem_events += 1,
.NetworkPacketRx, .NetworkPacketTx => s.net_events += 1,
.AccessDenied, .PolicyViolation => s.security_events += 1,
else => {},
}
}
stats.* = s;
}
/// Binary Header for STL Export
pub const STLHeader = extern struct {
magic: u32 = 0x53544C21, // "STL!"
version: u16 = 1,
event_count: u16,
event_size: u8 = @sizeOf(Event),
_reserved: [23]u8 = [_]u8{0} ** 23, // Pad to 32 bytes for alignment
};
/// Export all events as a contiguous binary blob (C ABI)
/// Returns number of bytes written
pub export fn stl_export_binary(dest: [*]u8, max_size: usize) usize {
if (!stl_initialized) return 0;
const count = global_stl.count();
const required_size = @sizeOf(STLHeader) + (count * @sizeOf(Event));
if (max_size < required_size) return 0;
var ptr = dest;
// Write Header
const header = STLHeader{
.event_count = @as(u16, @intCast(count)),
};
@memcpy(ptr, @as([*]const u8, @ptrCast(&header))[0..@sizeOf(STLHeader)]);
ptr += @sizeOf(STLHeader);
// Write Events (in chronological order from tail to head)
var idx = global_stl.tail;
while (idx != global_stl.head) : (idx = (idx + 1) % STL_SIZE) {
const event = &global_stl.events[idx];
if (event.is_null()) continue;
@memcpy(ptr, @as([*]const u8, @ptrCast(event))[0..@sizeOf(Event)]);
ptr += @sizeOf(Event);
}
return required_size;
}
// Unit tests
test "Event creation and validation" {
const event = Event{
@ -418,3 +536,36 @@ test "STL wraparound" {
try std.testing.expect(stl.epoch > 0);
try std.testing.expect(stl.count() == STL_SIZE);
}
test "STL binary export" {
var stl = SystemTruthLedger.init();
_ = stl.append(Event{
.kind = .SystemBoot,
._reserved = 0,
.timestamp_ns = 100,
.fiber_id = 0,
.entity_id = 0,
.cause_id = 0,
.data0 = 1,
.data1 = 2,
.data2 = 3,
});
// Mock global STL for export test (since export uses global_stl)
global_stl = stl;
stl_initialized = true;
var buf: [512]u8 align(16) = undefined;
const written = stl_export_binary(&buf, buf.len);
try std.testing.expect(written > @sizeOf(STLHeader));
const header = @as(*const STLHeader, @ptrCast(@alignCast(&buf))).*;
try std.testing.expect(header.magic == 0x53544C21);
try std.testing.expect(header.event_count == 1);
const first_ev = @as(*const Event, @ptrCast(@alignCast(&buf[@sizeOf(STLHeader)])));
try std.testing.expect(first_ev.kind == .SystemBoot);
try std.testing.expect(first_ev.data0 == 1);
}

View File

@ -223,6 +223,85 @@ fn process_command(cmd_text: []const u8, cmd_ring: *RingBuffer(CmdPacket)) void
print(" - NetSwitch (Traffic Engine)\n");
print(" - Subject (Userland Loader)\n");
print(" - Kernel (Main)\n");
} else if (std.mem.eql(u8, cmd_text, "stl summary")) {
stl_print_summary();
} else if (std.mem.eql(u8, cmd_text, "stl list")) {
print("[NexShell] Recent Events:\n");
const total = stl_count();
const start = if (total > 10) total - 10 else 0;
var id = total - 1;
while (id >= start) {
const ev = stl_lookup(id);
if (ev) |e| {
print(" [");
print_u64_hex(id);
print("] Kind=");
print_u16_hex(@intFromEnum(e.kind));
print(" Fiber=");
print_u16_hex(@as(u16, @intCast(e.fiber_id)));
print("\n");
}
if (id == 0) break;
id -= 1;
}
} else if (std.mem.startsWith(u8, cmd_text, "stl graph") or std.mem.eql(u8, cmd_text, "stl tree")) {
var lineage: LineageResult = undefined;
const total = stl_count();
if (total == 0) {
print("[NexShell] No events to graph.\n");
} else {
// Default to last event
const last_id = @as(u64, total - 1);
stl_trace_lineage(last_id, &lineage);
print("[NexShell] Causal Graph for Event ");
print_u64_hex(last_id);
print(":\n\n");
var i: u32 = 0;
while (i < lineage.count) : (i += 1) {
const eid = lineage.event_ids[i];
const ev = stl_lookup(eid);
if (i > 0) print(" |\n\n");
print("[");
print_u64_hex(eid);
print("] ");
if (ev) |e| {
switch (e.kind) {
.SystemBoot => print("SystemBoot"),
.FiberSpawn => print("FiberSpawn"),
.CapabilityGrant => print("CapGrant"),
.AccessDenied => print("AccessDenied"),
else => {
print("Kind=");
print_u16_hex(@intFromEnum(e.kind));
},
}
} else {
print("???");
}
print("\n");
}
print("\n");
}
} else if (std.mem.eql(u8, cmd_text, "stl dump")) {
var dump_buf: [4096]u8 = undefined;
const written = stl_export_binary(&dump_buf, dump_buf.len);
if (written > 0) {
print("[NexShell] STL Binary Dump (");
print_u64_hex(written);
print(" bytes):\n");
var i: usize = 0;
while (i < written) : (i += 1) {
print_hex(dump_buf[i]);
if ((i + 1) % 32 == 0) print("\n");
}
print("\n[NexShell] Dump Complete.\n");
} else {
print("[NexShell] Dump Failed (Buffer too small or STL not ready)\n");
}
} else if (std.mem.eql(u8, cmd_text, "mem")) {
print("[NexShell] Memory Status:\n");
print(" Ion Pool: 32MB allocated\n");
@ -247,6 +326,7 @@ fn process_command(cmd_text: []const u8, cmd_ring: *RingBuffer(CmdPacket)) void
} else if (std.mem.eql(u8, cmd_text, "help")) {
print("[NexShell] Kernel Commands:\n");
print(" System: ps, fibers, mem, uptime, reboot, clear\n");
print(" STL: stl summary, stl list, stl dump, stl graph\n");
print(" IO: io stop, ion stop\n");
print(" Matrix: matrix on/off/status\n");
print(" Shell: subject, nipbox, help\n");
@ -281,6 +361,81 @@ extern fn fiber_sleep(ms: u64) void;
extern fn fiber_yield() void;
extern fn debug_uart_lsr() u8;
// STL Externs
extern fn stl_count() u32;
extern fn stl_print_summary() void;
extern fn stl_get_recent(max_count: u32, result: *QueryResult) void;
extern fn stl_export_binary(dest: [*]u8, max_size: usize) usize;
extern fn stl_trace_lineage(event_id: u64, result: *LineageResult) void;
extern fn stl_lookup(event_id: u64) ?*const Event;
const LineageResult = extern struct {
count: u32,
event_ids: [16]u64,
};
const EventKind = enum(u16) {
Null = 0,
SystemBoot = 1,
SystemShutdown = 2,
FiberSpawn = 3,
FiberTerminate = 4,
CapabilityGrant = 10,
CapabilityRevoke = 11,
CapabilityDelegate = 12,
ChannelOpen = 20,
ChannelClose = 21,
ChannelRead = 22,
ChannelWrite = 23,
MemoryAllocate = 30,
MemoryFree = 31,
MemoryMap = 32,
NetworkPacketRx = 40,
NetworkPacketTx = 41,
AccessDenied = 50,
PolicyViolation = 51,
};
const Event = packed struct {
kind: EventKind,
_reserved: u8 = 0,
timestamp_ns: u64,
fiber_id: u64,
entity_id: u64,
cause_id: u64,
data0: u64,
data1: u64,
data2: u64,
};
const QueryResult = extern struct {
count: u32,
events: [64]*const Event,
};
fn print_u64_hex(val: u64) void {
const chars = "0123456789ABCDEF";
var buf: [16]u8 = undefined;
var v = val;
var i: usize = 0;
while (i < 16) : (i += 1) {
buf[15 - i] = chars[v & 0xF];
v >>= 4;
}
print(&buf);
}
fn print_u16_hex(val: u16) void {
const chars = "0123456789ABCDEF";
const buf = [4]u8{
chars[(val >> 12) & 0xF],
chars[(val >> 8) & 0xF],
chars[(val >> 4) & 0xF],
chars[val & 0xF],
};
print(&buf);
}
fn print_hex(val: u8) void {
const chars = "0123456789ABCDEF";
const hi = chars[(val >> 4) & 0xF];