jacquardSnapshot

← snapshot

6172 bytes
//! Writing a jacquard repository to where it lives.
//!
//! `~/jack/one/<name>/` is to jacquard what `~/github/one/<name>/` is to git:
//! the place a repository lives. See [`crate::store`] for the shape and for
//! reading one back.
//!
//! Milestone-1's engine holds everything in memory, so a repo "on disk" is a
//! directory this module writes and `store::load` reads — not a new storage
//! engine behind the engine. Nothing here pretends otherwise.

use std::collections::BTreeMap;
use std::path::Path;

use jac_object::ObjectStore as _;
use serde_json::{Value, json};

use crate::gitimport::ImportedCommit;
use crate::state::ServeRepo;

/// What the export writes alongside the objects.
#[derive(Debug)]
pub struct Manifest<'a> {
    /// URL-safe name this export is written under.
    pub slug: &'a str,
    /// Where it was read from.
    pub source: &'a str,
    /// The commit window that was asked for.
    pub commits_requested: usize,
    /// The path ceiling that was applied.
    pub paths_cap: usize,
}

const fn provenance_label(p: jac_core::Provenance) -> &'static str {
    match p {
        jac_core::Provenance::Agent => "agent",
        jac_core::Provenance::Mixed => "mixed",
        // Fail toward the weaker claim: an unrecognised variant is not
        // evidence that a person made this.
        _ => "human",
    }
}

/// Writes one imported repo to `<out>/<slug>/`.
///
/// Returns the manifest that was written, so a caller can report without
/// reading it back.
///
/// # Errors
/// When the output directory or any of its files cannot be written.
#[expect(
    clippy::too_many_lines,
    reason = "one repo is written as one sequence; splitting it would scatter the shape"
)]
pub fn write(
    out: &Path,
    manifest: &Manifest<'_>,
    repo: &ServeRepo,
    imported: &[ImportedCommit],
    snapshot_ids: &[jac_core::SnapshotId],
) -> std::io::Result<Value> {
    let dir = out.join(manifest.slug);
    let objects = dir.join("objects");
    std::fs::create_dir_all(&objects)?;

    // Blobs, addressed by the engine's digest so the manifest can point at
    // them without inventing a second naming scheme.
    let mut written: BTreeMap<String, usize> = BTreeMap::new();
    for commit in imported {
        for file in &commit.files {
            let digest = jac_object::blob_id(&jac_object::Blob::new(file.content.clone()));
            let hex = digest.digest().to_hex().as_str().to_owned();
            if written.contains_key(&hex) {
                continue;
            }
            std::fs::write(objects.join(&hex), &file.content)?;
            written.insert(hex, file.content.len());
        }
    }

    let mut tally = BTreeMap::from([("human", 0), ("agent", 0), ("mixed", 0)]);
    let mut assumed = 0usize;

    let snapshots: Vec<Value> = imported
        .iter()
        .zip(snapshot_ids)
        .map(|(c, id)| {
            let label = provenance_label(c.inferred.provenance);
            *tally.entry(label).or_insert(0) += 1;
            if c.inferred.assumed {
                assumed += 1;
            }
            let files: Vec<Value> = c
                .files
                .iter()
                .map(|f| {
                    json!({
                        "path": f.path,
                        "blob": jac_object::blob_id(&jac_object::Blob::new(f.content.clone())).digest().to_hex().as_str().to_owned(),
                        "bytes": f.content.len(),
                    })
                })
                .collect();
            let snapshot = repo.store().snapshot(*id).ok();
            json!({
                "id": id.digest().to_hex().as_str().to_owned(),
                "git_sha": c.commit.sha,
                "message": c.commit.subject,
                "at": c.commit.at,
                "author": {
                    "name": c.commit.author_name,
                    "email": c.commit.author_email,
                },
                "committer": {
                    "name": c.commit.committer_name,
                    "email": c.commit.committer_email,
                },
                "provenance": label,
                // The rule that produced the label, kept with the label.
                "inference": {
                    "because": c.inferred.because,
                    "assumed": c.inferred.assumed,
                },
                "parents": snapshot
                    .map(|s| {
                        s.parents
                            .iter()
                            .map(|p| p.digest().to_hex().as_str().to_owned())
                            .collect::<Vec<String>>()
                    })
                    .unwrap_or_default(),
                "files": files,
            })
        })
        .collect();

    std::fs::write(
        dir.join("snapshots.json"),
        format!("{}\n", serde_json::to_string_pretty(&snapshots)?),
    )?;

    let manifest_json = json!({
        "slug": manifest.slug,
        "source": manifest.source,
        "kind": crate::store::KIND,
        "snapshots": snapshots.len(),
        "objects": written.len(),
        "bytes": written.values().sum::<usize>(),
        "provenance": {
            "human": tally.get("human").copied().unwrap_or(0),
            "agent": tally.get("agent").copied().unwrap_or(0),
            "mixed": tally.get("mixed").copied().unwrap_or(0),
            "assumed": assumed,
        },
        "bounds": {
            "commits_requested": manifest.commits_requested,
            "paths_cap": manifest.paths_cap,
            "note": "a bounded slice of recent history, not a mirror: only the files \
                     these commits touched, text only, under 256 KiB each",
        },
        "founder": "imported",
        "honesty": "provenance is inferred. A co-author trailer naming a model, or a bot \
                    author, is evidence; everything else is a person by assumption and is \
                    marked so. Nothing here is attested — only a human can do that.",
    });
    std::fs::write(
        dir.join("manifest.json"),
        format!("{}\n", serde_json::to_string_pretty(&manifest_json)?),
    )?;

    Ok(manifest_json)
}