Skip to content
zit Timeline
Login

Timeline

50 descendants of d7dd9ea30b699786

2026-07-20
04:40
Merge h11-serve-socket: a stalled client can no longer wedge the server

zit serve accepts one connection at a time and read its request head and body with no deadline, so a single peer that opened a connection and then dribbled or withheld its request pinned the accept loop and starved every other client -- a real availability gap the in-memory fuzzers could not see, since they never touch the socket. The connection now runs under a whole-connection deadline: the request work races a timed sleep, and a peer that misses the deadline is dropped so the loop moves on. The bind is loopback only and the deadline is generous, so a legitimate request -- even at the reply ceiling -- always finishes far inside it; only a stalled peer is shed. Four end-to-end abuses -- a slow-loris head, a half-sent body, a mid-response disconnect, an idle-connection burst -- each prove the server still answers a fresh request and a real clone still converges, and the fix is shown to be load-bearing by wedging with it removed. A per-read idle timeout and a configurable value are left for later. check-in: 56c17d0675 user: dmestas tags: trunk

04:37
Prove sync liveness under a degraded, never-healed wire

The sync sweep proved convergence only after the network fully healed, so a protocol that converges on a perfect wire but livelocks under sustained loss would still pass.

Phase 2b runs a fresh session over a wire held at a fixed drop/dup rate, never healed, asserting the same convergence and termination oracles within a window scaled from the loss rate: expected rounds are the clean window over (1 - p_loss) squared per direction, times a safety factor, recomputed in a comment rather than a magic number. No corruption is injected -- a corrupted body is already a lost round.

The protocol converges comfortably: over 200 seeds the worst run took 7, 13, and 18 rounds at 10, 25, and 40 percent loss, against windows of 160, 228, and 356 -- no livelock. The gate joins at 10 percent; the soak sweeps the wider rate set. Pinning the window below the real need trips the liveness oracle, so the check is not vacuous. The liveness-under-faults discipline follows TigerBeetle's VOPR. check-in: 464b82c888 user: dmestas tags: h7-degraded

04:34
serve: bound each connection so a stalled client cannot wedge the server

zit serve accepts one connection to completion before the next (single-threaded, serve.zig), and receiveHead plus the body read blocked with no deadline. A loopback peer that dribbles a request head, half-sends a body, or connects idle and holds could pin the accept loop forever -- the socket read path the exchange fuzzers never reach, covered before only by happy-path interop.

Add a whole-connection deadline: the request work runs on its own unit of concurrency raced against a timed sleep, and draining the loser cancels the blocked read, freeing the loop. A per-read SO_RCVTIMEO is unusable here (the Io backend treats a socket-timeout EAGAIN as a bug). serve binds 127.0.0.1 only, so a legitimate same-host request finishes in milliseconds even at the 512 MiB body ceiling; the 5 s bound never fires on a real client and only sheds a stalled one.

Four end-to-end scenarios abuse a live server over loopback -- dribbled head, half-sent body, mid-response disconnect, idle-connection burst -- and assert with bounded waits that a fresh request is still answered and a clone still converges (sync 3.7). closed check-in: e87fb840d1 user: dmestas tags: h11-serve-socket

04:28
Merge h2-checkout-dst: crash-test the working tree, and a finding it surfaces

The working tree -- the one place a version-control tool can lose a user's data -- had no crash simulation; only the log store did. A new faulty directory layer tears and freezes writes at a seeded crash point, and a sweep replays an update across every crash cut, reopens, and checks four invariants. Three hold: an untracked file the update must preserve is never touched, a crashed update recovers to its target through the undo journal, and the checkout record never names a check-in whose tree did not finish materializing. The fourth does not, and that is the point of the harness: the payload write lands in place rather than through the write-temp-and-rename the metadata already uses, so a crash mid-write can leave a managed file spliced from old and new bytes -- torn, but recoverable. The gate runs the three that hold; the fourth is pinned by a test asserting the splice IS produced, so it flips the day the write is made atomic. Crash-point enumeration follows TigerBeetle's storage model. check-in: 5401ce245f user: dmestas tags: trunk

04:21
sim: working-tree crash-recovery DST sweep for update

Add sim/faulty_dir.zig, a decorator std.Io that cuts a run at a seeded mutating-op boundary the way a power loss does: the crashing write persists at most a torn prefix and every later mutation is a frozen no-op, so the checkout layer's own recovery moves no byte. Crash-point enumeration follows TigerBeetle's storage-fault-injection lineage, ported to the directory surface update.zig and checkout.zig write through.

Add sim/checkout_sweep.zig: it materializes a check-in, replays an A-to-B update.applyCheckin under a crash at every reachable cut, reopens, and asserts unmanaged files are preserved, the checkout record names only a complete target, and the tree recovers to the target via the undo journal. Joins the dst gate per seed at bounded cuts; the soak walks every boundary.

Finding: update.writePayload writes working files in place (createFile truncate, write, sync), not through the atomic write-temp+fsync+rename that checkout.write, the managed set, and the undo journal use. A crash mid-write can leave a file matching neither its pre- nor its post-image. The sweep pins this; a working-tree write-journal is the fix. closed check-in: 0cea021cf9 user: dmestas tags: h2-checkout-dst

04:19
Merge h3-crashsync: run a sync over a store that can tear, across crashes

The storage and network simulations ran off the same seed but never intersected, so no simulated exchange ever crossed a store that could lose its tail. This composes them: the sync client's content is backed by a real log over a tearable file, and between rounds -- on a seeded coin -- the store is crash-restarted mid-persist, reopened to its recovered prefix, and the session resumed with a fresh client. The existing safety and liveness oracles must survive the composition: no corrupt content accepted, the recovered set never exceeds what was held before the crash, and once both the network and the disk heal a fresh session still converges within the bounded window. Green across the gate and a deep 120-seed run; a retained switch that makes recovery accept a torn frame trips the oracle, so the check is not vacuous. The fault-composition follows TigerBeetle's VOPR. check-in: 9b689ebafe user: dmestas tags: trunk

04:13
Compose storage and network faults in the sync DST

The sync sweep and the storage sweep ran in disjoint PRNG streams off the same seed and never intersected: no exchange ever ran over a client store that could tear. A partial pull could leave torn frames that recovery accepts but a resumed session never converges past.

runCrashResume backs the client store with a real LogStore over a tearable FaultyFile instead of the pure MemStore, seeded healthy then armed to tear. Between phase-1 rounds, on a seeded coin, it power-cuts the store mid-persist, reopens it, reloads the recovered prefix, and resumes with a fresh Client over the same durable store, mirroring cmd/sync's loadContentStore for wire-name to log-digest identity.

The oracle keeps every existing sync invariant and adds the crash bite: post-recovery contents are a subset of the pre-crash held set, no corrupt frame crosses the boundary, and phase-2 liveness heals both domains before a fresh session converges in the bounded window (sync 3.7). One seeded PRNG stream drives the whole run, so a failure replays from the shared reproduce line.

The composed-fault and liveness-under-faults discipline follows TigerBeetle's VOPR. closed check-in: d2b5f729ef user: dmestas tags: h3-crashsync

04:11
Merge h9-fuzzcov: every fuzz target now guards its own reach

Only two targets asserted that their generator still reaches the interesting outcomes; the other 25 could rot to instant-rejection and pass every no-crash check while testing nothing. Six targets that split into distinct outcomes now carry a reached-outcomes floor above a measured minimum round count -- decode-bounded, target-size, predicates, adversarial sync parsers, logstore open, and unframe each must reach both their accept and reject arms, or the run fails. Thresholds are measured at a fixed seed and re-checked at a second, and each floor is proven to trip when the generator is biased to one arm. Single-outcome round-trip targets carry a one-line exemption instead of a vacuous assert. The measurement also named several generators that under-reach their target's arms; those are recorded for follow-up, not fixed here. check-in: 57d8994848 user: dmestas tags: trunk

04:06
fuzz: floor outcome reachability across the fuzz targets

serve_fuzz already fails a run when its generator stops reaching the dispatch arms it exists to exercise, and merge3 does the same for conflict shapes. Generalize that guard so a generator that has drifted to one outcome can no longer pass every no-crash round while testing nothing on the arm it dropped.

Six targets gain a measured reached-outcomes floor: delta decodeInt and targetSize (a decoded value vs MalformedDelta), artifact predicates (simple-pathname and wiki-name each accepting and rejecting), sync card parsers (push/igot/uvgimme each accepting and rejecting), logstore open (torn-tail recovery vs Corrupt), and transport deframe (a damaged container both decoding and being rejected). Every threshold is counted at a fixed seed, stated in the comment, and set below the arm's measured frequency; each floor gates above a minimum round count so short runs never trip it.

Every other target carries a one-line note on why its outcome space does not partition: round-trips have only a success arm, and several garbage generators reach only their reject arm, the accept arm covered by the round-trip and deterministic tests. closed check-in: 1abe1089c1 user: dmestas tags: h9-fuzzcov

04:00
Merge h5b-verbasserts: TigerStyle asserts on the cmd verb layer

The companion to the library assert pass, now on the two split verb files. Eighteen asserts stating invariants that already hold: in the read verbs (cmd/history.zig), that a name decodes to a full digest before it is rendered and that a windowed or -n-capped listing's shown count matches the walked set; in the mutating verbs (cmd/checkout.zig), the preconditions at each destructive entry -- a full valid target id before update, both bisect endpoints present, exactly one pending merge or special op, every revert target in the managed set, and the checkout-record and managed-set agreement commit assembles a manifest from. Asserts only, no behaviour change; all sit on paths the e2e and simulation sweeps already drive, and all survive them. No assert fired, so no finding. check-in: 58763bb760 user: dmestas tags: trunk

03:55
cmd: TigerStyle assert-density pass over the verb layer

Mirror the H5a assert idiom (state invariants that already hold, no behavior change) across the split read and mutating verbs. Asserts only; wire output and exit codes unchanged.

history.zig read verbs: - decodeFullId: a validated name decodes to a whole 20/32-byte digest, never a truncated prefix. - walkLog: every checkin/phantom line renders a full valid name, and the -n cap (parseLimitValue maps 0 to null) never overruns. - stat --artifacts / timeline: the listing projects the walked rid set, is capped by -n, and the printed count matches it. - tag add: the mint targets a check-in by its full artifact id.

checkout.zig mutating verbs: - update/amend: the applied/amended target is a full artifact id. - bisect: both endpoints present before the span move. - merge: exactly one pending op (merge_parent XOR special_op) before applyMerge; revert restores only managed paths. - commit: checkout record and managed set agree before the manifest assembles; root has no pending op, merge and special exclusive, a child's parent resolved, every captured card tracked. closed check-in: 99e0c73447 user: dmestas tags: h5b-verbasserts

03:53
Merge h4-interop: bidirectional fossil interop smoke, push side included

The interop smoke only ever went one way -- fossil confirming what zit authored, always pull or clone direction. The push side of the wire, where capability negotiation and igot/gimme ordering live, was never exercised against the reference. Two scenarios close it, same policy as before (zit authors every byte; fossil only confirms; a mismatch prints the diff and its RFC citation, exits nonzero, and is a ledger decision). zit pushes a grown check-in to a live fossil server and a fresh fossil checkout must reconstruct the file byte for byte; then fossil commits a zit-authored blob that zit pulls and reads back through the library, re-verifying content against its artifact name. The smoke log now opens with the exact fossil version each run, so the conformance claim always names what it was confirmed against. check-in: 3ad0bf90da user: dmestas tags: trunk

03:49
Smoke: push-direction fossil interop and a recorded fossil version

Every smoke scenario drove pull/clone only -- fossil pulling from zit, zit pulling from fossil -- so zit's push wire (its igot/gimme ordering and capability negotiation as the sync client uploading to a server) never met the reference implementation.

Two scenarios in src/smoke.zig close that gap under the file's standing policy: every compared byte is zit-authored, fossil only confirms. S7 has zit push a grown check-in to a live fossil server, then reads it back from a fossil checkout byte-for-byte (sync 3.3/3.7). S8 lets a fossil commit extend the shared repo with our bytes, has zit pull it, and confirms convergence through the library reader -- Repository.blob re-verifying content against its artifact name (sync 3.2/3.7, artifact 7.2). Both share a prelude that seeds a zit repo, has fossil clone it for a common project-code, grants the anonymous loopback sync user check-in, and disables the autosync the clone inherits.

Smoke also prints the fossil version verbatim at the head of its log, so the exact reference implementation the scenarios confirmed against is recorded wherever smoke runs. closed check-in: a2672203c0 user: dmestas tags: h4-interop

03:41
Merge h1-soak: the out-of-band deep soak the harnesses assume exists

The fuzz and simulation harnesses defer real depth to a nightly runner that was never written; the gate only ever ran a shallow smoke. scripts/ soak.sh is that runner. In a ReleaseSafe build it runs the deterministic simulation over many seeds and long step counts, then every fuzz target -- enumerated from the runner's own usage so the list cannot drift -- under an equal wall-clock budget per target, so a cheap target reaches far more rounds than an expensive one with no second cost table to maintain. A failure appends a runnable reproduce line to a dated log, drops a flag file, and exits nonzero; it never files or fixes anything, and its logs live outside the checkout. Proven to catch both a seeded simulation regression and a seeded fuzz regression, each replaying from the logged line. Installing the schedule and the first full nightly are operator steps, documented in the script and its README. check-in: 1b447f8b47 user: dmestas tags: trunk

03:36
Add scripts/soak.sh: out-of-band deep DST + fuzz soak runner

The pre-commit gate runs the DST and fuzz harnesses shallow. Every comment deferring depth to an out-of-band runner pointed at a script that did not exist; scripts/ held only mirror-export.sh. This is that runner.

On a ReleaseSafe build (asserts on) it runs the repostate+sync DST at a deep seed/step sweep, then every fuzz target enumerated from the runner's own usage output, so the list cannot drift, under an equal per-target wall-clock budget. --rounds grows from measured speed to fill each budget, so a cheap target sweeps millions of rounds while a pack round-trip sweeps thousands.

On the first failure it appends a runnable reproduce line to a dated log, drops a flag file, and exits non-zero; it never files and never fixes. It owns an explicit fresh seed per fuzz invocation, because the fuzz runner's buffered stderr is dropped on the error-return path: a crashing target prints only its error name, not the seed it flushed before running.

Logs and the flag live outside the repo. Installing the schedule and the first full nightly are operator actions, documented in the header and soak.README. closed check-in: 9f00c44a8f user: dmestas tags: h1-soak

03:34
Merge h8-readrot: simulate latent read rot, prove the read path detects it

The fault simulator only ever corrupted at crash and restart boundaries. It could not model a latent sector error: bit rot that surfaces during a read of a store that opened clean. A new read_rot_pct knob does that -- a read returns payload-derived, never-self-healing corruption for a frame that is intact on disk -- and a sweep reopens a healthy store, arms it, and reads every held hash. The oracle: each read returns exactly the stored bytes or a declared corruption error, never wrong bytes as success. The append open path re-runs the whole-frame checksum per read, so it catches the rot; the sweep is green and a companion test proves the sweep can fail by reading through the checksum-skipping read-only open. The knob is inert at zero, so every existing seed keeps its byte stream. The crash-point fault technique follows TigerBeetle's storage fault model. check-in: ba805e5913 user: dmestas tags: trunk

03:29
Add online read-fault injection to the storage DST sweep

FaultyFile injected corruption only at crash/restart boundaries. Add a read_rot_pct knob: a read of an intact byte interns one offset in its span as permanently corrupt (payload-derived mask, never self-heals) -- a latent sector error surfacing at content() time on a store that opened clean, independent of any crash. A healthy disk draws nothing, so existing sweeps keep a byte-identical PRNG stream.

New storage_sweep.readRotSweep joins the dst gate: after a healthy open, every content-bearing hash reads back either its exact bytes or a declared corruption error -- wrong bytes presented as success is SilentCorruption. The append open path re-runs each frame's whole-frame CRC per read, so latent rot is detected and the gate stays green. The verified-on-open read path (openReadOnly) trusts the scan-time CRC on an immutable-length assumption that bit rot violates; a test drives that path to prove the oracle fires SilentCorruption.

Read-fault model follows TigerBeetle's storage-fault-injection lineage. closed check-in: fde61b03b2 user: dmestas tags: h8-readrot

03:27
Merge h5a-asserts: TigerStyle assert pass on update, serve, transport

Assert density was collapsing exactly where the working tree and the wire are touched: update.zig held 2 asserts across 2127 lines, serve.zig 3, transport.zig 2 -- against the hundreds in the artifact and sync cores. This pass adds 17 asserts stating invariants that already hold: the checkout-state transitions in update (managed-set, record and rollback image agreement around each applyCheckin stage), the request/response postconditions in serve (media, arity and body-ceiling), and the frame, redirect and body-cap postconditions in transport. Asserts only, no behaviour change; where a result had to be named to assert on it the error mapping is preserved exactly. All survive the deterministic simulation sweeps, which is the point of landing them before the checkout crash-recovery sweep that will hammer update. check-in: 9654d06838 user: dmestas tags: trunk

03:21
Assert-density pass: update, serve, transport

Raise assert density in the three thinnest library modules, enforcing invariants that already hold. Asserts only, no behavior change.

update.zig: guard the checkout-state transition. applyCheckin asserts the desired managed set equals the target plan membership (validateTopology proves the paths unique); requireClean asserts a clean tree's plan membership equals the recorded managed set; recordUndoImage asserts the affected union covers both operands; commitMaterialization self-guards that the committed record names exactly the staged check-in and that the managed set is its membership; the merge path asserts one rollback pre-image per path and a shared pre-merge managed baseline.

serve.zig: a servable request is a POST bearing a request-valid compressed media type (§4 L759-764); an accepted body honors the §4 L1148-1149 ceiling; a compressed reply is application/x-fossil (§4.1); the accept loop honors its request bound.

transport.zig: a framed container carries its 4-byte length prefix (§4.1); an unframed reply stays within reply_body_cap (§4 r9); a followed redirect lands only on an http/https transport (§4 r12/r13). closed check-in: c9fb87f554 user: dmestas tags: h5a-asserts

03:06
Merge s5-checkout: extract the checkout verb family, completing the split

Fourth and final family extraction. A pure behaviour-preserving move: the mutating checkout verbs -- update, bisect, merge, revert, undo, redo, commit, amend -- and the helpers private to them (the undo journal, the dirty-tree gate, the checkout record, the commit and amend manifest assembly) move verbatim into src/cmd/checkout.zig, byte-identical to their source but for the pub the dispatch forces on the eight handlers. commitEpoch stays in main.zig because main itself builds the ambient clock from it; shared helpers stay pub and aliased. With this, main.zig holds only main, the verb dispatch, init, the shared helpers and the test surface -- the eleven-thousand-line file is now four cmd families plus a thin dispatch core. No helper duplicated, no cross-cmd import, test count unchanged. check-in: ae54e47b3c user: dmestas tags: trunk

02:59
Extract the mutating checkout verb family into src/cmd/checkout.zig

The final family extraction, completing the split of the CLI surface out of main.zig. A pure behaviour-preserving move: the mutating verbs -- update, bisect, merge, revert, undo, redo, commit, amend -- and the helpers and tests private to them move verbatim into src/cmd/checkout.zig, byte-identical to their source but for the pub the dispatch switch forces on the eight dispatched handlers.

The move is non-contiguous: the update error mapper, the digest wire helpers, mintControlArtifact, and commitEpoch stay in main.zig because they are shared with other verbs or with main itself, so moving them would cross a cmd boundary or duplicate them. Shared helpers stay pub and are aliased so the moved bodies read unchanged. No helper duplicated, no cross-cmd import, test count unchanged.

After this, main.zig retains only main, the verb dispatch, runInit, the genuinely shared helpers, and the test helpers. closed check-in: 9870033d32 user: dmestas tags: s5-checkout

02:33
Merge s4-history: extract the read verb family into src/cmd/history.zig

Third of four family extractions. A pure behaviour-preserving move: the read surface -- stat, log, leaves, timeline, branch, tag, info, ls, cat, artifact, annotate -- and the name-resolution and rendering helpers private to it move verbatim into src/cmd/history.zig, byte-identical to their source but for the pub the dispatch switch forces on the eleven handlers. The extraction is non-contiguous: the mutating checkout verbs interleave with the read verbs in the file and stay exactly in place. repoReadFail stays pub in main.zig because the sync family calls it, so moving it would cross cmd boundaries. Shared helpers stay pub and aliased so moved bodies read unchanged. No helper duplicated, no cross-cmd import, test count unchanged. check-in: 3bbc572acd user: dmestas tags: trunk

02:24
Extract read/history verb family into src/cmd/history.zig (S4)

Move the read-only inspection verbs — stat, log, leaves, timeline, branch, tag, info, ls, cat, artifact, annotate — together with the name-resolution and rendering helpers private to them out of src/main.zig into the new src/cmd/history.zig, mirroring the S2 sync and S3 tracking extractions.

Pure behaviour-preserving move: every relocated declaration is byte-identical to its main.zig source. The only changes are the new import prologue and the pub forced on the eleven dispatch-called handlers. Helpers shared with the still-in-main checkout verbs (repoReadFail, openRepo, reasonOf, firstCommentLine, renderBranchList, lessByHash, mintControlArtifact, and the per-verb help strings) stay pub in main.zig and are aliased back in the prologue.

The extraction is non-contiguous: the checkout family — update, bisect, merge, revert, undo, redo, commit, amend — is interleaved among the moved verbs and is left exactly in place.

main.zig keeps the verb-dispatch switch, now calling cmd_history.runStat and its siblings; its test root imports the new file so the moved inline test keeps running. closed check-in: 8e2c31c8ba user: dmestas tags: s4-history

01:52
Merge s3-tracking: extract the tracking verb family into src/cmd/tracking.zig

Second of four family extractions. A pure behaviour-preserving move: the eight working-tree handlers -- add, rm, mv, status, extras, clean, addremove, diff -- and the helpers private to them, including the whole diff-and-compare machinery, move verbatim into src/cmd/tracking.zig, byte-identical to their source but for the pub visibility the dispatch switch forces on the eight handlers. init stays in main.zig because clone bootstraps through it, and stat stays too; both are called by more than this family. Helpers shared with staying verbs stay pub in main.zig, aliased so the moved bodies read unchanged. No helper duplicated, no cross-cmd import created, test count unchanged. check-in: 1df44c39bd user: dmestas tags: trunk

01:46
Extract the tracking verb family into src/cmd/tracking.zig

Release step S3, the second of four family extractions from main.zig. A pure behaviour-preserving move: the eight working-tree tracking handlers -- add, rm, mv, status, extras, clean, addremove, diff -- and the helpers and tests private to them move verbatim into src/cmd/tracking.zig, byte-identical to their source but for the pub visibility the dispatch switch forces on the moved handlers. Helpers shared with other verbs (runStat, commit, merge, revert) stay pub in main.zig and are aliased back so the moved bodies read unchanged. No helper is duplicated and no cross-cmd import is created; runInit and runStat stay in main.zig. The test count is unchanged, so nothing was orphaned. main.zig keeps main, the verb dispatch, and the shared helpers. closed check-in: ed32969b32 user: dmestas tags: s3-tracking

2026-07-19
23:57
Merge s2-sync: extract the sync verb family into src/cmd/sync.zig

First of four family extractions from main.zig. A pure behaviour- preserving move: the eight network and store-transfer handlers -- serve, clone, open, pack, pull, push, sync, remote -- and the helpers private to them move verbatim into src/cmd/sync.zig, byte-identical to their source but for the pub visibility the file boundary forces on the handlers the dispatch switch still calls. Helpers shared with other verbs stay in main.zig, aliased back so the moved bodies read unchanged. No helper is duplicated and no cross-cmd import is created; the test count is unchanged, so nothing was orphaned. main.zig keeps main, the verb dispatch, and the shared helpers. check-in: 47a2717952 user: dmestas tags: trunk

23:45
Extract the sync verb family into src/cmd/sync.zig

Move the sync / network / store-transfer verbs — serve, clone, open, pack, pull, push, sync, remote — together with the helpers and tests private to them out of main.zig into a new src/cmd/sync.zig. This is a pure behaviour-preserving move: every relocated line is byte-identical to its main.zig source; only cmd/sync.zig's import prologue and the handlers' pub visibility are new.

Helpers still shared with other verbs stay in main.zig, now made pub and aliased back in cmd/sync.zig so the moved bodies call them by their original unqualified names. The verb-dispatch switch stays in main.zig and calls into the new file; main.zig's test block imports it so the moved inline tests keep running.

No behaviour change: the full test count is unchanged and the dead-file guard reaches cmd/sync.zig through the dispatch import, with no allowlist entry. closed check-in: 254575cbd8 user: dmestas tags: s2-sync

23:18
Merge s1-tidy: a dead-file guard before the cmd/ split

Zig runs only the tests reachable from a root the build compiles. A source file no root imports still compiles -- its tests just stop running, and nothing fails. That is the defining hazard of the coming main.zig split, so the guard lands first. A test walks every src/**/*.zig, builds the import graph, and asserts each file is reachable from one of the three roots: root.zig, main.zig's test block, or dst.zig. The failure names the file and the one-line import that fixes it. Six files that build.zig wires as their own roots are allowlisted, and the list itself is checked so a stale or newly-reachable entry fails rather than hiding a real orphan.

The DeadFilesDetector pattern is borrowed from TigerBeetle's src/tidy.zig. check-in: 60b10a6332 user: dmestas tags: trunk

23:11
Add tidy dead-file guard over the src test surface

Zig runs only the tests reachable from a root the build compiles. A .zig file that no root imports still compiles -- its tests simply stop running and coverage evaporates with no error. tidy.zig walks src/**/*.zig, builds the @import graph, and asserts every file is reachable from one of the three test roots: root.zig (exported module), main.zig's test block (CLI surface), and dst.zig (harnesses). An allowlist carries the files build.zig wires as their own roots -- e2e, smoke, and the benchmarks -- each verified rather than papered over, and a hygiene check fails if an allowlist entry goes stale or becomes reachable. On a miss the failure names the file and the one-line import that re-roots it.

This lands before the main.zig to src/cmd split so that move cannot silently orphan a file's tests.

Mechanism borrowed from TigerBeetle's src/tidy.zig DeadFilesDetector, which walks its own tree and fails the build when a file falls out of the import graph. closed check-in: a8d7f6b621 user: dmestas tags: s1-tidy

22:41
Merge p5b-docs: 0.1.0 release notes and an honest limitations table

Adds the release notes as a shipped asset and a limitations section to the README, both stating plainly what 0.1.0 does not do: diff is name-status against the current check-in only, the sync server code is an informational placeholder while the project code is real, the R and W cards are carried but not interpreted, and stash/grep/ui/settings and rev-to-rev diff are absent. The notes record what the conformance claim rests on -- smoke driving the real fossil binary in CI -- and cite the pack measurements to their existing benchmark rather than restating them as fresh claims. The version stamp is left for the ship step. check-in: 7ba838ab6c user: dmestas tags: trunk

22:39
Merge p5a-help: pin that help names every dispatched verb

Help already listed all dispatched verbs, but nothing held it that way: a verb added later could ship without a help line and no test would notice. An end-to-end pin now drives off the Verb enum itself and fails, naming the offender, if any dispatched verb is absent from zit help -- so the audit cannot silently rot as the command set grows. check-in: 7a0735c415 user: dmestas tags: trunk

22:35
Merge iss7b: read a fossil-legacy baseline instead of refusing files/merge

The #7 fix taught the timeline read path to tolerate fossil's 2008 cvs2fossil-era check-ins, whose F cards carry a dangling trailing separator the strict grammar has no production for. The baseline read path had the same defect: parseBaseline used the strict parser, so files and merge on a delta whose baseline is one of those legacy manifests failed as an unresolvable baseline. It now reads through parseForImport, the same tolerance, and threads the normalized copy the manifest borrows into the owned baseline so it is freed exactly once. The strict parser still runs first and remains the sole grammar oracle; any other malformation is still refused. check-in: cd63fa1766 user: dmestas tags: trunk

22:30
Read fossil's legacy delta baselines instead of dropping their files

The files/ls path eagerly reads a delta check-in's B-card baseline and parsed it through the strict reference parser. fossil's own 2008 cvs2fossil-era manifests carry F cards with a dangling trailing separator after the hash -- a space where a perm token would go -- which the section 4.2 grammar has no production for, so the strict parser rejects them as corrupt. When such a manifest is a delta's baseline, the rejection surfaced as BaselineUnresolvable and the check-in's whole file set vanished.

The target-manifest read path already tolerates this shape through parseForImport; the baseline path now does too. The strict parser still runs first and stays the sole grammar oracle; tolerance engages only when a well-framed, Z-valid artifact is rejected, strips exactly one trailing separator per F card, re-seals the checksum, and re-runs the strict parser -- any other malformation is still refused. The tolerated normalized copy is owned by the parsed baseline and freed with it. closed check-in: 7bb5af134e user: dmestas tags: iss7b

22:20
notes: name -n's timeline/log limit, not only its dry-run preview

The command summary described -n as previewing the destructive verbs alone; it also caps timeline and log at the most recent N, and -R reads a repository with no working tree. Both are stated so the summary does not undersell what shipped. closed check-in: 80913fe845 user: dmestas tags: p5b-docs

22:13
Guard that every dispatched verb appears in `zit help`

Add an end-to-end check that runs `zit help` and asserts every member of the dispatched Verb enum is listed as a command, keyed off the enum itself rather than a hand-copied list. A verb wired into the dispatcher without a help line now fails the build instead of shipping undiscoverable.

The match is anchored on the command column, so a name that appears only inside another verb's description — "sync" in serve's line, "stat" within "status" — cannot satisfy it; a dropped command entry is caught by name.

The shipped help already names all verbs, so this changes no output; it pins that state against future drift. closed check-in: 64b9a0ee71 user: dmestas tags: p5a-help

22:10
docs: honest 0.1.0 limitations table and release notes

Add a Limitations section to the README and a docs/RELEASE-NOTES-0.1.0.md release asset, both re-derived from current shipped behavior rather than assumed.

The caveats are the deliberate deferrals, verified against the binary and source: diff is working-tree-vs-current-check-in and name-status only, with no rev-to-rev form; the sync server code is a fixed informational placeholder while the per-repo project code is real (derived on init, persisted, adopted on clone); the R and W cards are parsed but not interpreted, whereas T and U are acted on; and stash/grep/ui/settings have no equivalent.

The notes name the local fossil 2.28 that the interop smoke passes against, flag that the runner version lags and must be confirmed at ship, and cite the pack re-deltification measurements to their benchmark commit bab9303056. check-in: ab4951c2e8 user: dmestas tags: p5b-docs

21:52
Merge iss7: read fossil's legacy check-ins instead of dropping history

timeline stopped partway through a large fossil-origin repository and lost every check-in at and beyond that point. The cause was not an I/O fault, as the shared "input/output error" wording suggested, but a strict parse: fossil's own 2008 cvs2fossil-era check-ins carry F cards with a dangling trailing separator after the hash, which the §4.2 grammar has no production for and the reference parser rejects as corrupt. Repository.manifest -- the timeline read path -- used that strict parser, so the first such check-in and all older ones vanished.

It now reads through parseForImport, the same read-path tolerance the diff baseline already relies on. The strict parser still runs first and stays the sole grammar oracle; tolerance engages only when a well-framed, Z-valid artifact is rejected, strips exactly one trailing separator per F card, re-seals the checksum, and re-runs the strict parser -- so any other malformation is still refused. Complete history renders again, down to the repository's 2007 root. check-in: 4e8d402bb6 user: dmestas tags: trunk

21:36
timeline: read fossil-legacy check-in manifests instead of dropping them

Repository.manifest parsed check-in manifests through the strict reference parser, which rejects fossil's own 2008 cvs2fossil-era manifests: their F cards carry a dangling trailing space after the hash, where the §4.2 perm token (1*tchar) permits none. On a large fossil-origin repository this made `zit timeline` stop at the first such check-in and exit non-zero, silently losing that check-in and every older one — a functional loss of history, surfaced as a misleading "input/output error".

Parse through parseForImport, the same read-path interop the diff baseline already relies on: the strict parser still runs first and stays the sole grammar oracle; the trailing-separator tolerance engages only when an otherwise well-framed, Z-valid artifact is rejected. OwnedManifest now also owns the normalized copy the manifest borrows when that tolerance fires. closed check-in: b503efb9f2 user: dmestas tags: iss7

21:01
Merge iss12: -R/--repo and -n on the read verbs, in one pass

A read verb could only reach a store by walking up from the cwd, and timeline/log could only emit the whole history. -R/--repo names a store directly, so timeline, log, leaves, branch, tag, info, ls, cat, artifact and annotate run against it with no working tree; a verb that reads or writes the checkout refuses -R by name rather than silently operating on the cwd's store. -n N caps timeline and log at the N most recent check-ins (0 means all), ordered by the same DAG the full listing uses. The -n spelling stays --dry-run on add/rm/clean/addremove, a per-verb overload matching fossil, so those verbs reject -n N and keep their boolean sense.

stat is left out of the -R grant on purpose: it interrogates only the store, but its handler takes no redirected repository, so it refuses -R by name rather than read the cwd's store while appearing to honor the flag. check-in: 43468196c2 user: dmestas tags: trunk

20:50
Merge iss6: open --manifest writes the working-tree manifest files

Repositories that enable fossil's manifest setting expect manifest, manifest.uuid and manifest.tags in the checkout, and their builds read them to stamp a version. A zit checkout could not produce them, so such a build failed or, worse, succeeded with no version embedded. zit open --manifest writes them; the bare flag means ru, and a value picks any combination of r (the check-in artifact), u (its hash) and t (its branch and symbolic tags). The choice is recorded in the checkout record, so every later update refreshes the files; absent flag writes nothing. The selection is explicit rather than read from the cloned repository because the two places fossil keeps it can disagree and only one syncs, so a checkout reading the setting could diverge from a fossil checkout carrying a local override while claiming fidelity. Contents match fossil byte for byte, including the sterilizing comment it appends to manifest. check-in: b38b3be769 user: dmestas tags: trunk

20:46
Refuse -R/--repo on amend

amend rewrites history, but it was in the -R accept arm alongside the read verbs, so `zit amend -R STORE trunk --comment X` from any directory minted a check-in in the named store — a write to a redirected repository the operator never had a checkout of. That is the exact surprise the -R refusal exists to prevent, and amend is not one of the authorized read verbs.

Move amend into the reject arm so -R on amend fails by name with the same "not supported here" diagnostic every other checkout-dependent verb already emits. amend keeps working normally in a checkout; it just no longer accepts -R at all. closed check-in: 4b5dca852c user: dmestas tags: iss12

20:43
Merge iss4: zit stat, the store's own byte accounting and artifact roll

The store could not be interrogated from outside a checkout: no way to ask how many artifacts it holds, which ones, or how many of its bytes are content rather than framing. zit stat answers from one pass over the frames -- store-bytes, content-bytes, frame overhead, artifact and delta counts. Content is summed from each frame's payload during the walk, never derived from the .zit file size, which also carries the store header, a per-frame header and padding; a size-derived figure would look like a result and be a fabrication, so the two stay separate fields and the overhead between them stays checkable. --artifacts lists the stored hashes for comparison across stores. The frame walk and its accounting live in storestat, shared with the storage benchmark, so the verb inherits the benchmark's exact-accounting test and the two cannot drift. check-in: 921678c019 user: dmestas tags: trunk

20:37
Merge iss8: pull converges past the server's unclustered window

A fossil server folds artifacts older than its ~100-entry unclustered window into clusters and stops announcing those members with igot -- the M cards are the announcement. The client stored a received cluster as ordinary content and read nothing from it, so a pull past that window fetched only the cluster and then terminated, silently, having gained nothing. Cluster membership now folds into the phantom set, so the next round gimmes the members. A pull that reaches a clean terminal still lacking artifacts the peer announced now exits nonzero, rather than reporting a success that leaves a repository quietly out of date. check-in: 8d3ebc0c9d user: dmestas tags: trunk

19:18
stat: the content-bytes e2e gets an oracle it shares no code with

The honesty test asserted that content-bytes below store-bytes and that the byte fields sum to the file. Both hold for anything of the form `size - k`: one because a subtraction is smaller, the other because it is that subtraction's identity. The test named the defect it existed to catch and could not see it.

`statOracle` now totals frames from the store's per-rid index -- `payloadLenOf` for content, `frameSpanOf` less payload for overhead -- and the test pins the printed fields to it. It shares nothing with `storestat.walk`, so `size - header_len` for content now fails, as does overhead taken across all frames. One fabrication stays invisible and the test says so: subtracting the walk's own overhead and config from the size IS the measurement.

`stat_help` sat between `status_help` and its doc comment, so that comment documented the wrong constant and claimed an e2e transcription `stat` lacked. Restored, documented, and written.

A repeated `--type` was silent last-wins; a usage error now. runStat claimed "one pass" over two traversals; the claim goes, not the open, which verifies what the walk trusts. closed check-in: 0e1f7a719a user: dmestas tags: iss4

19:15
Add -R/--repo and -n N to the read verbs

-R/--repo PATH names the repository to read instead of the one found by walking up from the current directory. It is lifted out of argv before verb dispatch, so one implementation serves every read verb and each verb's grammar is unchanged. Resolution requires only the .zit store, not the .zit-checkout discovery also demands: reading a repository with no working tree is the point of the option. PATH may name the directory holding .zit or the .zit file itself. Verbs that read or write a working tree refuse -R by name; accepting it and silently operating on the current checkout is the failure worth a diagnostic to prevent. The spelling matches fossil.

-n N (alias --limit N) shows the N most recent entries on timeline and log; 0 means all, as in fossil. This deliberately overloads -n, which remains --dry-run on add and rm, just as fossil overloads it between timeline and clean. A non-numeric or negative value is refused.

An expectation asserting log rejected -n is inverted: it recorded the absence of the limit, not a rule about the grammar.

timeline still defaults to the whole listing, not fossil's 20 entries. check-in: b25e01be95 user: dmestas tags: iss12

19:14
sync: absorb cluster membership so pull converges past the unclustered window

A fossil server folds artifacts past its ~100-entry unclustered window into cluster artifacts and stops announcing them with igot: the M cards become the announcement. The client stored a received cluster as ordinary content and read nothing from it, so no phantom appeared, the 3.7(a) continuation saw an empty table, and the exchange terminated after one round having fetched only the cluster. Repository size, not delta size, was the trigger; clone converges on the 8.2 RID cursor, not on gimme, so it was unaffected.

Client.absorbCluster folds the members into the phantom set with the disposition an igot for each would produce, so the next round gimmes them. Membership is validated before any is applied; a clone skips it for the reason it announces no igot. On the cfile path the container inflates into a scratch buffer, not through store.content, which would memoise plaintext.

Client.unresolvedPhantoms counts announced-but-unobtained artifacts; pull exits nonzero when a run reaches a clean terminal with any outstanding, so a pull that stops short of what the peer holds is no longer reported as success. closed check-in: 5535de0f45 user: dmestas tags: iss8

19:07
open: add --manifest[=r,u,t] for the working-tree manifest files

Repositories that enable fossil's `manifest` setting — fossil, althttpd, pikchr — expect `manifest`, `manifest.uuid` and `manifest.tags` in the checkout; their builds read them to stamp a version. A zit checkout could not produce them, so such a build failed or, worse, succeeded with no version embedded.

`zit open --manifest` writes them; the bare flag means `ru`, a value picks any combination of `r` (the check-in artifact), `u` (its hash) and `t` (its branch and symbolic tags). The choice is recorded in the checkout record, so every later `update` refreshes the files. Absent flag writes nothing.

The selection is explicit rather than read from the cloned repository because the two places fossil keeps it disagree and only one syncs: althttpd's versioned setting says `on`, its local config `rtu`. A checkout reading the setting could diverge from a fossil checkout carrying a local override while appearing to promise fidelity.

Contents match fossil byte for byte, including the sterilizing comment it appends to `manifest`. A tracked file of one of those names is never replaced. closed check-in: dc36496c3b user: dmestas tags: iss6

18:46
Add `zit stat`: the store's own byte accounting and artifact roll

The store could not be interrogated about its contents: no way to ask how many artifacts it holds, which ones, or how many of its bytes are artifact content rather than framing. `zit stat` answers that from the frames themselves, emitting `field value` lines: store-bytes, header-bytes, content-bytes, frame-overhead-bytes, config-bytes, artifacts, full-form, deltas. The byte fields account for the file exactly.

content-bytes and store-bytes stay separate on purpose. Content is measured by summing each artifact frame's payload length; it is never derived from the `.zit` size, which also carries the store header, a header per frame, and alignment padding. A size-derived figure would look like a result and be a fabrication, so the overhead separating the two is reported alongside and stays checkable.

`--artifacts` lists the stored hashes one per line in ascending order, so two stores' rolls compare with `comm`; `--type=full|delta` narrows by storage form, read from the index the open already built.

The frame walk and its accounting live in `storestat`, shared with the storage benchmark, so the two cannot drift. check-in: 7c2262c837 user: dmestas tags: iss4

17:32
Merge blockers: a failed mv changes nothing, and its recovery is exact (B4)

mv recorded a rename in the managed set and persisted it before attempting the filesystem move, with no rollback. A move into a nonexistent directory left the set naming a path that held no file: status invented a missing entry, commit refused over it, and the recovery mv --help documents did not work.

mv now validates that the destination's parent exists and refuses before touching anything, so the failing case records nothing.

Two further defects on the same path. An inverse rename replaced the pending record instead of dropping it, so 'mv a b' followed by 'mv b a' left a b-to-a rename queued; the next commit carrying a real change would then emit an old-name F card asserting the file was formerly named b, a name never committed. That is false provenance in a permanent artifact, produced by the exact sequence the help text advertises as recovery. mvRecord now nets an inverse rename back out.

The error path printed raw Zig error names — a user saw 'AccessDenied' where clean and init already print 'permission denied' through reasonOf. It now uses the same mapping.

A residual window remains: a crash between the move and the record write is not atomic, documented at the site. Full working-tree crash consistency is a separate concern. check-in: 33a2ceb6d1 user: dmestas tags: trunk

17:27
mv: a reversal cancels the rename record instead of replacing it

`mv --help` advertises `zit mv NEW OLD` as the way to undo a rename, so it is the sequence a user is most likely to run. It left the pending record replaced rather than cleared: OLD->NEW->OLD queued a §4.7.6 old-name F card asserting OLD was formerly NEW -- a name no manifest has ever carried. The next commit with any real change would write that false claim about the past into a permanent artifact, where it cannot be taken back. A rename and its inverse are a no-op, so the pair now nets the record out.

This survived the guard because the guard's oracle was indirect: commit reports "nothing to commit" on CONTENT alone, so a stranded record passes it silently and surfaces only at the next commit that carries a change. The record is now read back directly.

mv's move errors render through `reasonOf` rather than `@errorName`, so EACCES reads "permission denied" instead of "AccessDenied". This was deferred as errno-sweep work, but the mapping already exists and `init` and `clean` print through it -- reuse, not new scope. A raw Zig error name is an internal identifier, not a diagnostic. closed check-in: 7235a86bcd user: dmestas tags: blockers

16:38
mv: move the working file before recording; failed mv records nothing

mv recorded the rename durably before attempting the filesystem move and rolled nothing back when it failed, leaving the set naming a path that does not exist and commit ready to record a rename that never happened. The reversal mv --help promises -- "run zit mv NEW OLD" -- failed too: it saw the old path still occupied and refused the collision.

mvCommit now renames first, then records; if the record fails the file is renamed back and nothing is persisted. Move-first beats record-first because undoing a durable write needs a second durable write, which can itself fail, nesting a failure inside the failure handler. Renaming a file back cannot realistically fail -- the source directory and name are still free. Build the guarantee on the rollback that cannot fail.

mvCheckLegal gains a dest-parent-exists check: stat of a path under a missing directory fails, so the old collision check read "absent" and waved it through. Residual window: a crash between move and record leaves the file at NEW with the set naming OLD, recoverable with add NEW + rm OLD.

ENOENT and EACCES were both rendered "input/output error". check-in: d7dd9ea30b user: dmestas tags: blockers