Skip to content

blog: 179 features that change no bits - #877

Open
gHashTag wants to merge 1 commit into
mainfrom
blog/features-that-change-no-bits
Open

blog: 179 features that change no bits#877
gHashTag wants to merge 1 commit into
mainfrom
blog/features-that-change-no-bits

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Adds one blog post: features-that-change-no-bits, dated 2026-08-31.

What it reports

Material from the last 24 hours in openXC7/nextpnr-xilinx#165, plus the merged
changes it depends on:

  • The parity report (2026-08-27) named 179 missing BRAM configuration features
    as "the most concrete bitstream-parity item and the one I would fix first".
  • Its own author retracted that on 2026-08-28: the features are zero-codepoints,
    proven three independent ways (segbits, a bit2fasm round trip giving
    byte-identical 5 576 340 B frames in both arms, and a Vivado ML 2026.1 golden).
  • The real blocker was a clock route from a left-bank pad to a BUFG, which
    produced no feature-count delta and failed in two different-looking ways
    depending on the prjxray-db revision.
  • On 2026-08-30 three Sonata blinky bitstreams ran on a physical board, arm C
    being the one carrying openXC7/nextpnr#1.

Honesty notes

Build

npm run build:ci exits 0; the slug appears in dist/assets/Blog-D9NOt413.js
and gets its own chunk features-that-change-no-bits-DCKQ1Tl1.js.

Not merging — publication is the operator's decision.

🤖 Generated with Claude Code

A parity report between the himbaechel xilinx port and nextpnr-xilinx 0.9.3
ranked its own work items backwards: the 179 missing BRAM features are
zero-codepoints, and the blocker that kept a bitstream off a board produced no
feature-count delta at all. Adds the post with receipts for #165, its two
corrections, openXC7/nextpnr#1, prjxray-db#7, and the two open BUFIO items.

Every measurement is attributed upstream; none was reproduced here, and that is
stated in openQuestions.

Co-Authored-By: Claude <noreply@anthropic.com>
@gHashTag

gHashTag commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Status check, 2026-09-05. This PR has been open since 2026-08-31 with no activity — flagging it rather than letting it go quiet indefinitely.

Both CI failures look like repo-wide gates, not content issues:

  • Brain Health Check failed, but this PR only adds a blog post — no brain-region code touched. The gate's own failure-comment bot also hit a 403 Resource not accessible by integration when trying to post, which is a workflow token-permission bug independent of the PR content.
  • claude-review failed; worth a look at whether it flagged something real in the blog content or hit the same kind of infra issue.

The PR body already defers the merge decision to the operator ("Not merging — publication is the operator's decision"). Restating that here so it's visible without needing to open the diff: this is ready for a merge/hold/close call whenever convenient, not something blocking on more work.

gHashTag added a commit to gHashTag/trinity-fpga that referenced this pull request Sep 5, 2026
…n a real disk crisis

Posted the PR #877 status-check comment per explicit operator go-ahead
("post it as-is") after drafting it on request:
gHashTag/trinity#877 (comment).

Investigating before drafting corrected a standing mischaracterization from
OD3/OD9: #877 is the operator's own PR (authored via an earlier Claude Code
session, in their own repo), not a third-party-adjacent one needing extra
publish caution. Also found why it looked stuck: "Brain Health Check" is a
repo-wide CI gate that ran full brain-region tests against a blog-post-only
diff touching no brain code, and its own failure-comment bot hit an
unrelated 403 permissions bug. The PR body had already deferred the merge
call to the operator from the start -- not neglect, a parked proposal.

Separately: this cycle opened with a real disk crisis (0.18 GiB, then 127
MiB free) -- the third this session. It resolved on its own (disk recovered
to ~20 GiB, /tmp state including the compiled binary was gone, consistent
with a reboot or cache-clear outside this loop's control) before any
action was needed. This gave B21's hysteresis its first real-world test:
the first post-crisis reading correctly held the verdict at HALT for one
more confirmation despite the raw reading already showing recovery, then
cleared on the second confirming reading -- exactly the designed behavior,
now proven against production data instead of only a scratch copy.
gHashTag added a commit to gHashTag/trinity-fpga that referenced this pull request Sep 5, 2026
…sis (#746)

* feat(loop): add STATE.json/JOURNAL.md/dashboard.html for the t27 correctness loop

Structured backlog/done/anomalies tracking (STATE.json), narrative session
log (JOURNAL.md), and a static status page (dashboard.html) for the ongoing
autonomous work on the t27 Rust-to-Zig transpiler's corpus-correctness rate
and the openXC7/nextpnr-xilinx BUFR clock-enable collaboration.

Today's session: t27 corpus 74.2% -> 76.7% clean (497 specs), a silent
infinite-loop bug found and fixed in the compiler's copy-propagation pass,
all six optimizer passes swept to confirm no sibling instance survives.

* feat(loop): give tri_loopstate a runnable CLI, fix dashboard drift, commit skill

- src/tri/tri_loopstate_main.zig: standalone entry point for tri_loopstate.zig
  (zig build-exe, no root build.zig needed), exposing `status` and `check`
  subcommands. Proves a working Zig-0.16 Io-threaded pattern for file I/O and
  argv (pub fn main(init: std.process.Init) !u8) -- the first confirmed
  instance in this repo, verified by compiling and running against real files.
- src/tri/tri_loopstate.zig: adds liveCounts/extractReadoutNumber/checkDrift
  plus 6 new tests (13/13 pass), so a dashboard's numeric readout can be
  verified against STATE.json mechanically instead of by hand.
- src/tri/tri_loopstate.zig itself was still untracked despite being recorded
  as a "done" deliverable -- committing it now closes that gap.
- .trinity/loop/{STATE.json,JOURNAL.md,dashboard.html}: recorded this
  iteration's work, added loop.continuity_protocol so the next cron firing
  reads state instead of re-deriving it, added awaiting_operator_decision
  tracking, and fixed the dashboard's stale/self-contradictory readout and
  "why the loop stopped" section (mislabeled as current three days after it
  resumed).
- .claude/skills/fpga-bufr/SKILL.md: was untracked since 2026-09-01; commits
  the one skill artifact this loop has produced so far.

* fix(loop): correct a stale cron job ID the loop believed was live (A21)

STATE.json's loop.cron_job named 947e19e4, but CronList shows only one live
job this session -- eaa1cb07, created by this iteration's own /loop
invocation. 947e19e4 belonged to an earlier, now-terminated session:
session-scoped cron jobs die when the session exits, so it could not have
survived the restart loop.status itself already records. Corrected the
field and dashboard masthead; JOURNAL.md keeps the original (now-stale)
mentions in place per its append-only convention, with a correction note
appended rather than an edit in place.

* feat(loop): implement the three tripwires for "autonomous with tripwires"

The operator picked cooperation option 2 (autonomous with tripwires) for
the overnight loop. A 4-agent design workflow (3 independent proposals +
synthesis) turned that into exact, implementable semantics; this commit
builds them.

New in src/tri/tri_loopstate.zig:
- freeGiB(): a direct libc statvfs binding (no shell df), struct layout
  verified against the macOS SDK header and cross-checked live against df.
- decisionGateStatus() (clear/some_gated/all_gated) plus a real behavior
  change to nextItem(): a needs_operator_decision row is now skipped
  silently, every time, so it never blocks unrelated actionable work. Only
  total gridlock (all_gated) is a real tripwire.
- autoHealDrift()/rewriteReadoutNumber(): a plain numeric dashboard mismatch
  self-heals the same run; a MISSING label is left alone rather than
  guessed at.
- renderHaltBanner()/injectHaltBanner(): marker-based, idempotent -- a
  clear verdict actively erases a stale banner.
- evalDiskTier/readDiskThresholds/evaluateTripwires tie the three readings
  together.

New `tripwire` subcommand in tri_loopstate_main.zig ties it all together and
updates dashboard.html's halt banner. 30/30 tests pass.

Verified end-to-end against the real files, not just synthetic fixtures --
including a deliberately forced halt (temporarily raised the disk threshold
past current free space) to confirm the banner appears and clears for real.
That real-file run caught an actual bug unit tests missed: the halt
banner's own diagnostic text sits above the readout block and can echo a
label's exact words, which extractReadoutNumber's whole-document search
matched instead of the real cell, producing a false MISSING report that
would have wedged the loop permanently halted the first time it ever
legitimately halted for drift. Fixed by scoping the search to the readout
block; added a regression test reproducing the exact shape.

Deliberately not built, recorded rather than silently skipped: hysteresis/
flap-detection (backlog B21) and a GitHub escalation channel, which needs
an operator-designated tracking issue that doesn't exist yet (awaiting_
operator_decision OD6). Today the dashboard banner and one JOURNAL.md line
are the only two escalation channels.

* chore(loop): iteration 77, halted (disk 1.47 GiB free < 2.00 threshold)

The disk tripwire fired for real on its first live cron re-firing --
free space dropped from ~9.8 GiB to 1.47 GiB across the elapsed cycles,
corroborated independently with df -h. Per design: no backlog work this
iteration, no autonomous disk remediation attempted (operator-only call
per B16). Halt recorded in loop.halt/loop.status; dashboard banner written
automatically by `tripwire`. continuity_protocol's "don't re-run the
research workflow on a repeated /loop prompt" rule held on its first real
test.

* chore(loop): iteration 78, still halted (disk 1.45 GiB free)

Repeat check, same reason as iteration 77 -- no backlog work, no
remediation attempted. Freshly re-verified, not carried over from the
previous write.

* chore(loop): iteration 79, still halted (disk 1.29 GiB, declining)

Free space dropped further (1.47 -> 1.45 -> 1.29 GiB across three checks).
Not re-sending the already-sent push notification (no-spam intent) but
recorded an explicit notify_threshold_gib=1.0 so the next drop that
actually matters re-notifies mechanically rather than needing a fresh
judgment call each cycle.

* chore(loop): record joint disk investigation (D60/A23), still halted

Operator ran lsof/tmutil/du live during the halt -- ruled out deleted-open
files and local snapshots, found this session's own scratchpad had grown
to 3.1GB of stale FPGA probe artifacts (now cleared), and confirmed
CoreSimulator's own directories are unchanged from the session's first
reading (not an active real-time leak). Raw rm -rf on Caches/* mostly
failed on permission-protected dyld files; handed off the correct tool
(xcrun simctl runtime delete) instead.

* chore(loop): iteration 82, still halted (disk 1.27 GiB), sent 1h reminder

* chore(loop): iteration 83, RESUMED -- disk halt resolved (13.49 GiB free)

Operator explicitly named the runtime to remove (iOS 26.5); xcrun simctl
runtime delete <UUID> (note: needs the UUID from `runtime list`, not the
identifier `list runtimes` reports) freed it asynchronously, 1.2 -> 13.49
GiB, confirmed with a fresh tripwire run. Total halt: iterations 77-83,
~2 hours, zero backlog work and zero autonomous remediation attempted
during it, matching the design exactly.

* feat(loop): B14/B16 delivered -- BUFRCLK0-3 round-trip confirmed on fbg484

Resumed from the disk halt into real backlog work. GitHub sweep found
cavearr's BUFRCLK0/1/3 fuzzing campaign results on #149. The locally
patched db already carried the exact same 4 minted rows (an independent
cross-validation of an earlier pattern-based prediction). Built a synthetic
FASM and ran fasm2frames against xc7a200tfbg484-2 (AX7203's actual part):
exit 0, and verified the real bit pattern (frame 0x00421400/0x00421401,
word 50, bits 23+31 both halves) rather than trusting the return code
alone. Posted the confirmation to #149 with a recommendation against
forcing the still-unverified #172 PERFCLK pip.

B14 (round-trip deliverable) and B16 (disk decision) both closed.

* docs(loop): add dashboard narrative for the disk-halt-to-B14-delivery arc

* feat(loop): B17 delivered -- measure.py regression suite + git-history fix

Added gen/zig/test_measure.py in t27 (11 stdlib-only tests, verified with
a real fault injection). Writing the fixtures surfaced a real, previously
undocumented scope boundary in silent()'s detector regex. Also found and
fixed measure.py itself having zero git history -- a blanket gen/ ignore
caught this hand-written "only sanctioned instrument" tool, same failure
shape as this loop's own STATE.json being untracked for 76 iterations.

* chore(loop): B20 delivered, B15 stale blocker fixed

Extended the fpga-bufr skill's already-documented "watch PR state, not
just issue comments" lesson (A16) to also poll reviewDecision and
statusCheckRollup -- closes B20. Fixed a stale "disk" blocker on B15
(resolved hours ago); hansfbaier genuinely hasn't commented on #120, so it
stays correctly blocked for the real reason.

* chore(loop): iteration check -- no new GitHub activity, B18 count refreshed (53->23)

* feat(loop): save the periodic self-audit as a named, reusable workflow

The original /loop mandate asked for a "dynamic workflow per Claude Code
best practices" -- delivered so far only as two ad hoc Workflow-tool
invocations (research/decompose, tripwire design), never saved as a
standing asset. Closes that gap: .claude/workflows/loop-periodic-self-audit.js
is a real, invokable-by-name workflow (3 parallel finders -- infra health,
code/data freshness, external-collaboration drift -- synthesized into
concrete backlog items), scoped to find what's changed SINCE the last
audit rather than repeating already-done work. First validation run
launched to confirm it actually executes correctly, not just parses.

* fix(loop): self-audit's first run found 4 real bugs, all fixed and tested

.claude/workflows/loop-periodic-self-audit.js's first real invocation
(about 20 minutes after being created) found:

1. loop.iteration frozen at 76 for 9+ real iterations -- a direct
   recurrence of anomaly A7 at ~10x the original drift. Corrected to 88
   (highest number any commit actually names); continuity_protocol now
   mandates an explicit before-commit check. Filed OD7 for the real
   structural fix.
2. nextItem()/decisionGateStatus() had no check for the literal backlog
   status "blocked" (only blocked_by/needs_operator_decision) -- B8
   carries status:"blocked" with an empty blocked_by, and the tool was
   live-recommending it as the next actionable item. Fixed both, 2 new
   regression tests.
3. liveCounts() treated any non-empty anomaly status as closed, silently
   undercounting anomalies_open (5 reported, 13 real) because A13/A15's
   status TEXT plainly says they're still unresolved. Replaced with
   anomalyIsOpen(): still-open phrases checked first (so A13's own
   coincidental "corrected" substring, describing a diagnosis correction
   not a resolution, doesn't get misread), then a resolved-verb whitelist,
   defaulting unmatched text to open. 5 new tests.
4. The audit workflow itself hardcoded this ephemeral worktree's path,
   contradicting its own "run from the repo root" instruction. Fixed to
   discover the live working tree at invocation time.

36/36 tests pass (was 30). Also fixed B6's stale blocked_by (named the
now-completed B7 instead of the real blocker) and cross-posted the
PERFCLK recommendation to #172 for hansfbaier specifically.

Filed OD8 (not fixed blind): t27's branch has diverged from
origin/master with unmerged edits to the same compiler files this loop
patches -- a rebase decision, not something to attempt unsupervised.

* fix(tri): migrate simple_synth_report.zig to Zig 0.16's Io-threaded main()

First real production file migrated using the pattern proven in
tri_loopstate_main.zig (B10's own survey groundwork): pub fn main(init:
std.process.Init), argv via init.minimal.args.toSlice(), file I/O via
std.Io.Dir.cwd()+init.io. Also fixed two pre-existing bugs the migration
surfaced: bare `return;` statements in a `!u8`-returning function (masked
until the argsAlloc error was fixed first), and a genuine memory leak
(parsed.deinit() was never called -- silent under the old page_allocator,
surfaced immediately once init.gpa's debug-tracked allocator was used).

Verified by running, not just compiling: --help, a missing-file error
path, and a real JSON parse all produce correct output with zero leaks.

* chore(loop): iteration 89, B10 sized precisely and one file migrated

137 files use the old Zig 0.16 APIs; only 12 have their own main() and are
independently migratable without touching main.zig's own unmigrated
Io-threading (the real structural blocker, not the file count). Migrated
and verified simple_synth_report.zig as proof (see ff60eb1); 9 more of
the 12 remain for a future iteration.

* chore(loop): iteration 90 -- cavearr confirms round-trip, prjxray-db PR being prepared

* fix(tri): migrate wave9_generator.zig to Zig 0.16's Io-threaded main()

Second file in B10's "quick wins" batch. Beyond the already-known args/fs
API changes, this one surfaced two more from the same family, not
previously catalogued:
- std.ArrayListUnmanaged(T){} no longer default-initializes (needs
  `.empty` explicitly) -- same trap already fixed once in tri_loopstate.zig
  this session, now confirmed to recur across the codebase.
- ArrayList(u8).writer(allocator) no longer exists; use
  .print(allocator, fmt, args) directly on the list.
- std.heap.GeneralPurposeAllocator -> std.heap.DebugAllocator (matches
  A1's own documented rename, confirmed here in test blocks that
  `zig build-exe` silently never compiles -- only `zig test` catches it).

Verified by running, not just compiling: --help, a real 3-worker
docker-compose generation into a fresh nested directory, a second run
into the same directory (exercises the PathAlreadyExists branch), and the
too-many-workers error path. Both pre-existing unit tests (generateWorker,
generateCompose) still pass unmodified.

* chore(loop): iteration 91, B10 second quick win + subprocess-migration sizing

* fix(tri): migrate sacred_synth_report.zig to Zig 0.16, fix a use-after-free

Third file in B10's quick-wins batch. Migration itself is the now-familiar
pattern: pub fn main(init: std.process.Init), argsAlloc -> args.toSlice,
std.fs.cwd()+stat+readAll -> Dir.readFileAlloc, io threaded through
runSacredSynthReportCommand into parseYosysJson.

Running the success path (never previously exercised -- the repo has no
fpga/openxc7-synth/sacred_alu.json, so this code path may never have run
against real data before) surfaced a genuine, pre-existing use-after-free:
countCellTypes() stored entry.key_ptr.* (a slice into whatever allocator
parsed the JSON) directly into the returned stats.module_name, while
parseYosysJson's arena backing that JSON gets destroyed the instant it
returns. Printing the module name after that is a dangling-pointer read --
segfaulted immediately on real data. Notably countCellTypes already took
an `allocator` parameter that was unused (`_ = allocator;`), suggesting
the fix (dupe the string into an allocator that outlives the arena) was
the original intent, just never implemented.

Fixed by having countCellTypes dupe the name into its allocator parameter,
and having parseYosysJson pass gpa (not the doomed arena allocator) at
that one call site. Updated the existing test to free the now-owned
string and added an assertion on module_name itself, which the test
previously never checked -- exactly the gap that let this ship unnoticed.

Verified by running all three output formats (human/csv/json) against a
real parsed JSON, plus --help and the missing-file error path.

* chore(loop): iteration 92, B10 third quick win found and fixed a real segfault

* chore(loop): iteration 93, corrected B10 sizing -- zero of 7 remaining files are quick wins

* fix(tri): migrate testnet_rewards.zig to Zig 0.16, first Io.Clock file

Fourth file in B10's batch, and the first to need the newly-investigated
Io.Clock/Timestamp system (std.time.timestamp() is gone entirely, not
renamed). Verified the replacement empirically before using it: a
throwaway program calling std.Io.Clock.real.now(io).toSeconds() matched
`date +%s` exactly.

Threaded `io: std.Io` through every function/method that needed a
timestamp -- RewardEntry.isVested/canClaim/timeUntilVesting,
NodeRewardEntry.init/updateUptime/addJob/isHealthy,
RewardsManager.init/registerNode/recordNodeActivity/addBugBounty/
getStats/exportToJson -- and through all 15 affected unit tests, using
std.testing.io (the stdlib's own ready-made Io for test contexts) rather
than hand-rolling one. Centralized the repeated
`@intCast(...toSeconds())` pattern into one nowSeconds(io) helper.

Also fixed two already-known traps encountered along the way:
ArrayListUnmanaged(T) inferred-`.{}` field values need `.empty` (same
issue as the explicit `T{}` form, already fixed once in tri_loopstate.zig
and wave9_generator.zig this session) and ArrayList.writer(allocator) ->
.print(allocator, fmt, args).

Verified by running: zig test (28/28, all 15 io-threaded tests among
them), then the actual binary against all four subcommands (leaderboard,
register, stats, claim) plus the no-args usage/error path.

* chore(loop): iteration 94, B10 fourth file + Clock/Timestamp pattern proven

* chore(loop): iteration 95, halted (disk 0.23 GiB free, critical)

Disk crashed much harder than the first halt episode -- 0.23 GiB at
detection (fluctuating 0.2-0.8 GiB since), well below the prior crisis's
1.2-1.5 GiB low point and close to the zero-crash zone that previously
took down the harness entirely. No backlog work this iteration, no
autonomous remediation attempted, matching the design exactly.

* chore(loop): iteration 96, RESUMED -- disk halt resolved without sacrificing a runtime

Second disk crisis (0.23 GiB, worse than the first episode's 1.2 GiB low
point). Found and deleted an already-unusable duplicate iOS 26.5 disk
image (xcrun simctl runtime list flagged it explicitly) -- zero functional
loss, freed 0.2 -> 6.0 GiB on its own, no real runtime sacrificed this
time.

* fix(tri): migrate testnet_faucet.zig to Zig 0.16, first real socket file

Fifth file in B10's batch, and the first to need real networking -- the
last of the three unexplored API surfaces this migration turned up
(subprocess spawn, Io.Clock, and now raw sockets).

Researched std.Io.net before touching the file: std.posix.socket/bind/
listen are all gone, replaced by a genuinely higher-level API --
IpAddress.listen(&addr, io, .{.reuse_address=true}) does what used to take
socket()+setsockopt()+bind()+listen() in one call, returning a Server.
Verified the whole client-server round trip empirically in a throwaway
program (spawn a server thread, connect a real client, confirm the byte
stream) before touching real code -- same discipline as every other API
claim this session.

The migration itself turned out smaller than feared: runFaucetServer()
never actually called accept() (a stub that binds+listens then sleeps
forever), so there was no read/write loop to port, just the listen call
itself. Also fixed std.Thread.sleep (gone, replaced by
std.Io.sleep(io, duration, clock)) and threaded the already-proven
Clock/Timestamp pattern through 6 more functions and all 15 tests.

Verified by running: zig test (28/28), the CLI drip/status commands, and
the actual server command -- confirmed genuinely listening via `lsof
-iTCP:PORT -sTCP:LISTEN`, not just "the process didn't crash". Found and
fixed a third instance of the same pre-existing-leak-masked-by-page_
allocator pattern from earlier files this session: runFaucetCli() never
freed response.tx_hash on the success path.

* chore(loop): iteration 98, B10 5th file (sockets proven) + B18 reviewed not guessed + B19 filed OD9

testnet_faucet.zig migrated to Zig 0.16 Io-threading (first socket-using file,
std.Io.net.IpAddress.listen/connect verified via lsof against a real bound port;
also found std.Thread.sleep is gone, replaced with std.Io.sleep). Third leak
found+fixed in this migration series (tx_hash never freed on success path).

B18: read the actual 17-file diff instead of re-counting -- it's one coherent
in-progress body of work matching origin/master's already-merged PRs #3017-3027,
not scattered litter. Untracked 6 pure-noise Coq build byproducts (t27 #3188,
8d980d937); left the rest for the operator (updated blocked_by with the real
reconciliation question) instead of guessing whether to commit or discard.

B19: PR #877 escalation not acted on -- filed as OD9, needs an explicit go-ahead
distinct from the openXC7 issue-comment channel's standing authorization.

* fix(tri): migrate testnet_explorer.zig to Zig 0.16, B10 sixth file, clean pass

Sockets: same std.Io.net.Server pattern proven in testnet_faucet.zig (D72),
applied cleanly -- deinit(io)/start(io), std.Thread.sleep -> std.Io.sleep.
Also fixed three ArrayList(u8).writer(allocator).print(...) call sites in
handleGetNodes() that zig test could not see (main() is dead code under
test) but zig build-exe caught immediately.

Verified: zig test 24/24, zig build-exe clean, ran every subcommand against
the real binary including confirming the server subcommand is genuinely
listening via lsof. No leaks found this time.

Also records this cycle's openXC7 collaboration check: prjxray-db#13 and
prjxray#14 are up, cross-referenced, addressed to hansfbaier for review --
read in full, nothing to correct, no action needed on this end.

* fix(tri): migrate cyrillic_guard.zig to Zig 0.16, B10 seventh file + two real bugs

Subprocess: std.process.run(gpa,io,options) -> {term,stdout,stderr} replaces
Child.run() directly, verified with a throwaway program before touching real
code. This closes out the last of four Zig 0.16 API families this migration
needed (file I/O, Clock/Timestamp, sockets, subprocess). Also migrated
std.fs.cwd() usages (Dir.cwd().readFileAlloc/openDir/statFile/walk, all now
take an io param) and main() to the Init-based signature.

Running the built binary against real directory arguments (main() is dead
code under zig test, so only build-exe + actually running it catches this)
found two genuine pre-existing bugs:

- checkPath/walkDirectory each returned a same-shaped but distinct anonymous
  struct, so the directory-redirect branch never type-checked under
  build-exe. Fixed with a shared CheckPathResult type.
- statFile does not throw error.IsDir for a directory in this stdlib, so
  pointing the tool at a directory silently fell through to the single-file
  path and reported a false "0 files checked, no Cyrillic found". Fixed by
  checking stat.kind == .directory explicitly.

Fixing the second exposed a third: walkDirectory read entry.path (relative
to the walked directory) through checkFile's cwd-relative open, correct only
when scanning "." -- fixed by joining path + entry.path before reading.

Verified: zig test 3/3, zig build-exe clean, ran the real binary against all
three code paths (no staged files, a file with real Cyrillic literals, a
directory with one clean and one Cyrillic file).

* fix(tri): migrate sacred_bench.zig to Zig 0.16, B10 complete, five real bugs found

Subprocess: std.process.run(gpa,io,options) replaces three Child.exec() call
sites, using the same pattern proven in cyrillic_guard.zig. This closes B10 --
all four Zig 0.16 API families it needed (file I/O, Clock/Timestamp, sockets,
subprocess) now have proven patterns across 8 independently-verified files.

Running the actual binary end-to-end (main() and both print functions had
zero prior test coverage) found five pre-existing defects, none of them
migration mechanics:

- Two print_row closures used C printf-style specifiers (%-11s, %8.2f) with
  mismatched arg counts against Zig's {s}/{d} placeholders -- a guaranteed
  compile failure in any Zig version. Rewrote as valid Zig format strings.
- Both closures accessed fields on a still-optional BenchmarkResult after an
  `if (result != null)` check, which doesn't narrow the type. Fixed with
  `if (result) |r|`.
- Two call sites declared `const results` then called the mutating
  `.deinit()` -- fixed to `var`.
- The test's CSV fixture had an uncommented header row that the `#`-only
  comment-skip logic tried to parse as data, crashing parseFloat at runtime.
- Two real CLI-invocation bugs: the installed iverilog 13.0 doesn't
  recognize --version (needs -V) or +define+NAME=VAL (needs -D) -- verified
  against the real installed binary.

Added a print-functions test (previously zero coverage). Verified end-to-end
with real iverilog/vvp: the fixed binary reaches iverilog, which now runs and
fails on a genuine RTL-completeness gap (three Verilog modules referenced but
not compiled) -- captured and reported correctly, proving the subprocess path
works. That RTL gap is a separate FPGA-domain issue, out of scope here.

Also fixed sacred_commands.zig, an orphaned two-line wrapper (nothing calls
it, no build.zig resolves its @import targets) whose calls were stale against
both wrapped functions' current signatures and missing `try` on an !void
return -- itself a pre-existing compile error, fixed for future consistency.

B10 backlog item marked completed; main.zig's own migration is split out as
a new tracked item (B22) since it's a different scale/risk profile than any
file done under this survey.

* chore(loop): iteration 102, B22 sized -- one real blocker found, filed as OD10

Surveyed main.zig's local-import graph (BFS, not repeated zig build-exe) to
size the migration properly instead of diving in blind. 249 of 250 reachable
files resolve cleanly; one broken import (tri_farm.zig -> deleted
local_farm.zig, 5 dangling call sites) blocks the whole graph from
compiling. Traced to 36f3863 (HSLM-training extraction), which deleted
the file but never updated its caller.

The deleted code (a Docker-based Wave-9 worker-farm manager) is fully
recoverable from git history, but whether to restore it or remove the dead
call sites is a product decision tied to that refactor's intent, not
something the code itself answers. Filed as OD10 rather than guessing;
B22 marked needs_operator_decision with blocked_by naming the reason.

* feat(tri): B21 -- disk-halt hysteresis and flap detection in tri_loopstate

tripwire was a stateless single-reading snapshot: a free-space bounce right
at a threshold flapped the verdict every cycle even when disk pressure
hadn't really changed. Two new pure state machines fix that:

- applyDiskHysteresis: a raw halt reading always wins immediately, but
  recovering out of halt needs N consecutive non-halt readings (default 2,
  tunable via loop.tripwires.disk_recovery_confirmations_needed). A dip
  back to halt before confirmation resets the streak.
- detectFlap/updateHaltEpisodes: tracks halt-episode start iterations in a
  rolling window (iteration count, not wall-clock time -- keeps this
  dependency-free); 3+ episodes in the window (tunable) surfaces a flap
  warning even while the current reading is clear. Old episodes are pruned
  once they age out of the window.

New writeTripwireHysteresis persists both to loop.tripwires on every
tripwire run -- this is the first time the tool writes STATE.json back
instead of only reading it. 9 new tests (49 total). Found and fixed a real
leak while writing the round-trip test: mutating the parsed JSON tree with
the caller's allocator instead of the document's own arena leaked silently;
fixed by using st.doc.arena.allocator() for tree mutations.

Verified against real STATE.json in a scratch copy before touching the live
file: forced halt -> held at halt through 1/2 confirmations -> cleared at
2/2; seeded a 3-episode flap and confirmed the warning fires while verdict
stays RUNNING (flap is informational, not a hard tripwire, per spec).
Confirmed episode pruning works.

Deliberately deferred: auto-appending a formal anomalies[] row on flap
detection (ID numbering, dedup, and narrative authoring are a distinct,
larger task) -- flap surfaces as a printed warning + persisted history for
now, documented as a bounded follow-up rather than left ambiguous.

Also fixed a stale loop.tripwires._note that claimed hysteresis was
unimplemented, and discovered (by running the rebuilt binary against
production data for the first time) that the backlog is now fully
decision-gated -- B18/B19/B22 all need an explicit operator call, B6 is
externally blocked. Recorded plainly rather than manufactured busywork.

* chore(loop): iteration 104, B19/OD9 resolved, B21 hysteresis proven on a real disk crisis

Posted the PR #877 status-check comment per explicit operator go-ahead
("post it as-is") after drafting it on request:
gHashTag/trinity#877 (comment).

Investigating before drafting corrected a standing mischaracterization from
OD3/OD9: #877 is the operator's own PR (authored via an earlier Claude Code
session, in their own repo), not a third-party-adjacent one needing extra
publish caution. Also found why it looked stuck: "Brain Health Check" is a
repo-wide CI gate that ran full brain-region tests against a blog-post-only
diff touching no brain code, and its own failure-comment bot hit an
unrelated 403 permissions bug. The PR body had already deferred the merge
call to the operator from the start -- not neglect, a parked proposal.

Separately: this cycle opened with a real disk crisis (0.18 GiB, then 127
MiB free) -- the third this session. It resolved on its own (disk recovered
to ~20 GiB, /tmp state including the compiled binary was gone, consistent
with a reboot or cache-clear outside this loop's control) before any
action was needed. This gave B21's hysteresis its first real-world test:
the first post-crisis reading correctly held the verdict at HALT for one
more confirmation despite the raw reading already showing recovery, then
cleared on the second confirming reading -- exactly the designed behavior,
now proven against production data instead of only a scratch copy.

* build: declare tri-loopstate, skipped under -Dci

The reachability ratchet reported 396 against a baseline of 395. The orphan was
src/tri/tri_loopstate_main.zig -- an entry point that imports tri_loopstate.zig
but which nothing imports in turn, so it is invisible to both the @import walk
and the b.path() seed list.

Its own header says "there is no build.zig anywhere in this repo that targets
it". That was true when it was written and is not any more, and the same header
asks for exactly this: "wire this in".

Guarded behind !ci_mode, following the pattern already used for the GUI and
raylib targets. The reason differs -- those need system libraries, this needs a
newer compiler: the file uses std.process.Init, which is Zig 0.16, and CI still
runs 0.15.2. Declaring it unconditionally fails the whole build there.

The b.path() stays literal inside the guard, so the ratchet still sees the
entry point: it reads build.zig as text rather than running it.

Verified: `zig build loopstate` succeeds on 0.16, and `-Dci=true` lists no
loopstate step.

* ci: guard the last 0.16-only target, and give the migrated files a real gate

"Validate VIBEE Codegen" was still red on this branch after the previous fix,
on a different file:

    src/tri/sacred_synth_report.zig:278:30: error: root source file struct
    'process' has no member named 'Init'

The earlier commit guarded tri-loopstate because the compiler named it. The
compiler names the first failure, not the class. Enumerating properly: ten
files on this branch now carry `main(init: std.process.Init)` -- the whole B10
migration campaign -- and exactly one of them, tri-sacred-synth-report, is an
installed executable that CI builds. The other nine are modules or unreferenced
entry points, which Zig never compiles.

The underlying fact is worth stating plainly: NEITHER toolchain builds this
tree. 0.15.2 fails on the migrated files; 0.16.0 fails on ~20 targets still
written against 0.15 APIs (measured, not assumed -- `zig build -Dci=true` on
0.16 locally). So bumping CI is not the fix, and won't be until the migration
finishes.

That makes the guard necessary, and on its own it would be a quiet loss: both
guarded executables would have no automated check at all, verified once by hand
by whoever migrated them and never again. A migration campaign that removes its
own files from CI as it proceeds is the shape of problem that only surfaces
much later.

So the guard ships with its replacement. .github/workflows/zig-0-16-migrated.yml
fetches a real 0.16 toolchain and compiles every file carrying that signature.
The list is derived by grep, never hand-maintained: the next file someone
migrates is covered the moment it lands. A derived list can silently match
nothing and pass green, so an empty result is a hard failure there, not a pass.

Verified before pushing, both directions:
  - all 10 migrated files compile under 0.16          (10 ok / 0 fail)
  - the step body runs verbatim under local bash       (exit 0)
  - the same body in a tree with no matches            (exit 1, guard fires)
  - `zig build -Dci=true --list-steps` no longer lists sacred-synth-report
  - `zig build --list-steps` still does
  - the b.path() literal stays inside the guard, so the reachability ratchet,
    which reads build.zig as text, still sees the entry point

while-read rather than mapfile in the workflow: the only bash on this machine is
3.2, which has no mapfile, and a construct that cannot be run before pushing is
how CI-only breakage happens.

* ci: pin the 0.16 job's target, and handle the one file that needs libc

The job I added one commit ago failed on its first run, on src/trinet/main.zig:

    std/c.zig:10647:12: error: dependency on libc must be explicitly specified
    pub extern "c" fn open(path: [*:0]const u8, oflag: O, ...) c_int;
    referenced by: open: src/trinet/serial.zig:33:21

I had verified all ten files locally and they passed. The verification was real
and still missed this: macOS links libc implicitly, Linux requires -lc, and I
checked on a macOS laptop against a Linux runner. "Verified locally" was true
and not worth much, because the thing that differed was the platform.

Two changes, both aimed at that gap rather than at the symptom:

-target is now pinned to x86_64-linux and named in an env var next to the
command that reproduces it. On the runner this is a no-op; off the runner it is
the entire point -- `zig build-exe <file> -target x86_64-linux` now fails on a
laptop exactly the way CI does. Confirmed by reproducing the original error
locally that way before writing the fix.

Files needing libc are retried with -lc and reported as "ok (needs -lc)" rather
than -lc being passed to everything. Which files pull in libc is a fact about
those files; linking it into the nine that don't would quietly weaken what this
job proves in order to make one line shorter.

Re-verified by extracting the step body from the YAML and running it verbatim
under the pinned target: 10 ok / 0 fail, with src/trinet/main.zig correctly
labelled as the one needing -lc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant