From 6f4ee2fb026c4c9b0890a817a04f1bb0fa3e518c Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 10:47:49 +0700 Subject: [PATCH 01/13] fix(brain): EventBus.poll applies max_events after the since filter poll(since, allocator, max_events) scanned only the first max_events buffered events and then filtered them by `since`. Whenever those oldest events were at or before `since`, it returned nothing even though newer events matched: with three old events and two new ones, poll(boundary, a, 1) returned 0 events instead of 1. The scan now walks the whole buffer and stops once max_events events have passed the filter, which is what the doc comment already promised ("Returns events with timestamp > since ... Limited by max_events"). poll(0, a, n) is unchanged. Found by the new `tri stress --health` probe limit-after-filter. A module test pins it: it fails on the old code (0 events) and passes on this one. Refs #1326 Co-Authored-By: Claude Opus 5.5 --- src/brain/reticular_formation.zig | 38 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/brain/reticular_formation.zig b/src/brain/reticular_formation.zig index 6b0f1d6f20..331a8df32b 100644 --- a/src/brain/reticular_formation.zig +++ b/src/brain/reticular_formation.zig @@ -449,27 +449,24 @@ pub const EventBus = struct { pub fn poll(self: *EventBus, since: i64, allocator: std.mem.Allocator, max_events: usize) ![]AgentEventRecord { self.mutex.lock(); - // First pass: count matching events for exact capacity - const limit = if (max_events == 0) self.count else @min(self.count, max_events); + // First pass: count matching events for exact capacity. + // `max_events` caps the events that pass the `since` filter, so the + // scan covers the whole buffer and stops only once the cap is reached. + const cap = if (max_events == 0) self.count else max_events; var match_count: usize = 0; var i: usize = 0; - while (i < limit) : (i += 1) { + while (i < self.count and match_count < cap) : (i += 1) { const idx = (self.head_idx + i) % MAX_EVENTS; - if (self.buffer[idx].timestamp > since) { - match_count += 1; - if (max_events > 0 and match_count >= max_events) break; - } + if (self.buffer[idx].timestamp > since) match_count += 1; } // Allocate with exact capacity var results = try std.ArrayList(AgentEventRecord).initCapacity(allocator, match_count); errdefer results.deinit(allocator); - // Second pass: collect events + // Second pass: collect the same `match_count` events, oldest first i = 0; - while (i < limit) : (i += 1) { - if (max_events > 0 and results.items.len >= max_events) break; - + while (i < self.count and results.items.len < match_count) : (i += 1) { const idx = (self.head_idx + i) % MAX_EVENTS; const stored = &self.buffer[idx]; @@ -812,6 +809,25 @@ test "EventBus poll with max_events limit" { try std.testing.expectEqual(@as(usize, 2), events.len); } +test "EventBus poll applies max_events after the since filter" { + const allocator = std.testing.allocator; + var bus = EventBus.init(allocator); + defer bus.deinit(); + + // Three events at or before `boundary`, then two strictly after it. + for (0..3) |_| try bus.publish(.agent_spawned, .{ .agent_spawned = .{ .agent_id = "old" } }); + const boundary = std.time.milliTimestamp(); + while (std.time.milliTimestamp() <= boundary) std.Thread.sleep(std.time.ns_per_ms / 4); + for (0..2) |_| try bus.publish(.agent_spawned, .{ .agent_spawned = .{ .agent_id = "new" } }); + + // Two events match; the cap of 1 must return the older of those two, + // not stop after scanning the first (non-matching) buffered event. + const events = try bus.poll(boundary, allocator, 1); + defer allocator.free(events); + try std.testing.expectEqual(@as(usize, 1), events.len); + try std.testing.expectEqualStrings("new", events[0].data.agent_spawned.agent_id); +} + test "EventBus poll returns empty when no events" { const allocator = std.testing.allocator; var bus = EventBus.init(allocator); From 3a271708388a40d4a34bbdc1a5aad1ed595b10cf Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 11:09:06 +0700 Subject: [PATCH 02/13] feat(tri): tri stress --health runs brain self-check probes `tri stress --health` was a stub that printed "not implemented yet" and no Score: line, so the brain-ci Brain Health Check failed on every run as "not measured". It now runs 17 probes against the three brain regions linked into tri: basal_ganglia (8: claim, refuse, heartbeat, complete, abandon, TTL expiry, shard accounting, an 8-thread one-winner-per-task race), reticular_formation (5: string ownership, FIFO order, since filter, limit-after-filter, bounded ring) and locus_coeruleus (4: exponential, capped and monotone, linear and constant, jitter bounds). Each probe uses fresh private instances (never the process globals) and its own leak-checking allocator; a leak fails the probe. The output lists every probe as PASS or FAIL with its claim, then one plain-ASCII line Score: /100 ( of probes passed) where n = 100 * passed / total, then "Status: HEALTHY" or "Status: UNHEALTHY". The command exits 1 if any probe failed. Nothing in the score is a constant: removing the basal_ganglia live-claim check, the locus_coeruleus cap or the poll fix drops it to 88.2, 82.4 and 94.1. Not covered: other src/brain regions (not linked into tri), load and soak behaviour. The other stress modes still only print a notice. Refs #1326 Co-Authored-By: Claude Opus 5.5 --- src/tri/brain_health.zig | 541 +++++++++++++++++++++++++++++++++++++++ src/tri/tri_commands.zig | 15 +- 2 files changed, 553 insertions(+), 3 deletions(-) create mode 100644 src/tri/brain_health.zig diff --git a/src/tri/brain_health.zig b/src/tri/brain_health.zig new file mode 100644 index 0000000000..e2b384e51e --- /dev/null +++ b/src/tri/brain_health.zig @@ -0,0 +1,541 @@ +//! `tri stress --health` -- brain self-check probes. +//! +//! WHAT THE SCORE MEANS +//! +//! Score = 100 * passed / total, over the probes in `probes` below and +//! nothing else. There is no weighting, no baseline and no constant: a +//! probe passes only if every assertion inside it held on this run. +//! +//! Each probe drives real behaviour of one of the three brain regions that +//! are linked into `tri` (see build.zig, `tri` module imports): +//! +//! basal_ganglia task claim registry (claim, heartbeat, complete, +//! abandon, TTL expiry, shard accounting, races) +//! reticular_formation event bus (ownership of strings, FIFO order, +//! `since` filter, `max_events` limit, bounded ring) +//! locus_coeruleus retry backoff (exponential, cap, linear, constant, +//! jitter bounds) +//! +//! Probes build fresh private instances. They never touch the process-wide +//! singletons (`getGlobal`), so the score does not depend on what else the +//! process has done. Every probe runs on its own leak-checking allocator +//! (`std.heap.DebugAllocator`); a probe that leaks is counted as failed even +//! if all its assertions held. +//! +//! WHAT IT DOES NOT COVER +//! +//! - The other regions under src/brain/ (they are not linked into `tri`, so +//! a probe here could only test a copy, not the shipped code). +//! - Load, soak or throughput behaviour. The one concurrent probe checks +//! correctness of a claim race, not speed. +//! +//! OUTPUT CONTRACT (read by .github/workflows/brain-ci.yml) +//! +//! Exactly one line starts with `Score: `, is plain ASCII with no colour +//! codes, and has the form `Score: /100 ( of probes passed)`. +//! The last line is `Status: HEALTHY` when every probe passed and +//! `Status: UNHEALTHY` otherwise. The process exits 1 when any probe failed. + +const std = @import("std"); +const basal_ganglia = @import("basal_ganglia"); +const reticular_formation = @import("reticular_formation"); +const locus_coeruleus = @import("locus_coeruleus"); + +const Registry = basal_ganglia.Registry; +const EventBus = reticular_formation.EventBus; +const BackoffPolicy = locus_coeruleus.BackoffPolicy; + +const Probe = struct { + region: []const u8, + name: []const u8, + /// One line: the property the probe asserts. + claim: []const u8, + run: *const fn (std.mem.Allocator) anyerror!void, +}; + +/// The complete list of probes. The score is computed over this list only. +const probes = [_]Probe{ + .{ .region = "basal_ganglia", .name = "claim-free-task", .claim = "an unclaimed task can be claimed and reads back as owned by that agent", .run = &bgClaimFreeTask }, + .{ .region = "basal_ganglia", .name = "refuse-second-claimant", .claim = "a live claim refuses a second agent and counts the conflict", .run = &bgRefuseSecondClaimant }, + .{ .region = "basal_ganglia", .name = "heartbeat-owner-only", .claim = "only the owner can heartbeat a claim; unknown tasks cannot be heartbeated", .run = &bgHeartbeatOwnerOnly }, + .{ .region = "basal_ganglia", .name = "complete-releases", .claim = "completion is owner-only and frees the task for another agent", .run = &bgCompleteReleases }, + .{ .region = "basal_ganglia", .name = "abandon-releases", .claim = "abandoning is owner-only and frees the task for another agent", .run = &bgAbandonReleases }, + .{ .region = "basal_ganglia", .name = "ttl-expiry", .claim = "a claim past its TTL is invalid and cleanupExpired removes only it", .run = &bgTtlExpiry }, + .{ .region = "basal_ganglia", .name = "shard-accounting", .claim = "64 claims are all visible via count, getShardStats and listClaims", .run = &bgShardAccounting }, + .{ .region = "basal_ganglia", .name = "one-winner-per-task", .claim = "8 threads racing for 16 tasks yield exactly one winner per task", .run = &bgOneWinnerPerTask }, + .{ .region = "reticular_formation", .name = "owns-published-strings", .claim = "an event polls back intact after the publisher's buffers are overwritten", .run = &rfOwnsPublishedStrings }, + .{ .region = "reticular_formation", .name = "fifo-order", .claim = "publish and publishBatch events poll back oldest-first in order", .run = &rfFifoOrder }, + .{ .region = "reticular_formation", .name = "since-filter", .claim = "poll(since) returns only events stamped after since", .run = &rfSinceFilter }, + .{ .region = "reticular_formation", .name = "limit-after-filter", .claim = "max_events caps the events that pass the since filter, not the events scanned", .run = &rfLimitAfterFilter }, + .{ .region = "reticular_formation", .name = "bounded-ring", .claim = "capacity+7 publishes keep capacity events, evict the 7 oldest, count them", .run = &rfBoundedRing }, + .{ .region = "locus_coeruleus", .name = "exponential-doubling", .claim = "default policy waits 1000 ms, doubles per attempt, stops at 60000 ms", .run = &lcExponentialDoubling }, + .{ .region = "locus_coeruleus", .name = "capped-and-monotone", .claim = "delays never decrease or exceed max_ms over attempts 0..63 (table and computed paths)", .run = &lcCappedAndMonotone }, + .{ .region = "locus_coeruleus", .name = "linear-and-constant", .claim = "linear adds linear_increment per attempt, constant never changes, both capped", .run = &lcLinearAndConstant }, + .{ .region = "locus_coeruleus", .name = "jitter-bounds", .claim = "uniform jitter stays in [base, 2*base); phi jitter is base*0.618 or base*1.618", .run = &lcJitterBounds }, +}; + +const regions = [_][]const u8{ "basal_ganglia", "reticular_formation", "locus_coeruleus" }; + +// --------------------------------------------------------------------------- +// Runner +// --------------------------------------------------------------------------- + +var note_buf: [256]u8 = undefined; +var note: []const u8 = ""; + +/// Fails the current probe with a note explaining what was observed. +fn check(ok: bool, comptime fmt: []const u8, args: anytype) error{ProbeFailed}!void { + if (ok) return; + note = std.fmt.bufPrint(¬e_buf, fmt, args) catch "(failure note too long)"; + return error.ProbeFailed; +} + +const Outcome = struct { + passed: bool, + detail: []const u8, +}; + +fn runProbe(p: Probe, detail_buf: []u8) Outcome { + note = ""; + var dbg: std.heap.DebugAllocator(.{}) = .init; + const result = p.run(dbg.allocator()); + const leaked = dbg.deinit() == .leak; + + if (result) |_| { + if (leaked) return .{ .passed = false, .detail = "leaked memory (see allocator report on stderr)" }; + return .{ .passed = true, .detail = "" }; + } else |err| { + const detail = if (err == error.ProbeFailed) + note + else + std.fmt.bufPrint(detail_buf, "error.{s}", .{@errorName(err)}) catch "error"; + return .{ .passed = false, .detail = detail }; + } +} + +/// Runs every probe, prints the report and exits 1 if any probe failed. +pub fn run() !void { + var out_buf: [4096]u8 = undefined; + var file_writer = std.fs.File.stdout().writer(&out_buf); + const out = &file_writer.interface; + + try out.print( + \\Brain health -- tri stress --health + \\Probes drive the brain regions linked into tri, on fresh private instances, + \\each on its own leak-checking allocator (a leak fails the probe). + \\The score is 100 * passed / total over the probes listed below. + \\ + \\ + , .{}); + try out.flush(); + + var passed: usize = 0; + var region_passed = [_]usize{0} ** regions.len; + var region_total = [_]usize{0} ** regions.len; + + for (probes) |p| { + var detail_buf: [128]u8 = undefined; + const outcome = runProbe(p, &detail_buf); + if (outcome.passed) passed += 1; + for (regions, 0..) |r, i| { + if (std.mem.eql(u8, r, p.region)) { + region_total[i] += 1; + if (outcome.passed) region_passed[i] += 1; + } + } + try out.print(" {s} {s:<20} {s:<23} {s}\n", .{ + if (outcome.passed) "PASS" else "FAIL", + p.region, + p.name, + p.claim, + }); + if (!outcome.passed) try out.print(" -> {s}\n", .{outcome.detail}); + try out.flush(); + } + + const total = probes.len; + const score = 100.0 * @as(f64, @floatFromInt(passed)) / @as(f64, @floatFromInt(total)); + + try out.print("\nPassed: {d}/{d} probes (", .{ passed, total }); + for (regions, 0..) |r, i| { + if (i > 0) try out.print(", ", .{}); + try out.print("{s} {d}/{d}", .{ r, region_passed[i], region_total[i] }); + } + try out.print(")\n", .{}); + try out.print("Not covered: other src/brain regions (not linked into tri); load and soak behaviour.\n", .{}); + try out.print("Score: {d:.1}/100 ({d} of {d} probes passed)\n", .{ score, passed, total }); + try out.print("Status: {s}\n", .{if (passed == total) "HEALTHY" else "UNHEALTHY"}); + try out.flush(); + + if (passed != total) std.process.exit(1); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Blocks until the millisecond clock has moved past `t`. +fn waitPastMs(t: i64) void { + while (std.time.milliTimestamp() <= t) std.Thread.sleep(std.time.ns_per_ms / 4); +} + +fn newBus(a: std.mem.Allocator) !*EventBus { + const bus = try a.create(EventBus); + bus.* = EventBus.init(a); + return bus; +} + +fn freeBus(a: std.mem.Allocator, bus: *EventBus) void { + bus.deinit(); + a.destroy(bus); +} + +/// True when `task_id` is listed with `agent_id` as its owner. +fn ownedBy(reg: *Registry, a: std.mem.Allocator, task_id: []const u8, agent_id: []const u8) !bool { + const claims = try reg.listClaims(a); + defer reg.freeClaims(a, claims); + for (claims) |c| { + if (std.mem.eql(u8, c.task_id, task_id)) return std.mem.eql(u8, c.agent_id, agent_id); + } + return false; +} + +// --------------------------------------------------------------------------- +// basal_ganglia probes +// --------------------------------------------------------------------------- + +fn bgClaimFreeTask(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + try check(reg.checkClaim("t1") == .not_found, "fresh registry reports t1 as {s}", .{@tagName(reg.checkClaim("t1"))}); + try check(try reg.claim(a, "t1", "agent-a", 60_000), "claim of a free task returned false", .{}); + try check(reg.checkClaim("t1") == .claimed, "after claim, t1 is {s}", .{@tagName(reg.checkClaim("t1"))}); + try check(try ownedBy(®, a, "t1", "agent-a"), "t1 is not listed as owned by agent-a", .{}); + try check(reg.checkClaim("t-other") == .not_found, "an unclaimed task reads as {s}", .{@tagName(reg.checkClaim("t-other"))}); +} + +fn bgRefuseSecondClaimant(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + try check(try reg.claim(a, "t1", "agent-a", 60_000), "first claim returned false", .{}); + try check(!(try reg.claim(a, "t1", "agent-b", 60_000)), "second agent took a live claim", .{}); + try check(try ownedBy(®, a, "t1", "agent-a"), "after the refused claim t1 is not owned by agent-a", .{}); + const s = reg.getStats(); + try check(s.claim_attempts == 2 and s.claim_success == 1 and s.claim_conflicts == 1, "stats attempts={d} success={d} conflicts={d}, want 2/1/1", .{ s.claim_attempts, s.claim_success, s.claim_conflicts }); +} + +fn bgHeartbeatOwnerOnly(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + try check(try reg.claim(a, "t1", "agent-a", 60_000), "claim returned false", .{}); + try check(!reg.heartbeat("t1", "agent-b"), "a non-owner heartbeat was accepted", .{}); + try check(reg.heartbeat("t1", "agent-a"), "the owner's heartbeat was refused", .{}); + try check(!reg.heartbeat("t-missing", "agent-a"), "a heartbeat on an unknown task was accepted", .{}); + const s = reg.getStats(); + try check(s.heartbeat_calls == 3 and s.heartbeat_success == 1, "heartbeat stats calls={d} success={d}, want 3/1", .{ s.heartbeat_calls, s.heartbeat_success }); +} + +fn bgCompleteReleases(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + try check(try reg.claim(a, "t1", "agent-a", 60_000), "claim returned false", .{}); + try check(!reg.complete("t1", "agent-b"), "a non-owner completed the task", .{}); + try check(reg.complete("t1", "agent-a"), "the owner could not complete the task", .{}); + try check(reg.checkClaim("t1") == .expired, "after completion t1 is {s}, want expired", .{@tagName(reg.checkClaim("t1"))}); + try check(!reg.complete("t1", "agent-a"), "a completed task was completed twice", .{}); + try check(try reg.claim(a, "t1", "agent-b", 60_000), "another agent could not claim a completed task", .{}); + try check(reg.checkClaim("t1") == .claimed, "after re-claim t1 is {s}", .{@tagName(reg.checkClaim("t1"))}); +} + +fn bgAbandonReleases(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + try check(try reg.claim(a, "t1", "agent-a", 60_000), "claim returned false", .{}); + try check(!reg.abandon("t1", "agent-b"), "a non-owner abandoned the task", .{}); + try check(reg.abandon("t1", "agent-a"), "the owner could not abandon the task", .{}); + try check(reg.checkClaim("t1") == .expired, "after abandon t1 is {s}, want expired", .{@tagName(reg.checkClaim("t1"))}); + try check(try reg.claim(a, "t1", "agent-b", 60_000), "another agent could not claim an abandoned task", .{}); + try check(try ownedBy(®, a, "t1", "agent-b"), "after re-claim t1 is not owned by agent-b", .{}); +} + +fn bgTtlExpiry(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + try check(try reg.claim(a, "short", "agent-a", 1), "claim with ttl 1 ms returned false", .{}); + try check(try reg.claim(a, "long", "agent-a", 60_000), "claim with ttl 60 s returned false", .{}); + const start = std.time.milliTimestamp(); + waitPastMs(start + 3); + try check(reg.checkClaim("short") == .expired, "a claim 3 ms past a 1 ms TTL is {s}", .{@tagName(reg.checkClaim("short"))}); + try check(reg.checkClaim("long") == .claimed, "a 60 s claim is {s} after 3 ms", .{@tagName(reg.checkClaim("long"))}); + const removed = reg.cleanupExpired(); + try check(removed == 1, "cleanupExpired removed {d}, want 1", .{removed}); + try check(reg.count() == 1, "count after cleanup is {d}, want 1", .{reg.count()}); +} + +fn bgShardAccounting(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + const n = 64; + var buf: [32]u8 = undefined; + for (0..n) |i| { + const id = try std.fmt.bufPrint(&buf, "shard-task-{d}", .{i}); + try check(try reg.claim(a, id, "agent-a", 60_000), "claim of {s} returned false", .{id}); + } + try check(reg.count() == n, "count is {d}, want {d}", .{ reg.count(), n }); + + const per_shard = reg.getShardStats(); + var sum: usize = 0; + var used: usize = 0; + for (per_shard) |c| { + sum += c; + if (c > 0) used += 1; + } + try check(sum == n, "shard stats sum to {d}, want {d}", .{ sum, n }); + try check(used > 1, "all {d} claims landed in one shard", .{n}); + + const claims = try reg.listClaims(a); + defer reg.freeClaims(a, claims); + try check(claims.len == n, "listClaims returned {d}, want {d}", .{ claims.len, n }); + for (claims) |c| try check(c.is_valid, "listed claim {s} is not valid", .{c.task_id}); +} + +const race_threads = 8; +const race_tasks = 16; + +const RaceShared = struct { + reg: *Registry, + alloc: std.mem.Allocator, + go: std.atomic.Value(bool) = .init(false), + errored: std.atomic.Value(bool) = .init(false), + wins: [race_tasks]std.atomic.Value(u32) = [_]std.atomic.Value(u32){.init(0)} ** race_tasks, +}; + +fn raceWorker(shared: *RaceShared, agent_index: usize) void { + var agent_buf: [32]u8 = undefined; + const agent = std.fmt.bufPrint(&agent_buf, "racer-{d}", .{agent_index}) catch unreachable; + while (!shared.go.load(.acquire)) std.atomic.spinLoopHint(); + var task_buf: [32]u8 = undefined; + for (0..race_tasks) |k| { + // Each racer walks the tasks from a different start, so claims collide. + const t = (k + agent_index) % race_tasks; + const task = std.fmt.bufPrint(&task_buf, "race-{d}", .{t}) catch unreachable; + const won = shared.reg.claim(shared.alloc, task, agent, 60_000) catch { + shared.errored.store(true, .release); + return; + }; + if (won) _ = shared.wins[t].fetchAdd(1, .acq_rel); + } +} + +fn bgOneWinnerPerTask(a: std.mem.Allocator) !void { + var reg = Registry.init(a); + defer reg.deinit(); + + var shared = RaceShared{ .reg = ®, .alloc = a }; + var threads: [race_threads]std.Thread = undefined; + var spawned: usize = 0; + defer { + shared.go.store(true, .release); + for (threads[0..spawned]) |th| th.join(); + } + for (0..race_threads) |i| { + threads[i] = try std.Thread.spawn(.{}, raceWorker, .{ &shared, i }); + spawned += 1; + } + shared.go.store(true, .release); + for (threads[0..spawned]) |th| th.join(); + spawned = 0; + + try check(!shared.errored.load(.acquire), "a racer got an error from claim()", .{}); + var total: u32 = 0; + for (&shared.wins, 0..) |*w, t| { + const v = w.load(.acquire); + total += v; + try check(v == 1, "task race-{d} had {d} winners, want exactly 1", .{ t, v }); + } + try check(total == race_tasks, "{d} wins over {d} tasks", .{ total, race_tasks }); + try check(reg.count() == race_tasks, "registry holds {d} claims, want {d}", .{ reg.count(), race_tasks }); + const s = reg.getStats(); + const attempts: u64 = race_threads * race_tasks; + try check(s.claim_attempts == attempts and s.claim_success == race_tasks and s.claim_conflicts == attempts - race_tasks, "stats attempts={d} success={d} conflicts={d}, want {d}/{d}/{d}", .{ s.claim_attempts, s.claim_success, s.claim_conflicts, attempts, race_tasks, attempts - race_tasks }); +} + +// --------------------------------------------------------------------------- +// reticular_formation probes +// --------------------------------------------------------------------------- + +fn rfOwnsPublishedStrings(a: std.mem.Allocator) !void { + const bus = try newBus(a); + defer freeBus(a, bus); + + var task_buf = "task-77".*; + var agent_buf = "agent-a".*; + var err_buf = "disk full".*; + try bus.publish(.task_failed, .{ .task_failed = .{ .task_id = &task_buf, .agent_id = &agent_buf, .err_msg = &err_buf } }); + @memset(&task_buf, 'X'); + @memset(&agent_buf, 'X'); + @memset(&err_buf, 'X'); + + const events = try bus.poll(0, a, 0); + defer a.free(events); + try check(events.len == 1, "poll returned {d} events, want 1", .{events.len}); + try check(events[0].event_type == .task_failed, "event type is {s}", .{@tagName(events[0].event_type)}); + const d = events[0].data.task_failed; + try check(std.mem.eql(u8, d.task_id, "task-77"), "task_id reads back as '{s}'", .{d.task_id}); + try check(std.mem.eql(u8, d.agent_id, "agent-a"), "agent_id reads back as '{s}'", .{d.agent_id}); + try check(std.mem.eql(u8, d.err_msg, "disk full"), "err_msg reads back as '{s}'", .{d.err_msg}); +} + +fn rfFifoOrder(a: std.mem.Allocator) !void { + const bus = try newBus(a); + defer freeBus(a, bus); + + for (0..5) |i| try bus.publish(.agent_idle, .{ .agent_idle = .{ .agent_id = "agent-a", .idle_ms = i } }); + var batch: [3]EventBus.BatchEvent = undefined; + for (&batch, 0..) |*b, i| b.* = .{ .event_type = .agent_idle, .data = .{ .agent_idle = .{ .agent_id = "agent-a", .idle_ms = 5 + i } } }; + try bus.publishBatch(&batch); + + const events = try bus.poll(0, a, 0); + defer a.free(events); + try check(events.len == 8, "poll returned {d} events, want 8", .{events.len}); + for (events, 0..) |e, i| { + try check(e.data.agent_idle.idle_ms == i, "position {d} holds event {d}", .{ i, e.data.agent_idle.idle_ms }); + } +} + +/// Publishes 3 "old" events, waits for the clock to tick, publishes 2 "new" +/// ones and returns the timestamp that separates them. +fn publishOldThenNew(bus: *EventBus) !i64 { + for (0..3) |_| try bus.publish(.agent_spawned, .{ .agent_spawned = .{ .agent_id = "old" } }); + const boundary = std.time.milliTimestamp(); + waitPastMs(boundary); + for (0..2) |_| try bus.publish(.agent_spawned, .{ .agent_spawned = .{ .agent_id = "new" } }); + return boundary; +} + +fn rfSinceFilter(a: std.mem.Allocator) !void { + const bus = try newBus(a); + defer freeBus(a, bus); + const boundary = try publishOldThenNew(bus); + + const newer = try bus.poll(boundary, a, 0); + defer a.free(newer); + try check(newer.len == 2, "poll(since=boundary) returned {d} events, want 2", .{newer.len}); + for (newer) |e| try check(std.mem.eql(u8, e.data.agent_spawned.agent_id, "new"), "poll(since) returned an event older than since", .{}); + + const all = try bus.poll(0, a, 0); + defer a.free(all); + try check(all.len == 5, "poll(since=0) returned {d} events, want 5", .{all.len}); + + const none = try bus.poll(std.time.milliTimestamp() + 60_000, a, 0); + defer a.free(none); + try check(none.len == 0, "poll(since=future) returned {d} events, want 0", .{none.len}); +} + +fn rfLimitAfterFilter(a: std.mem.Allocator) !void { + const bus = try newBus(a); + defer freeBus(a, bus); + const boundary = try publishOldThenNew(bus); + + const first_two = try bus.poll(0, a, 2); + defer a.free(first_two); + try check(first_two.len == 2, "poll(since=0, max=2) returned {d} events, want 2", .{first_two.len}); + for (first_two) |e| try check(std.mem.eql(u8, e.data.agent_spawned.agent_id, "old"), "poll(max=2) did not return the two oldest events", .{}); + + const one_new = try bus.poll(boundary, a, 1); + defer a.free(one_new); + try check(one_new.len == 1, "poll(since=boundary, max=1) returned {d} events, want 1 (2 events match)", .{one_new.len}); + try check(std.mem.eql(u8, one_new[0].data.agent_spawned.agent_id, "new"), "poll(since, max=1) returned an event older than since", .{}); +} + +fn rfBoundedRing(a: std.mem.Allocator) !void { + const bus = try newBus(a); + defer freeBus(a, bus); + + const cap = bus.buffer.len; + const extra = 7; + for (0..cap + extra) |i| try bus.publish(.agent_idle, .{ .agent_idle = .{ .agent_id = "agent-a", .idle_ms = i } }); + + const s = bus.getStats(); + try check(s.buffered == cap, "buffered {d}, want capacity {d}", .{ s.buffered, cap }); + try check(s.published == cap + extra, "published {d}, want {d}", .{ s.published, cap + extra }); + try check(s.trim_count == extra, "trim_count {d}, want {d}", .{ s.trim_count, extra }); + try check(s.peak_buffered == cap, "peak_buffered {d}, want {d}", .{ s.peak_buffered, cap }); + + const events = try bus.poll(0, a, 0); + defer a.free(events); + try check(events.len == cap, "poll returned {d} events, want {d}", .{ events.len, cap }); + try check(events[0].data.agent_idle.idle_ms == extra, "oldest kept event is {d}, want {d}", .{ events[0].data.agent_idle.idle_ms, extra }); + try check(events[cap - 1].data.agent_idle.idle_ms == cap + extra - 1, "newest event is {d}, want {d}", .{ events[cap - 1].data.agent_idle.idle_ms, cap + extra - 1 }); +} + +// --------------------------------------------------------------------------- +// locus_coeruleus probes +// --------------------------------------------------------------------------- + +fn lcExponentialDoubling(a: std.mem.Allocator) !void { + _ = a; + const policy = BackoffPolicy.init(); + const want = [_]u64{ 1000, 2000, 4000, 8000, 16000, 32000, 60000, 60000, 60000 }; + for (want, 0..) |w, attempt| { + const got = policy.nextDelay(@intCast(attempt)); + try check(got == w, "attempt {d}: delay {d} ms, want {d} ms", .{ attempt, got, w }); + } +} + +fn expectCappedMonotone(policy: BackoffPolicy, label: []const u8) !void { + var prev: u64 = 0; + for (0..64) |attempt| { + const got = policy.nextDelay(@intCast(attempt)); + try check(got <= policy.max_ms, "{s} attempt {d}: {d} ms exceeds max {d}", .{ label, attempt, got, policy.max_ms }); + try check(got >= prev, "{s} attempt {d}: {d} ms is below previous {d}", .{ label, attempt, got, prev }); + prev = got; + } +} + +fn lcCappedAndMonotone(a: std.mem.Allocator) !void { + _ = a; + try expectCappedMonotone(BackoffPolicy.init(), "default"); + + // Non-default parameters bypass the lookup table and take the computed path. + const custom = BackoffPolicy{ .initial_ms = 250, .multiplier = 3.0, .max_ms = 90_000 }; + try check(custom.nextDelay(0) == 250, "custom attempt 0: {d} ms, want 250", .{custom.nextDelay(0)}); + try check(custom.nextDelay(1) == 750, "custom attempt 1: {d} ms, want 750", .{custom.nextDelay(1)}); + try expectCappedMonotone(custom, "custom"); +} + +fn lcLinearAndConstant(a: std.mem.Allocator) !void { + _ = a; + const linear = BackoffPolicy{ .strategy = .linear }; + try check(linear.nextDelay(0) == 1000, "linear attempt 0: {d} ms, want 1000", .{linear.nextDelay(0)}); + try check(linear.nextDelay(4) == 5000, "linear attempt 4: {d} ms, want 5000", .{linear.nextDelay(4)}); + try check(linear.nextDelay(100) == 60000, "linear attempt 100: {d} ms, want cap 60000", .{linear.nextDelay(100)}); + + const constant = BackoffPolicy{ .strategy = .constant, .initial_ms = 1500 }; + for ([_]u32{ 0, 9, 40 }) |attempt| { + const got = constant.nextDelay(attempt); + try check(got == 1500, "constant attempt {d}: {d} ms, want 1500", .{ attempt, got }); + } +} + +fn lcJitterBounds(a: std.mem.Allocator) !void { + _ = a; + const uniform = BackoffPolicy{ .jitter_type = .uniform }; + const phi = BackoffPolicy{ .jitter_type = .phi_weighted }; + for (0..200) |_| { + const u = uniform.nextDelay(0); + try check(u >= 1000 and u < 2000, "uniform jitter on 1000 ms gave {d} ms, want [1000, 2000)", .{u}); + const p = phi.nextDelay(0); + try check(p == 618 or p == 1618, "phi jitter on 1000 ms gave {d} ms, want 618 or 1618", .{p}); + } + // Jitter is applied before the cap: a large base must still respect max_ms. + const capped = uniform.nextDelay(10); + try check(capped <= uniform.max_ms, "uniform jitter at attempt 10 gave {d} ms, above max {d}", .{ capped, uniform.max_ms }); +} diff --git a/src/tri/tri_commands.zig b/src/tri/tri_commands.zig index 7c95cceea3..40cad57797 100644 --- a/src/tri/tri_commands.zig +++ b/src/tri/tri_commands.zig @@ -43,6 +43,7 @@ const quantum_cosmic = @import("commands/quantum_cosmic.zig"); const basal_ganglia = @import("basal_ganglia"); const reticular_formation = @import("reticular_formation"); const locus_coeruleus = @import("locus_coeruleus"); +const brain_health = @import("brain_health.zig"); // Re-export multi-cluster types and command pub const NodeTier = multi_cluster.NodeTier; pub const NodeEntry = multi_cluster.NodeEntry; @@ -791,9 +792,17 @@ pub fn runTaskClaimCommand(allocator: std.mem.Allocator, args: []const []const u _ = args; std.debug.print("{s}⚠️ task-claim: TODO - not implemented yet{s}\n", .{ YELLOW, RESET }); } -/// Stress Test Command - Run stress tests -/// Usage: tri stress-test [options] +/// Stress Test Command +/// Usage: tri stress --health +/// +/// `--health` runs the brain self-check probes in brain_health.zig and prints +/// `Score: /100` (fraction of probes passed). It exits 1 if any probe fails. +/// The other modes (--scan, --record, --metrics, --history) are not +/// implemented yet and only print a notice. pub fn runStressTestCommand(args: []const []const u8) !void { - _ = args; + for (args) |arg| { + if (std.mem.eql(u8, arg, "--health")) return brain_health.run(); + } std.debug.print("{s}⚠️ stress-test: TODO - not implemented yet{s}\n", .{ YELLOW, RESET }); + std.debug.print(" only `tri stress --health` (brain self-check probes) is implemented\n", .{}); } From 49dfec797722364aed19ad74f4e17134b5a9d011 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 11:09:15 +0700 Subject: [PATCH 03/13] fix(ci): brain health step reads the number right after Score: The Brain Health Check parsed the first number anywhere on the Score: line, so a colour escape such as ESC[32m before "Score:" read as 32. It now reads only the number directly after "Score:" on the first Score: line, integer or decimal. The HEALTHY check used `grep -q "HEALTHY"`, which also matches "UNHEALTHY", so every run with a Status line reported healthy. It now matches the whole line "Status: HEALTHY". A run above the threshold with failed probes now gets a warning annotation instead of passing silently. The threshold (80), the not-measured and unparseable branches, and `|| true` are unchanged. The comment block describes what the score measures and states that the CLI Smoke Test, which fails on any failed probe, only runs after brain-unit, brain-integration and brain-stress succeed. Closes #1326 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/brain-ci.yml | 38 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/brain-ci.yml b/.github/workflows/brain-ci.yml index 7d9e0706b2..39ebd64198 100644 --- a/.github/workflows/brain-ci.yml +++ b/.github/workflows/brain-ci.yml @@ -49,17 +49,30 @@ jobs: # Run health check and capture output ./zig-out/bin/tri stress --health 2>&1 | tee /tmp/health-output.txt || true + # What the score is: `tri stress --health` (src/tri/brain_health.zig) + # runs a fixed list of probes against the three brain regions linked + # into tri -- basal_ganglia, reticular_formation, locus_coeruleus -- + # and prints exactly one plain-ASCII line + # Score: /100 ( of probes passed) + # where n = 100 * passed / total, followed by "Status: HEALTHY" (all + # probes passed) or "Status: UNHEALTHY". It exits 1 on any failed + # probe. This step ignores that exit code (`|| true`) and gates only + # on the threshold, so a run with a few failed probes (3 of 17 is + # 82.4) still passes here; it gets a warning annotation below. + # The CLI Smoke Test job runs the same command without `|| true` and + # fails on any failed probe, but it only runs after brain-unit, + # brain-integration and brain-stress have succeeded. + # # An absent measurement is not a measurement of zero. # # `grep "Score:" ... || echo "0"` used to turn "the command printed no # Score: line at all" into the NUMBER 0, which the threshold check # below then reported as "Brain health 0 below threshold 80". That - # reads as a measured regression. It is not one: `tri stress --health` - # currently prints "stress-test: TODO - not implemented yet", so there - # has never been a score to compare. The gate has failed on every run - # in its history, including on main, for that reason alone -- and a - # check that is always red carries as little information as one that - # is always green. + # reads as a measured regression. It was not one: until the probes + # above existed, `tri stress --health` printed "stress-test: TODO - + # not implemented yet", so there was never a score to compare, and the + # gate failed on every run for that reason alone. If the Score: line + # ever disappears again, this step says "not measured", not "0". # # Same class as A36/A37, where an unmeasured field rendered as an FPGA # measurement on a public route. Report what happened, not a number @@ -77,7 +90,12 @@ jobs: # "Score: 92" would have parsed as empty and reached bc as an empty # string. A Score: line we cannot parse is also "not measured" -- it # is not zero either. - HEALTH_SCORE=$(grep "Score:" /tmp/health-output.txt | grep -oE "[0-9]+(\.[0-9]+)?" | head -1 || true) + # + # Only the number directly after "Score:" on the first Score: line is + # read. Taking the first number anywhere on the line would read "32" + # out of a colour escape such as ESC[32m placed before "Score:". + SCORE_LINE=$(grep -m1 "Score:" /tmp/health-output.txt || true) + HEALTH_SCORE=$(printf '%s\n' "$SCORE_LINE" | sed -nE 's/.*Score:[[:space:]]*([0-9]+(\.[0-9]+)?).*/\1/p') if [ -z "$HEALTH_SCORE" ]; then echo "HEALTH_SCORE=" >> $GITHUB_OUTPUT echo "HEALTHY=unknown" >> $GITHUB_OUTPUT @@ -87,11 +105,13 @@ jobs: fi echo "HEALTH_SCORE=$HEALTH_SCORE" >> $GITHUB_OUTPUT - # Check if healthy - if grep -q "HEALTHY" /tmp/health-output.txt; then + # Check if healthy. Match the whole status line: a bare + # `grep -q "HEALTHY"` also matches "UNHEALTHY". + if grep -qx "Status: HEALTHY" /tmp/health-output.txt; then echo "HEALTHY=true" >> $GITHUB_OUTPUT else echo "HEALTHY=false" >> $GITHUB_OUTPUT + echo "::warning::brain health $HEALTH_SCORE/100: at least one probe failed -- see the FAIL rows in the output above" fi # Fail if below threshold From 9097fc9c49288aecba28d24a22a30e7b918d5245 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 11:19:10 +0700 Subject: [PATCH 04/13] fix(ci): grant brain-ci the token permission its PR comments need The repository default GITHUB_TOKEN is read-only and brain-ci.yml had no permissions block, so "Comment Health on PR" failed with 403 "Resource not accessible by integration" on the first PR run where the health check passed (run 37176287100). The same applies to the stress comment and the Critical State Notification. Those steps were unreachable while the health check failed first. Grant contents: read and pull-requests: write at workflow level. A PR from a fork still receives a read-only token. Refs #1326 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/brain-ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/brain-ci.yml b/.github/workflows/brain-ci.yml index 39ebd64198..a5159cf8a0 100644 --- a/.github/workflows/brain-ci.yml +++ b/.github/workflows/brain-ci.yml @@ -14,6 +14,16 @@ env: BRAIN_HEALTH_THRESHOLD: "80" # Minimum health score for merge STRESS_TEST_THRESHOLD: "270" # Minimum stress test score (270/300 = 90%) +# The repository's default GITHUB_TOKEN is read-only, so every github-script +# step here that comments on the PR (health, stress, critical state) failed +# with 403 "Resource not accessible by integration". Nobody saw it while the +# health check failed first and those steps were skipped. pull-requests: write +# covers listing and posting comments on a PR; contents: read keeps checkout +# working. A PR from a fork still gets a read-only token. +permissions: + contents: read + pull-requests: write + jobs: # ========================================================================= # Phase 0: Quick Health Check (fast feedback, runs on all commits) From e3f9c02e701e0d66ef81664e0f6b09a0007abb49 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 12:33:31 +0700 Subject: [PATCH 05/13] fix(brain): stress_test reads claims through the sharded registry stress_test.zig still read `registry.claims`, the single map the registry had before it was split into 16 shards, so the file stopped compiling (13 errors) and test-brain-stress could not run. Count with Registry.count() and look a claim up in the one shard Registry.getShardIndex names, under that shard's read lock. The tests themselves are unchanged: 261/261 pass on zig 0.15.2. Co-Authored-By: Claude Opus 5.5 --- src/brain/stress_test.zig | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/brain/stress_test.zig b/src/brain/stress_test.zig index 2f3dd2668d..7c563ecac2 100644 --- a/src/brain/stress_test.zig +++ b/src/brain/stress_test.zig @@ -25,6 +25,16 @@ const alerts = @import("alerts.zig"); const allocator = std.testing.allocator; +/// The registry is sharded (16 maps keyed by Wyhash of the task id), so a +/// claim lives in exactly one shard, the one Registry.getShardIndex names. +/// Replaces the pre-sharding `registry.claims.get(id)` lookup. +fn findClaim(registry: *basal_ganglia.Registry, task_id: []const u8) ?basal_ganglia.TaskClaim { + const shard = ®istry.shards[basal_ganglia.Registry.getShardIndex(task_id)]; + shard.rwlock.lockShared(); + defer shard.rwlock.unlockShared(); + return shard.claims.get(task_id); +} + // ═══════════════════════════════════════════════════════════════════════════════ // TEST CONFIGURATION // ═══════════════════════════════════════════════════════════════════════════════ @@ -56,7 +66,7 @@ test "Stress: Basal Ganglia - 10,000 sequential claims" { } try std.testing.expectEqual(STRESS_TASK_COUNT, successful); - try std.testing.expectEqual(STRESS_TASK_COUNT, registry.claims.count()); + try std.testing.expectEqual(STRESS_TASK_COUNT, registry.count()); } test "Stress: Basal Ganglia - claim completion cycle" { @@ -90,7 +100,7 @@ test "Stress: Basal Ganglia - claim completion cycle" { // Note: Completed claims remain in registry with status=.completed // They are not automatically removed - try std.testing.expectEqual(@as(usize, 1000), registry.claims.count()); + try std.testing.expectEqual(@as(usize, 1000), registry.count()); } test "Stress: Basal Ganglia - heartbeat refresh under load" { @@ -159,7 +169,7 @@ test "Stress: Basal Ganglia - memory pressure with large IDs" { _ = try registry.claim(allocator, task_id, agent_id, 60000); } - try std.testing.expectEqual(@as(usize, 100), registry.claims.count()); + try std.testing.expectEqual(@as(usize, 100), registry.count()); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -693,7 +703,7 @@ test "Stress: Integration - full brain circuit under load" { const stats = event_bus.getStats(); const point = telemetry.TelemetryPoint{ .timestamp = std.time.milliTimestamp(), - .active_claims = registry.claims.count(), + .active_claims = registry.count(), .events_published = stats.published, .events_buffered = stats.buffered, .health_score = 100.0, @@ -745,7 +755,7 @@ test "Stress: Integration - concurrent agent simulation" { } // All unique tasks should be claimed - try std.testing.expectEqual(@as(usize, num_agents * tasks_per_agent), registry.claims.count()); + try std.testing.expectEqual(@as(usize, num_agents * tasks_per_agent), registry.count()); // All events published const stats = event_bus.getStats(); @@ -802,7 +812,7 @@ test "Stress: Integration - failure recovery simulation" { // Note: Since we reuse the same task_id for each cycle, // the registry removes old (completed) claims and adds new ones. // So we have 100 total claims (one per unique task_id) - try std.testing.expectEqual(@as(usize, 100), registry.claims.count()); + try std.testing.expectEqual(@as(usize, 100), registry.count()); // Verify events const stats = event_bus.getStats(); @@ -822,7 +832,7 @@ test "Stress: Edge case - empty string IDs" { try std.testing.expect(claimed); // Should be able to retrieve - const entry = registry.claims.get(""); + const entry = findClaim(®istry, ""); try std.testing.expect(entry != null); } @@ -835,7 +845,7 @@ test "Stress: Edge case - very long IDs" { const claimed = try registry.claim(allocator, long_id, "agent-long", 60000); try std.testing.expect(claimed); - try std.testing.expectEqual(@as(usize, 1), registry.claims.count()); + try std.testing.expectEqual(@as(usize, 1), registry.count()); } test "Stress: Edge case - special characters in IDs" { @@ -859,7 +869,7 @@ test "Stress: Edge case - special characters in IDs" { try std.testing.expect(claimed); } - try std.testing.expectEqual(special_ids.len, registry.claims.count()); + try std.testing.expectEqual(special_ids.len, registry.count()); } test "Stress: Edge case - zero TTL" { @@ -873,7 +883,7 @@ test "Stress: Edge case - zero TTL" { // The claim is created but TTL of 0 means it's at the boundary // Let's just verify it was claimed successfully - const entry = registry.claims.get("zero-ttl-task"); + const entry = findClaim(®istry, "zero-ttl-task"); try std.testing.expect(entry != null); try std.testing.expectEqual(@as(u64, 0), entry.?.ttl_ms); } @@ -888,7 +898,7 @@ test "Stress: Edge case - maximum TTL" { try std.testing.expect(claimed); // Should be valid - const entry = registry.claims.get("max-ttl-task"); + const entry = findClaim(®istry, "max-ttl-task"); if (entry) |claim| { try std.testing.expect(claim.isValid()); } @@ -918,7 +928,7 @@ test "Stress: Edge case - rapid claim/release cycles" { // All claims are completed but remain in registry // Since we reuse the same task_id, we have 100 claims (one per unique task) - try std.testing.expectEqual(@as(usize, 100), registry.claims.count()); + try std.testing.expectEqual(@as(usize, 100), registry.count()); } test "Stress: Edge case - event bus timestamp boundaries" { @@ -1002,11 +1012,11 @@ test "Stress: Memory - allocate and free 10,000 claims" { allocator.free(agent_id); } - try std.testing.expectEqual(@as(usize, 10_000), registry.claims.count()); + try std.testing.expectEqual(@as(usize, 10_000), registry.count()); // Free all registry.reset(); - try std.testing.expectEqual(@as(usize, 0), registry.claims.count()); + try std.testing.expectEqual(@as(usize, 0), registry.count()); } test "Stress: Memory - event buffer memory pressure" { From 83071667a54e652676a306e354b5b79573357a37 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 12:33:51 +0700 Subject: [PATCH 06/13] fix(brain): metrics_dashboard deinit and overwrite no longer corrupt or leak Restoring test-brain ran the dashboard's own tests for the first time in months (integration_test.zig imports the file by path). They found: - RegionMetrics.deinit freed the alert through raw_metrics.allocator after raw_metrics.deinit() had set the map to undefined: a segfault at 0xaaaa... whenever a region carried an alert. Read the allocator first. - setMetric/setMetricOwned on an existing key leaked the new key copy and the replaced value, because HashMap.put keeps the stored key. Copy the key only on first insert and free the replaced value. - Five tests leaked memory they owned (the singletons collect() creates, buffers setMetric copies) and one asserted a 23-byte prefix of a name formatAscii truncates to 20 bytes. The assertions now match the code. test-brain: 151/151 pass, no leaks. Co-Authored-By: Claude Opus 5.5 --- src/brain/metrics_dashboard.zig | 63 ++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/src/brain/metrics_dashboard.zig b/src/brain/metrics_dashboard.zig index 993da353e6..1eb1ae10ac 100644 --- a/src/brain/metrics_dashboard.zig +++ b/src/brain/metrics_dashboard.zig @@ -121,30 +121,45 @@ pub const RegionMetrics = struct { } pub fn deinit(self: *RegionMetrics) void { + // Read the allocator before raw_metrics.deinit(): HashMap.deinit sets + // the map to undefined, so raw_metrics.allocator is garbage afterwards. + const allocator = self.raw_metrics.allocator; var iter = self.raw_metrics.iterator(); while (iter.next()) |entry| { - self.raw_metrics.allocator.free(entry.key_ptr.*); - self.raw_metrics.allocator.free(entry.value_ptr.*); + allocator.free(entry.key_ptr.*); + allocator.free(entry.value_ptr.*); } self.raw_metrics.deinit(); - if (self.alert) |a| self.raw_metrics.allocator.free(a); + if (self.alert) |a| allocator.free(a); } /// Set a raw metric value (copies both key and value) pub fn setMetric(self: *RegionMetrics, allocator: std.mem.Allocator, key: []const u8, value: []const u8) !void { - const key_copy = try allocator.dupe(u8, key); - errdefer allocator.free(key_copy); const value_copy = try allocator.dupe(u8, value); errdefer allocator.free(value_copy); - try self.raw_metrics.put(key_copy, value_copy); + try self.putOwnedValue(allocator, key, value_copy); } /// Set a raw metric value, taking ownership of the value (must be allocated with same allocator) pub fn setMetricOwned(self: *RegionMetrics, allocator: std.mem.Allocator, key: []const u8, value: []const u8) !void { - const key_copy = try allocator.dupe(u8, key); - errdefer allocator.free(key_copy); - // Value is already allocated, use it directly - try self.raw_metrics.put(key_copy, value); + try self.putOwnedValue(allocator, key, value); + } + + /// Store an owned value under a copy of key. HashMap.put on an existing + /// key keeps the stored key and drops the old value, so a plain put of + /// fresh copies leaked the new key copy and the replaced value on every + /// overwrite. Copy the key only on first insert; free the replaced value. + fn putOwnedValue(self: *RegionMetrics, allocator: std.mem.Allocator, key: []const u8, value: []const u8) !void { + const gop = try self.raw_metrics.getOrPut(key); + if (gop.found_existing) { + allocator.free(gop.value_ptr.*); + } else { + gop.key_ptr.* = allocator.dupe(u8, key) catch |err| { + self.raw_metrics.removeByPtr(gop.key_ptr); + return err; + }; + } + gop.value_ptr.* = value; } }; @@ -549,6 +564,10 @@ pub fn quickScan(allocator: std.mem.Allocator) !struct { test "AggregateMetrics collect all regions" { const allocator = std.testing.allocator; + // collect() creates the basal-ganglia and reticular-formation singletons + // with this allocator; release them so the leak checker sees a clean exit. + defer basal_ganglia.resetGlobal(allocator); + defer reticular_formation.resetGlobal(allocator); var metrics = AggregateMetrics.init(allocator); defer metrics.deinit(); @@ -584,6 +603,10 @@ test "AggregateMetrics collect all regions" { test "AggregateMetrics overall health calculation" { const allocator = std.testing.allocator; + // collect() creates the basal-ganglia and reticular-formation singletons + // with this allocator; release them so the leak checker sees a clean exit. + defer basal_ganglia.resetGlobal(allocator); + defer reticular_formation.resetGlobal(allocator); var metrics = AggregateMetrics.init(allocator); defer metrics.deinit(); @@ -608,6 +631,10 @@ test "RegionMetrics set and get" { test "quickScan returns healthy status" { const allocator = std.testing.allocator; + // collect() creates the basal-ganglia and reticular-formation singletons + // with this allocator; release them so the leak checker sees a clean exit. + defer basal_ganglia.resetGlobal(allocator); + defer reticular_formation.resetGlobal(allocator); var result = try quickScan(allocator); defer { for (result.problematic_regions.items) |r| allocator.free(r); @@ -621,6 +648,10 @@ test "quickScan returns healthy status" { test "AggregateMetrics exportJson is valid" { const allocator = std.testing.allocator; + // collect() creates the basal-ganglia and reticular-formation singletons + // with this allocator; release them so the leak checker sees a clean exit. + defer basal_ganglia.resetGlobal(allocator); + defer reticular_formation.resetGlobal(allocator); var metrics = AggregateMetrics.init(allocator); defer metrics.deinit(); @@ -696,8 +727,10 @@ test "RegionMetrics setMetricOwned takes ownership" { var metrics = RegionMetrics.init(allocator, "Test", "Function"); defer metrics.deinit(); + // setMetricOwned copies the key and takes ownership of the value only, + // so the caller still owns (and frees) its key buffer. const key_copy = try allocator.dupe(u8, "owned_key"); - errdefer allocator.free(key_copy); + defer allocator.free(key_copy); const value_copy = try allocator.dupe(u8, "owned_value"); errdefer allocator.free(value_copy); @@ -1471,8 +1504,9 @@ test "AggregateMetrics formatAscii long names" { const output = buffer.items; - // Should truncate or handle long name - try std.testing.expect(std.mem.indexOf(u8, output, "VeryLongRegionNameThatE") != null); + // formatAscii truncates region names to 20 bytes (name[0..20]). + try std.testing.expect(std.mem.indexOf(u8, output, "VeryLongRegionNameTh") != null); + try std.testing.expect(std.mem.indexOf(u8, output, "VeryLongRegionNameThat") == null); } test "AggregateMetrics formatDetailed with all fields" { @@ -1655,8 +1689,11 @@ test "RegionMetrics many metrics" { // Add many metrics for (0..100) |i| { + // setMetric copies key and value, so these buffers stay ours to free. const key = try std.fmt.allocPrint(allocator, "metric_{d}", .{i}); + defer allocator.free(key); const value = try std.fmt.allocPrint(allocator, "value_{d}", .{i}); + defer allocator.free(value); try metrics.setMetric(allocator, key, value); } From 0573de98c7d47af9cc9e3e098838069b76f59559 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 12:33:57 +0700 Subject: [PATCH 07/13] fix(build): restore the brain test steps brain-ci calls #517 (42490a222) removed test-basal-ganglia, test-reticular-formation, test-locus-coeruleus, test-brain and test-brain-stress from build.zig while brain-ci.yml kept calling them, so every Unit Tests leg failed with "no step named ..." and nothing behind it ran. Point the steps back at the source files, which were never removed. Not restored: test-intraparietal (intraparietal_sulcus.zig is stubs around the hslm library that left this repo and does not compile) and test-hslm (hslm is tested in gHashTag/trinity-training; no such step ever existed). Co-Authored-By: Claude Opus 5.5 --- build.zig | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/build.zig b/build.zig index 65599f7651..8c4447c388 100644 --- a/build.zig +++ b/build.zig @@ -2902,4 +2902,161 @@ pub fn build(b: *std.Build) void { }); const run_arena_tests = b.addRunArtifact(arena_tests); test_step.dependOn(&run_arena_tests.step); + + // ═══════════════════════════════════════════════════════════════════════════════ + // S³AI BRAIN — region tests run by .github/workflows/brain-ci.yml + // ═══════════════════════════════════════════════════════════════════════════════ + // + // 42490a22 (#517) removed every test-, test-brain and + // test-brain-stress step from this file while brain-ci.yml kept calling + // them, so each Unit Tests job died on "no step named 'test-basal-ganglia'" + // and Integration, Stress and CLI Smoke never ran behind it. The source + // files and their test blocks were never removed; these steps point back + // at them. Only regions whose tests compile and pass on 0.15.2 are here. + // + // Not restored, on purpose: + // test-intraparietal -- src/brain/intraparietal_sulcus.zig wraps the + // hslm library that moved to gHashTag/trinity-training. What is left + // are stubs (GF16 = TF3 = f32, PHI = 3.0) that do not compile, and its + // tests assert behaviour of the moved library. + // test-hslm -- hslm lives in gHashTag/trinity-training and is tested + // there; nothing named hslm is in this tree. + const brainModule = struct { + fn make( + bb: *std.Build, + t: std.Build.ResolvedTarget, + o: std.builtin.OptimizeMode, + root: []const u8, + imports: []const std.Build.Module.Import, + ) *std.Build.Module { + return bb.createModule(.{ + .root_source_file = bb.path(root), + .target = t, + .optimize = o, + .imports = imports, + }); + } + }.make; + + const bg_mod = brainModule(b, target, optimize, "src/brain/basal_ganglia.zig", &.{}); + const rf_mod = brainModule(b, target, optimize, "src/brain/reticular_formation.zig", &.{}); + const lc_mod = brainModule(b, target, optimize, "src/brain/locus_coeruleus.zig", &.{}); + const bg_rf: []const std.Build.Module.Import = &.{ + .{ .name = "basal_ganglia", .module = bg_mod }, + .{ .name = "reticular_formation", .module = rf_mod }, + }; + const amygdala_b_mod = brainModule(b, target, optimize, "src/brain/amygdala.zig", &.{}); + const persistence_b_mod = brainModule(b, target, optimize, "src/brain/persistence.zig", &.{}); + const telemetry_b_mod = brainModule(b, target, optimize, "src/brain/telemetry.zig", &.{}); + const thalamus_b_mod = brainModule(b, target, optimize, "src/brain/thalamus_logs.zig", &.{}); + const pfc_b_mod = brainModule(b, target, optimize, "src/brain/prefrontal_cortex.zig", &.{}); + const hh_b_mod = brainModule(b, target, optimize, "src/brain/health_history.zig", &.{}); + const microglia_b_mod = brainModule(b, target, optimize, "src/brain/microglia.zig", &.{}); + const alerts_b_mod = brainModule(b, target, optimize, "src/brain/alerts.zig", &.{}); + const visualization_b_mod = brainModule(b, target, optimize, "src/brain/visualization.zig", &.{}); + const learning_b_mod = brainModule(b, target, optimize, "src/brain/learning.zig", &.{}); + const evolution_b_mod = brainModule(b, target, optimize, "src/brain/evolution_simulation.zig", &.{}); + const state_recovery_b_mod = brainModule(b, target, optimize, "src/brain/state_recovery.zig", bg_rf); + const federation_b_mod = brainModule(b, target, optimize, "src/brain/federation.zig", bg_rf); + const async_b_mod = brainModule(b, target, optimize, "src/brain/async_processor.zig", bg_rf); + const metrics_b_mod = brainModule(b, target, optimize, "src/brain/metrics_dashboard.zig", &.{ + .{ .name = "basal_ganglia", .module = bg_mod }, + .{ .name = "reticular_formation", .module = rf_mod }, + .{ .name = "locus_coeruleus", .module = lc_mod }, + .{ .name = "amygdala", .module = amygdala_b_mod }, + .{ .name = "prefrontal_cortex", .module = pfc_b_mod }, + .{ .name = "telemetry", .module = telemetry_b_mod }, + .{ .name = "health_history", .module = hh_b_mod }, + .{ .name = "microglia", .module = microglia_b_mod }, + }); + const simulation_b_mod = brainModule(b, target, optimize, "src/brain/simulation.zig", &.{ + .{ .name = "basal_ganglia", .module = bg_mod }, + .{ .name = "reticular_formation", .module = rf_mod }, + .{ .name = "locus_coeruleus", .module = lc_mod }, + }); + const observability_b_mod = brainModule(b, target, optimize, "src/brain/observability_export.zig", &.{ + .{ .name = "basal_ganglia", .module = bg_mod }, + .{ .name = "reticular_formation", .module = rf_mod }, + .{ .name = "metrics_dashboard", .module = metrics_b_mod }, + }); + const admin_b_mod = brainModule(b, target, optimize, "src/brain/admin.zig", &.{ + .{ .name = "basal_ganglia", .module = bg_mod }, + .{ .name = "reticular_formation", .module = rf_mod }, + .{ .name = "state_recovery", .module = state_recovery_b_mod }, + .{ .name = "telemetry", .module = telemetry_b_mod }, + }); + const sebo_b_mod = brainModule(b, target, optimize, "src/brain/sebo.zig", &.{ + .{ .name = "evolution_simulation", .module = evolution_b_mod }, + }); + + // One step per region: the region file is the test root, so the step + // runs exactly that file's own test blocks. + const brain_region_steps = [_]struct { step: []const u8, desc: []const u8, mod: *std.Build.Module }{ + .{ .step = "test-basal-ganglia", .desc = "Run Basal Ganglia tests (src/brain/basal_ganglia.zig)", .mod = bg_mod }, + .{ .step = "test-reticular-formation", .desc = "Run Reticular Formation tests (src/brain/reticular_formation.zig)", .mod = rf_mod }, + .{ .step = "test-locus-coeruleus", .desc = "Run Locus Coeruleus tests (src/brain/locus_coeruleus.zig)", .mod = lc_mod }, + }; + for (brain_region_steps) |r| { + const region_tests = b.addTest(.{ .root_module = r.mod }); + b.step(r.step, r.desc).dependOn(&b.addRunArtifact(region_tests).step); + } + + // test-brain: the aggregator src/brain/brain.zig (AgentCoordination over + // the regions above) plus src/brain/integration_test.zig (cross-region + // scenarios). brain.zig also names intraparietal_sulcus, perf_dashboard + // and benchmarks; none of its tests reference them, and Zig analyses an + // @import only when it is referenced, so they are not wired here. + const brain_agg_mod = brainModule(b, target, optimize, "src/brain/brain.zig", &.{ + .{ .name = "basal_ganglia", .module = bg_mod }, + .{ .name = "reticular_formation", .module = rf_mod }, + .{ .name = "locus_coeruleus", .module = lc_mod }, + .{ .name = "amygdala", .module = amygdala_b_mod }, + .{ .name = "persistence", .module = persistence_b_mod }, + .{ .name = "telemetry", .module = telemetry_b_mod }, + .{ .name = "thalamus_logs", .module = thalamus_b_mod }, + .{ .name = "prefrontal_cortex", .module = pfc_b_mod }, + .{ .name = "health_history", .module = hh_b_mod }, + .{ .name = "microglia", .module = microglia_b_mod }, + .{ .name = "metrics_dashboard", .module = metrics_b_mod }, + .{ .name = "state_recovery", .module = state_recovery_b_mod }, + .{ .name = "admin", .module = admin_b_mod }, + .{ .name = "alerts", .module = alerts_b_mod }, + .{ .name = "simulation", .module = simulation_b_mod }, + .{ .name = "evolution_simulation", .module = evolution_b_mod }, + .{ .name = "sebo", .module = sebo_b_mod }, + .{ .name = "observability_export", .module = observability_b_mod }, + .{ .name = "visualization", .module = visualization_b_mod }, + .{ .name = "learning", .module = learning_b_mod }, + .{ .name = "federation", .module = federation_b_mod }, + .{ .name = "async_processor", .module = async_b_mod }, + }); + // integration_test.zig reaches metrics_dashboard.zig by file path, so that + // file is compiled as part of this module and needs its imports here too. + const brain_integration_mod = brainModule(b, target, optimize, "src/brain/integration_test.zig", &.{ + .{ .name = "basal_ganglia", .module = bg_mod }, + .{ .name = "reticular_formation", .module = rf_mod }, + .{ .name = "locus_coeruleus", .module = lc_mod }, + .{ .name = "amygdala", .module = amygdala_b_mod }, + .{ .name = "prefrontal_cortex", .module = pfc_b_mod }, + .{ .name = "telemetry", .module = telemetry_b_mod }, + .{ .name = "health_history", .module = hh_b_mod }, + .{ .name = "alerts", .module = alerts_b_mod }, + .{ .name = "state_recovery", .module = state_recovery_b_mod }, + .{ .name = "learning", .module = learning_b_mod }, + .{ .name = "federation", .module = federation_b_mod }, + .{ .name = "async_processor", .module = async_b_mod }, + .{ .name = "microglia", .module = microglia_b_mod }, + }); + const brain_tests_step = b.step("test-brain", "Run brain aggregator + cross-region integration tests (src/brain/brain.zig, src/brain/integration_test.zig)"); + brain_tests_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = brain_agg_mod })).step); + brain_tests_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = brain_integration_mod })).step); + + // test-brain-stress: src/brain/stress_test.zig, 10,000-claim and + // 20,000-event load tests over basal_ganglia, reticular_formation, + // locus_coeruleus, telemetry and alerts. It imports them by file path, so + // it is a single module with no imports of its own. + const brain_stress_tests = b.addTest(.{ + .root_module = brainModule(b, target, optimize, "src/brain/stress_test.zig", &.{}), + }); + b.step("test-brain-stress", "Run brain stress tests (src/brain/stress_test.zig)").dependOn(&b.addRunArtifact(brain_stress_tests).step); } From f8d82f8b8cccff153685a4fe83250f94a3312334 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 12:34:05 +0700 Subject: [PATCH 08/13] fix(ci): brain-ci runs only steps that exist and measure something - Unit Tests: basal-ganglia, reticular-formation, locus-coeruleus only; intraparietal and hslm dropped with the reason in the file. - Stress: pipefail-safe exit code plus zig's own "N/M tests passed" count. A missing count is NOT MEASURED and fails, never 0. The 270/300 threshold and the hard-coded 100/100 PR table are gone; nothing ever produced them. - CLI Smoke: only `tri stress --health`, no `|| true`. task stats, task list, event stats and stress --scan are unimplemented and exit 0. - Export Brain Metrics job and the --record/--history steps removed: they call unimplemented subcommands, and the health job wrote "snapshot recorded" whether or not anything was. - Report shows each job's real result instead of FAIL for skipped; the verdict still requires every job to succeed. Weekly artifact name is now unique (`with:` never expanded $(date)). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/brain-ci.yml | 277 +++++++++++++++++---------------- 1 file changed, 140 insertions(+), 137 deletions(-) diff --git a/.github/workflows/brain-ci.yml b/.github/workflows/brain-ci.yml index a5159cf8a0..2bfdf69bbb 100644 --- a/.github/workflows/brain-ci.yml +++ b/.github/workflows/brain-ci.yml @@ -12,7 +12,10 @@ on: env: BRAIN_HEALTH_THRESHOLD: "80" # Minimum health score for merge - STRESS_TEST_THRESHOLD: "270" # Minimum stress test score (270/300 = 90%) + # STRESS_TEST_THRESHOLD ("270", i.e. 270/300) is gone. Nothing ever printed a + # score out of 300: test-brain-stress is a zig test binary, and the parser + # read the first integer on a "Score:" line that never appeared, or "0". + # The stress gate is now "every test in the binary passed, none leaked". # The repository's default GITHUB_TOKEN is read-only, so every github-script # step here that comments on the PR (health, stress, critical state) failed @@ -177,11 +180,10 @@ jobs: }); } - - name: Record Health Snapshot - if: always() - run: | - ./zig-out/bin/tri stress --record || echo "Health record skipped" - echo "X Health snapshot recorded to Hippocampus" >> $GITHUB_STEP_SUMMARY + # "Record Health Snapshot" is gone. It ran `tri stress --record`, which + # prints "stress-test: TODO - not implemented yet" and exits 0, and then + # wrote "Health snapshot recorded to Hippocampus" into the job summary + # unconditionally. Nothing was recorded; the summary line was false. # ========================================================================= # Phase 1: Build Check (compile validation) @@ -214,6 +216,18 @@ jobs: # ========================================================================= # Phase 2: Unit Tests — Individual Brain Regions # ========================================================================= + # Each entry is `zig build test-`; build.zig declares the steps in + # its "Brain CI test steps" section. #517 (42490a222) removed every one of + # them while this matrix kept calling them, so every leg failed with "no + # step named ..." and nothing below Unit Tests ever ran. + # + # Not restored, on purpose: + # - intraparietal: src/brain/intraparietal_sulcus.zig does not compile. It + # is a stub (GF16 and TF3 are `f32`, PHI is 3.0) whose TF3 type exists + # nowhere else, wrapped around the hslm model that left this repository. + # A green leg would be a leg for code that cannot be built. + # - hslm: the HSLM model moved to gHashTag/trinity-training; its tests run + # there. build.zig never had a test-hslm step. brain-unit: name: 🧠 Unit Tests runs-on: ubuntu-latest @@ -226,8 +240,6 @@ jobs: - basal-ganglia - reticular-formation - locus-coeruleus - - intraparietal - - hslm steps: - name: Setup Zig uses: mlugg/setup-zig@v2 @@ -241,7 +253,10 @@ jobs: - name: Test ${{ matrix.region }} run: | - zig build test-${{ matrix.region }} -Dci=true + # The exit code is the gate: zig build fails on any failed or leaking + # test. --summary all prints "N/M tests passed" so the log shows how + # many tests ran, not just that the step ended. + zig build test-${{ matrix.region }} -Dci=true --summary all # ========================================================================= # Phase 3: Integration Test — Brain Aggregator @@ -264,7 +279,12 @@ jobs: - name: Test Brain Integration run: | - zig build test-brain -Dci=true + # test-brain runs two binaries: src/brain/brain.zig (the aggregator + # that re-exports every region) and src/brain/integration_test.zig + # (cross-region scenarios; it imports metrics_dashboard.zig by path, + # so the dashboard's own tests run in that binary too). The exit code + # is the gate. + zig build test-brain -Dci=true --summary all # ========================================================================= # Phase 4: Stress Test — "Functional MRI" Gate @@ -288,50 +308,72 @@ jobs: - name: Run Stress Test id: stress run: | - # Run stress test and capture output - zig build test-brain-stress -Dci=true 2>&1 | tee /tmp/stress-output.txt - - # Extract score from output - STRESS_SCORE=$(grep "Score:" /tmp/stress-output.txt | tail -1 | grep -oE "[0-9]+" || echo "0") - echo "STRESS_SCORE=$STRESS_SCORE" >> $GITHUB_OUTPUT - - # Check if passed - if grep -q "PASS" /tmp/stress-output.txt; then - echo "PASSED=true" >> $GITHUB_OUTPUT - else - echo "PASSED=false" >> $GITHUB_OUTPUT + # What is measured: `zig build test-brain-stress` compiles + # src/brain/stress_test.zig (10k-claim, 20k-event, concurrent and + # edge-case tests) together with the region files it imports by path, + # whose own tests run in the same binary. The result is the test + # count zig prints: "Build Summary: ...; / tests + # passed[; failed][; leaked]". + # + # What used to be here: the output went through `| tee` without + # pipefail, so a failed build still exited 0; "Score:" was grepped + # out of output that never contains it and `|| echo "0"` turned the + # missing line into the number 0; and the 270/300 threshold compared + # that 0 against a scale nothing produces. As in the health step + # above: an absent measurement is NOT MEASURED, never 0. + set +e + zig build test-brain-stress -Dci=true --summary all 2>&1 | tee /tmp/stress-output.txt + BUILD_EXIT=${PIPESTATUS[0]} + set -e + echo "BUILD_EXIT=$BUILD_EXIT" >> $GITHUB_OUTPUT + + SUMMARY=$(grep -m1 "^Build Summary:" /tmp/stress-output.txt || true) + TESTS_PASSED=$(printf '%s\n' "$SUMMARY" | sed -nE 's#.* ([0-9]+)/([0-9]+) tests passed.*#\1#p') + TESTS_TOTAL=$(printf '%s\n' "$SUMMARY" | sed -nE 's#.* ([0-9]+)/([0-9]+) tests passed.*#\2#p') + TESTS_LEAKED=$(printf '%s\n' "$SUMMARY" | sed -nE 's#.*; ([0-9]+) leaked.*#\1#p') + + if [ -z "$TESTS_PASSED" ] || [ -z "$TESTS_TOTAL" ] || [ "$TESTS_TOTAL" -eq 0 ]; then + echo "RESULT=not-measured" >> $GITHUB_OUTPUT + echo "::error::brain stress was NOT MEASURED -- the build summary carries no '/ tests passed' count (zig build exit $BUILD_EXIT), so no test result exists. This is not a score of 0. Summary line: ${SUMMARY:-}" + tail -40 /tmp/stress-output.txt + exit 1 fi - # Fail if below threshold - if [ "$STRESS_SCORE" -lt "$STRESS_TEST_THRESHOLD" ]; then - echo "::error::Stress test score $STRESS_SCORE below threshold $STRESS_TEST_THRESHOLD" + echo "TESTS_PASSED=$TESTS_PASSED" >> $GITHUB_OUTPUT + echo "TESTS_TOTAL=$TESTS_TOTAL" >> $GITHUB_OUTPUT + echo "TESTS_LEAKED=${TESTS_LEAKED:-0}" >> $GITHUB_OUTPUT + + # The exit code is checked as well as the count: when the test binary + # crashes (a segfault, a panic) zig can still print "N/N tests + # passed", and only the non-zero exit says the run did not finish. + if [ "$BUILD_EXIT" -ne 0 ] || [ "$TESTS_PASSED" -ne "$TESTS_TOTAL" ] || [ "${TESTS_LEAKED:-0}" -ne 0 ]; then + echo "RESULT=fail" >> $GITHUB_OUTPUT + echo "::error::brain stress FAILED: $TESTS_PASSED/$TESTS_TOTAL tests passed, ${TESTS_LEAKED:-0} leaked, zig build exit $BUILD_EXIT (a non-zero exit with every test passed means the test binary crashed -- see the log above)" exit 1 fi + echo "RESULT=pass" >> $GITHUB_OUTPUT + echo "brain stress: $TESTS_PASSED/$TESTS_TOTAL tests passed, 0 leaked" - name: Comment Stress Test on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | - const score = '${{ steps.stress.outputs.STRESS_SCORE }}'; - const passed = '${{ steps.stress.outputs.PASSED }}' === 'true'; - const threshold = '${{ env.STRESS_TEST_THRESHOLD }}'; + // Every number below comes from the step outputs, i.e. from + // zig's own build summary. The old body hard-coded a per-region + // table of "100/100" that no step ever measured. + const passedCount = '${{ steps.stress.outputs.TESTS_PASSED }}'; + const total = '${{ steps.stress.outputs.TESTS_TOTAL }}'; + const leaked = '${{ steps.stress.outputs.TESTS_LEAKED }}'; + const passed = '${{ steps.stress.outputs.RESULT }}' === 'pass'; - const emoji = passed ? 'white_check_mark' : 'x'; const status = passed ? 'PASS' : 'FAIL'; const color = passed ? 'green' : 'red'; const body = `## 🧪 Brain Stress Test Results\n\n` + - `**Score:** ${score}/300\n` + + `**Tests:** ${passedCount}/${total} passed, ${leaked} leaked\n` + `**Status:** :${color}_circle: ${status}\n` + - `**Threshold:** ${threshold}/300\n\n` + - `### Test Phases\n` + - `| Phase | Score |\n` + - `|-------|-------|\n` + - `| Basal Ganglia | 100/100 |\n` + - `| Locus Coeruleus | 100/100 |\n` + - `| Reticular Formation | 100/100 |\n\n` + - `${passed ? 'X' : 'x'} Stress test ${passed ? 'passed' : 'failed'}`; + `**Gate:** every test in \`zig build test-brain-stress\` passes and none leaks\n`; // Find or create comment const { data: comments } = await github.rest.issues.listComments({ @@ -361,25 +403,26 @@ jobs: }); } - - name: Record Stress Test Result + # `tri stress --record` is no longer called here: it is an unimplemented + # subcommand that exits 0, and this job never built tri, so it could not + # have run anyway. .trinity/brain_health_history.jsonl is no longer + # uploaded as a result: nothing in CI writes it; the checked-in copy is a + # one-line snapshot from 2026-03-20. + - name: Stress Test Summary if: always() run: | - # Record to Hippocampus - ./zig-out/bin/tri stress --record || echo "Record skipped" - - # Upload stress test output echo "## 🧪 Stress Test Output" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - cat /tmp/stress-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -60 /tmp/stress-output.txt >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "no stress output was produced" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY - - name: Upload Brain Scan Results + - name: Upload Stress Test Output if: always() uses: actions/upload-artifact@v4 with: name: brain-stress-results - path: | - /tmp/stress-output.txt - .trinity/brain_health_history.jsonl + path: /tmp/stress-output.txt retention-days: 30 # ========================================================================= @@ -413,62 +456,27 @@ jobs: - name: CLI Commands Test run: | - echo "Testing tri task commands..." - ./zig-out/bin/tri task stats || true - ./zig-out/bin/tri task list || true - - echo "Testing tri event commands..." - ./zig-out/bin/tri event stats || true - - echo "Running tri stress --health..." + # Only commands that measure something are run here, and none is + # wrapped in `|| true`. + # + # `tri stress --health` runs the 17 brain probes and exits 1 if ANY + # probe fails -- stricter than the health job, which gates on the + # score threshold. + # + # Dropped, because each one prints ": TODO - not implemented + # yet" and exits 0, so calling it tested only that the binary starts: + # tri task stats, tri task list (task-claim) + # tri event stats (event-stream) + # tri stress --scan (stress-test; only --health exists) + # Add them back when they do something. ./zig-out/bin/tri stress --health - echo "Testing tri stress --scan..." - ./zig-out/bin/tri stress --scan - - # ========================================================================= - # Phase 6: Brain Metrics Export (Prometheus format) - # ========================================================================= - brain-metrics: - name: 📊 Export Brain Metrics - runs-on: ubuntu-latest - needs: cli-smoke - if: always() - timeout-minutes: 5 - steps: - - name: Setup Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.15.2 - - - name: Checkout Trinity - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Build TRI Binary - run: | - # `zig build tri` is a RUN step -- build.zig declares it as - # "Run TRI - Unified Trinity CLI" -- so this launched the REPL with - # stdin closed and segfaulted, while the step called itself a build. - # `zig build` installs the binary without running it, which is what - # a step named "Build TRI Binary" should do. - zig build -Dci=true - ls -la zig-out/bin/tri - - - name: Export Metrics - run: | - echo "# Brain Metrics Export" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - ./zig-out/bin/tri stress --metrics >> $GITHUB_STEP_SUMMARY - - - name: Upload Metrics Artifact - uses: actions/upload-artifact@v4 - with: - name: brain-metrics - path: | - .trinity/brain_health_history.jsonl - retention-days: 90 + # Phase 6 "Export Brain Metrics" is gone. Its only measuring step was + # `tri stress --metrics`, which prints "stress-test: TODO - not implemented + # yet" and exits 0, and its artifact was the checked-in + # .trinity/brain_health_history.jsonl, which nothing in CI writes. It ran + # with `if: always()`, so it reported success even when every job above it + # had failed. Re-add it when `tri stress --metrics` exports something. # ========================================================================= # Final Gate: Brain Health Report @@ -484,14 +492,19 @@ jobs: run: | echo "# 🧠 S³AI Brain Health Report" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "| Phase | Status | Details |" >> $GITHUB_STEP_SUMMARY + # Each row shows the job's real result (success / failure / + # cancelled / skipped). The old table printed "FAIL" for anything + # but success, so a job that never ran because an earlier one failed + # read as a second failure. Jobs here have no `if:` of their own, so + # "skipped" means an upstream job failed. + echo "| Phase | Result | Details |" >> $GITHUB_STEP_SUMMARY echo "|-------|--------|---------|" >> $GITHUB_STEP_SUMMARY - echo "| Health Check | ${{ needs.brain-health-check.result == 'success' && 'X PASS' || 'x FAIL' }} | Score check |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build-check.result == 'success' && 'X PASS' || 'x FAIL' }} | Compile check |" >> $GITHUB_STEP_SUMMARY - echo "| Unit Tests | ${{ needs.brain-unit.result == 'success' && 'X PASS' || 'x FAIL' }} | Brain regions |" >> $GITHUB_STEP_SUMMARY - echo "| Integration | ${{ needs.brain-integration.result == 'success' && 'X PASS' || 'x FAIL' }} | Cross-module |" >> $GITHUB_STEP_SUMMARY - echo "| Stress Test | ${{ needs.brain-stress.result == 'success' && 'X PASS' || 'x FAIL' }} | Functional MRI |" >> $GITHUB_STEP_SUMMARY - echo "| CLI Smoke | ${{ needs.cli-smoke.result == 'success' && 'X PASS' || 'x FAIL' }} | Commands work |" >> $GITHUB_STEP_SUMMARY + echo "| Health Check | ${{ needs.brain-health-check.result }} | tri stress --health score vs threshold |" >> $GITHUB_STEP_SUMMARY + echo "| Build | ${{ needs.build-check.result }} | zig build -Dci=true |" >> $GITHUB_STEP_SUMMARY + echo "| Unit Tests | ${{ needs.brain-unit.result }} | test-basal-ganglia, test-reticular-formation, test-locus-coeruleus |" >> $GITHUB_STEP_SUMMARY + echo "| Integration | ${{ needs.brain-integration.result }} | test-brain |" >> $GITHUB_STEP_SUMMARY + echo "| Stress Test | ${{ needs.brain-stress.result }} | test-brain-stress, all tests pass, none leak |" >> $GITHUB_STEP_SUMMARY + echo "| CLI Smoke | ${{ needs.cli-smoke.result }} | tri stress --health, every probe passes |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Verdict" >> $GITHUB_STEP_SUMMARY @@ -525,9 +538,9 @@ jobs: const body = `## 🚨 CRITICAL: Brain Health Below Threshold\n\n` + `The brain health CI gate has failed. This PR **cannot be merged** until:\n\n` + `1. All brain region unit tests pass\n` + - `2. Integration tests pass\n` + - `3. Stress test score >= 270/300\n` + - `4. Brain health score >= 80/100\n\n` + + `2. Integration tests pass (\`zig build test-brain\`)\n` + + `3. Every stress test passes with no leaks (\`zig build test-brain-stress\`)\n` + + `4. Brain health score >= 80/100, and every probe passes in CLI Smoke\n\n` + `Please review the failed job logs and fix the issues.`; await github.rest.issues.createComment({ @@ -556,25 +569,18 @@ jobs: with: submodules: recursive - - name: Build TRI Binary - run: | - # `zig build tri` is a RUN step -- build.zig declares it as - # "Run TRI - Unified Trinity CLI" -- so this launched the REPL with - # stdin closed and segfaulted, while the step called itself a build. - # `zig build` installs the binary without running it, which is what - # a step named "Build TRI Binary" should do. - zig build -Dci=true - ls -la zig-out/bin/tri - + # tri was built here only for `tri stress --record` and `--history`, + # both unimplemented subcommands that exit 0. They and the build are + # gone; the stress test below is the measurement. - name: Run Full Stress Test run: | - zig build test-brain-stress -Dci=true 2>&1 | tee /tmp/weekly-stress.txt - - - name: Record Weekly Snapshot - run: | - ./zig-out/bin/tri stress --record + # pipefail: without it `| tee` hid a failed stress run and the + # weekly job went green on a red test binary. + set -o pipefail + zig build test-brain-stress -Dci=true --summary all 2>&1 | tee /tmp/weekly-stress.txt - name: Generate Trend Report + if: always() run: | echo "# 📈 Weekly Brain Health Trend Report" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -582,21 +588,18 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "## Stress Test Results" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/weekly-stress.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Health History" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - ./zig-out/bin/tri stress --history 2>&1 || echo "No history available" >> $GITHUB_STEP_SUMMARY + tail -60 /tmp/weekly-stress.txt >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "no stress output was produced" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + # `with:` is not a shell, so `$(date +%Y%m%d)` was never expanded: every + # week uploaded an artifact literally named + # "weekly-brain-report-$(date +%Y%m%d)". The run id is unique per run. - name: Upload Weekly Report + if: always() uses: actions/upload-artifact@v4 with: - name: weekly-brain-report-$(date +%Y%m%d) - path: | - /tmp/weekly-stress.txt - .trinity/brain_health_history.jsonl + name: weekly-brain-report-${{ github.run_id }} + path: /tmp/weekly-stress.txt retention-days: 365 # ========================================================================= From f4170dde11493435804ed1b7d878182d5a1528db Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 12:53:47 +0700 Subject: [PATCH 09/13] fix(build): link libc for the state_recovery brain module captureState() calls std.c.getpid(). macOS links libc implicitly, so test-brain passed locally, but on ubuntu-latest the integration binary failed to compile with 'dependency on libc must be explicitly specified' (brain-ci run 37180861619). A cross-compile for x86_64-linux-gnu reproduces the error before this change and compiles cleanly after it. Refs #1331 Co-Authored-By: Claude Opus 5.5 --- build.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.zig b/build.zig index 8c4447c388..2e762ed4a5 100644 --- a/build.zig +++ b/build.zig @@ -2957,6 +2957,9 @@ pub fn build(b: *std.Build) void { const learning_b_mod = brainModule(b, target, optimize, "src/brain/learning.zig", &.{}); const evolution_b_mod = brainModule(b, target, optimize, "src/brain/evolution_simulation.zig", &.{}); const state_recovery_b_mod = brainModule(b, target, optimize, "src/brain/state_recovery.zig", bg_rf); + // captureState() calls std.c.getpid(). macOS links libc implicitly, so this + // only fails on Linux: "dependency on libc must be explicitly specified". + state_recovery_b_mod.link_libc = true; const federation_b_mod = brainModule(b, target, optimize, "src/brain/federation.zig", bg_rf); const async_b_mod = brainModule(b, target, optimize, "src/brain/async_processor.zig", bg_rf); const metrics_b_mod = brainModule(b, target, optimize, "src/brain/metrics_dashboard.zig", &.{ From 2d85861c7377ffa4174e0a95b698dbb32dc86ad2 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 13:02:56 +0700 Subject: [PATCH 10/13] fix(ci): brain-ci merge gate fails red instead of skipping The gate needed brain-health-report without always(), so whenever the report failed the gate was skipped. A skipped check renders grey, not red: on #1328 (run 37176676345) the report failed and the Merge Gate showed skipped. With always() the gate runs and its existing check fails it for any report result other than success. Refs #1331 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/brain-ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/brain-ci.yml b/.github/workflows/brain-ci.yml index 2bfdf69bbb..723f4d9d4c 100644 --- a/.github/workflows/brain-ci.yml +++ b/.github/workflows/brain-ci.yml @@ -609,7 +609,11 @@ jobs: name: 🔒 Merge Gate runs-on: ubuntu-latest needs: [brain-health-report] - if: github.event_name == 'pull_request' + # always(): without it a failed report makes this job "skipped", and a + # skipped check is grey, not red -- on #1328 (run 37176676345) the report + # failed and the gate showed skipped. Now it runs and the check below + # fails it for any report result other than success. + if: always() && github.event_name == 'pull_request' timeout-minutes: 2 steps: - name: Check Merge Eligibility From 291ac8b24308ee39703393b6ea5363c166079b79 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 13:50:23 +0700 Subject: [PATCH 11/13] build(brain): spell out brain test roots so the S01 checker can see them The capability index (external/t27/tools/trinity_manifest.py) finds build targets only as `const X = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path(...) }) })` and `b.step("literal", ...)`. The helper-built roots and the region-step loop showed up as `test:None` and hid three steps. Each brain test root and step is now written out, so every target has a precise name a capability card can own. No test changes: the same files are compiled with the same imports. Co-Authored-By: Claude Opus 5.5 --- build.zig | 65 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/build.zig b/build.zig index 2e762ed4a5..45ebd8b713 100644 --- a/build.zig +++ b/build.zig @@ -2993,23 +2993,36 @@ pub fn build(b: *std.Build) void { }); // One step per region: the region file is the test root, so the step - // runs exactly that file's own test blocks. - const brain_region_steps = [_]struct { step: []const u8, desc: []const u8, mod: *std.Build.Module }{ - .{ .step = "test-basal-ganglia", .desc = "Run Basal Ganglia tests (src/brain/basal_ganglia.zig)", .mod = bg_mod }, - .{ .step = "test-reticular-formation", .desc = "Run Reticular Formation tests (src/brain/reticular_formation.zig)", .mod = rf_mod }, - .{ .step = "test-locus-coeruleus", .desc = "Run Locus Coeruleus tests (src/brain/locus_coeruleus.zig)", .mod = lc_mod }, - }; - for (brain_region_steps) |r| { - const region_tests = b.addTest(.{ .root_module = r.mod }); - b.step(r.step, r.desc).dependOn(&b.addRunArtifact(region_tests).step); - } + // runs exactly that file's own test blocks. Each test root is spelled out + // as b.createModule(.{ .root_source_file = b.path(...) }) and each step + // name is a literal, because the S01 capability checker + // (external/t27/tools/trinity_manifest.py) finds build targets by those + // patterns; a root passed through a helper or a loop is invisible to it. + const bg_tests = b.addTest(.{ .root_module = b.createModule(.{ + .root_source_file = b.path("src/brain/basal_ganglia.zig"), + .target = target, + .optimize = optimize, + }) }); + b.step("test-basal-ganglia", "Run Basal Ganglia tests (src/brain/basal_ganglia.zig)").dependOn(&b.addRunArtifact(bg_tests).step); + const rf_tests = b.addTest(.{ .root_module = b.createModule(.{ + .root_source_file = b.path("src/brain/reticular_formation.zig"), + .target = target, + .optimize = optimize, + }) }); + b.step("test-reticular-formation", "Run Reticular Formation tests (src/brain/reticular_formation.zig)").dependOn(&b.addRunArtifact(rf_tests).step); + const lc_tests = b.addTest(.{ .root_module = b.createModule(.{ + .root_source_file = b.path("src/brain/locus_coeruleus.zig"), + .target = target, + .optimize = optimize, + }) }); + b.step("test-locus-coeruleus", "Run Locus Coeruleus tests (src/brain/locus_coeruleus.zig)").dependOn(&b.addRunArtifact(lc_tests).step); // test-brain: the aggregator src/brain/brain.zig (AgentCoordination over // the regions above) plus src/brain/integration_test.zig (cross-region // scenarios). brain.zig also names intraparietal_sulcus, perf_dashboard // and benchmarks; none of its tests reference them, and Zig analyses an // @import only when it is referenced, so they are not wired here. - const brain_agg_mod = brainModule(b, target, optimize, "src/brain/brain.zig", &.{ + const brain_agg_imports: []const std.Build.Module.Import = &.{ .{ .name = "basal_ganglia", .module = bg_mod }, .{ .name = "reticular_formation", .module = rf_mod }, .{ .name = "locus_coeruleus", .module = lc_mod }, @@ -3032,10 +3045,10 @@ pub fn build(b: *std.Build) void { .{ .name = "learning", .module = learning_b_mod }, .{ .name = "federation", .module = federation_b_mod }, .{ .name = "async_processor", .module = async_b_mod }, - }); + }; // integration_test.zig reaches metrics_dashboard.zig by file path, so that // file is compiled as part of this module and needs its imports here too. - const brain_integration_mod = brainModule(b, target, optimize, "src/brain/integration_test.zig", &.{ + const brain_integration_imports: []const std.Build.Module.Import = &.{ .{ .name = "basal_ganglia", .module = bg_mod }, .{ .name = "reticular_formation", .module = rf_mod }, .{ .name = "locus_coeruleus", .module = lc_mod }, @@ -3049,17 +3062,31 @@ pub fn build(b: *std.Build) void { .{ .name = "federation", .module = federation_b_mod }, .{ .name = "async_processor", .module = async_b_mod }, .{ .name = "microglia", .module = microglia_b_mod }, - }); + }; + const brain_agg_tests = b.addTest(.{ .root_module = b.createModule(.{ + .root_source_file = b.path("src/brain/brain.zig"), + .target = target, + .optimize = optimize, + .imports = brain_agg_imports, + }) }); + const brain_integration_tests = b.addTest(.{ .root_module = b.createModule(.{ + .root_source_file = b.path("src/brain/integration_test.zig"), + .target = target, + .optimize = optimize, + .imports = brain_integration_imports, + }) }); const brain_tests_step = b.step("test-brain", "Run brain aggregator + cross-region integration tests (src/brain/brain.zig, src/brain/integration_test.zig)"); - brain_tests_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = brain_agg_mod })).step); - brain_tests_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = brain_integration_mod })).step); + brain_tests_step.dependOn(&b.addRunArtifact(brain_agg_tests).step); + brain_tests_step.dependOn(&b.addRunArtifact(brain_integration_tests).step); // test-brain-stress: src/brain/stress_test.zig, 10,000-claim and // 20,000-event load tests over basal_ganglia, reticular_formation, // locus_coeruleus, telemetry and alerts. It imports them by file path, so // it is a single module with no imports of its own. - const brain_stress_tests = b.addTest(.{ - .root_module = brainModule(b, target, optimize, "src/brain/stress_test.zig", &.{}), - }); + const brain_stress_tests = b.addTest(.{ .root_module = b.createModule(.{ + .root_source_file = b.path("src/brain/stress_test.zig"), + .target = target, + .optimize = optimize, + }) }); b.step("test-brain-stress", "Run brain stress tests (src/brain/stress_test.zig)").dependOn(&b.addRunArtifact(brain_stress_tests).step); } From 40a5315601b473e31f6841b59811b1305478e2f7 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 15:49:52 +0700 Subject: [PATCH 12/13] build(reproduce): vendor gHashTag/t27@39e06aa0 and measure trinity/brain.regions The brain test steps this PR restores (test-basal-ganglia, test-reticular-formation, test-locus-coeruleus, test-brain, test-brain-stress) and their six src/brain test roots had no S01 card, so the capability index failed on each as UNASSIGNED_TARGET. gHashTag/t27#5965 (39e06aa0, Closes gHashTag/t27#5953) adds the card trinity/brain.regions that owns exactly those eleven targets. - tools/contracts.py vendor --revision 39e06aa0: external/t27 and the lock move from de2a1aca; 76 files (the new card is the 76th). It also brings gHashTag/t27#5474: the S01 checker skips build.zig comments and counts the vendored copies apart, and the acceptance of mcp.needle-mcp and mcp.trinity-mcp becomes zig build -Dci=true && test -x zig-out/bin/. - specs/reproduce/capabilities.t27: trinity/brain.regions is in RUN. The profile's test step does not compile its roots, so its five zig build test- commands are its only measure in the index. mcp.needle-mcp and mcp.trinity-mcp leave KNOWN_BLOCKED: they were blocked on steps that exist only in commented-out lines, and their new acceptance is the install the profile already makes. Co-Authored-By: Claude Opus 5.5 --- external/t27/specs/tools/mcp/needle.t27 | 4 +- external/t27/specs/tools/mcp/trinity.t27 | 4 +- external/t27/specs/trinity/README.md | 41 ++- .../trinity/capabilities/brain.regions.t27 | 52 +++ .../trinity/capabilities/mcp.needle-mcp.t27 | 6 +- .../trinity/capabilities/mcp.trinity-mcp.t27 | 6 +- .../research.unreferenced-sources.t27 | 8 +- external/t27/specs/trinity/project.t27 | 21 +- external/t27/tools/trinity_manifest.py | 310 +++++++++++++++--- specs/reproduce/capabilities.t27 | 15 +- specs/reproduce/contracts.t27 | 32 +- 11 files changed, 399 insertions(+), 100 deletions(-) create mode 100644 external/t27/specs/trinity/capabilities/brain.regions.t27 diff --git a/external/t27/specs/tools/mcp/needle.t27 b/external/t27/specs/tools/mcp/needle.t27 index fba7f9fd14..8349bd621f 100644 --- a/external/t27/specs/tools/mcp/needle.t27 +++ b/external/t27/specs/tools/mcp/needle.t27 @@ -35,7 +35,7 @@ pub const RESOURCES_ABOUT : [0]str = []; pub const TOOLS_NOTE : str = "Input schema keys were not extracted for this server (schemas are inline Zig literals); names and descriptions only. The .claude-plugin/.mcp.json command is an absolute path into a developer checkout; its home prefix is written as here (no home paths in specs)."; ; true when the server is a third-party package launched by npx or an outside binary. pub const EXTERNAL : bool = false; -pub const AGENTS : [0]str = []; -pub const AGENTS_NOTE : str = "No source binds an agent letter to this server: docs/agents/AGENTS_ALPHABET.md, specs/OWNERS.md and .claude/agents/*.md do not name it."; +pub const AGENTS : [2]str = ["T", "C"]; +pub const AGENTS_NOTE : str = "docs/agents/AGENTS_ALPHABET.md \"Who holds a tool, and who holds a skill\" binds every tool to the Queen (T). Domain lead C: Held: ABOUT is \"Zig MCP server for AST-aware code editing ... structural search and replace, quality gates, preview\" and needle_quality_gates is \"Run quality gates: parse check, AST analysis, violation detection\" -- parser/AST is C's ground (KEY_FILES t27/compiler/parser/, EXIT_INVARIANT \"Generated code compiles, AST is valid\"). No second letter: the card lists six tools, all editing/parse tools."; pub const WITNESS : str = "source-parse"; pub const ENABLED : bool = true; diff --git a/external/t27/specs/tools/mcp/trinity.t27 b/external/t27/specs/tools/mcp/trinity.t27 index a65b33a833..7440fc6656 100644 --- a/external/t27/specs/tools/mcp/trinity.t27 +++ b/external/t27/specs/tools/mcp/trinity.t27 @@ -35,7 +35,7 @@ pub const RESOURCES_ABOUT : [0]str = []; pub const TOOLS_NOTE : str = "Static list parsed from writeToolsList (210 entries; 32 entries have an unbalanced trailing brace in the source literal and were parsed after adding or removing one). Cell-generated tools from data/cells/mcp_tools.json are appended at runtime and are not listed here. .trinity/mcp_schemas.json lists a different, shorter set (29 names) and is not used as the source."; ; true when the server is a third-party package launched by npx or an outside binary. pub const EXTERNAL : bool = false; -pub const AGENTS : [0]str = []; -pub const AGENTS_NOTE : str = "No source binds an agent letter to this server: docs/agents/AGENTS_ALPHABET.md, specs/OWNERS.md and .claude/agents/*.md do not name it."; +pub const AGENTS : [2]str = ["T", "B"]; +pub const AGENTS_NOTE : str = "docs/agents/AGENTS_ALPHABET.md \"Who holds a tool, and who holds a skill\" binds every tool to the Queen (T). Domain lead B: Changed from B,O to B: ABOUT leads with \"exposes the tri pipeline, Golden Chain links\", and the chain_* tools run \"Chain Link 0\" through \"Chain Link 25\" (spec, codegen, test, bench, fix, docs, verdict, git, loop, \"Chain Link 20: Auto-deploy to cloud\") -- B's DOMAIN is literally Build / Pipeline with KEY_FILES build.tri and src/tri/pipeline/. O was dropped: the coordination tools the O case rested on are swarm_task_add / swarm_assign / job_start / job_cancel, which fit J's \"all jobs assigned\" at least as well, and the same ABOUT also lists swarm, cloud/farm, FPGA, doctor, issue, deploy, experience, oracle and needle -- a dozen subjects, so no second letter can claim it over the rest."; pub const WITNESS : str = "source-parse"; pub const ENABLED : bool = true; diff --git a/external/t27/specs/trinity/README.md b/external/t27/specs/trinity/README.md index 094c6a7db4..412cb2feed 100644 --- a/external/t27/specs/trinity/README.md +++ b/external/t27/specs/trinity/README.md @@ -45,21 +45,25 @@ owns its contract. ``` python3 tools/trinity_manifest.py inventory --trinity-root python3 tools/trinity_manifest.py check -python3 tools/trinity_manifest.py --self-check # negative control: thirteen planted defects, each reported +python3 tools/trinity_manifest.py --self-check # negative control: sixteen planted defects, each reported, and the comment and vendored-copy rules end to end ``` `inventory` refuses a tree with a modified tracked file. It reads `build.zig` (every `addExecutable` / `addTest` / `addLibrary`, every `b.step`, every `installArtifact` and the `if` that guards it), `build.zig.zon`, `.gitmodules`, the tracked tree (one entry per directory), the `.t27` / `.tri` / -`.vibee` / `.zig` counts with the website mirror (`apps/website/public/t27/files/`) set apart, +`.vibee` / `.zig` counts with the website mirror (`apps/website/public/t27/files/`) and the +consumer's vendored copies of this repository's contracts (`external/t27/`) each set apart, the reachability of every `.zig` file from the files `build.zig` names through relative -`@import`, `.trinity/registry.json` and the vendored catalog counts. +`@import`, `.trinity/registry.json` and the vendored catalog counts. `build.zig` and the `.zig` +files are read with their `//` comments blanked in place; a `//` inside a string or a multiline +string line is kept, so a step, an install or an `@import` that exists only in a comment is not +counted. `check` fails on: a pinned revision or a count that differs from the inventory; a dirty inventory; a target `build.zig` defines that no card owns, or two cards own, or a card owns that the build does not define; a default-installed target on a non-headless card or a `!ci_mode`-guarded target on a headless one; a `trinity:` path that is not tracked; a mirrored -file cited as canonical; a `DIALECT` that disagrees with the extension of `CANONICAL_SPEC`; a +or vendored file cited as canonical; a `DIALECT` that disagrees with the extension of `CANONICAL_SPEC`; a backend claimed by a card that is not executable, adapter or research; two owners for one canonical spec; two cards with one `ID`; `EVIDENCE = "measured"` without `ACCEPTANCE` and `EVIDENCE_SOURCE`; a work package without a card or a card naming a package the project does @@ -97,13 +101,18 @@ in a repository or a public CI log, command and revision recorded in `EVIDENCE_S path is a file of this repository; `:` names another repository and is recorded, not checked. -## What the inventory measured at gHashTag/trinity@976df517 (2026-09-12) - -- 51 executables, 6 libraries, 73 tests and 68 steps in `build.zig`; 46 targets installed by - `zig build -Dci=true`, 5 guarded by `!ci_mode` (the raylib canvas and the node GUI). -- 31 `.t27` files outside the website mirror, 1044 inside it; 764 `.tri`; 1981 `.vibee` (1428 of - them under `deploy/trinity-nexus`); 2830 `.zig`, of which 749 are reachable from the 173 files - `build.zig` names and 2081 are not. +## What the inventory measured at gHashTag/trinity@976df517 (2026-09-12, corrected 2026-10-01) + +- 51 executables, 6 libraries, 73 tests and 66 steps in `build.zig`; 46 targets installed by + `zig build -Dci=true`, 3 guarded by `!ci_mode` (photon-demo, photon-immersive and the node GUI). + The steps `needle-mcp` and `trinity-mcp` and the installs of `trinity-canvas` and + `trinity-canvas-wasm-check` exist only in commented-out lines. Until 2026-10-01 the inventory + read comments and counted 68 steps and 5 guarded installs, and the two MCP cards owned the + two steps (gHashTag/trinity#989). +- 31 `.t27` files outside the website mirror, 1044 inside it, none under `external/t27/` at this + revision; 764 `.tri`; 1981 `.vibee` (1428 of them under `deploy/trinity-nexus`); 2830 `.zig`, of + which 748 are reachable from the 173 files `build.zig` names and 2082 are not (749 and 2081 + until 2026-10-01: one file is reached only through a commented-out `@import`). - Four pinned dependencies (`emsdk`, `raylib`, `zig_hdc`, `zig_golden_float`) and one submodule (`external/zig-golden-float`, a second, unpinned reference to the same repository). - 29 commands in `.trinity/registry.json`; the vendored catalog holds 856 distinct specs from 8 @@ -111,6 +120,12 @@ not checked. - The measured evidence of the headless profile is one public CI run (`Build & Test`, ubuntu-latest, zig 0.15.2) in which `zig build -Dci=true` succeeded; the test step is piped through `tee` there and its exit code is not measured (gHashTag/trinity#616). +- One card is ahead of the pin. `brain.regions` (2026-10-04, gHashTag/t27#5953) owns the eleven + brain test targets that gHashTag/trinity#1333 puts back into `build.zig`: five steps and six + `src/brain` test roots. None of them exists at this revision, so `check` reports eleven + `UNKNOWN_TARGET` findings, and `report.json` keeps them until S01 re-pins. Against an + inventory of gHashTag/trinity@291ac8b24, the head of #1333, the card has no finding and none of + the eleven is unassigned. `research.unreferenced-sources` no longer names `src/brain`. ## The compiler matrix (S02) @@ -551,7 +566,7 @@ python3 tools/trinity_queen_views.py --self-check --zig [--sysroot `specs/fpga/adapter.t27` (card: `trinity/fpga.adapter`) is the FPGA adapter contract of [gHashTag/t27#3573](https://github.com/gHashTag/t27/issues/3573): the versioned inputs a caller brings (bitstream path, sha256 tied to its -provenance, board identity under the full-IDCODE rule — the full 32-bit value +provenance, board identity under the full-IDCODE rule -- the full 32-bit value recorded beside the printed nibble-dropped form, so a masked match can never pass), the configuration (flasher, cable, sram/flash target), the eight distinct error statuses, and the receipt schema `trinity.fpga-receipt.v1` @@ -562,5 +577,5 @@ bit-exact` line. No hardware run may be inferred from synthesis (#3573's law). `tools/trinity_fpga_adapter.py check` holds the receipts of `conformance/trinity/fpga_adapter.json` to the contract and `self-check` plants every defect; the device receipts are the stage-2 runs of -dmitrii-f-t27/trinity-memory on the AX7203 — the golden chunk (dense5 and +dmitrii-f-t27/trinity-memory on the AX7203 -- the golden chunk (dense5 and baseline2, 320/320 Y lines bit-exact) and the #65 measurements. diff --git a/external/t27/specs/trinity/capabilities/brain.regions.t27 b/external/t27/specs/trinity/capabilities/brain.regions.t27 new file mode 100644 index 0000000000..3665ed72fa --- /dev/null +++ b/external/t27/specs/trinity/capabilities/brain.regions.t27 @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/trinity/capabilities/brain.regions.t27 -- capability trinity/brain.regions: The brain region tests of src/brain +; One card of the Trinity project manifest (specs/trinity/README.md, gHashTag/t27#3563 = S01 of +; gHashTag/trinity#988). tools/trinity_manifest.py check holds every card to the inventory of the +; pinned consumer tree gHashTag/trinity@976df5174: the targets it owns exist in build.zig and belong +; to no other card, the paths it names are tracked, the dialect matches the spec it cites and a +; measured claim names its command and its source. The consumer vendors a byte-identical copy and +; generates its manifest from these files (S12). ASCII only (L3), English only (LANG-EN). +; This card is the exception to the first of those: its eleven targets exist in build.zig from +; gHashTag/trinity#1333 on and not at the pin, so a check against the pinned inventory reports each +; of them as UNKNOWN_TARGET until S01 re-pins (gHashTag/t27#5953). Its paths do exist at the pin. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module trinity_capability_brain_regions; + +pub const KIND : str = "capability"; +pub const ID : str = "trinity/brain.regions"; +pub const TITLE : str = "The brain region tests of src/brain: the basal ganglia, reticular formation and locus coeruleus units, the aggregate and integration suite, and the stress suite"; +; Who owns the implementation this card describes. +pub const OWNER_REPO : str = "gHashTag/trinity"; +; executable | declared | adapter | external | deprecated | out-of-scope | research | catalog-only +pub const DISPOSITION : str = "executable"; +; headless | web | native | fpga | training | network | research; a target installed by +; zig build -Dci=true is headless, a target guarded by !ci_mode is not. +pub const PROFILE : str = "headless"; +; The canonical spec: a path in this repository, trinity: for a spec authored in the +; consumer, : elsewhere, "" while none exists. Never a mirrored copy. +pub const CANONICAL_SPEC : str = ""; +pub const DIALECT : str = "none"; +; Implementation and generated paths of the pinned tree (trinity: prefix), checked to exist. +pub const IMPLEMENTATION : [6]str = ["trinity:src/brain/basal_ganglia.zig", "trinity:src/brain/reticular_formation.zig", "trinity:src/brain/locus_coeruleus.zig", "trinity:src/brain/brain.zig", "trinity:src/brain/integration_test.zig", "trinity:src/brain/stress_test.zig"]; +pub const GENERATED : [0]str = []; +; zig | c | rust | verilog | wasm | ts | swift | python | none; only executable, adapter and research cards claim one. +pub const BACKEND : str = "zig"; +; build.zig targets this card owns: exe:, lib:, test:, step:; patterns allowed. +pub const TARGETS : [11]str = ["step:test-basal-ganglia", "step:test-reticular-formation", "step:test-locus-coeruleus", "step:test-brain", "step:test-brain-stress", "test:src/brain/basal_ganglia.zig", "test:src/brain/reticular_formation.zig", "test:src/brain/locus_coeruleus.zig", "test:src/brain/brain.zig", "test:src/brain/integration_test.zig", "test:src/brain/stress_test.zig"]; +; The command whose success is the acceptance of this capability; "" when nothing is claimed. +pub const ACCEPTANCE : str = "zig build test-basal-ganglia -Dci=true && zig build test-reticular-formation -Dci=true && zig build test-locus-coeruleus -Dci=true && zig build test-brain -Dci=true && zig build test-brain-stress -Dci=true"; +; measured | declared | specified | external | not-claimed, as docs/system/project.md defines them. +pub const EVIDENCE : str = "measured"; +pub const EVIDENCE_SOURCE : str = "https://github.com/gHashTag/trinity/actions/runs/37184075728 (push) and https://github.com/gHashTag/trinity/actions/runs/37184077352 (pull_request) -- brain-ci.yml on ubuntu-latest with zig 0.15.2 at gHashTag/trinity@291ac8b24 (the head of gHashTag/trinity#1333), each step run as zig build -Dci=true --summary all with its exit code as the gate, the same counts in both runs: test-basal-ganglia 51/51, test-reticular-formation 48/48, test-locus-coeruleus 52/52, test-brain 151/151 (138 from brain.zig and 13 from integration_test.zig), test-brain-stress 261/261 with 0 leaked. That commit is not the pinned 976df517, whose build.zig defines none of these targets."; +; The work package of gHashTag/trinity#988 that owns this capability's contract. +pub const WORK_PACKAGE : str = "S03"; +pub const NOTE : str = "S03 decides retire or attach for the sources research.unreferenced-sources names; gHashTag/trinity#1333 attached these six test roots, and this card takes src/brain off that list (gHashTag/t27#5953). At the pin build.zig names basal_ganglia.zig, locus_coeruleus.zig and reticular_formation.zig only as imported modules of another root, and defines no brain test or step. src/brain is handwritten Zig: specs/brain/brain.t27 lists its files in a comment table, but nothing generates them from it or checks them against it, so no canonical spec is claimed. Measured are these test steps, not a run of the brain inside a service."; +pub const ENABLED : bool = true; + +test card_is_bound_to_its_package { + assert KIND == "capability"; + assert ID == "trinity/brain.regions"; + assert WORK_PACKAGE == "S03"; + assert ENABLED == true; +} diff --git a/external/t27/specs/trinity/capabilities/mcp.needle-mcp.t27 b/external/t27/specs/trinity/capabilities/mcp.needle-mcp.t27 index 2b96f5bc5f..39a27d89b9 100644 --- a/external/t27/specs/trinity/capabilities/mcp.needle-mcp.t27 +++ b/external/t27/specs/trinity/capabilities/mcp.needle-mcp.t27 @@ -30,12 +30,12 @@ pub const GENERATED : [0]str = []; ; zig | c | rust | verilog | wasm | ts | swift | python | none; only executable, adapter and research cards claim one. pub const BACKEND : str = "zig"; ; build.zig targets this card owns: exe:, lib:, test:, step:; patterns allowed. -pub const TARGETS : [4]str = ["exe:needle-mcp", "step:needle-mcp", "step:needle-test", "test:src/needle/mod.zig"]; +pub const TARGETS : [3]str = ["exe:needle-mcp", "step:needle-test", "test:src/needle/mod.zig"]; ; The command whose success is the acceptance of this capability; "" when nothing is claimed. -pub const ACCEPTANCE : str = "zig build needle-mcp"; +pub const ACCEPTANCE : str = "zig build -Dci=true && test -x zig-out/bin/needle-mcp"; ; measured | declared | specified | external | not-claimed, as docs/system/project.md defines them. pub const EVIDENCE : str = "measured"; -pub const EVIDENCE_SOURCE : str = "https://github.com/gHashTag/trinity/actions/runs/34678824282 -- Build & Test on ubuntu-latest with zig 0.15.2 at 79ffb034 (the pinned 976df517 differs from it only in apps/website/src/data/signalHealth.json): zig build -Dci=true succeeded. zig build test is piped through tee there, so its exit code is not measured (gHashTag/trinity#616)."; +pub const EVIDENCE_SOURCE : str = "https://github.com/gHashTag/trinity/actions/runs/34678824282 -- Build & Test on ubuntu-latest with zig 0.15.2 at 79ffb034 (the pinned 976df517 differs from it only in apps/website/src/data/signalHealth.json): zig build -Dci=true succeeded, and that build installs zig-out/bin/needle-mcp. zig build test is piped through tee there, so its exit code is not measured (gHashTag/trinity#616). build.zig at the pin defines no step needle-mcp: its b.step line is commented out (lines 1481-1483, an interactive stdio service an MCP client starts), so zig build needle-mcp answers that no such step exists. Measured is the build of the server; no run of the server is."; ; The work package of gHashTag/trinity#988 that owns this capability's contract. pub const WORK_PACKAGE : str = "S06"; pub const NOTE : str = ""; diff --git a/external/t27/specs/trinity/capabilities/mcp.trinity-mcp.t27 b/external/t27/specs/trinity/capabilities/mcp.trinity-mcp.t27 index c14274709e..7dd0d20933 100644 --- a/external/t27/specs/trinity/capabilities/mcp.trinity-mcp.t27 +++ b/external/t27/specs/trinity/capabilities/mcp.trinity-mcp.t27 @@ -30,12 +30,12 @@ pub const GENERATED : [1]str = ["trinity:.trinity/mcp_schemas.json"]; ; zig | c | rust | verilog | wasm | ts | swift | python | none; only executable, adapter and research cards claim one. pub const BACKEND : str = "zig"; ; build.zig targets this card owns: exe:, lib:, test:, step:; patterns allowed. -pub const TARGETS : [2]str = ["exe:trinity-mcp", "step:trinity-mcp"]; +pub const TARGETS : [1]str = ["exe:trinity-mcp"]; ; The command whose success is the acceptance of this capability; "" when nothing is claimed. -pub const ACCEPTANCE : str = "zig build trinity-mcp"; +pub const ACCEPTANCE : str = "zig build -Dci=true && test -x zig-out/bin/trinity-mcp"; ; measured | declared | specified | external | not-claimed, as docs/system/project.md defines them. pub const EVIDENCE : str = "measured"; -pub const EVIDENCE_SOURCE : str = "https://github.com/gHashTag/trinity/actions/runs/34678824282 -- Build & Test on ubuntu-latest with zig 0.15.2 at 79ffb034 (the pinned 976df517 differs from it only in apps/website/src/data/signalHealth.json): zig build -Dci=true succeeded. zig build test is piped through tee there, so its exit code is not measured (gHashTag/trinity#616)."; +pub const EVIDENCE_SOURCE : str = "https://github.com/gHashTag/trinity/actions/runs/34678824282 -- Build & Test on ubuntu-latest with zig 0.15.2 at 79ffb034 (the pinned 976df517 differs from it only in apps/website/src/data/signalHealth.json): zig build -Dci=true succeeded, and that build installs zig-out/bin/trinity-mcp. zig build test is piped through tee there, so its exit code is not measured (gHashTag/trinity#616). build.zig at the pin defines no step trinity-mcp: its b.step line is commented out (lines 1525-1527, an interactive stdio service an MCP client starts), so zig build trinity-mcp answers that no such step exists. Measured is the build of the server; no run of the server is."; ; The work package of gHashTag/trinity#988 that owns this capability's contract. pub const WORK_PACKAGE : str = "S06"; pub const NOTE : str = "The t27 tool card records 210 static tool literals, 32 with an unbalanced trailing brace in the source; .trinity/mcp_schemas.json (29 names) disagrees with the code and the code is the source. S06: specs/tools/mcp_protocol.t27 states the server's protocol behaviour (substring routing, no negotiation, 210 advertised tools of which 32 are malformed JSON, failures as isError results, unknown tools run through the generic executor, cancellation and permissions declared but not imported); conformance/trinity/mcp_fixtures.json holds the offline fixtures."; diff --git a/external/t27/specs/trinity/capabilities/research.unreferenced-sources.t27 b/external/t27/specs/trinity/capabilities/research.unreferenced-sources.t27 index 32df21cdc2..2fbb7aac07 100644 --- a/external/t27/specs/trinity/capabilities/research.unreferenced-sources.t27 +++ b/external/t27/specs/trinity/capabilities/research.unreferenced-sources.t27 @@ -12,7 +12,7 @@ module trinity_capability_research_unreferenced_sources; pub const KIND : str = "capability"; pub const ID : str = "trinity/research.unreferenced-sources"; -pub const TITLE : str = "Source directories that no file named by build.zig reaches through @import: src/phi-engine, src/sacred, src/consciousness, src/brain, src/tvc, src/storm and others"; +pub const TITLE : str = "Source directories that no file named by build.zig reaches through @import: src/phi-engine, src/sacred, src/consciousness, src/tvc, src/storm and others"; ; Who owns the implementation this card describes. pub const OWNER_REPO : str = "gHashTag/trinity"; ; executable | declared | adapter | external | deprecated | out-of-scope | research | catalog-only @@ -25,7 +25,7 @@ pub const PROFILE : str = "headless"; pub const CANONICAL_SPEC : str = ""; pub const DIALECT : str = "none"; ; Implementation and generated paths of the pinned tree (trinity: prefix), checked to exist. -pub const IMPLEMENTATION : [6]str = ["trinity:src/phi-engine", "trinity:src/sacred", "trinity:src/consciousness", "trinity:src/brain", "trinity:src/tvc", "trinity:src/storm"]; +pub const IMPLEMENTATION : [5]str = ["trinity:src/phi-engine", "trinity:src/sacred", "trinity:src/consciousness", "trinity:src/tvc", "trinity:src/storm"]; pub const GENERATED : [0]str = []; ; zig | c | rust | verilog | wasm | ts | swift | python | none; only executable, adapter and research cards claim one. pub const BACKEND : str = "zig"; @@ -35,10 +35,10 @@ pub const TARGETS : [0]str = []; pub const ACCEPTANCE : str = ""; ; measured | declared | specified | external | not-claimed, as docs/system/project.md defines them. pub const EVIDENCE : str = "not-claimed"; -pub const EVIDENCE_SOURCE : str = "tools/trinity_manifest.py inventory: of 2830 tracked .zig files, 749 are reachable from the 173 files build.zig names and 2081 are not; the per-directory counts are in conformance/trinity/inventory.json (zig_reachability). S03 decides retire or attach."; +pub const EVIDENCE_SOURCE : str = "tools/trinity_manifest.py inventory: of 2830 tracked .zig files, 748 are reachable from the 173 files build.zig names and 2082 are not (until 2026-10-01 the walk read comments and counted 749: src/background_agent/db/issue_bindings.zig is reached only through a commented-out @import); the per-directory counts are in conformance/trinity/inventory.json (zig_reachability). S03 decides retire or attach."; ; The work package of gHashTag/trinity#988 that owns this capability's contract. pub const WORK_PACKAGE : str = "S01"; -pub const NOTE : str = ""; +pub const NOTE : str = "src/brain was named here until 2026-10-04 (gHashTag/t27#5953). From gHashTag/trinity#1333 on, build.zig names six of its files as test roots, owned by trinity/brain.regions, so it is no longer a directory build.zig does not reach. Not all of it is reached: at the pin 43 of its 46 .zig files are unreachable and are among the 2082 above, and at gHashTag/trinity@291ac8b24 (the head of #1333) 37 of 46 are."; pub const ENABLED : bool = true; test card_is_bound_to_its_package { diff --git a/external/t27/specs/trinity/project.t27 b/external/t27/specs/trinity/project.t27 index 858619da64..709f33b34d 100644 --- a/external/t27/specs/trinity/project.t27 +++ b/external/t27/specs/trinity/project.t27 @@ -31,21 +31,28 @@ pub const DISPOSITIONS : [8]str = ["executable", "declared", "adapter", "externa pub const EVIDENCE_TAGS : [5]str = ["measured", "declared", "specified", "external", "not-claimed"]; ; The three spec dialects the consumer carries; only t27 is compiled by t27c. pub const DIALECTS : [4]str = ["t27", "tri", "vibee", "none"]; -; Counted by the inventory over tracked files; the website mirror is never canonical. +; Counted by the inventory over tracked files. Neither the website mirror nor the consumer's +; vendored copies of this repository's contracts (external/t27/, none at the pinned revision) +; is ever canonical; each is counted apart. pub const T27_CANONICAL_FILES : u16 = 31; pub const T27_MIRROR_FILES : u16 = 1044; +pub const T27_VENDORED_FILES : u16 = 0; pub const TRI_FILES : u16 = 764; pub const VIBEE_FILES : u16 = 1981; pub const ZIG_FILES : u16 = 2830; -; .zig files no file named by build.zig reaches through a relative @import. -pub const ZIG_UNREACHABLE_FILES : u16 = 2081; -; build.zig at the pinned revision: artifacts, steps, and what zig build -Dci=true installs. +; .zig files no file named by build.zig reaches through a relative @import; an @import in a +; comment reaches nothing (until 2026-10-01 one did, and this read 2081). +pub const ZIG_UNREACHABLE_FILES : u16 = 2082; +; build.zig at the pinned revision, read from its code and not from its comments: artifacts, +; steps, and what zig build -Dci=true installs. The steps needle-mcp and trinity-mcp and the +; installs of trinity-canvas and trinity-canvas-wasm-check exist only in commented-out lines; +; until 2026-10-01 they were counted, and these read 68 steps and 5 guarded installs. pub const BUILD_EXECUTABLES : u8 = 51; pub const BUILD_LIBRARIES : u8 = 6; pub const BUILD_TESTS : u8 = 73; -pub const BUILD_STEPS : u8 = 68; +pub const BUILD_STEPS : u8 = 66; pub const INSTALLED_BY_DEFAULT : u8 = 46; -pub const INSTALLED_GUARDED : u8 = 5; +pub const INSTALLED_GUARDED : u8 = 3; ; build.zig.zon dependencies and their pins, in the order the file declares them; .gitmodules paths. pub const DEPENDENCIES : [4]str = ["emsdk", "raylib", "zig_hdc", "zig_golden_float"]; pub const DEPENDENCY_PINS : [4]str = ["git+https://github.com/emscripten-core/emsdk?ref=4.0.9#3bcf1dcd01f040f370e10fe673a092d9ed79ebb5", "git+https://github.com/raylib-zig/raylib-zig#cd71c85d571027ac8033357f83b124ee051825b3", "https://github.com/gHashTag/zig-hdc/archive/b73b2fa29874f4e30297f167a6ea8a3c2ded9d32.tar.gz", "https://github.com/gHashTag/zig-golden-float/archive/e7ce32885de2a8c50b7b6a3030d0592202145dd1.tar.gz"]; @@ -82,7 +89,7 @@ test most_of_the_zig_tree_is_not_reached_by_the_build_graph { test the_default_install_set_is_a_subset_of_the_artifacts { assert INSTALLED_BY_DEFAULT + INSTALLED_GUARDED <= BUILD_EXECUTABLES + BUILD_LIBRARIES; - assert INSTALLED_GUARDED == 5; + assert INSTALLED_GUARDED == 3; } test the_epic_has_twelve_packages_and_two_pinned_numeric_packages { diff --git a/external/t27/tools/trinity_manifest.py b/external/t27/tools/trinity_manifest.py index 56f7f42142..2f77c0e4d5 100644 --- a/external/t27/tools/trinity_manifest.py +++ b/external/t27/tools/trinity_manifest.py @@ -17,15 +17,18 @@ Checked (exit 1 on any finding, each with an exact path or name): * the project spec pins the revision the inventory was taken from, and the inventory was taken from a tree with no modified tracked file (a dirty inventory is refused); - * the dialect counts the project spec states equal the counts in the inventory, and the - website mirror (apps/website/public/t27/files/) is never counted as canonical; + * the dialect counts the project spec states equal the counts in the inventory, and neither + the website mirror (apps/website/public/t27/files/) nor the consumer's vendored copies of + this repository's contracts (external/t27/, locked by its specs/reproduce/contracts.t27) + is ever counted as canonical -- each is counted apart; * every executable, library, test and step of build.zig is assigned to exactly one card (patterns are allowed, `test:src/trinity_node/*`); a card cannot own a target that the build does not define; * a target installed by default (`zig build -Dci=true`) belongs to a headless card, a target guarded by `!ci_mode` does not; * every `trinity:` path a card names is a tracked file or directory of the pinned tree; - a t27 path it names exists in this repository; a mirror path is never canonical; + a t27 path it names exists in this repository; a mirror or vendored path is never + canonical; * DIALECT agrees with the extension of CANONICAL_SPEC; BACKEND is one of the compiler's targets or "none", and only executable, adapter and research cards may claim one; * two cards cannot claim the same canonical spec under different owners, and no two @@ -40,6 +43,22 @@ compiler remains the authority on the files themselves -- t27c parses, typechecks and seals them in the other gates, and this tool refuses a card the grammar cannot read. +HOW THE ZIG SOURCES ARE READ +---------------------------- +build.zig and every .zig file the reachability walk opens are read with their `//` +comments blanked first (`///` and `//!` included), byte for byte, so every offset and line +number is the file's own. String literals, character literals and multiline string lines +(the ones opened by two backslashes) are kept whole: a `//` inside them is text, not a +comment. A step, an install, a path or an @import that exists only in a comment is +therefore not counted. Until 2026-10-01 they were: at gHashTag/trinity@976df517 the steps +needle-mcp and trinity-mcp, the installs of trinity-canvas and trinity-canvas-wasm-check, and +one reachable file (src/background_agent/db/issue_bindings.zig, through a commented @import) +existed only in comments, and two cards owned the steps and named `zig build needle-mcp` / +`zig build trinity-mcp` as their acceptance -- commands that answer "no step named" +(gHashTag/trinity#989). Not done: the content of a multiline string is still text the +patterns can match; build.zig carries no multiline string at the pin, and blanking those +lines as well changes no reachability there (measured). + Usage: python3 tools/trinity_manifest.py inventory --trinity-root DIR [--out conformance/trinity/inventory.json] python3 tools/trinity_manifest.py check [--inventory PATH] [--specs specs/trinity] [--report conformance/trinity/report.json] @@ -67,6 +86,11 @@ REPORT = "conformance/trinity/report.json" SPECS = "specs/trinity" MIRROR = "apps/website/public/t27/files/" +# The consumer's byte-identical copies of this repository's contracts (specs/trinity/**, the +# canonical specs the cards name and this checker), locked by its specs/reproduce/contracts.t27 +# (S12). They are t27's files, not the consumer's own specs: counted apart, never canonical. +VENDORED = "external/t27/" +NEVER_CANONICAL = (MIRROR, VENDORED) CONSUMER = "gHashTag/trinity" DISPOSITIONS = ("executable", "declared", "adapter", "external", "deprecated", "out-of-scope", "research", "catalog-only") @@ -79,7 +103,7 @@ "KIND": "str", "ID": "str", "NAME": "str", "CONSUMER_REPO": "str", "PINNED_REVISION": "str", "PINNED_AT": "str", "T27_REVISION": "str", "PROFILES": "arr", "INITIAL_PROFILE": "str", "DISPOSITIONS": "arr", "EVIDENCE_TAGS": "arr", "DIALECTS": "arr", - "T27_CANONICAL_FILES": "u16", "T27_MIRROR_FILES": "u16", "TRI_FILES": "u16", "VIBEE_FILES": "u16", + "T27_CANONICAL_FILES": "u16", "T27_MIRROR_FILES": "u16", "T27_VENDORED_FILES": "u16", "TRI_FILES": "u16", "VIBEE_FILES": "u16", "ZIG_FILES": "u16", "ZIG_UNREACHABLE_FILES": "u16", "BUILD_EXECUTABLES": "u8", "BUILD_LIBRARIES": "u8", "BUILD_TESTS": "u8", "BUILD_STEPS": "u8", "INSTALLED_BY_DEFAULT": "u8", "INSTALLED_GUARDED": "u8", @@ -169,7 +193,40 @@ def root_source(fields: dict) -> str | None: return None +def blank_comments(src: str) -> str: + """Zig source with every `//` comment (`///` and `//!` included) replaced by spaces, one + for one, so every offset and line number of the result is the source's own. + + A `//` is a comment only outside a literal. String and character literals are skipped + with their escapes (a quote held in '"' must not open a string); a multiline string + line -- opened by two backslashes, running to the end of its line -- is skipped whole.""" + out = list(src) + i, n = 0, len(src) + while i < n: + c = src[i] + if src.startswith("//", i): + end = src.find("\n", i) + end = n if end < 0 else end + for k in range(i, end): + out[k] = " " + i = end + elif src.startswith("\\\\", i): + end = src.find("\n", i) + i = n if end < 0 else end + elif c in "\"'": + i += 1 + while i < n and src[i] != c: + i += 2 if src[i] == "\\" else 1 + i += 1 + else: + i += 1 + return "".join(out) + + def parse_build_zig(text: str) -> dict: + # A step, an artifact, an install, an option or a path written only in a comment is not + # declared by the build: read the code, with the comments blanked in place. + text = blank_comments(text) lines = text.split("\n") artifacts, seen = [], set() for m in re.finditer(r"(?:const|var)\s+(\w+)\s*=\s*b\.(addExecutable|addTest|addLibrary|addStaticLibrary|addSharedLibrary|addObject)\(", text): @@ -211,7 +268,11 @@ def guards(line_idx: int) -> list[str]: g = guards(line - 1) installs.append({"var": m.group(1), "key": art["key"] if art else None, "name": art["name"] if art else m.group(1), "line": line, "guards": g, "default": not g}) - return {"artifacts": artifacts, "steps": steps, "options": options, "installs": installs} + # Every .zig file build.zig names: artifact roots and the roots of named modules + # (`b.createModule(.{ .root_source_file = b.path("...") })`) that reach an artifact + # through `.imports` rather than through a relative @import. + zig_paths = sorted({m.group(1) for m in re.finditer(r'b\.path\("([^"]+\.zig)"\)', text)}) + return {"artifacts": artifacts, "steps": steps, "options": options, "installs": installs, "zig_paths": zig_paths} def parse_zon(text: str) -> dict: @@ -232,7 +293,8 @@ def parse_gitmodules(text: str) -> list[dict]: def zig_reachability(root: pathlib.Path, files: list[str], roots: list[str]) -> dict: """Files reachable from the roots build.zig names through relative @import("x.zig"); - package/module imports (no .zig suffix) are recorded by name, not followed.""" + package/module imports (no .zig suffix) are recorded by name, not followed. An @import + in a comment imports nothing: each file is read with its comments blanked.""" tracked = set(files) cache: dict[str, list[str]] = {} modules: set[str] = set() @@ -242,7 +304,7 @@ def imports_of(path: str) -> list[str]: return cache[path] out = [] try: - text = (root / path).read_text(encoding="utf-8", errors="replace") + text = blank_comments((root / path).read_text(encoding="utf-8", errors="replace")) except OSError: cache[path] = out return out @@ -298,6 +360,20 @@ def read_json(path: pathlib.Path): return None +def dialects_of(files: list[str]) -> dict: + """The dialect counts of a tracked file list. A .t27 file under the website mirror or under + the vendored copies of t27's contracts is counted apart, and is never canonical.""" + ext = lambda e: [f for f in files if f.endswith("." + e)] + t27_all = ext("t27") + t27_canonical = [f for f in t27_all if not f.startswith(NEVER_CANONICAL)] + return { + "t27_canonical": len(t27_canonical), "t27_mirror": len([f for f in t27_all if f.startswith(MIRROR)]), + "t27_vendored": len([f for f in t27_all if f.startswith(VENDORED)]), + "tri": len(ext("tri")), "vibee": len(ext("vibee")), "zig": len(ext("zig")), + "mirror_prefix": MIRROR, "vendored_prefix": VENDORED, "t27_canonical_files": t27_canonical, + } + + def inventory(root: pathlib.Path) -> dict: if not (root / ".git").exists() and not (root / ".git").is_file(): raise SystemExit(f"trinity_manifest: {root} is not a git checkout") @@ -311,15 +387,9 @@ def inventory(root: pathlib.Path) -> dict: zon = parse_zon((root / "build.zig.zon").read_text(encoding="utf-8")) gitmodules = (root / ".gitmodules") submodules = parse_gitmodules(gitmodules.read_text(encoding="utf-8")) if gitmodules.exists() else [] - ext = lambda e: [f for f in files if f.endswith("." + e)] - t27_all, tri, vibee, zig = ext("t27"), ext("tri"), ext("vibee"), ext("zig") - t27_mirror = [f for f in t27_all if f.startswith(MIRROR)] - t27_canonical = [f for f in t27_all if not f.startswith(MIRROR)] # Every file build.zig names is part of the declared build graph: artifact roots and the - # roots of named modules (`b.createModule(.{ .root_source_file = b.path("...") })`) that - # reach an artifact through `.imports` rather than through a relative @import. - build_text = (root / "build.zig").read_text(encoding="utf-8") - roots = sorted({a["root"] for a in build["artifacts"] if a["root"]} | {m.group(1) for m in re.finditer(r'b\.path\("([^"]+\.zig)"\)', build_text)}) + # roots of named modules, read from the code of build.zig and never from its comments. + roots = sorted({a["root"] for a in build["artifacts"] if a["root"]} | set(build["zig_paths"])) reach = zig_reachability(root, files, roots) reach["roots_named_by_build_zig"] = len(roots) registry = read_json(root / ".trinity/registry.json") or {} @@ -337,10 +407,7 @@ def inventory(root: pathlib.Path) -> dict: "committed_at": git(root, "log", "-1", "--format=%cI", "HEAD").strip(), "tracked_files": len(files), "top_level": dict(sorted(top.items(), key=lambda kv: (-kv[1], kv[0]))), - "dialects": { - "t27_canonical": len(t27_canonical), "t27_mirror": len(t27_mirror), "tri": len(tri), "vibee": len(vibee), "zig": len(zig), - "mirror_prefix": MIRROR, "t27_canonical_files": t27_canonical, - }, + "dialects": dialects_of(files), "build": { "options": build["options"], "executables": [a for a in build["artifacts"] if a["kind"] == "exe"], @@ -470,6 +537,11 @@ def path_exists(inv: dict, ref: str, t27_root: pathlib.Path) -> bool: def check(inv: dict, specs_dir: pathlib.Path, t27_root: pathlib.Path) -> tuple[list, dict]: findings: list[tuple[str, str]] = [] + if "t27_vendored" not in inv.get("dialects", {}): + # Written before the vendored copies were counted apart, so its canonical count may + # include them: judging it by the current rules would compare two different populations. + return [("UNREADABLE", "the inventory carries no dialects.t27_vendored: it was taken by a checker that counted the " + "vendored copies of t27 (external/t27/) as canonical; take it again with `inventory --trinity-root DIR`")], {} project_path = specs_dir / "project.t27" if not project_path.exists(): return [("MISSING_PROJECT", f"{project_path} does not exist")], {} @@ -488,7 +560,8 @@ def check(inv: dict, specs_dir: pathlib.Path, t27_root: pathlib.Path) -> tuple[l findings.append(("PIN_MISMATCH", f"project.t27 names {p.get('CONSUMER_REPO')}, the inventory is of {inv.get('repo')}")) d, b = inv["dialects"], inv["build"] counts = { - "T27_CANONICAL_FILES": d["t27_canonical"], "T27_MIRROR_FILES": d["t27_mirror"], "TRI_FILES": d["tri"], "VIBEE_FILES": d["vibee"], + "T27_CANONICAL_FILES": d["t27_canonical"], "T27_MIRROR_FILES": d["t27_mirror"], "T27_VENDORED_FILES": d["t27_vendored"], + "TRI_FILES": d["tri"], "VIBEE_FILES": d["vibee"], "ZIG_FILES": d["zig"], "ZIG_UNREACHABLE_FILES": inv["zig_reachability"]["unreachable"], "BUILD_EXECUTABLES": len({a["key"] for a in b["executables"]}), "BUILD_LIBRARIES": len({a["key"] for a in b["libraries"]}), "BUILD_TESTS": len({a["key"] for a in b["tests"]}), "BUILD_STEPS": len(b["steps"]), @@ -565,6 +638,9 @@ def check(inv: dict, specs_dir: pathlib.Path, t27_root: pathlib.Path) -> tuple[l if spec_ref: if spec_ref.startswith("trinity:" + MIRROR): findings.append(("MIRROR_AS_CANONICAL", f"{f}: CANONICAL_SPEC {spec_ref} is a mirrored copy, not a source")) + if spec_ref.startswith("trinity:" + VENDORED): + findings.append(("MIRROR_AS_CANONICAL", f"{f}: CANONICAL_SPEC {spec_ref} is the consumer's vendored copy of a t27 " + "contract, not a source; name the path in this repository")) if not path_exists(inv, spec_ref, t27_root): findings.append(("ABSENT_PATH", f"{f}: CANONICAL_SPEC {spec_ref} does not exist")) ext_map = {".t27": "t27", ".tri": "tri", ".vibee": "vibee"} @@ -616,9 +692,12 @@ def check(inv: dict, specs_dir: pathlib.Path, t27_root: pathlib.Path) -> tuple[l "version": 1, "generated_by": "tools/trinity_manifest.py check", "consumer": {"repo": inv.get("repo"), "sha": inv.get("sha"), "committed_at": inv.get("committed_at")}, - "canonical_specs": {"t27_files_in_trinity_outside_the_mirror": d["t27_canonical"], "tri_files": d["tri"], "vibee_files": d["vibee"], + "canonical_specs": {"t27_files_in_trinity_outside_the_mirror_and_the_vendored_contracts": d["t27_canonical"], "tri_files": d["tri"], + "vibee_files": d["vibee"], "note": "files of three dialects; only .t27 is compiled by t27c, counts cannot be added into a generation claim"}, "mirrored_files": {"t27_mirror": d["t27_mirror"], "prefix": MIRROR, "note": "vendored copies of other repositories' specs; never canonical"}, + "vendored_contracts": {"t27_vendored": d["t27_vendored"], "prefix": VENDORED, + "note": "byte-identical copies of this repository's contracts that the consumer locks (S12); never canonical"}, "catalog_snapshot": inv["catalog_snapshot"], "build": {k: counts[k] for k in ("BUILD_EXECUTABLES", "BUILD_LIBRARIES", "BUILD_TESTS", "BUILD_STEPS", "INSTALLED_BY_DEFAULT", "INSTALLED_GUARDED")}, "zig_unreachable": {"count": inv["zig_reachability"]["unreachable"], "by_dir": inv["zig_reachability"]["unreachable_by_dir"]}, @@ -652,6 +731,7 @@ def check(inv: dict, specs_dir: pathlib.Path, t27_root: pathlib.Path) -> tuple[l pub const DIALECTS : [4]str = ["t27", "tri", "vibee", "none"]; pub const T27_CANONICAL_FILES : u16 = 1; pub const T27_MIRROR_FILES : u16 = 1; +pub const T27_VENDORED_FILES : u16 = 0; pub const TRI_FILES : u16 = 0; pub const VIBEE_FILES : u16 = 0; pub const ZIG_FILES : u16 = 2; @@ -697,10 +777,11 @@ def check(inv: dict, specs_dir: pathlib.Path, t27_root: pathlib.Path) -> tuple[l """ -def fixture_inventory(sha: str, dirty: bool = False) -> dict: +def fixture_inventory(sha: str, dirty: bool = False, vendored: int = 0) -> dict: return {"version": 1, "repo": CONSUMER, "sha": sha, "dirty": dirty, "committed_at": "2026-09-12T00:00:00Z", "tracked_files": 4, - "tree": tree_of(["build.zig", "src/main.zig", "src/lost.zig", "specs/a.t27", MIRROR + "x.t27"]), - "dialects": {"t27_canonical": 1, "t27_mirror": 1, "tri": 0, "vibee": 0, "zig": 2, "mirror_prefix": MIRROR, "t27_canonical_files": ["specs/a.t27"]}, + "tree": tree_of(["build.zig", "src/main.zig", "src/lost.zig", "specs/a.t27", MIRROR + "x.t27", VENDORED + "specs/a.t27"]), + "dialects": {"t27_canonical": 1, "t27_mirror": 1, "t27_vendored": vendored, "tri": 0, "vibee": 0, "zig": 2, "mirror_prefix": MIRROR, + "vendored_prefix": VENDORED, "t27_canonical_files": ["specs/a.t27"]}, "build": {"options": [], "executables": [{"key": "exe:tri", "kind": "exe", "name": "tri", "root": "src/main.zig"}], "libraries": [], "tests": [], "steps": [{"key": "step:tri", "name": "tri"}], "installs": [], "installed_by_default": ["exe:tri"], "installed_guarded": []}, "dependencies": {"dependencies": [{"name": "zig_hdc", "url": "https://example.invalid/zig-hdc.tar.gz", "hash": "x"}]}, @@ -723,12 +804,136 @@ def card(**kw) -> str: return fill(GOOD_CARD, **base) +# A build.zig in which each shape the comment blanker must tell apart occurs once: a step, a +# path and an install that exist only in comments (`//`, `///`, `//!`); a `//` inside a string, +# inside a string after an escaped quote, and inside a multiline string line; a character +# literal that holds a quote, with a comment after it; and real code after all of them. +BUILD_FIXTURE = r'''const std = @import("std"); +pub fn build(b: *std.Build) void { + const ci_mode = b.option(bool, "ci", "CI mode") orelse false; + const tri = b.addExecutable(.{ .name = "tri", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig") }) }); + b.installArtifact(tri); + const tri_step = b.step("tri", "Run tri; the manual is at https://example.invalid/tri"); + tri_step.dependOn(&b.addRunArtifact(tri).step); + // const ghost_step = b.step("ghost", "a step that exists only in a comment"); + /// b.step("ghost-doc", "in a doc comment"); + //! b.step("ghost-top", "in a top-level doc comment"); + const quote = '"'; // b.step("ghost-char", "after a character literal that holds a quote"); + const said = "an escaped \" and then // which is still inside the string"; + const usage = + \\zig build tri // a multiline string line, not a comment + ; + _ = .{ quote, said, usage }; + // _ = b.path("src/ghost.zig"); + if (!ci_mode) { + const gui = b.addExecutable(.{ .name = "gui", .root_module = b.createModule(.{ .root_source_file = b.path("src/gui.zig") }) }); + // b.installArtifact(gui); + _ = gui; + } +} +''' +# The .zig files of that build: one real @import and one that exists only in a comment. +ZIG_FIXTURE = { + "src/main.zig": 'const real = @import("real.zig");\n// const ghost = @import("ghost.zig");\npub fn main() void {}\n', + "src/real.zig": "pub const x = 1;\n", + "src/ghost.zig": "pub const y = 2;\n", + "src/gui.zig": "pub fn main() void {}\n", +} + + +def parser_self_check() -> list[str]: + """The comment blanker and parse_build_zig, on BUILD_FIXTURE: what is in a comment is gone, + what is in a literal is kept, and every line stays where it was.""" + failures = [] + src = BUILD_FIXTURE + blank = blank_comments(src) + if len(blank) != len(src) or [i for i, c in enumerate(blank) if c == "\n"] != [i for i, c in enumerate(src) if c == "\n"]: + failures.append("blanking comments must keep every offset and every line break of the source") + kept = [ln for ln in src.split("\n") if "still inside the string" in ln or "a multiline string line" in ln] + if len(kept) != 2: + failures.append(f"the fixture must carry the two literal lines this control reads, found {len(kept)}") + for ln in kept: + if ln not in blank.split("\n"): + failures.append(f"a `//` inside a literal is text, and this line was changed: {ln.strip()}") + b = parse_build_zig(src) + steps = [s["key"] for s in b["steps"]] + if steps != ["step:tri"]: + failures.append(f"a step in a comment (//, ///, //!, after a quote held in a character literal) is no step, got {steps}") + tri = next((s for s in b["steps"] if s["name"] == "tri"), {}) + if tri.get("description") != "Run tri; the manual is at https://example.invalid/tri": + failures.append(f"a `//` inside a string is not a comment: the step description reads {tri.get('description')!r}") + if tri.get("line") != src[: src.index('b.step("tri"')].count("\n") + 1: + failures.append(f"the step must keep the line it has in the source, got {tri.get('line')}") + installs = [(i["key"], i["default"]) for i in b["installs"]] + if installs != [("exe:tri", True)]: + failures.append(f"an installArtifact in a comment installs nothing, got {installs}") + artifacts = sorted(a["key"] for a in b["artifacts"]) + if artifacts != ["exe:gui", "exe:tri"]: + failures.append(f"the code after the comments must still be read, got artifacts {artifacts}") + if b["zig_paths"] != ["src/gui.zig", "src/main.zig"]: + failures.append(f"a b.path in a comment names no file, got {b['zig_paths']}") + return failures + + +def inventory_self_check(tmp: pathlib.Path) -> list[str]: + """inventory() end to end over a planted consumer repository, so the wiring is held and not + only the functions it calls: the comments of build.zig and of the sources are not read, + and a .t27 file under the mirror or under the vendored copies is not canonical.""" + failures = [] + repo = tmp / "consumer" + tree = dict(ZIG_FIXTURE) + tree.update({ + "build.zig": BUILD_FIXTURE, + "build.zig.zon": '.{ .name = .fixture, .version = "0.0.0", .dependencies = .{ .zig_hdc = .{ .url = "https://example.invalid/zig-hdc.tar.gz", .hash = "x" } } }\n', + "specs/a.t27": "module a;\n", MIRROR + "x.t27": "module x;\n", VENDORED + "specs/trinity/project.t27": "module trinity_project;\n", + }) + g = ["git", "-C", str(repo), "-c", "user.name=t27", "-c", "user.email=t27@example.invalid", "-c", "commit.gpgsign=false"] + try: + subprocess.run(["git", "init", "-q", str(repo)], check=True, capture_output=True) + for path, text in tree.items(): + (repo / path).parent.mkdir(parents=True, exist_ok=True) + (repo / path).write_text(text) + subprocess.run(g + ["add", "-A"], check=True, capture_output=True) + subprocess.run(g + ["commit", "-q", "-m", "fixture"], check=True, capture_output=True) + except (OSError, subprocess.CalledProcessError) as e: + return [f"the planted consumer repository could not be built ({e}); nothing about inventory() was checked"] + inv = inventory(repo) + b, r, d = inv["build"], inv["zig_reachability"], inv["dialects"] + if [s["key"] for s in b["steps"]] != ["step:tri"] or b["installed_by_default"] != ["exe:tri"] or b["installed_guarded"]: + failures.append(f"inventory() read build.zig's comments: steps {[s['key'] for s in b['steps']]}, " + f"installed {b['installed_by_default']}, guarded {b['installed_guarded']}") + # build.zig is not a file build.zig names, so it is unreachable by this definition -- as it + # is in the real inventory. + if r["roots_named_by_build_zig"] != 2 or r["unreachable_files"] != ["build.zig", "src/ghost.zig"]: + failures.append(f"inventory() followed a path or an @import that exists only in a comment: " + f"{r['roots_named_by_build_zig']} roots, unreachable {r['unreachable_files']}") + if (d["t27_canonical"], d["t27_mirror"], d["t27_vendored"]) != (1, 1, 1): + failures.append(f"inventory() must count the mirror and the vendored copies apart from the canonical .t27: {d}") + return failures + + def self_check() -> int: sha = "a" * 40 - failures = [] + failures = parser_self_check() + d = dialects_of(["specs/a.t27", MIRROR + "x.t27", VENDORED + "specs/trinity/project.t27", "src/main.zig"]) + if (d["t27_canonical"], d["t27_mirror"], d["t27_vendored"], d["t27_canonical_files"]) != (1, 1, 1, ["specs/a.t27"]): + failures.append(f"a mirrored or a vendored .t27 is never canonical, got {d}") with tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp) - (root / "specs/a.t27").parent.mkdir(parents=True) + for path, text in ZIG_FIXTURE.items(): + (root / path).parent.mkdir(parents=True, exist_ok=True) + (root / path).write_text(text) + reach = zig_reachability(root, sorted(ZIG_FIXTURE), ["src/main.zig"]) + if reach["unreachable_files"] != ["src/ghost.zig", "src/gui.zig"]: + failures.append(f"an @import in a comment imports nothing, got unreachable {reach['unreachable_files']}") + # The planted repository is read by `git`; a GIT_DIR or GIT_INDEX_FILE inherited from a + # hook would point every one of those commands at another repository. + saved = {k: os.environ.pop(k) for k in [k for k in os.environ if k.startswith("GIT_")]} + try: + failures += inventory_self_check(root) + finally: + os.environ.update(saved) + (root / "specs/a.t27").parent.mkdir(parents=True, exist_ok=True) (root / "specs/a.t27").write_text("module a;\n") def run(project_text: str, cards: dict[str, str], inv: dict) -> list[str]: @@ -741,37 +946,49 @@ def run(project_text: str, cards: dict[str, str], inv: dict) -> list[str]: (specs / "project.t27").write_text(project_text) for name, text in cards.items(): (specs / "capabilities" / name).write_text(text) - findings, _ = check(inv, specs, root) + try: + findings, _ = check(inv, specs, root) + except Exception as e: # a crash is not a verdict: name it, so the case says why it failed + return [f"CRASH {type(e).__name__}: {e}"] return [code for code, _ in findings] good = run(fill(GOOD_PROJECT, sha=sha), {"cli.tri.t27": card()}, fixture_inventory(sha)) if good: failures.append(f"the clean fixture must pass, got {good}") - cases = { - "DUPLICATE_OWNERSHIP": ({"cli.tri.t27": card(targets='"exe:tri"', n=1), "cli.other.t27": card(mod="cli_other", id="cli.other", owner="gHashTag/other", targets='"step:tri"', n=1)}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "ABSENT_PATH": ({"cli.tri.t27": card(impl="trinity:src/gone.zig")}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "AMBIGUOUS_DIALECT": ({"cli.tri.t27": card(dialect="tri")}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "DIRTY": ({"cli.tri.t27": card()}, fixture_inventory(sha, dirty=True), fill(GOOD_PROJECT, sha=sha)), - "UNSUPPORTED_BACKEND": ({"cli.tri.t27": card(disp="external", backend="verilog")}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "UNASSIGNED_TARGET": ({"cli.tri.t27": card(targets='"exe:tri"', n=1)}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "PIN_MISMATCH": ({"cli.tri.t27": card()}, fixture_inventory("b" * 40), fill(GOOD_PROJECT, sha=sha)), - "EVIDENCE_UNSUPPORTED": ({"cli.tri.t27": card(source="")}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "MIRROR_AS_CANONICAL": ({"cli.tri.t27": card(spec="trinity:" + MIRROR + "x.t27")}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "PROFILE_MISMATCH": ({"cli.tri.t27": card(profile="web")}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - "WORK_PACKAGE_UNCOVERED": ({"cli.tri.t27": card()}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha).replace('WORK_PACKAGES : [1]str = ["S01"]', 'WORK_PACKAGES : [2]str = ["S01", "S02"]').replace('WORK_PACKAGE_ISSUES : [1]str = ["https://github.com/gHashTag/t27/issues/3563"]', 'WORK_PACKAGE_ISSUES : [2]str = ["https://github.com/gHashTag/t27/issues/3563", "https://github.com/gHashTag/t27/issues/3564"]')), - "COUNT_MISMATCH": ({"cli.tri.t27": card()}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha).replace("TRI_FILES : u16 = 0", "TRI_FILES : u16 = 7")), - "UNKNOWN_TARGET": ({"cli.tri.t27": card(targets='"exe:tri", "step:tri", "exe:ghost"', n=3)}, fixture_inventory(sha), fill(GOOD_PROJECT, sha=sha)), - } - for code, (cards, inv, project_text) in cases.items(): + stale = fixture_inventory(sha) + del stale["dialects"]["t27_vendored"] + project = fill(GOOD_PROJECT, sha=sha) + # (what is planted, the code that must report it, cards, inventory, project) + cases = [ + ("two owners of one spec", "DUPLICATE_OWNERSHIP", {"cli.tri.t27": card(targets='"exe:tri"', n=1), "cli.other.t27": card(mod="cli_other", id="cli.other", owner="gHashTag/other", targets='"step:tri"', n=1)}, fixture_inventory(sha), project), + ("an untracked path", "ABSENT_PATH", {"cli.tri.t27": card(impl="trinity:src/gone.zig")}, fixture_inventory(sha), project), + ("a dialect the spec is not", "AMBIGUOUS_DIALECT", {"cli.tri.t27": card(dialect="tri")}, fixture_inventory(sha), project), + ("a dirty inventory", "DIRTY", {"cli.tri.t27": card()}, fixture_inventory(sha, dirty=True), project), + ("a backend on an external card", "UNSUPPORTED_BACKEND", {"cli.tri.t27": card(disp="external", backend="verilog")}, fixture_inventory(sha), project), + ("a target nobody owns", "UNASSIGNED_TARGET", {"cli.tri.t27": card(targets='"exe:tri"', n=1)}, fixture_inventory(sha), project), + ("another revision", "PIN_MISMATCH", {"cli.tri.t27": card()}, fixture_inventory("b" * 40), project), + ("measured without a source", "EVIDENCE_UNSUPPORTED", {"cli.tri.t27": card(source="")}, fixture_inventory(sha), project), + ("the website mirror as canonical", "MIRROR_AS_CANONICAL", {"cli.tri.t27": card(spec="trinity:" + MIRROR + "x.t27")}, fixture_inventory(sha), project), + ("a vendored copy of a t27 contract as canonical", "MIRROR_AS_CANONICAL", {"cli.tri.t27": card(spec="trinity:" + VENDORED + "specs/a.t27")}, fixture_inventory(sha), project), + ("a default install on a web card", "PROFILE_MISMATCH", {"cli.tri.t27": card(profile="web")}, fixture_inventory(sha), project), + ("a package without a card", "WORK_PACKAGE_UNCOVERED", {"cli.tri.t27": card()}, fixture_inventory(sha), project.replace('WORK_PACKAGES : [1]str = ["S01"]', 'WORK_PACKAGES : [2]str = ["S01", "S02"]').replace('WORK_PACKAGE_ISSUES : [1]str = ["https://github.com/gHashTag/t27/issues/3563"]', 'WORK_PACKAGE_ISSUES : [2]str = ["https://github.com/gHashTag/t27/issues/3563", "https://github.com/gHashTag/t27/issues/3564"]')), + ("a dialect count that differs", "COUNT_MISMATCH", {"cli.tri.t27": card()}, fixture_inventory(sha), project.replace("TRI_FILES : u16 = 0", "TRI_FILES : u16 = 7")), + ("vendored copies the project does not state", "COUNT_MISMATCH", {"cli.tri.t27": card()}, fixture_inventory(sha, vendored=3), project), + ("a target build.zig does not define", "UNKNOWN_TARGET", {"cli.tri.t27": card(targets='"exe:tri", "step:tri", "exe:ghost"', n=3)}, fixture_inventory(sha), project), + ("an inventory without the vendored count", "UNREADABLE", {"cli.tri.t27": card()}, stale, project), + ] + for planted, code, cards, inv, project_text in cases: got = run(project_text, cards, inv) if code not in got: - failures.append(f"planted {code}, the gate reported {got}") + failures.append(f"planted {planted}: {code} expected, the gate reported {got}") if failures: print("trinity_manifest --self-check: FAIL") for f in failures: print(" " + f) return 1 - print("trinity_manifest --self-check: PASS (clean fixture passes; 13 planted defects each reported under their own code)") + print(f"trinity_manifest --self-check: PASS (clean fixture passes; {len(cases)} planted defects each reported under their own code; " + "a step, an install, a path and an @import that exist only in comments are not read, a `//` inside a literal is; " + "the mirror and the vendored copies are counted apart, end to end through inventory())") return 0 @@ -798,7 +1015,8 @@ def main() -> int: b = inv["build"] print(f"trinity_manifest: {inv['repo']}@{inv['sha'][:9]} -> {args.out}: {len(b['executables'])} executables, {len(b['libraries'])} libraries, " f"{len(b['tests'])} tests, {len(b['steps'])} steps, {len(b['installed_by_default'])} installed by default, " - f"{inv['dialects']['t27_canonical']} canonical .t27 / {inv['dialects']['t27_mirror']} mirrored, {inv['dialects']['tri']} .tri, " + f"{inv['dialects']['t27_canonical']} canonical .t27 / {inv['dialects']['t27_mirror']} mirrored / " + f"{inv['dialects']['t27_vendored']} vendored from t27, {inv['dialects']['tri']} .tri, " f"{inv['dialects']['vibee']} .vibee, zig unreachable {inv['zig_reachability']['unreachable']}/{inv['zig_reachability']['zig_files']}") return 0 if args.command == "check": diff --git a/specs/reproduce/capabilities.t27 b/specs/reproduce/capabilities.t27 index 60d33ffa87..9fe3e6c22c 100644 --- a/specs/reproduce/capabilities.t27 +++ b/specs/reproduce/capabilities.t27 @@ -19,9 +19,12 @@ pub const ID : str = "reproduce/capabilities"; ; Run as the card writes the command, with -Dci=true added to every `zig build` in it (the cards are ; headless, and the headless profile is -Dci=true), stdin closed, at most RUN_TIMEOUT_S seconds each. ; A tracked file the command changes is recorded with the run and restored after it. -pub const RUN : [11]str = [ +; trinity/brain.regions owns five named steps and six test roots that the profile's test step does not +; compile (gHashTag/t27#5953), so its five `zig build test-` commands are its only measure here. +pub const RUN : [12]str = [ "trinity/abi.c-api", "trinity/agent.phi-loop", + "trinity/brain.regions", "trinity/cli.tri", "trinity/convert.b2t", "trinity/examples.zig", @@ -41,10 +44,12 @@ pub const NOT_RUN_REASONS : [3]str = [ ]; ; Capabilities known to be blocked, and what blocks each. One that measures complete fails the index ; until its line is removed, and one that is blocked and not here fails it too. -pub const KNOWN_BLOCKED : [3]str = ["trinity/mcp.needle-mcp", "trinity/mcp.trinity-mcp", "trinity/registry.commands"]; -pub const KNOWN_BLOCKED_REASONS : [3]str = [ - "zig build needle-mcp: there is no such step -- build.zig comments it out (around line 1507), and the S01 inventory counts it because tools/trinity_manifest.py does not skip // lines", - "zig build trinity-mcp: there is no such step -- build.zig comments it out (around line 1551), as for needle-mcp", +; mcp.needle-mcp and mcp.trinity-mcp left this list with the vendoring of gHashTag/t27@39e06aa0: their +; acceptance was `zig build needle-mcp` / `zig build trinity-mcp`, steps that exist only in commented-out +; lines of build.zig, and gHashTag/t27#5474 made it `zig build -Dci=true && test -x zig-out/bin/`, +; the install the headless profile already makes. +pub const KNOWN_BLOCKED : [1]str = ["trinity/registry.commands"]; +pub const KNOWN_BLOCKED_REASONS : [1]str = [ "zig build export-registry && git diff --exit-code .trinity/registry.json cannot pass: the export writes the time it ran into generated_at" ]; ; Findings of the S01 checker that are drift since the inventory S01 pinned (gHashTag/trinity@976df517) diff --git a/specs/reproduce/contracts.t27 b/specs/reproduce/contracts.t27 index 0216a24395..a3071bbf34 100644 --- a/specs/reproduce/contracts.t27 +++ b/specs/reproduce/contracts.t27 @@ -23,8 +23,8 @@ module reproduce_contracts; pub const KIND : str = "contract-lock"; pub const ID : str = "reproduce/contracts"; pub const T27_REPO : str = "https://github.com/gHashTag/t27"; -pub const T27_REVISION : str = "de2a1aca2402afc050049eb9886fa5d9929b7927"; -pub const T27_REVISION_AT : str = "2026-10-01T15:21:11Z"; +pub const T27_REVISION : str = "39e06aa0ea955eece4d9b84e41192e2a36e5d276"; +pub const T27_REVISION_AT : str = "2026-10-04T08:44:49Z"; pub const VENDOR_DIR : str = "external/t27"; pub const COMPILER : str = "t27c, the package bootstrap/ of the Cargo workspace of gHashTag/t27"; pub const COMPILER_PATHS : [5]str = [ @@ -35,11 +35,11 @@ pub const COMPILER_PATHS : [5]str = [ "bootstrap/src" ]; pub const COMPILER_IDS : [5]str = [ - "46722d0846e561c5460008712639595ba10835ff", + "4dda364d4eb8fbeacb119413d375c5ce9be7ac98", "e76ab927ff80c0cbeed7a915a5cc3feac491c02b", - "41263df91bde3d8bffb28c4555a2fd4681d09029", + "6f2adc3eee0fce2260842ba67096b3f472a146dc", "98e6d64110d018294431689a144734b0f6cde455", - "81ea48976983b029c527f455540e15d862862c3b" + "0c32f36797e73ad3f5e6b598b23ac5bc7cf7dc8a" ]; ; The Zig t27 runs its spec tests with (ZIG_VERSION in .github/workflows/oracle-nightly.yml at the ; revision): t27c test-report writes code for it, so check --t27c wants it on PATH. @@ -81,7 +81,7 @@ pub const TEST_BLOCKED_ERRORS : [11]str = [ pub const TEST_NONE : [2]str = ["specs/tools/mcp/needle.t27", "specs/tools/mcp/trinity.t27"]; pub const TESTS_RUN_MIN : u32 = 77; ; Every vendored file, sorted, and its sha256. -pub const FILES : [75]str = [ +pub const FILES : [76]str = [ "conformance/trinity/fpga_adapter.json", "specs/api/c_abi.t27", "specs/api/tri_api_loop.t27", @@ -107,6 +107,7 @@ pub const FILES : [75]str = [ "specs/trinity/capabilities/archive.legacy.t27", "specs/trinity/capabilities/bench.suite.t27", "specs/trinity/capabilities/bot.tri-bot.t27", + "specs/trinity/capabilities/brain.regions.t27", "specs/trinity/capabilities/catalog.spec-mirror.t27", "specs/trinity/capabilities/catalog.world-scan.t27", "specs/trinity/capabilities/cli.tri.t27", @@ -158,7 +159,7 @@ pub const FILES : [75]str = [ "tools/trinity_fpga_adapter.py", "tools/trinity_manifest.py" ]; -pub const SHA256 : [75]str = [ +pub const SHA256 : [76]str = [ "7ae9ac4cfa12dad2031a570401067fd8b3f76dce36e034026b7289c49a2cb037", "45f24257632e6874936c6c861577cfb3e7dfe9980d45a46cc270402ece70beef", "4f475cb07922ffa10a38fae730d2a10a6a6e512eda20c78a1f873856a789522a", @@ -171,10 +172,10 @@ pub const SHA256 : [75]str = [ "bebdc00ac0668a113958a3571a8c150155c6799de52129bcf898361e1090a153", "0b2bf5af0c55b2947d39b83c3fbec42e38c3672c5d1e48fe90a4e98b6b9a12b9", "1ec86e7561e1830b7f478ffc835f028522b5297c375296b7674990b182956235", - "e2bff492844bb71720dc0caa8615e6025c5bcbf68978e17da5e89ef661adb563", - "6d12259c8f9ce6b5c139b041f23558ef50c0f6a3edb22569fb4e2559f4ca3b54", + "c190bc0aa0ad7f34b9772f5ef3a569746b0373916b1d52827ed832e8865d4ad3", + "c3be9d879e41be06a5ebc1cc3dbb770c632deaae86f2c6a443df37a72586b91c", "c832c848030f8a8bf1a6661c10cb66c61b5e4cd53b7478a22fdfd57cd755a03b", - "8775eb3ee0282422ffb8c90dfeab6927559a25f894c2a20adc082f70547b04ad", + "627cc5d9594941436848eccb032e71b22168cf4c8b3064638837d8916b018d9f", "6336a6b10f16c0b0bf7e47bd4b23f8a84d7cee370e79aea21460274d7e0bf292", "2f44ec37731d354202474d2074769c31c6582d9e0486fe224c2daa8f6188bb47", "ffd65a9544a8d1bbfaa1a92b670c9e73137add7ef85524adf4ca692df16d5831", @@ -184,6 +185,7 @@ pub const SHA256 : [75]str = [ "90181cc2b6306945600f0e2a0202eca1788884a96bce5db64d16d820cf62ade5", "c3d50f765c9a9c0003446e9d2fa3df72d86d3c75c599c10498df31dfed1768f6", "b7dd2658343c40cb234c9abcafc5e9b9d492a4a0f4649b4c767009477b94b342", + "3e06b6b7b1df75d7c70dcd5bac52b7e381836103308c6c1bb01f7e741f7cb87a", "56bd42a76af68ab0c5d29cee4888d01b3a34216068371aa1d9db128277871aee", "dd87dea212ee6b002f1beb0da611d8599e23f4c3ea31a1460404131532feac0c", "21b016c6929f2280608718aef11682faa3254a6524af4cb3e50b6bbd43c79b37", @@ -195,8 +197,8 @@ pub const SHA256 : [75]str = [ "e0c9d0e21f7bbd9253fd5acdae3f3581ccc2cb38aa284315a98a2397e3ec1543", "9c72e0708c09aa65265bc7a089d77f35949d4304f42c67826f5ae269ac268264", "f055f3e19d5672b8a2a023c30ceb98afe73b1002e00c50a9eed5041cdb51da81", - "f6faf793a7547b31fda5fa9a40da0698c7b8f1100425bb474dd73b6d69ec127b", - "70156b4f00e3f1c9ce6951510a37233b65f156c48b9c7a1b5e038c17f00d514e", + "216f0ad33f1f4416f0243d298ceba0e0d96952fc4852b4e7e7867ef49d3972a6", + "aa736594cc93f43f2153e2b555ca44b7f6bb35bfcfa7078957d54bd38714a993", "41d804d63c7cea9c3bed9025f52bd8d5f576855b3cd8b065800d6e6ba53f99a9", "d3462e16b085abcac92fe0307513800bd1e11907889cdf65484c5ac103c12c97", "58fe891a66adf08ce55b8b9fd7a6b8d8cf1080549e4ac678068174abbb58b88a", @@ -210,7 +212,7 @@ pub const SHA256 : [75]str = [ "7c9c089d1b173d6d76941827409038da1d98eddfa3704b85c4d9203b417b2c3d", "68db08e2c74473cafa7fa60d467626f0b74106730d4f4b3c418442765125b168", "ceeade29b5422ff8ecbad180c425515d7a8962cebb7683aef5b35643c0ddc312", - "c5a5f4d86e39a33fe2eee3119242a951a9e942f8d2d1e8eddeda8f6ff0b0e9e4", + "9dea85455c2d0e7bf1536bc11b9ddf6ecc7437ed028db479357280d56e797d1f", "d1a693acd15cae25043e02fbfee7b30b09e309447e18c625574272912603eece", "2f44c7a542950430b6d29c51b912e967e045efb52490fb39ddcdfdd232f233ed", "f4cbc7903cba83f5b77de3ea9da6843b4ad6074a3fc695555573e68537ebdb07", @@ -228,12 +230,12 @@ pub const SHA256 : [75]str = [ "feb86eeefa90d0d29d448f031ca0ecebc52235f088d0697ef4cd0920d126ef60", "14dd9424bebdc314016df1e39e6ee13dc8ad8b6508cd23518885b006275d6476", "eabb5fbd560deceb47ad6ae9f8bc4f70a2b91e2504e8026ba09387963560b9f2", - "be8ecd253a1d1147bc19c61f76ae5f18a7a2a7e1aec301a7b4ca14bbc4d4fc10", + "527e23f251e5c88b93ce18294f5369a22360c7eb9c5c34a6464f7710742337f2", "eafbd92646695ec39378467b8ac4dad4381f2482007d13894a75b82bf1877b16", "365952af756282bd5f43c023843fa2f133348b922dc1dcd6b4db9f85034fbfd4", "bf26288bfe3ad776ed7f7a58c63c4793a72149b50669cee212c5a0c6d6cd484e", "11984d77c49424f7f30c6a11de2613a077aca03f89bbfce6d6f674bffdc1d99d", - "766d0017cb35bba3c3fb6a17eaac71204f447e112063e788cab2a5a073b8b6b1" + "aa856e991cc2074f54ca367c19ea5a8cfc15ef900eb87fc468e840e10d3aba22" ]; pub const ENABLED : bool = true; From 56040636efd3b645809d1e9c439851bed53fed31 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 4 Oct 2026 16:52:47 +0700 Subject: [PATCH 13/13] build(reproduce): raise the contract-test floor to the 78 that run at t27@39e06aa0 The vendoring of gHashTag/t27@39e06aa0 added one spec test, the card_is_bound_to_its_package test of specs/trinity/capabilities/ brain.regions.t27; no other vendored spec changed its number of tests. t27c built at 39e06aa0 with zig 0.16.0 measures 58 specs and 78 tests, all passing, so a floor left at 77 would let one test vanish unseen. The other ledgers were re-checked against the new vendor and stand as they are: the eleven TEST_BLOCKED specs fail with the same first error each, lotus.t27 still drops 48 top-level tokens, and the two specs/tools/mcp specs still declare no test. Co-Authored-By: Claude Opus 5.5 --- specs/reproduce/contracts.t27 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specs/reproduce/contracts.t27 b/specs/reproduce/contracts.t27 index a3071bbf34..facd1b742d 100644 --- a/specs/reproduce/contracts.t27 +++ b/specs/reproduce/contracts.t27 @@ -79,7 +79,9 @@ pub const TEST_BLOCKED_ERRORS : [11]str = [ "spec.zig:24:27: error: use of undeclared identifier 'Trit'" ]; pub const TEST_NONE : [2]str = ["specs/tools/mcp/needle.t27", "specs/tools/mcp/trinity.t27"]; -pub const TESTS_RUN_MIN : u32 = 77; +; 78 at gHashTag/t27@39e06aa0: 58 specs measured, all 78 of their tests passing. It was 77 at de2a1aca; +; the one test of specs/trinity/capabilities/brain.regions.t27 is the only test this vendoring added. +pub const TESTS_RUN_MIN : u32 = 78; ; Every vendored file, sorted, and its sha256. pub const FILES : [76]str = [ "conformance/trinity/fpga_adapter.json", @@ -247,5 +249,5 @@ test the_lock_names_its_source { test the_ledgers_hold_together { assert PARSE_DISCARD_TOKENS[0] == 48; - assert TESTS_RUN_MIN == 77; + assert TESTS_RUN_MIN == 78; }