prikk 0.22.0

Prikk CLI initial scaffold.
//! Shared CLI end-to-end test harness (DC-67).
//!
//! DC-61, DC-65, and DC-66's test files each rolled their own `commit`/`seal`/key setup — copy-pasted
//! three times before this consolidation. Every test in `dc67_ordinary_use_conformance.rs` uses this
//! module instead of a fourth (and fifth, sixth, ...) copy. Existing files are left as they are; this
//! is not a retrofit, only the point past which no one should copy-paste it again.

#![allow(dead_code)]

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

pub fn prikk(repo: &Path) -> Command {
    let mut cmd = Command::new(env!("CARGO_BIN_EXE_prikk"));
    cmd.current_dir(repo);
    cmd
}

pub fn ok(output: &Output, what: &str) {
    assert!(
        output.status.success(),
        "{what} failed (status {:?})\nstdout: {}\nstderr: {}",
        output.status.code(),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
    );
}

/// DC-84: a temp-directory-naming suffix that is genuinely collision-free across threads of one test
/// binary, not just across separate processes. Every prikk-cli integration test file that built its
/// own `unique_repo`/`unique_root`/`unique_dir` from `process::id()` plus a nanosecond timestamp
/// shared the same latent defect DC-83 found and measured: `process::id()` is constant for every
/// thread of one process, so it cannot distinguish two racing threads of the *same* binary, and a
/// bare-barrier stress test showed real nanosecond collisions under thread contention (214 in
/// 128,000 samples — a rate, not a hypothetical). The `fetch_add` sequence number below is the only
/// part that actually guarantees uniqueness, regardless of clock resolution or thread scheduling —
/// confirmed by the same stress test at zero collisions once added (see the crate's
/// `helper_uniqueness` test module). Process id and the timestamp are kept: the id still separates
/// this binary's temp directories from an unrelated process using the same scheme, and the timestamp
/// keeps directory names human-orderable.
pub fn unique_suffix() -> String {
    static SEQUENCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let sequence = SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    format!("{}-{nanos}-{sequence}", std::process::id())
}

pub fn unique_repo(tag: &str) -> PathBuf {
    let mut dir = std::env::temp_dir();
    dir.push(format!("prikk-cli-dc67-{tag}-{}", unique_suffix()));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

pub const AUTHOR_KEY_ID: &str = "dc67-test-author";
pub const AUTHOR_SEED_HEX: &str =
    "3300445566778899001122334455667788990011223344556677889900112233";
pub const MAINTAINER_KEY_ID: &str = "dc67-test-maintainer";
pub const MAINTAINER_SEED: [u8; 32] = [
    0x71, 0x71, 0x82, 0x82, 0x93, 0x93, 0xa4, 0xa4, 0xb5, 0xb5, 0xc6, 0xc6, 0xd7, 0xd7, 0xe8, 0xe8,
    0xf9, 0xf9, 0x0a, 0x0a, 0x1b, 0x1b, 0x2c, 0x2c, 0x3d, 0x3d, 0x4e, 0x4e, 0x5f, 0x5f, 0x60, 0x60,
];

pub fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}

pub fn maintainer_public_key_hex() -> String {
    use prikk_store::MaintainerSigner;
    let signer =
        prikk_store::Ed25519MaintainerSigner::from_seed(MAINTAINER_KEY_ID, &MAINTAINER_SEED)
            .expect("fixed maintainer seed derives a valid signer");
    hex(&signer.public_key_bytes())
}

pub fn init(repo: &Path) {
    ok(&prikk(repo).arg("init").output().unwrap(), "init");
}

/// Commit on `ref_name` (repository-relative, e.g. `"heads/main"`).
pub fn commit(repo: &Path, ref_name: &str, message: &str) -> Output {
    prikk(repo)
        .env("PRIKK_AUTHOR_KEY_ID", AUTHOR_KEY_ID)
        .env("PRIKK_AUTHOR_SEED", AUTHOR_SEED_HEX)
        .args(["commit", "--ref", ref_name, "-m", message])
        .output()
        .unwrap()
}

/// Trust the fixed maintainer key (idempotent-enough for repeated calls within one test — only
/// `seal` itself must succeed).
pub fn trust_maintainer(repo: &Path) {
    let _ = prikk(repo)
        .args([
            "trust",
            "maintainer",
            "add",
            "--key-id",
            MAINTAINER_KEY_ID,
            "--public-key",
            &maintainer_public_key_hex(),
        ])
        .output()
        .unwrap();
}

/// Seal `ref_name`, trusting the fixed maintainer key first.
pub fn seal(repo: &Path, ref_name: &str) -> Output {
    trust_maintainer(repo);
    prikk(repo)
        .env("PRIKK_MAINTAINER_KEY_ID", MAINTAINER_KEY_ID)
        .env("PRIKK_MAINTAINER_SEED", hex(&MAINTAINER_SEED))
        .args(["seal", "--allow-no-audit", "--ref", ref_name])
        .output()
        .unwrap()
}

/// One generation: write `path` with `content`, commit, seal — the mutate/commit/seal cycle §3
/// defines a "generation" as. Asserts both steps succeed.
pub fn generation(repo: &Path, ref_name: &str, path: &str, content: &[u8], message: &str) {
    std::fs::write(repo.join(path), content).unwrap();
    ok(
        &commit(repo, ref_name, message),
        &format!("commit: {message}"),
    );
    ok(&seal(repo, ref_name), &format!("seal: {message}"));
}

pub fn branch_create(repo: &Path, name: &str, from: &str) -> Output {
    trust_maintainer(repo);
    prikk(repo)
        .env("PRIKK_MAINTAINER_KEY_ID", MAINTAINER_KEY_ID)
        .env("PRIKK_MAINTAINER_SEED", hex(&MAINTAINER_SEED))
        .args(["branch", "create", name, "--from", from])
        .output()
        .unwrap()
}

pub fn branch_close(repo: &Path, name: &str) -> Output {
    trust_maintainer(repo);
    prikk(repo)
        .env("PRIKK_MAINTAINER_KEY_ID", MAINTAINER_KEY_ID)
        .env("PRIKK_MAINTAINER_SEED", hex(&MAINTAINER_SEED))
        .args(["branch", "close", name])
        .output()
        .unwrap()
}

pub fn tag_create(repo: &Path, name: &str, target: &str) -> Output {
    trust_maintainer(repo);
    prikk(repo)
        .env("PRIKK_MAINTAINER_KEY_ID", MAINTAINER_KEY_ID)
        .env("PRIKK_MAINTAINER_SEED", hex(&MAINTAINER_SEED))
        .args(["tag", "create", name, "--target", target])
        .output()
        .unwrap()
}

pub fn verify(repo: &Path) -> Output {
    prikk(repo).arg("verify").output().unwrap()
}

/// Append an attributable torn tail to the shared ref-log container, by duplicating (truncated) the
/// header of whichever real record currently sits last in the file.
///
/// RFC 102 Stage 4: under the old per-ref-file model, appending *any* trailing garbage to a ref's own
/// log file simulated "this ref's own torn write" -- the file's identity alone did the attribution.
/// The shared container instead attributes a torn tail to a ref via the frame header's own
/// `ref_name_key` field (`refs/container.rs`'s `trailing_tail_ref_name_key`), which requires at least
/// a full header's worth of intact bytes (magic(8) + version(2) + ref_name_key(32) + body_len(8) +
/// checksum(32) = 82) to even attempt reading. Bare garbage shorter than that is simply unattributable
/// to any ref -- not a torn write for the ref under test, a torn write for nobody. This duplicates the
/// last real frame's own header (plus a few body bytes, to stay a torn *record* rather than a torn
/// *header*) so the appended bytes carry a genuine, correctly-attributed `ref_name_key` -- whichever
/// ref actually owns the container's current last record, which every caller here has arranged to be
/// the ref under test by publishing to it most recently.
pub fn append_torn_ref_log_tail(container_path: &Path) {
    const MAGIC: &[u8; 8] = b"PREFCON1";
    const HEADER_LEN: usize = 8 + 2 + 32 + 8 + 32;
    let bytes = std::fs::read(container_path).unwrap();
    let start = bytes
        .windows(MAGIC.len())
        .rposition(|window| window == MAGIC)
        .expect("ref log container has at least one real record to duplicate");
    let end = (start + HEADER_LEN + 8).min(bytes.len());
    assert!(
        end < bytes.len(),
        "duplicated span must land inside the real record's body, not consume it entirely, \
         or the result would be a complete record rather than a torn one"
    );
    let torn = bytes[start..end].to_vec();
    let mut file = std::fs::OpenOptions::new()
        .append(true)
        .open(container_path)
        .unwrap();
    use std::io::Write as _;
    file.write_all(&torn).unwrap();
}

pub fn copy_dir_recursive(src: &Path, dst: &Path) {
    std::fs::create_dir_all(dst).unwrap();
    for entry in std::fs::read_dir(src).unwrap() {
        let entry = entry.unwrap();
        let file_type = entry.file_type().unwrap();
        let dst_path = dst.join(entry.file_name());
        if file_type.is_dir() {
            copy_dir_recursive(&entry.path(), &dst_path);
        } else {
            std::fs::copy(entry.path(), &dst_path).unwrap();
        }
    }
}

/// DC-67 criterion 2, the load-bearing technique: copy `repo`'s `.prikk` into a fresh directory,
/// `checkout --patch-materialize` it there, and return the rebuilt worktree's root. `verify` passing
/// proves history is *structurally* valid; reading files back from the returned root and asserting
/// their bytes is what proves it is *semantically* correct.
///
/// `checkout --patch-materialize` takes the **repository** path (the directory containing `.prikk`),
/// not an output directory — passing the wrong one is silently plausible and was gotten wrong twice
/// during DC-66 verification.
pub fn rebuild_from_sealed_history(repo: &Path, tag: &str) -> PathBuf {
    let materialize_root = unique_repo(&format!("{tag}-materialize"));
    std::fs::create_dir_all(materialize_root.join(".prikk")).unwrap();
    copy_dir_recursive(&repo.join(".prikk"), &materialize_root.join(".prikk"));
    let out = prikk(&materialize_root)
        .arg("checkout")
        .arg("--patch-materialize")
        .output()
        .unwrap();
    ok(&out, "checkout --patch-materialize");
    materialize_root
}