Skip to content
zit Artifact [17de67519c]
Login

Artifact [17de67519c]

Artifact 17de67519c0ff87b093e670badf633296e5e0985aa4b158291260205cc22a4ed:


//! zit — the shared repo-view loader (CLI-layer, NOT part of the embeddable
//! `zit` library).
//!
//! Compiled only into the `zit` executable (imported by main.zig); never
//! re-exported from root.zig. It bridges the persisted `LogStore` (bytes) to the
//! derived `RepoState` (branches, leaves, tags, current-trunk) that the query
//! verbs — `leaves` first, then `branch`/`tag`/`info` — read. One `load`, one
//! `deinit`: a verb builds the whole crosslinked view in a single call and asks
//! its questions of `RepoState` directly, never re-deriving CLI-side.
//!
//! Determinism seam: this module touches no clock, RNG, filesystem, or network.
//! It operates on an already-open `LogStore` (whose I/O capability was injected
//! at construction), so the effectful file handling stays in the CLI layer.

const std = @import("std");
const zit = @import("zit");
const artifact = zit.artifact;
const repostate = zit.repostate;
const logstore = @import("zit_logstore");

/// Lowercase-hex identity space. The `RepoState`/`MemStore` identity for a
/// check-in is its 40/64-char artifact name — the lowercase hex of the log's
/// binary digest. P-card parents and T-card targets are already that hex on the
/// wire, so encoding `LogStore.hashOf` to hex puts every reference in one space.
const hex_digits = "0123456789abcdef";

/// The RFC repo-state §6.1 default-branch fallback: `main-branch` is used only
/// when the config setting is unset. Never the branch of a check-in that
/// carries its own active branch tag.
const default_branch: []const u8 = "trunk";

/// A fully crosslinked view of a repository, ready for the derived queries of
/// repo-state §6-9. Pinned in place by the caller: `state` holds an
/// `ArtifactStore` that borrows `&self.store`, so the value must not be moved
/// after `load`.
pub const RepoView = struct {
    /// Identity space only: interned hashes (hex names), no content bytes. Leaf
    /// derivation reads no content, so the store stays a pure hash↔rid map.
    store: repostate.MemStore,
    /// The derived state every query reads: leaf table, branch tags, closed
    /// markers, event times. Borrows `&self.store`.
    state: repostate.RepoState,
    /// The live config frame, retained so `main_branch` can borrow its bytes.
    /// null when the log carries no config (repo-state §6.1: caller owns the
    /// default).
    owned_config: ?logstore.OwnedConfig,
    /// The repo-state §6.1 default-branch name: the config `main-branch` value
    /// when set and non-empty, else `"trunk"`. Threaded into `branchOf`; never a
    /// hardcoded literal in the presence of a setting.
    main_branch: []const u8,

    /// Every failure `load` can surface: allocation, a storage device error, or
    /// a structurally corrupt check-in (a manifest/control with no parseable
    /// timestamp). A closed set so the CLI can switch exhaustively.
    pub const LoadError = error{ OutOfMemory, Storage, Corrupt };

    /// Crosslink every persisted check-in and control artifact into a derived
    /// `RepoState`. `self` is initialized in place; `log` is borrowed for the
    /// duration and still owned by the caller. Transactional: on success the
    /// caller owns the view and must `deinit` it; on any error `load` tears down
    /// everything it built, so the caller neither can nor should `deinit`.
    ///
    /// Sequence: read the config (resolving `main_branch`), then walk the frames
    /// in persisted order. A frame whose bytes are not a structural artifact (a
    /// file blob) is skipped; a check-in manifest is crosslinked with its
    /// parents and tags; a control artifact's tags are ingested; every other
    /// artifact kind has no effect on the check-in DAG.
    pub fn load(self: *RepoView, gpa: std.mem.Allocator, log: *logstore.LogStore) LoadError!void {
        self.store = repostate.MemStore.init(gpa);
        self.state = repostate.RepoState.init(gpa, self.store.store());
        self.owned_config = null;
        self.main_branch = default_branch;
        // Self-clean on failure: a partially built view frees itself so the
        // caller's contract is "deinit only a view that loaded".
        errdefer {
            if (self.owned_config) |*oc| oc.deinit(gpa);
            self.state.deinit();
            self.store.deinit();
        }

        self.owned_config = try log.readConfig(gpa);
        if (self.owned_config) |oc| {
            // An empty stored value is treated as unset (defensive: commit
            // always writes a real name), so `main_branch` is never empty and
            // `branchOf`'s `len != 0` precondition always holds.
            if (oc.config.default_branch.len != 0) self.main_branch = oc.config.default_branch;
        }
        std.debug.assert(self.main_branch.len != 0);

        const rids = try log.ridsInPersistedOrder(gpa);
        defer gpa.free(rids);
        // A bounded plaintext memo for this one sweep: bases persist strictly
        // before their deltas, so resolving rids in persisted order keeps each
        // base hot and collapses delta-chain read-amplification to ~1x. Costs
        // nothing on an all-raw log (raw content is never cached).
        var cache = logstore.PlaintextCache.init();
        defer cache.deinit(gpa);
        // Bounded by the persisted artifact count; each rid is visited once.
        for (rids) |rid| try self.linkOne(gpa, log, rid, &cache);
    }

    /// Frees the config frame and the derived state. Call only on a view that
    /// `load` returned successfully — a failed `load` already tore itself down.
    /// `state` borrows `store`, so it is torn down first.
    pub fn deinit(self: *RepoView, gpa: std.mem.Allocator) void {
        if (self.owned_config) |*oc| oc.deinit(gpa);
        self.state.deinit();
        self.store.deinit();
        self.* = undefined;
    }

    /// The lowercase-hex artifact name of `rid` in this view's identity space —
    /// what a verb prints and compares against the current checkout. `rid` is a
    /// `RepoState` rid (the `MemStore` space), not a `LogStore` rid.
    pub fn hashOf(self: *RepoView, rid: repostate.Rid) []const u8 {
        return self.store.store().hashOf(rid);
    }

    /// The `RepoState` rid for a lowercase-hex artifact name, or null when the
    /// name was never interned in this view. A verb resolves the current
    /// checkout's version this way to mark the corresponding leaf (§7.5).
    pub fn lookup(self: *RepoView, hash: []const u8) ?repostate.Rid {
        return self.store.store().lookup(hash);
    }

    /// Crosslink or ingest one persisted frame. A parse failure means the bytes
    /// are not a structural artifact (a file blob) — skipped, not an error,
    /// matching the "crosslink whatever parses" model. Allocation failure
    /// propagates.
    fn linkOne(self: *RepoView, gpa: std.mem.Allocator, log: *logstore.LogStore, rid: logstore.Rid, cache: *logstore.PlaintextCache) LoadError!void {
        const bytes = (try log.contentCached(gpa, rid, cache)) orelse return; // phantom: no content to crosslink
        defer gpa.free(bytes);
        var m = artifact.parse(gpa, bytes) catch |e| switch (e) {
            error.OutOfMemory => return error.OutOfMemory,
            else => return, // not a structural artifact — a file blob, skipped
        };
        defer m.deinit();
        // Only check-in manifests and control artifacts affect the check-in DAG
        // and its tags; clusters, wiki, tickets, technotes, and attachments do
        // not.
        if (m.kind != .manifest and m.kind != .control) return;
        try self.link(gpa, log, rid, &m);
    }

    /// Feed one manifest or control artifact into `RepoState`. Both carry a hash,
    /// an event timestamp, and tag cards; a manifest additionally carries its
    /// parent references (crosslinked into the DAG), while a control's hash gains
    /// no event — only its tags are ingested (repo-state §4.2, §7).
    fn link(self: *RepoView, gpa: std.mem.Allocator, log: *logstore.LogStore, rid: logstore.Rid, m: *const artifact.Manifest) LoadError!void {
        var hex_buf: [64]u8 = undefined;
        const hash = hexName(log.hashOf(rid), &hex_buf);
        // A check-in or control artifact without a parseable D-card timestamp is
        // structurally corrupt — no silent skip of a malformed check-in.
        const d = m.d orelse return error.Corrupt;
        const mtime = repostate.parseMtime(d.timestamp) orelse return error.Corrupt;

        var tags: std.ArrayList(repostate.TagOp) = .empty;
        defer tags.deinit(gpa);
        // Bounded by the manifest's own T-card count.
        for (m.t) |tc| try tags.append(gpa, tagOpOf(tc));

        switch (m.kind) {
            .manifest => {
                var parents: std.ArrayList([]const u8) = .empty;
                defer parents.deinit(gpa);
                // Bounded by the manifest's own P-card token count; each `next`
                // strictly shrinks the iterator.
                for (m.p) |pc| {
                    var it = pc.hashes();
                    while (it.next()) |parent| try parents.append(gpa, parent);
                }
                _ = try self.state.crosslink(.{
                    .hash = hash,
                    .mtime = mtime,
                    .parents = parents.items,
                    .tags = tags.items,
                });
            },
            .control => {
                _ = try self.state.ingestControlTags(hash, mtime, tags.items);
            },
            else => unreachable, // linkOne admits only manifest/control
        }
    }
};

/// Map a parsed T card (repo-state §5.1 sign syntax) to a `RepoState` tag op:
/// `+` singleton, `-` cancel, `*` propagate. `name` is the sign-stripped,
/// escape-decoded tag name; `target` is null for a `*` self-tag; `value` passes
/// through. Total over `TSign`.
fn tagOpOf(tc: artifact.TCard) repostate.TagOp {
    return .{
        .type = switch (tc.sign) {
            .add => .singleton,
            .cancel => .cancel,
            .propagate => .propagate,
        },
        .name = tc.name,
        .target = tc.target,
        .value = tc.value,
    };
}

/// Encode a binary digest to its lowercase-hex artifact name into `out`,
/// returning the used prefix. A 20-byte (sha1) or 32-byte (sha3-256) digest
/// yields a 40- or 64-char name — always ≤ 64.
fn hexName(raw: []const u8, out: *[64]u8) []const u8 {
    std.debug.assert(raw.len >= 1 and raw.len <= 32);
    for (raw, 0..) |b, i| {
        out[i * 2] = hex_digits[b >> 4];
        out[i * 2 + 1] = hex_digits[b & 0x0f];
    }
    return out[0 .. raw.len * 2];
}

// ===========================================================================
// Tests — the loader's job is to reproduce the repo-state §6-7 derived views
// from persisted bytes. Fixtures are assembled with `artifact.create` (the
// library serializer) and stored through a real `LogStore`; every expected
// value comes from the repo-state RFC text, never from Fossil.
// ===========================================================================

const storage = @import("zit_storage");
const testing = std.testing;

/// A tmp-backed writable `LogStore` fixture plus the file handle it borrows.
/// Mirrors the FileStorage/tmp pattern the logstore tests use. `deinit` tears
/// the store down; the tmp dir is the caller's to clean up.
const Fixture = struct {
    fs: storage.FileStorage,
    log: logstore.LogStore,

    /// Opens a fresh empty `.zit`-style log in `tmp`. `hash_kind` 0 accepts the
    /// 32-byte sha3-256 digests the fixtures use. Initialized in place: the
    /// store's `StorageFile` borrows `&self.fs`, so `self` must not be moved
    /// after this call.
    fn init(self: *Fixture, tmp: *std.testing.TmpDir, io: std.Io) !void {
        const raw = try tmp.dir.createFile(io, "log", .{ .read = true });
        self.fs = storage.FileStorage.init(io, raw, 0);
        self.log = try logstore.LogStore.open(testing.allocator, self.fs.storageFile(), 0);
    }

    fn deinit(self: *Fixture) void {
        self.log.deinit(testing.allocator);
    }

    /// Serialize `m`, store it under its own sha3-256 digest (the content-address
    /// commit uses), and return the 64-hex artifact name (caller frees).
    fn put(self: *Fixture, m: *const artifact.Manifest) ![]u8 {
        const bytes = try artifact.create(m, testing.allocator);
        defer testing.allocator.free(bytes);
        var digest: [32]u8 = undefined;
        std.crypto.hash.sha3.Sha3_256.hash(bytes, &digest, .{});
        _ = try self.log.put(&digest, bytes);
        var name_buf: [64]u8 = undefined;
        return testing.allocator.dupe(u8, hexName(&digest, &name_buf));
    }
};

/// A skeleton check-in manifest (repo-state §6.1: a root carries the propagating
/// branch + sym tags; a child carries a P card and inherits by propagation).
/// `parent` null ⇒ a root check-in with the `trunk` branch tags; non-null ⇒ a
/// child of that parent name. `branch_value`/`extra_tags` let a test place an
/// explicit branch or closed tag. Files are irrelevant to leaf derivation, so
/// there are none.
const CheckinSpec = struct {
    ts: []const u8,
    parent: ?[]const u8 = null,
    /// When set, the root's branch/sym tags use this value instead of "trunk".
    branch_value: []const u8 = "trunk",
    /// Extra T cards appended after the branch tags (e.g. a self closed tag).
    extra_tags: []const artifact.TCard = &.{},
};

fn manifestOf(spec: CheckinSpec, tag_buf: []artifact.TCard, parent_buf: []artifact.PCard) artifact.Manifest {
    const is_root = spec.parent == null;
    var n: usize = 0;
    if (is_root) {
        tag_buf[n] = .{ .sign = .propagate, .name = "branch", .target = null, .value = spec.branch_value };
        n += 1;
    }
    for (spec.extra_tags) |t| {
        tag_buf[n] = t;
        n += 1;
    }
    var p_slice: []const artifact.PCard = &.{};
    if (spec.parent) |parent| {
        parent_buf[0] = .{ .parents = parent, .count = 1 };
        p_slice = parent_buf[0..1];
    }
    return .{
        .arena_state = std.heap.ArenaAllocator.init(testing.allocator),
        .kind = .manifest,
        .a = null,
        .b = null,
        .c = .{ .comment = "c" },
        .d = .{ .timestamp = spec.ts },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "u" },
        .z = null,
        .w = null,
        .f = &.{},
        .j = &.{},
        .m = &.{},
        .p = p_slice,
        .q = &.{},
        .t = tag_buf[0..n],
    };
}

/// Store a check-in built from `spec`, returning its 64-hex name (caller frees).
fn putCheckin(fx: *Fixture, spec: CheckinSpec) ![]u8 {
    var tag_buf: [8]artifact.TCard = undefined;
    var parent_buf: [1]artifact.PCard = undefined;
    var m = manifestOf(spec, &tag_buf, &parent_buf);
    defer m.deinit();
    return fx.put(&m);
}

/// Store a control artifact carrying `tags` (repo-state §4.2/§7.4 tag mutation).
/// `create` classifies a D/T/U artifact with no C card and no parents as a
/// control (tag) artifact.
fn putControl(fx: *Fixture, ts: []const u8, tags: []const artifact.TCard) ![]u8 {
    var m: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(testing.allocator),
        .kind = .control,
        .a = null,
        .b = null,
        .c = null,
        .d = .{ .timestamp = ts },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "u" },
        .z = null,
        .w = null,
        .f = &.{},
        .j = &.{},
        .m = &.{},
        .p = &.{},
        .q = &.{},
        .t = tags,
    };
    defer m.deinit();
    return fx.put(&m);
}

/// Load a view over `fx.log`. Caller pins `view` and `deinit`s it.
fn loadView(view: *RepoView, fx: *Fixture) !void {
    try view.load(testing.allocator, &fx.log);
}

/// Resolve a 64-hex name to its `RepoState` rid via the view's identity space.
fn ridOf(view: *RepoView, name: []const u8) ?repostate.Rid {
    return view.store.store().lookup(name);
}

/// Whether `name` appears among the view's open leaves.
fn openLeafPresent(view: *RepoView, name: []const u8) bool {
    const rid = ridOf(view, name) orelse return false;
    var it = view.state.leaves();
    while (it.next()) |leaf| {
        if (leaf == rid) return view.state.isOpenLeaf(rid);
    }
    return false;
}

test "repoview §7.1: a single-commit repo loads its lone root as the open leaf" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    const rid = ridOf(&view, root).?;
    try testing.expect(view.state.isLeaf(rid)); // §7.1: no same-branch child
    try testing.expect(view.state.isOpenLeaf(rid)); // §7.4: no closed tag
    try testing.expectEqualStrings("trunk", view.state.branchOf(rid, view.main_branch));
}

test "repoview §7.1: a closed leaf stays in leaves() but isOpenLeaf is false (closed derived, not stored)" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // A root that self-closes: an active +closed tag on itself (§7.4).
    const closed = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = null, .value = null }};
    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00", .extra_tags = &closed });
    defer testing.allocator.free(root);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    const rid = ridOf(&view, root).?;
    try testing.expect(view.state.isLeaf(rid)); // §7.1: still a leaf-table member
    try testing.expect(!view.state.isOpenLeaf(rid)); // §7.4: closed ⇒ not open
}

test "repoview §7.2: a check-in with a same-branch primary child is not a leaf" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);
    const child = try putCheckin(&fx, .{ .ts = "2020-01-02T00:00:00", .parent = root });
    defer testing.allocator.free(child);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    try testing.expect(!view.state.isLeaf(ridOf(&view, root).?)); // §7.2: has a same-branch child
    try testing.expect(view.state.isLeaf(ridOf(&view, child).?)); // the tip is the leaf
}

test "repoview §7.2: a same-branch child via a non-primary (merge) P-card disqualifies the parent leaf" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // A root and a second independent root, both on trunk; then a merge child
    // whose PRIMARY parent is `mainline` and whose SECOND (merge, non-primary)
    // parent is `side`. §7.2: the merge edge still disqualifies `side` as a leaf.
    const mainline = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(mainline);
    const side = try putCheckin(&fx, .{ .ts = "2020-01-02T00:00:00" });
    defer testing.allocator.free(side);

    // Build the merge child by hand: two P tokens, primary then merge.
    var pbuf: [130]u8 = undefined;
    const joined = try std.fmt.bufPrint(&pbuf, "{s} {s}", .{ mainline, side });
    var m: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(testing.allocator),
        .kind = .manifest,
        .a = null,
        .b = null,
        .c = .{ .comment = "merge" },
        .d = .{ .timestamp = "2020-01-03T00:00:00" },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "u" },
        .z = null,
        .w = null,
        .f = &.{},
        .j = &.{},
        .m = &.{},
        .p = &.{.{ .parents = joined, .count = 2 }},
        .q = &.{},
        .t = &.{},
    };
    defer m.deinit();
    const child = try fx.put(&m);
    defer testing.allocator.free(child);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    // Both parents are on trunk (default fallback); the merge child is on trunk.
    // §7.2: neither parent is a leaf — `side` is reached only via a merge edge.
    try testing.expect(!view.state.isLeaf(ridOf(&view, mainline).?));
    try testing.expect(!view.state.isLeaf(ridOf(&view, side).?));
    try testing.expect(view.state.isLeaf(ridOf(&view, child).?));
}

test "repoview §7.3: open builds the full leaf table over every crosslinked check-in" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // A linear chain of three: only the tip is a leaf; the two interior
    // check-ins are excluded because each has a same-branch child (§7.3 full
    // rebuild over every check-in, roots included).
    const a = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(a);
    const b = try putCheckin(&fx, .{ .ts = "2020-01-02T00:00:00", .parent = a });
    defer testing.allocator.free(b);
    const c = try putCheckin(&fx, .{ .ts = "2020-01-03T00:00:00", .parent = b });
    defer testing.allocator.free(c);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    var count: usize = 0;
    var it = view.state.leaves();
    while (it.next()) |_| count += 1;
    try testing.expectEqual(@as(usize, 1), count);
    try testing.expect(view.state.isLeaf(ridOf(&view, c).?));
}

test "repoview §7.3: a root check-in with no parent loads as a leaf" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // Two independent roots (no parent at all): §7.3 says a parentless check-in
    // is a leaf when it has no same-branch child, and MUST be included.
    const r1 = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(r1);
    const r2 = try putCheckin(&fx, .{ .ts = "2020-01-02T00:00:00" });
    defer testing.allocator.free(r2);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    try testing.expect(view.state.isLeaf(ridOf(&view, r1).?));
    try testing.expect(view.state.isLeaf(ridOf(&view, r2).?));
}

test "repoview §7.4: a leaf without a closed tag is open; a control artifact closes it" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);

    // Open before any closed tag.
    {
        var view: RepoView = undefined;
        try loadView(&view, &fx);
        defer view.deinit(testing.allocator);
        try testing.expect(view.state.isOpenLeaf(ridOf(&view, root).?)); // §7.4
    }

    // A control artifact applies an active +closed tag to the leaf (§7.4: the
    // closed status is an active tag-cross-reference row, tagtype > 0).
    const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = root, .value = null }};
    const ctl = try putControl(&fx, "2020-01-02T00:00:00", &close);
    defer testing.allocator.free(ctl);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);
    const rid = ridOf(&view, root).?;
    try testing.expect(view.state.isLeaf(rid)); // still a leaf
    try testing.expect(!view.state.isOpenLeaf(rid)); // §7.4: now closed
}

test "repoview §7.4: a cancel (-closed) control reopens a previously closed leaf" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);
    const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = root, .value = null }};
    const ctl_close = try putControl(&fx, "2020-01-02T00:00:00", &close);
    defer testing.allocator.free(ctl_close);
    // A later cancel row (tagtype 0) removes the active closed marker (§7.4:
    // closed is only "an assignment, not a cancellation").
    const reopen = [_]artifact.TCard{.{ .sign = .cancel, .name = "closed", .target = root, .value = null }};
    const ctl_open = try putControl(&fx, "2020-01-03T00:00:00", &reopen);
    defer testing.allocator.free(ctl_open);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);
    try testing.expect(view.state.isOpenLeaf(ridOf(&view, root).?)); // reopened
}

test "repoview §7.5: two open leaves on one branch both load (ambiguous tip)" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // A root with two children on the same branch: a fork. §7.5: both children
    // are open leaves of trunk, so each is an ambiguous leaf of the other.
    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);
    const c1 = try putCheckin(&fx, .{ .ts = "2020-01-02T00:00:00", .parent = root });
    defer testing.allocator.free(c1);
    const c2 = try putCheckin(&fx, .{ .ts = "2020-01-03T00:00:00", .parent = root });
    defer testing.allocator.free(c2);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    const r1 = ridOf(&view, c1).?;
    const r2 = ridOf(&view, c2).?;
    try testing.expect(view.state.isOpenLeaf(r1));
    try testing.expect(view.state.isOpenLeaf(r2));
    try testing.expect(!view.state.isLeaf(ridOf(&view, root).?)); // parent forked
    try testing.expect(view.state.isAmbiguousLeaf(r1)); // §7.5: another open leaf on the branch
    try testing.expect(view.state.isAmbiguousLeaf(r2));
}

test "repoview §6.1: the configured main-branch names the default branch; absent config falls back to trunk" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();

    // (a) No config frame: a tagless check-in falls back to "trunk". But our
    // root carries a propagating branch tag, so assert on a value-carrying root
    // named "trunk" and on the fallback via main_branch.
    {
        var fx: Fixture = undefined;
        try fx.init(&tmp, testing.io);
        defer fx.deinit();
        const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
        defer testing.allocator.free(root);
        var view: RepoView = undefined;
        try loadView(&view, &fx);
        defer view.deinit(testing.allocator);
        try testing.expectEqualStrings("trunk", view.main_branch); // §6.1: unset ⇒ trunk
    }

    // (b) A config frame naming a different main-branch: the loader reads it and
    // threads it into main_branch (§6.1: MUST read the configured value).
    {
        var fx: Fixture = undefined;
        try fx.init(&tmp, testing.io);
        defer fx.deinit();
        try fx.log.putConfig(.{ .hash_policy = 0, .default_branch = "mainline", .project_id = "p" });
        try fx.log.commit();
        // A check-in with NO branch tag (a bare control-free child is awkward, so
        // use a root but assert the fallback name only when a tagless check-in is
        // present). Here assert on main_branch itself — the §6.1 configured value.
        const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00", .branch_value = "mainline" });
        defer testing.allocator.free(root);
        var view: RepoView = undefined;
        try loadView(&view, &fx);
        defer view.deinit(testing.allocator);
        try testing.expectEqualStrings("mainline", view.main_branch); // §6.1: configured value
        try testing.expectEqualStrings("mainline", view.state.branchOf(ridOf(&view, root).?, view.main_branch));
    }
}

test "repoview §6.1: a cancelled branch tag falls back to the configured main branch (tagtype>0 filter)" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // A root on trunk, then a control that CANCELS its branch tag. §6.1: only an
    // active (tagtype > 0) branch tag names a branch; a cancelled one falls back
    // to the default branch exactly like a missing one.
    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00", .branch_value = "feature" });
    defer testing.allocator.free(root);
    const cancel = [_]artifact.TCard{.{ .sign = .cancel, .name = "branch", .target = root, .value = "feature" }};
    const ctl = try putControl(&fx, "2020-01-02T00:00:00", &cancel);
    defer testing.allocator.free(ctl);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);
    // The cancelled branch tag is inactive, so branchOf returns the fallback.
    try testing.expectEqualStrings("trunk", view.state.branchOf(ridOf(&view, root).?, view.main_branch));
}

test "repoview §6.1: a check-in's absent P-card parent is an eventless phantom" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // A child whose P card names a parent hash never stored: the loader interns
    // that name as a phantom rid with no event row. §6.1: such a rid has no
    // mtime, so it is not a resolvable tip — the guard predicate a checkout-mark
    // resolver relies on (mtimeOf == null ⇒ nothing to mark).
    const absent = "a" ** 64;
    const child = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00", .parent = absent });
    defer testing.allocator.free(child);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    const phantom = view.lookup(absent) orelse return error.PhantomNotInterned;
    try testing.expect(view.state.mtimeOf(phantom) == null); // eventless
    try testing.expect(view.state.mtimeOf(ridOf(&view, child).?) != null); // child bears an event
}

test "repoview §7.7: a closed non-leaf does not appear among open leaves (enumeration ≠ name resolution)" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // A root, then a child that is the tip. Close the root (a non-leaf). §7.7:
    // leaf enumeration answers "no same-branch descendant and still open"; the
    // closed root is neither a leaf nor an open leaf regardless of any name it
    // might resolve.
    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);
    const child = try putCheckin(&fx, .{ .ts = "2020-01-02T00:00:00", .parent = root });
    defer testing.allocator.free(child);
    const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = root, .value = null }};
    const ctl = try putControl(&fx, "2020-01-03T00:00:00", &close);
    defer testing.allocator.free(ctl);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);

    try testing.expect(!openLeafPresent(&view, root)); // not a leaf at all
    try testing.expect(openLeafPresent(&view, child)); // the actual open leaf
}

test "repoview: a non-artifact file blob is skipped, not an error" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    // Store raw non-artifact bytes (a file blob) alongside one real check-in.
    // The loader skips the blob (parse fails) and still derives the check-in.
    const blob = "this is not a manifest\n";
    var bdigest: [32]u8 = undefined;
    std.crypto.hash.sha3.Sha3_256.hash(blob, &bdigest, .{});
    _ = try fx.log.put(&bdigest, blob);
    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);

    var view: RepoView = undefined;
    try loadView(&view, &fx);
    defer view.deinit(testing.allocator);
    try testing.expect(view.state.isOpenLeaf(ridOf(&view, root).?));
}

test "repoview: load is leak-free under every allocation failure" {
    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx: Fixture = undefined;
    try fx.init(&tmp, testing.io);
    defer fx.deinit();

    const root = try putCheckin(&fx, .{ .ts = "2020-01-01T00:00:00" });
    defer testing.allocator.free(root);
    const child = try putCheckin(&fx, .{ .ts = "2020-01-02T00:00:00", .parent = root });
    defer testing.allocator.free(child);
    const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = child, .value = null }};
    const ctl = try putControl(&fx, "2020-01-03T00:00:00", &close);
    defer testing.allocator.free(ctl);

    const Ctx = struct {
        fx: *Fixture,
        fn run(a: std.mem.Allocator, self: @This()) !void {
            var view: RepoView = undefined;
            try view.load(a, &self.fx.log);
            view.deinit(a);
        }
    };
    try testing.checkAllAllocationFailures(testing.allocator, Ctx.run, .{Ctx{ .fx = &fx }});
}

test "repoview fuzz: load never crashes on adversarial or mutated artifact bytes" {
    // Invariant (repo-state §7): building a view over arbitrary persisted bytes
    // returns only a declared LoadError or a consistent state — never a crash,
    // out-of-bounds read, or unhandled trap. Non-artifact frames are skipped;
    // a structurally corrupt check-in is a declared error.
    var prng = std.Random.DefaultPrng.init(0x1eaf5eed);
    const rand = prng.random();

    // A valid baseline manifest to mutate.
    var base_tag: [8]artifact.TCard = undefined;
    var base_p: [1]artifact.PCard = undefined;
    var base_m = manifestOf(.{ .ts = "2020-01-01T00:00:00" }, &base_tag, &base_p);
    const base = try artifact.create(&base_m, testing.allocator);
    base_m.deinit();
    defer testing.allocator.free(base);

    var round: usize = 0;
    while (round < 256) : (round += 1) {
        var tmp = testing.tmpDir(.{});
        defer tmp.cleanup();
        var fx: Fixture = undefined;
        try fx.init(&tmp, testing.io);
        defer fx.deinit();

        // Put a handful of frames: some random bytes, some byte-flipped copies of
        // the valid baseline. Each is stored under its own digest.
        var k: usize = 0;
        const frames = rand.intRangeAtMost(usize, 1, 5);
        while (k < frames) : (k += 1) {
            var payload: std.ArrayList(u8) = .empty;
            defer payload.deinit(testing.allocator);
            if (rand.boolean()) {
                const len = rand.intRangeAtMost(usize, 1, 128);
                try payload.resize(testing.allocator, len);
                rand.bytes(payload.items);
            } else {
                try payload.appendSlice(testing.allocator, base);
                const flips = rand.intRangeAtMost(usize, 1, 6);
                var f: usize = 0;
                while (f < flips) : (f += 1) {
                    const idx = rand.intRangeLessThan(usize, 0, payload.items.len);
                    payload.items[idx] ^= rand.int(u8);
                }
            }
            var digest: [32]u8 = undefined;
            std.crypto.hash.sha3.Sha3_256.hash(payload.items, &digest, .{});
            _ = fx.log.put(&digest, payload.items) catch continue;
        }

        var view: RepoView = undefined;
        view.load(testing.allocator, &fx.log) catch |e| {
            // Only the declared, closed error set may surface; a failed load has
            // already torn itself down, so no deinit here.
            switch (e) {
                error.OutOfMemory, error.Storage, error.Corrupt => {},
            }
            continue;
        };
        view.deinit(testing.allocator);
    }
}