Skip to content
zit Artifact [8956c58726]
Login

Artifact [8956c58726]

Artifact 8956c58726b78e860b74868b1216416ca6bb21db25a641d30faad116838a076f:


//! Sparse-cone classification (draft-fossil-sparse-cone-00 sections 5.1,
//! 5.2, 6.3): which artifacts a cone-scoped transfer keeps, and which
//! absences are deliberate. Shared by the clone/pull/sync client layer and
//! the cone-aware server, which is why it lives in the library rather than
//! the CLI: both sides must classify identically or a server could withhold
//! what its client would have kept.

const std = @import("std");
const artifact = @import("artifact.zig");
const repostate = @import("repostate.zig");
const cone_mod = @import("cone.zig");

/// Sparse-cone RFC sections 5.1 and 6.3, Wave-2 posture: decide, per held
/// artifact, whether a cone-scoped clone persists it. KEEPS every artifact
/// that parses as structural (manifests, sub-manifest nodes, control, wiki,
/// tickets, technotes, forum, attachments, clusters -- holding more than the
/// section 5.1 minimum is sound and keeps this pass simple), every blob an
/// in-cone F card names, every attachment source, and -- conservatively --
/// every blob nothing references at all. ELIDES exactly the blobs whose
/// every reference is an out-of-cone F card. Eliding writes nothing new:
/// phantoms never reach disk, and the next open re-interns the dangling
/// references, which the local cone then classifies as elided.
pub const SparseClassification = struct {
    /// Rids a cone-scoped persist keeps (structural + in-cone-referenced +
    /// unreferenced-held).
    keep: std.DynamicBitSetUnmanaged,
    /// Rids referenced by at least one OUT-of-cone F card. `out_only` minus
    /// `keep` is the elided set -- the only absence class sync must not
    /// spontaneously request (RFC section 5.2 rule 3). A phantom referenced
    /// by nothing (cluster-only, unversioned) is NOT here and stays a
    /// transit phantom: suppression never widens past what a filename
    /// proves out-of-cone.
    out_only: std.DynamicBitSetUnmanaged,

    pub fn elided(self: *const SparseClassification, rid: repostate.Rid) bool {
        // A rid interned AFTER classification (an arrival mid-run) is beyond
        // the bitsets and is never elided -- new content is wanted content.
        if (rid >= self.keep.capacity()) return false;
        return self.out_only.isSet(rid) and !self.keep.isSet(rid);
    }

    pub fn deinit(self: *SparseClassification, gpa: std.mem.Allocator) void {
        self.keep.deinit(gpa);
        self.out_only.deinit(gpa);
        self.* = undefined;
    }
};

pub fn classifyKeepRids(
    gpa: std.mem.Allocator,
    store: repostate.ArtifactStore,
    sparse: *const cone_mod.Cone,
) error{OutOfMemory}!SparseClassification {
    const n = store.count();
    var keep = try std.DynamicBitSetUnmanaged.initEmpty(gpa, n + 1);
    errdefer keep.deinit(gpa);
    var referenced = try std.DynamicBitSetUnmanaged.initEmpty(gpa, n + 1);
    errdefer referenced.deinit(gpa);

    var arena_state = std.heap.ArenaAllocator.init(gpa);
    defer arena_state.deinit();

    var rid: repostate.Rid = 1;
    while (rid <= n) : (rid += 1) {
        if (!store.held(rid)) continue;
        const plain = store.content(rid) orelse continue;
        _ = arena_state.reset(.retain_capacity);
        const aa = arena_state.allocator();
        const im = artifact.parseForImport(aa, plain) catch |e| switch (e) {
            error.OutOfMemory => return error.OutOfMemory,
            // Not structural: a content blob. Classified by its referencers.
            else => continue,
        };
        keep.set(rid); // every structural artifact persists (spine included)
        // F cards: flat runs, delta patch runs, and sub-manifest leaf runs all
        // name blobs by full decoded path -- one rule covers all three.
        for (im.manifest.f) |fc| {
            const h = fc.hash orelse continue; // delete marker names nothing
            const target = store.lookup(h) orelse continue; // absent = already phantom
            if (sparse.covers(fc.filename)) {
                keep.set(target);
            } else {
                referenced.set(target);
            }
        }
        // An attachment's source blob is non-file content (section 5.1's
        // "non-manifest structural artifacts dense" extends to what they
        // directly carry).
        if (im.manifest.a) |a| {
            if (a.source) |src| {
                if (store.lookup(src)) |target| keep.set(target);
            }
        }
    }

    // A held blob nothing referenced stays: unknown provenance is kept, and
    // "referenced only out-of-cone" is the sole elision.
    rid = 1;
    while (rid <= n) : (rid += 1) {
        if (!store.held(rid)) continue;
        if (keep.isSet(rid)) continue;
        if (!referenced.isSet(rid)) keep.set(rid);
    }
    return .{ .keep = keep, .out_only = referenced };
}