//! The submanifest conversion pass (#61): re-mint a fossil corpus as a new
//! project with S-card tree manifests from the first commit.
//!
//! The load-bearing observation is the RE-MINT CASCADE RULE: an artifact's
//! name changes only when its BYTES change, and (with content-blob names
//! kept, the phase-1 posture) bytes change only for an artifact that either
//! gains the S-tree structure (every check-in manifest) or references a
//! re-minted artifact in a hash-bearing card. So the remap surface is:
//!
//! check-in manifests all re-mint (S-trees + remapped P/Q/B; D/C/U and
//! the R card survive -- R is over the file SET, which
//! is preserved exactly)
//! control artifacts re-mint iff a T card targets a remapped check-in
//! events (technotes) re-mint iff their P chain or T target remaps
//! wiki / forum P/I/G reference their own kind's chain -- verbatim
//! unless an ancestor re-minted (transitively closed)
//! attachments target a wiki NAME or ticket uuid + a CONTENT hash
//! -- verbatim in phase 1
//! tickets K ids are uuids, not artifact hashes -- verbatim
//! clusters dropped (sync housekeeping, not history)
//! content blobs verbatim (phase 1 keeps their names)
//! PGP signatures cannot survive rehashing: stripped and counted
//!
//! The census pass below is the converter's front door: it classifies the
//! whole corpus, counts the remap surface and the casualties, and is the
//! baseline the count-reconciliation acceptance check audits against.
const std = @import("std");
const zit = @import("zit");
const cli = zit.cli;
const artifact = zit.artifact;
const logstore = zit.logstore;
const Repository = zit.Repository;
/// One corpus walk: every stored artifact classified by parsed type, with
/// the phase-1 remap surface and casualty counts. Phantoms hold no bytes
/// and are skipped (they are the sync layer's business, not history's).
pub const Census = struct {
total: u64 = 0,
phantoms: u64 = 0,
blobs: u64 = 0,
checkins: u64 = 0,
submanifests: u64 = 0,
controls: u64 = 0,
wikis: u64 = 0,
tickets: u64 = 0,
attachments: u64 = 0,
events: u64 = 0,
forums: u64 = 0,
clusters: u64 = 0,
/// Check-in manifests wrapped in a PGP signature: the signature is a
/// stated casualty of any conversion (stripped and counted).
signed: u64 = 0,
pub fn structural(self: *const Census) u64 {
return self.checkins + self.submanifests + self.controls + self.wikis +
self.tickets + self.attachments + self.events + self.forums + self.clusters;
}
};
const pgp_prefix = "-----BEGIN PGP SIGNED MESSAGE-----";
/// Walk the source store and classify every artifact. Bounded by the
/// store's artifact count; each artifact is parsed once with the Z-tolerant
/// window parser (a parse failure is a content blob, by construction --
/// fossil's artifact grammar is what separates structure from content).
pub fn census(
gpa: std.mem.Allocator,
repo: *Repository,
) error{ OutOfMemory, Storage, Corrupt }!Census {
const log = repo.logStore(gpa) catch |e| switch (e) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.Storage,
};
var out: Census = .{};
const n: u64 = log.artifactCount();
var rid: u64 = 1;
while (rid <= n) : (rid += 1) {
out.total += 1;
const r: logstore.Rid = @intCast(rid);
if (log.isPhantom(r)) {
out.phantoms += 1;
continue;
}
const bytes = (log.content(gpa, r) catch |e| switch (e) {
error.OutOfMemory => return error.OutOfMemory,
error.Storage => return error.Storage,
error.Corrupt => return error.Corrupt,
}) orelse {
out.phantoms += 1; // masked (shunned) reads as absent: not history
continue;
};
defer gpa.free(bytes);
const signed = std.mem.startsWith(u8, bytes, pgp_prefix);
var m = artifact.parseWindow(gpa, bytes) catch |e| switch (e) {
error.OutOfMemory => return error.OutOfMemory,
else => {
out.blobs += 1;
continue;
},
};
defer m.deinit();
switch (m.kind) {
.manifest => {
out.checkins += 1;
if (signed) out.signed += 1;
},
.submanifest => out.submanifests += 1,
.cluster => out.clusters += 1,
.control => out.controls += 1,
.wiki => out.wikis += 1,
.ticket => out.tickets += 1,
.attachment => out.attachments += 1,
.event => out.events += 1,
.forum => out.forums += 1,
}
}
return out;
}
/// Render the census for the operator: what converts, what carries, what
/// dies. The converter's own accounting reconciles against these numbers.
pub fn renderCensus(c: *const Census, streams: cli.Streams) void {
streams.out.print(
"census: {d} artifacts — {d} check-ins to re-mint ({d} PGP-signed: signatures strip)," ++
" {d} content blobs carried verbatim\n",
.{ c.total, c.checkins, c.signed, c.blobs },
) catch {};
if (c.controls + c.wikis + c.tickets + c.attachments + c.events + c.forums != 0) {
streams.out.print(
"census: {d} control, {d} wiki, {d} ticket, {d} attachment, {d} technote, {d} forum" ++
" — carried, re-minting only where a reference remaps\n",
.{ c.controls, c.wikis, c.tickets, c.attachments, c.events, c.forums },
) catch {};
}
if (c.clusters != 0) {
streams.out.print("census: {d} cluster(s) dropped (sync housekeeping, not history)\n", .{c.clusters}) catch {};
}
if (c.submanifests != 0) {
streams.out.print("census: {d} sub-manifest slice(s) already present in the source\n", .{c.submanifests}) catch {};
}
if (c.phantoms != 0) {
streams.out.print("census: {d} phantom/masked name(s) skipped — clone the origin to completeness first\n", .{c.phantoms}) catch {};
}
}
test "census: an empty-ish census sums structurally" {
// Goal: the structural() partition is total over ArtifactType — a new
// member without a census bucket fails the exhaustive switch in census()
// at compile time, and this pin keeps the arithmetic honest.
var c: Census = .{ .checkins = 2, .controls = 1, .clusters = 3 };
try std.testing.expectEqual(@as(u64, 6), c.structural());
}