//! 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 };
}