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