sprawling-storage 0.0.11

The append-only, hash-chained Ledger of a sprawling city, its content store, checkpoints and worktrees.
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

//! What a checkpoint stages, scans and commits: the scope boundary, the
//! staged-secret refusal, and the boundary an empty scope means.

use super::super::provenance::{ModelChoice, Provenance};
use super::*;
use kernel::{Address, B3Hash, Effort, RunId};
use std::path::Path;

fn resident() -> Provenance {
    Provenance::new(
        RunId::parse("018f5b2a-0000-7000-8000-000000000001").unwrap(),
        Address::parse("work/resident").unwrap(),
        B3Hash::digest(b"a city"),
        ModelChoice {
            id: "test-model".to_owned(),
            effort: Some(Effort::Medium),
        },
    )
}
fn write(root: &Path, rel: &str, body: &str) {
    let path = root.join(rel);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).unwrap();
    }
    std::fs::write(path, body).unwrap();
}

#[test]
fn a_staged_secret_refuses_the_commit_and_never_echoes_it() {
    let tmp = tempfile::tempdir().unwrap();
    let token = ["sk-ant-api03-", "Zx9yQ2mK4pL7", "vB1nC5tR8sD3"].concat();
    write(tmp.path(), "work/leak.env", &format!("KEY={token}"));
    let mut checkpoint = Checkpoint::open(tmp.path()).unwrap();
    let err = match checkpoint.wave_pre(&["work".to_owned()], TimeMs::new(0), &resident()) {
        Err(err) => err,
        Ok(_) => panic!("a staged secret must refuse the commit"),
    };
    let rendered = err.to_string();
    assert!(rendered.contains("work/leak.env"), "{rendered}");
    assert!(
        !rendered.contains(&token) && !rendered.contains("Zx9yQ2mK4pL7"),
        "positions only, never the bytes: {rendered}"
    );
    let ax = err.into_ax();
    assert_eq!(*ax.code(), kernel::AxCode::SecretEgress);
}

/// Five trailers, in the order and the spelling a reader outside the
/// city was promised. Read as git reads them: the message is parsed
/// line by line off the commit object, which is what
/// `git interpret-trailers --parse` walks.
#[test]
fn every_commit_the_city_makes_names_the_session_that_made_it() {
    let tmp = tempfile::tempdir().unwrap();
    write(tmp.path(), "work/note.md", "a line");
    let mut checkpoint = Checkpoint::open(tmp.path()).unwrap();
    let of = resident();
    checkpoint
        .ensure_base(&["work".to_owned()], TimeMs::new(1_000), &of)
        .unwrap();

    let repo = git2::Repository::open(tmp.path()).unwrap();
    let commit = repo.head().unwrap().peel_to_commit().unwrap();
    let message = commit.message().unwrap();
    let trailers: Vec<String> = message
        .lines()
        .filter(|line| line.starts_with("Sprawling-"))
        .map(str::to_owned)
        .collect();
    let city = kernel::B3Hash::digest(b"a city").to_string();
    assert_eq!(
        trailers,
        vec![
            format!("Sprawling-Run: {}", of.run()),
            "Sprawling-Actor: work/resident".to_owned(),
            "Sprawling-Model: test-model".to_owned(),
            "Sprawling-Effort: medium".to_owned(),
            format!("Sprawling-City: {city}"),
        ]
    );
    assert!(message.starts_with("checkpoint: work\n\n"), "{message}");

    let author = commit.author();
    assert_eq!(author.name().unwrap(), "work/resident");
    assert_eq!(
        author.email().unwrap(),
        format!("work/resident@{}.sprawling", city.get(..12).unwrap())
    );
}

/// A restart must not open a window a credential can walk through.
/// A new process remembers no checkpoint, so the scan compares against
/// HEAD - which a wave checkpoint deliberately does not move, and which is
/// therefore an ancestor of the last checkpoint. Comparing against an
/// ancestor reads more, never less, so what changed before the
/// restart is still read.
#[test]
fn a_change_made_before_a_restart_is_still_read_after_it() {
    let tmp = tempfile::tempdir().unwrap();
    write(tmp.path(), "work/clean.md", "nothing here");
    let mut first = Checkpoint::open(tmp.path()).unwrap();
    first
        .ensure_base(&["work".to_owned()], TimeMs::new(0), &resident())
        .unwrap();
    first
        .wave_pre(&["work".to_owned()], TimeMs::new(1_000), &resident())
        .unwrap();
    let token = ["sk-ant-api03-", "Zx9yQ2mK4pL7", "vB1nC5tR8sD3"].concat();
    write(tmp.path(), "work/leak.env", &format!("KEY={token}"));
    drop(first);
    let mut second = Checkpoint::open(tmp.path()).unwrap();
    let outcome = second.wave_pre(&["work".to_owned()], TimeMs::new(2_000), &resident());
    let err = match outcome {
        Err(err) => err,
        Ok(_) => panic!("the restart lost the scan of what changed before it"),
    };
    let rendered = err.to_string();
    assert!(rendered.contains("work/leak.env"), "{rendered}");
    assert!(!rendered.contains(&token), "positions only: {rendered}");
}

/// The alias rule at the checkpoint door (`crates/storage/spec/Alias.lean` §8-25): a scope
/// whose path crosses a link refuses the whole wave, because a name
/// that leads into `.git` would capture reserved bytes under a lying
/// name - and a `file_discarded` restoration would write back through
/// it.
#[test]
fn a_wave_whose_scope_crosses_a_link_is_refused() {
    let tmp = tempfile::tempdir().unwrap();
    write(tmp.path(), "work/notes.md", "a line");
    write(tmp.path(), ".git/hooks/pre-run", "hook-body");
    if !crate::alias::tests::place_link(
        true,
        &tmp.path().join(".git").join("hooks"),
        &tmp.path().join("work").join("alias-dir"),
    ) {
        return;
    }
    let mut checkpoint = Checkpoint::open(tmp.path()).unwrap();
    let err = checkpoint
        .wave_pre(&["work".to_owned()], TimeMs::new(0), &resident())
        .unwrap_err();
    assert_eq!(
        *err.into_ax().code(),
        kernel::AxCode::OutsideWriteDomain,
        "the refusal names the write-domain rule"
    );
}

/// A HEAD that cannot be read is not an empty city. Read as "no
/// history", it would make the checkpoint a parentless root commit and cut
/// the Ledger's oid off from everything before it, with nobody told.
#[test]
fn a_head_that_cannot_be_read_refuses_the_checkpoint_instead_of_orphaning_it() {
    let tmp = tempfile::tempdir().unwrap();
    write(tmp.path(), "work/note.md", "a line");
    let mut first = Checkpoint::open(tmp.path()).unwrap();
    first
        .ensure_base(&["work".to_owned()], TimeMs::new(0), &resident())
        .unwrap();
    drop(first);
    let branch = git2::Repository::open(tmp.path())
        .unwrap()
        .head()
        .unwrap()
        .name()
        .unwrap()
        .to_owned();
    std::fs::write(tmp.path().join(".git").join(&branch), "not an oid\n").unwrap();

    let mut second = Checkpoint::open(tmp.path()).unwrap();
    let outcome = second.wave_pre(&["work".to_owned()], TimeMs::new(1_000), &resident());
    assert!(
        matches!(
            outcome,
            Err(StorageError::Checkpoint {
                op: "read HEAD",
                ..
            })
        ),
        "{outcome:?}"
    );
}

/// Removes the loose object a staged file was written as, so the scan meets
/// an index entry whose bytes it cannot read.
fn lose_the_object_of(root: &Path, rel: &str) {
    let id = git2::Repository::open(root)
        .unwrap()
        .index()
        .unwrap()
        .get_path(Path::new(rel), 0)
        .unwrap()
        .id
        .to_string();
    let (dir, file) = id.split_at(2);
    std::fs::remove_file(root.join(".git").join("objects").join(dir).join(file)).unwrap();
}

/// A staged blob the scan cannot read refuses the checkpoint, on the first
/// checkpoint (every index entry) and on a later one (the staged diff):
/// skipping it would let bytes into the tree that no scan read.
#[test]
fn a_staged_blob_the_scan_cannot_read_refuses_the_checkpoint() {
    let first = tempfile::tempdir().unwrap();
    write(first.path(), "work/a.md", "a");
    let mut checkpoint = Checkpoint::open(first.path()).unwrap();
    checkpoint.stage_scopes(&["work".to_owned()]).unwrap();
    lose_the_object_of(first.path(), "work/a.md");
    let on_first = checkpoint.scan_staged();

    let later = tempfile::tempdir().unwrap();
    write(later.path(), "work/a.md", "a");
    let mut checkpoint = Checkpoint::open(later.path()).unwrap();
    checkpoint
        .ensure_base(&["work".to_owned()], TimeMs::new(0), &resident())
        .unwrap();
    write(later.path(), "work/b.md", "b");
    checkpoint.stage_scopes(&["work".to_owned()]).unwrap();
    lose_the_object_of(later.path(), "work/b.md");
    let on_later = checkpoint.scan_staged();

    let refused = |result: Result<(), StorageError>| matches!(result, Err(StorageError::Checkpoint { op, .. }) if op == "scan a staged file");
    assert_eq!((refused(on_first), refused(on_later)), (true, true));
}

/// A staging file is half of somebody's write, and the writer renames it
/// away whenever it likes: the walk must not open it, whether a prefix
/// scope walks past it or a wave names it as the file it touched
/// (storage D30). Both spellings are asked of their one authority.
#[test]
fn a_staging_file_beside_a_document_is_never_staged() {
    let tmp = tempfile::tempdir().unwrap();
    write(tmp.path(), "work/Roadmap.md", "the document");
    let document = kernel::layout::document_staging_name(std::ffi::OsStr::new("Roadmap.md"));
    let document = format!("work/{}", document.to_string_lossy());
    write(tmp.path(), &document, "half of the next version");
    write(tmp.path(), "work/.edit.rs.part", "half of an edit");
    let mut checkpoint = Checkpoint::open(tmp.path()).unwrap();

    let walked = checkpoint.stage_scopes(&["work".to_owned()]).unwrap();
    let named = checkpoint
        .stage_scopes(&[document.clone(), "work/.edit.rs.part".to_owned()])
        .unwrap();

    assert_eq!(
        (walked, named),
        (vec!["work/Roadmap.md".to_owned()], Vec::<String>::new())
    );
}