Artifact e3c38d2692a2f469c423c0ab2924553ad4f9c649805f58c4e6397e231c85bc5f:
- File
src/checkpoint.zig
— part of check-in
[9033c13f21]
at
2026-09-17 17:04:25
on branch trunk
— Remove issue-tracker numbers from source and documents
The tracker is disposable and does not live with the source, so a number is a dangling pointer. Every zit#NN and #NN reference in code comments, test names, help text, smoke labels, identifiers, release notes and errata is replaced by enough context to stand alone, or by a short plain statement of the problem. No behaviour changed. (user: dmestas, size: 27563) [annotate] [blame] [check-ins using]
//! The durable rebuild checkpoint (hybrid-history acceleration). //! //! A `.zit-cache/checkpoint` sidecar records one prefix-anchored claim: "as of //! check-in C, whose frame closes the log prefix `[0, prefix_key.end)`, the //! fully-verified file set fingerprints to this value" — plus the crosslink //! record body for that prefix, so `Repository.open`'s miss path can //! `RepoView.resumeFrom` the prefix instead of re-parsing a permanently flat //! F-card segment on every from-scratch derive (submanifests §7.4 applied to //! the segment §7.4 cannot reach). //! //! SIDECAR ONLY, by design: a checkpoint never writes the log. An earlier //! revision had a `tree` shape that pinned the anchor's file set as stored //! SUBMANIFEST artifacts; it was retired because everything it bought is had //! for free elsewhere — the rebuild win lives entirely in the prefix record, //! `verify` recomputes the canonical tree in memory as its oracle, and the //! §6.2 subtree-sharing bytes re-materialize at the first real S-emitting //! commit at identical cost, attributed to a commit somebody actually made. //! Minting them early only created permanent, unreferenced artifacts and the //! policy apparatus to keep automation from doing it by accident. //! //! Advisory-tier all the way down, exactly like `repocache.zig`, on which the //! container is modelled: deleting the file changes no answer, every //! validation failure is a MISS (never an error), `--no-cache` bypasses it, //! and the claim is independently re-derivable (`verify`). Strictly local, //! never synced, never history: the sidecar appears in no artifact and //! crosses no wire. const std = @import("std"); const artifact = @import("artifact.zig"); const logstore = @import("logstore.zig"); const repocache = @import("repocache.zig"); const repository = @import("repository.zig"); const repostate = @import("repostate.zig"); const repoview = @import("repoview.zig"); const storage = @import("storage.zig"); const crc32 = @import("crc32.zig"); /// The sidecar's member inside the advisory `.zit-cache/` directory (the home /// the consolidation reserved for it). Ignore-glob'd and never committed; /// disposable by construction. pub const checkpoint_filename: []const u8 = repocache.cache_dirname ++ "/checkpoint"; const magic = "ZITCKPNT"; // v2 carried the retired `tree` shape's fields; v3 is witness-only and drops // them from the layout. An old sidecar misses and is rebuilt (or advanced) // on the next explicit build or healing open. The body it replays is a run // of crosslink records in `repocache`'s wire format, so this bumps whenever // that record format does -- v5 with the per-file hash; an old // body replayed under a new reader would read as a corrupt cache. const format_version: u16 = 5; /// The sidecar's identity claim, exactly what the header stores. The hash /// slice borrows the load's backing buffer (`Loaded.buf`). pub const Claim = struct { /// The check-in whose frame closes the prefix. checkin_hash: []const u8, /// `artifact.fileSetFingerprint` over C's complete file set — the cheap /// structural witness `verify` re-derives from real history. fileset_fingerprint: [32]u8, /// The log prefix the record body distills; `logstore.validatePrefix` is /// the O(1) revalidation on every load. prefix_key: logstore.LogStore.LogKey, /// The shun-table fingerprint at capture: a record body distilled under an /// old mask may carry a record the mask must now hide, so any difference /// is a MISS (the same rule `.zit-cache` extension follows). shun_fp: u32, }; /// Bytes before the variable spans: /// `magic(8) | format_version(2) | reserved(2) | prefix_end(8) | /// prefix_frame_count(8) | prefix_last_frame_offset(8) | /// prefix_last_header_crc(4) | shun_fp(4) | checkin_hash_len(1) | /// fileset_fingerprint(32) | main_branch_len(2) | body_len(4)`. const fixed_prefix_len: usize = 8 + 2 + 2 + 8 + 8 + 8 + 4 + 4 + 1 + 32 + 2 + 4; // = 83 const checksum_len: usize = 4; /// A validated load: `claim`, `main_branch` and `body` slice into `buf`. pub const Loaded = struct { buf: []u8, claim: Claim, main_branch: []const u8, body: []const u8, pub fn deinit(self: *Loaded, gpa: std.mem.Allocator) void { gpa.free(self.buf); self.* = undefined; } }; /// Evidence seam for the paranoid-bypass tests: every `tryLoad` entry counts, /// BEFORE the sidecar is opened. `--no-cache` must leave this untouched — the /// promise is "trust nothing derived", and a bypass that still consults the /// sidecar is not one. Single-threaded like every derive path. pub var tryload_consults: u64 = 0; /// Read and fully validate the sidecar against the LIVE log: magic, version, /// self-checksum over every pre-checksum byte, structural spans that exactly /// account for the file size, and a prefix key that still validates /// (`validatePrefix` — the log may have GROWN, which is the whole point; /// truncation or a rewritten boundary is a miss). Returns null on ANY failure /// — a checkpoint is advisory, so an unreadable one just means "derive from /// the log". The caller still owns the shun-fingerprint compare (it needs an /// open scan) and the anchor-resolution check. pub fn tryLoad(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, log_file: storage.StorageFile) ?Loaded { tryload_consults += 1; var file = dir.openFile(io, checkpoint_filename, .{}) catch return null; defer file.close(io); const st = file.stat(io) catch return null; const size = std.math.cast(usize, st.size) orelse return null; if (size < fixed_prefix_len + checksum_len) return null; const buf = gpa.alloc(u8, size) catch return null; const n = file.readPositionalAll(io, buf, 0) catch { gpa.free(buf); return null; }; if (n != size) { gpa.free(buf); return null; } const loaded = parse(buf) orelse { gpa.free(buf); return null; }; if (!logstore.LogStore.validatePrefix(log_file, loaded.claim.prefix_key)) { gpa.free(buf); return null; } return loaded; } /// The pure structural half of `tryLoad`, split out so corruption tests can /// drive it byte-by-byte. `buf` is the whole file; the returned slices borrow /// it. Null on any framing violation — never partial trust. fn parse(buf: []u8) ?Loaded { if (buf.len < fixed_prefix_len + checksum_len) return null; if (!std.mem.eql(u8, buf[0..8], magic)) return null; if (std.mem.readInt(u16, buf[8..10], .little) != format_version) return null; // The whole-file checksum first: no declared length below is trusted // before the bytes carrying it prove intact. const stored_crc = std.mem.readInt(u32, buf[buf.len - checksum_len ..][0..4], .little); var crc = crc32.Crc32.init(); crc.update(buf[0 .. buf.len - checksum_len]); if (crc.final() != stored_crc) return null; if (std.mem.readInt(u16, buf[10..12], .little) != 0) return null; // reserved const key: logstore.LogStore.LogKey = .{ .end = std.mem.readInt(u64, buf[12..20], .little), .frame_count = std.mem.readInt(u64, buf[20..28], .little), .last_frame_offset = std.mem.readInt(u64, buf[28..36], .little), .last_header_crc = std.mem.readInt(u32, buf[36..40], .little), }; const shun_fp = std.mem.readInt(u32, buf[40..44], .little); const ckh_len: usize = buf[44]; var fp: [32]u8 = undefined; @memcpy(&fp, buf[45..77]); const mb_len: usize = std.mem.readInt(u16, buf[77..79], .little); const body_len: usize = std.mem.readInt(u32, buf[79..83], .little); // Artifact names are 40 or 64 lowercase hex. if (ckh_len != 40 and ckh_len != 64) return null; if (mb_len == 0) return null; // Exact byte accounting: the declared spans plus framing must equal the // file, so neither truncation nor trailing garbage survives. const want = fixed_prefix_len + ckh_len + mb_len + body_len + checksum_len; if (want != buf.len) return null; var pos: usize = fixed_prefix_len; const ckh = buf[pos..][0..ckh_len]; pos += ckh_len; const mb = buf[pos..][0..mb_len]; pos += mb_len; const body = buf[pos..][0..body_len]; return .{ .buf = buf, .claim = .{ .checkin_hash = ckh, .fileset_fingerprint = fp, .prefix_key = key, .shun_fp = shun_fp, }, .main_branch = mb, .body = body, }; } /// Atomically materialize the sidecar (temp + fsync + rename, the sequence /// every sibling writer uses). Every failure is swallowed: a checkpoint is an /// optimization, never a correctness dependency, so the worst outcome of a /// failed write is the derive it would have skipped. pub fn store( gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, claim: Claim, main_branch: []const u8, body: []const u8, ) void { std.debug.assert(claim.checkin_hash.len == 40 or claim.checkin_hash.len == 64); std.debug.assert(main_branch.len != 0); if (!repocache.ensureCacheDir(io, dir)) return; // `body` is the prefix record stream — hundreds of megabytes on a large // store — so it is written to the file in place rather than concatenated // into a second resident copy, the checksum folded incrementally in the // reader's byte order (the same shape as `repocache.store`). var header: std.ArrayList(u8) = .empty; defer header.deinit(gpa); appendHeader(gpa, &header, claim, main_branch, body.len) catch return; var crc = crc32.Crc32.init(); crc.update(header.items); crc.update(body); var trailer: [checksum_len]u8 = undefined; std.mem.writeInt(u32, &trailer, crc.final(), .little); std.debug.assert(header.items.len == fixed_prefix_len + claim.checkin_hash.len + main_branch.len); var af = dir.createFileAtomic(io, checkpoint_filename, .{ .replace = true }) catch return; defer af.deinit(io); af.file.writePositionalAll(io, header.items, 0) catch return; af.file.writePositionalAll(io, body, header.items.len) catch return; af.file.writePositionalAll(io, &trailer, header.items.len + body.len) catch return; af.file.sync(io) catch return; af.replace(io) catch return; } fn appendHeader( gpa: std.mem.Allocator, out: *std.ArrayList(u8), claim: Claim, main_branch: []const u8, body_len: usize, ) error{OutOfMemory}!void { try out.appendSlice(gpa, magic); try appendInt(gpa, out, u16, format_version); try appendInt(gpa, out, u16, 0); // reserved try appendInt(gpa, out, u64, claim.prefix_key.end); try appendInt(gpa, out, u64, claim.prefix_key.frame_count); try appendInt(gpa, out, u64, claim.prefix_key.last_frame_offset); try appendInt(gpa, out, u32, claim.prefix_key.last_header_crc); try appendInt(gpa, out, u32, claim.shun_fp); try out.append(gpa, @intCast(claim.checkin_hash.len)); try out.appendSlice(gpa, &claim.fileset_fingerprint); try appendInt(gpa, out, u16, @intCast(main_branch.len)); try appendInt(gpa, out, u32, @intCast(body_len)); try out.appendSlice(gpa, claim.checkin_hash); try out.appendSlice(gpa, main_branch); } fn appendInt(gpa: std.mem.Allocator, out: *std.ArrayList(u8), comptime T: type, v: T) error{OutOfMemory}!void { var b: [@sizeOf(T)]u8 = undefined; std.mem.writeInt(T, &b, v, .little); try out.appendSlice(gpa, &b); } /// Delete the sidecar. A missing file is success, not an error — the file is /// disposable by construction (`zit checkpoint drop`). pub fn drop(io: std.Io, dir: std.Io.Dir) void { dir.deleteFile(io, checkpoint_filename) catch {}; } // --------------------------------------------------------------------------- // The builder. One pass, sidecar only: enumerate C's file set through the // §9.3 seam, fingerprint it, capture the prefix record body (reusing an // existing sidecar's body and its delta-free attestation when possible), and // atomically write the sidecar. The log is opened READ-ONLY — a checkpoint // build cannot write history, structurally. // --------------------------------------------------------------------------- pub const BuildError = error{ OutOfMemory, Storage, Corrupt, /// `C` is absent from the log, interned only as a phantom, or (for the /// null-checkin default) the repository has no leaves to anchor on. CheckinNotFound, /// `C` names an artifact that is not a check-in manifest. NotACheckin, }; pub const BuildReport = struct { checkin_hash_buf: [64]u8, checkin_hash_len: u8, files: usize, prefix_frames: u64, pub fn checkinHash(self: *const BuildReport) []const u8 { return self.checkin_hash_buf[0..self.checkin_hash_len]; } }; /// Build (or rebuild) the checkpoint for `repo` anchored at `checkin` (a full /// lowercase-hex check-in name), or at the NEWEST crosslinked check-in when /// null. Ancestry plays no role: a resume replays the record body below the /// prefix boundary and parses the tail above it, which is correct for any /// record-boundary prefix whatever the open branches look like — the old /// merge-base-of-all-leaves anchor collapsed to a near-root commit (a /// two-frame prefix) on any repository with one dormant branch leaf, i.e. on /// every real imported fossil history. Idempotent: rebuilding over unchanged /// history rewrites an identical sidecar and touches nothing else. pub fn build( gpa: std.mem.Allocator, io: std.Io, repo: *repository.Repository, dir: std.Io.Dir, checkin: ?[]const u8, ) BuildError!BuildReport { // 1. C: named, or the newest crosslinked check-in — maximal coverage. const c_rid: repostate.Rid = blk: { if (checkin) |hex| break :blk repo.view.lookup(hex) orelse return error.CheckinNotFound; break :blk repo.view.state.newestCheckin() orelse return error.CheckinNotFound; }; // 3. C's file set, through the one §9.3 seam (`Repository.files` walks // trees via `flattenIfTree`; no raw F-card iteration here). var fl = (repo.files(gpa, c_rid) catch |e| return mapFilesError(e)) orelse return error.CheckinNotFound; // interned phantom: no content to enumerate defer fl.deinit(gpa); var entries: std.ArrayList(artifact.FCard) = .empty; defer entries.deinit(gpa); var files_it = fl.iterator(); while (files_it.next()) |f| try entries.append(gpa, f); const fingerprint = try artifact.fileSetFingerprint(gpa, entries.items); // 4. The log, read-only: a checkpoint build cannot write history. var file = dir.openFile(io, repository.repo_filename, .{}) catch return error.Storage; defer file.close(io); const st = file.stat(io) catch return error.Storage; var fs = storage.FileStorage.init(io, file, st.size); var log = logstore.LogStore.openReadOnly(gpa, fs.storageFile()) catch |e| return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, error.Corrupt => error.Corrupt, }; defer log.deinit(gpa); // 5. C's frame closes the prefix. const c_hex = repo.view.hashOf(c_rid); var digest: [logstore.max_hash_len]u8 = undefined; const c_raw = digest[0 .. c_hex.len / 2]; _ = std.fmt.hexToBytes(c_raw, c_hex) catch return error.Corrupt; const c_logrid = log.lookup(c_raw) orelse return error.CheckinNotFound; const key = log.logKeyAt(c_logrid) catch |e| return switch (e) { error.Storage => error.Storage, error.Corrupt => error.Corrupt, }; // 6. The prefix record body, from the cheapest source. ADVANCE: a valid // existing sidecar whose prefix is a prefix of ours already carries // [0, L0); only the tail (L0, L) is parsed. COLD: capture the whole // prefix view-free. Delta (B-card) manifests are captured like any // other: records carry no file data, and the fingerprint's file-set // resolution reads through baselines the same way every file read // does. var body: std.ArrayList(u8) = .empty; defer body.deinit(gpa); var advanced_from: ?u64 = null; reuse: { var old = tryLoad(gpa, io, dir, fs.storageFile()) orelse break :reuse; defer old.deinit(gpa); if (old.claim.shun_fp != log.shunFingerprint()) break :reuse; if (old.claim.prefix_key.end > key.end) break :reuse; // anchor moved backward: rebuild cold body.appendSlice(gpa, old.body) catch return error.OutOfMemory; advanced_from = old.claim.prefix_key.end; } const capture_from: u64 = advanced_from orelse logstore.header_len; repoview.captureRecords(gpa, &log, capture_from, key.end, &body) catch |e| return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, error.Corrupt => error.Corrupt, }; var report: BuildReport = .{ .checkin_hash_buf = undefined, .checkin_hash_len = @intCast(c_hex.len), .files = entries.items.len, .prefix_frames = key.frame_count, }; @memcpy(report.checkin_hash_buf[0..c_hex.len], c_hex); // 7. The sidecar, last. `store` swallows failures: the checkpoint is an // optimization, and nothing durable preceded it. store(gpa, io, dir, .{ .checkin_hash = c_hex, .fileset_fingerprint = fingerprint, .prefix_key = key, .shun_fp = log.shunFingerprint(), }, repo.view.main_branch, body.items); return report; } /// Opportunistically CREATE a checkpoint where none exists, from the full /// derive that just ran — the one event that proves a checkpoint would have /// paid for itself, and the one place its payload is already in hand: /// `whole_body` is the derive's captured record body; records ride in /// persisted order, so the checkpoint body is its byte-PREFIX cut at the /// anchor's frame boundary (`repoview.recordPrefixBelow`). Creation costs /// one prefix-key walk, the cut scan, and a file-set fingerprint — no /// artifact re-parses. When no cut exists (the anchor unresolvable, the /// body unrecognizable) skip silently; an explicit build or a later derive /// covers it. Sidecar-only like every build; an existing sidecar (even an /// invalid one) is left to `advance`/`build`. pub fn autoCreate( gpa: std.mem.Allocator, io: std.Io, repo: *repository.Repository, dir: std.Io.Dir, log: *logstore.LogStore, whole_body: []const u8, ) void { var probe = dir.openFile(io, checkpoint_filename, .{}) catch |e| switch (e) { error.FileNotFound => null, else => return, }; if (probe) |*p| { p.close(io); return; // the operator's (or a prior derive's) sidecar: advance owns it } // The anchor: the newest crosslinked check-in — the same choice `build` // makes, so the sidecars they write never differ. const a = repo.view.state.newestCheckin() orelse return; // an unborn repository has nothing to anchor const c_hex = repo.view.hashOf(a); // The prefix key at the anchor, and the derive body cut at its boundary. var digest: [logstore.max_hash_len]u8 = undefined; const c_raw = digest[0 .. c_hex.len / 2]; _ = std.fmt.hexToBytes(c_raw, c_hex) catch return; const c_logrid = log.lookup(c_raw) orelse return; const key = log.logKeyAt(c_logrid) catch return; const body = repoview.recordPrefixBelow(log, whole_body, key.end) orelse return; // The fingerprint — the only real work left. var fl = (repo.files(gpa, a) catch return) orelse return; defer fl.deinit(gpa); var entries: std.ArrayList(artifact.FCard) = .empty; defer entries.deinit(gpa); var files_it = fl.iterator(); while (files_it.next()) |f| entries.append(gpa, f) catch return; const fingerprint = artifact.fileSetFingerprint(gpa, entries.items) catch return; store(gpa, io, dir, .{ .checkin_hash = c_hex, .fileset_fingerprint = fingerprint, .prefix_key = key, .shun_fp = log.shunFingerprint(), }, repo.view.main_branch, body); } /// How many bytes the log must have grown past a checkpoint's prefix before /// the open path bothers advancing it. Below this, the resume already covers /// the tail cheaply and an advance would spend more than it saves; above it, /// the one-time advance cost (a tail parse plus one file-set fingerprint) /// keeps the checkpoint from drifting toward a full re-derive. pub const advance_min_growth_bytes: u64 = 4 << 20; /// Opportunistically ADVANCE an existing checkpoint to the current mainline /// anchor. Called from `Repository.open`'s healing paths behind the growth /// and anchor-progress gates. Sidecar-only like every build, so there is /// nothing an automatic act could make permanent. Best-effort end to end: /// every refusal or failure is silent, and the worst outcome is the /// checkpoint staying where it was. No-op when no checkpoint exists — the /// operator opts in by building once; this only keeps that choice fresh. pub fn advance(gpa: std.mem.Allocator, io: std.Io, repo: *repository.Repository, dir: std.Io.Dir) void { var file = dir.openFile(io, repository.repo_filename, .{}) catch return; defer file.close(io); const st = file.stat(io) catch return; var fs = storage.FileStorage.init(io, file, st.size); var loaded = tryLoad(gpa, io, dir, fs.storageFile()) orelse return; defer loaded.deinit(gpa); if (st.size < loaded.claim.prefix_key.end) return; // tryLoad validated, but stat raced if (st.size - loaded.claim.prefix_key.end < advance_min_growth_bytes) return; // Progress gate: the growth above may be blobs alone, with the anchor // unmoved — advancing then would rewrite the same prefix forever. The // would-be anchor is the same newest check-in `build` selects, compared // by hash so no frame walk is paid to discover there is nothing to do. { const a = repo.view.state.newestCheckin() orelse return; if (std.mem.eql(u8, repo.view.hashOf(a), loaded.claim.checkin_hash)) return; } _ = build(gpa, io, repo, dir, null) catch return; } // --------------------------------------------------------------------------- // The verifier (DESIGN §6): re-derive every claim from real history alone. // The sidecar's record body and fingerprint are COMPARANDS here, never // inputs — the derivation and the file set are rebuilt independently and the // sidecar must match them byte for byte. The §6.2 canonical-tree oracle runs // entirely IN MEMORY: `buildTree` over the independently derived file set // exercises the canonical construction without storing a byte. // --------------------------------------------------------------------------- pub const Verdict = union(enum) { ok: BuildReport, no_checkpoint, /// The sidecar does not validate against the live log (unparseable, or a /// prefix key the log no longer backs) — the same conditions the open /// path treats as a silent miss, named for the operator. stale, /// §6.1: an independently captured prefix record body differs from the /// sidecar's — a resume from this sidecar would not equal a full derive. derivation_mismatch, /// The independently derived file set at C fingerprints differently — /// the deep check the open path deliberately never pays for. fileset_mismatch, }; /// Verify the sidecar against real history: open the repository with BOTH /// derived sidecars bypassed, re-capture the prefix record body, re-enumerate /// C's file set through the §9.3 seam, and compare. Reads the sidecar only to /// know what is being claimed; changes nothing. pub fn verify(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) BuildError!Verdict { // The claim (comparand). Absence is its own verdict; any validation // failure — container or prefix key — is `stale`. var probe = dir.openFile(io, checkpoint_filename, .{}) catch return .no_checkpoint; probe.close(io); var file = dir.openFile(io, repository.repo_filename, .{}) catch return error.Storage; defer file.close(io); const st = file.stat(io) catch return error.Storage; var fs = storage.FileStorage.init(io, file, st.size); var loaded = tryLoad(gpa, io, dir, fs.storageFile()) orelse return .stale; defer loaded.deinit(gpa); // The paranoid derivation: trust nothing derived. var repo: repository.Repository = undefined; repository.Repository.open(&repo, gpa, io, dir, .{ .bypass_cache = true, .mode = .full }) catch |e| return switch (e) { error.OutOfMemory => error.OutOfMemory, error.NotARepo, error.CannotRead => error.Storage, error.Corrupt => error.Corrupt, }; defer repo.deinit(gpa); var log = logstore.LogStore.openReadOnly(gpa, fs.storageFile()) catch |e| return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, error.Corrupt => error.Corrupt, }; defer log.deinit(gpa); if (loaded.claim.shun_fp != log.shunFingerprint()) return .stale; // §6.1: the prefix record body, re-captured from the log alone. { var body: std.ArrayList(u8) = .empty; defer body.deinit(gpa); repoview.captureRecords(gpa, &log, logstore.header_len, loaded.claim.prefix_key.end, &body) catch |e| return switch (e) { error.OutOfMemory => error.OutOfMemory, error.Storage => error.Storage, error.Corrupt => error.Corrupt, }; if (!std.mem.eql(u8, body.items, loaded.body)) return .derivation_mismatch; } // C's file set, independently, through the seam. const c_rid = repo.view.lookup(loaded.claim.checkin_hash) orelse return .stale; var fl = (repo.files(gpa, c_rid) catch |e| return mapFilesError(e)) orelse return .stale; defer fl.deinit(gpa); var entries: std.ArrayList(artifact.FCard) = .empty; defer entries.deinit(gpa); var fit = fl.iterator(); while (fit.next()) |f| try entries.append(gpa, f); const fingerprint = try artifact.fileSetFingerprint(gpa, entries.items); if (!std.mem.eql(u8, &fingerprint, &loaded.claim.fileset_fingerprint)) return .fileset_mismatch; // The in-memory canonical-tree oracle: batch construction over the // independently derived set exercises the §6.2 construction end to end // (its own invariants assert the canonical shape); nothing is stored, so // this proves without adding. { var built = try artifact.buildTree(gpa, entries.items, artifact.min_k, .sha3_256); built.deinit(); } var report: BuildReport = .{ .checkin_hash_buf = undefined, .checkin_hash_len = @intCast(loaded.claim.checkin_hash.len), .files = entries.items.len, .prefix_frames = loaded.claim.prefix_key.frame_count, }; @memcpy(report.checkin_hash_buf[0..loaded.claim.checkin_hash.len], loaded.claim.checkin_hash); return .{ .ok = report }; } fn mapFilesError(e: repository.Repository.FilesError) BuildError { return switch (e) { error.OutOfMemory => error.OutOfMemory, error.NotManifest => error.NotACheckin, // The check-in's own history is unreadable through the seam — the // repository, not the request, is at fault. error.NotARepo, error.CannotRead => error.Storage, error.Corrupt, error.BaselineUnresolvable, error.BaselineIsDelta => error.Corrupt, }; }