jacquardSnapshot

← snapshot

6443 bytes
//! Writing an imported repository to disk.
//!
//! Milestone-1 jacquard has no persistence: everything lives for the process
//! and the docs say so. That is fine for a running surface and useless for an
//! import you want to keep, so this defines one explicit on-disk shape rather
//! than pretending the engine grew a store.
//!
//! ```text
//! <out>/<slug>/
//!   manifest.json    what this is, where it came from, and what was bounded
//!   snapshots.json   the chain, each with its provenance and why
//!   objects/<hex>    blob bytes, addressed by the engine's own digest
//! ```
//!
//! The addresses in `snapshots.json` are the engine's, not git's. That is the
//! whole point: a jacquard snapshot id has provenance hashed into it, so the
//! same tree imported as human work and as agent work lands on two different
//! addresses. Re-reading these files gives back the same ids.

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.
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": "jacquard-export/1",
        "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",
        },
        "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)
}