sim-lib-doc-store 0.1.0

Relational document store projections for SIM office documents.
Documentation
// conformance: document projections and evidence graphs persist through checked relational plans.

use sim_kernel::{Cx, Expr, Value, testing::bare_cx as cx};
use sim_lib_doc_core::{Doc, DocId, DocKind, Edit, Evidence, ExternalRef, LinkRole};
use tempfile::TempDir;

use crate::{DocStore, evidence};

const LEGACY_FIXTURE: &[u8] = include_bytes!("../fixtures/legacy-doc-store-v1.sqlite");

fn store() -> (TempDir, DocStore) {
    let dir = tempfile::tempdir().unwrap();
    let store = DocStore::create(&dir.path().join("docs.sqlite")).unwrap();
    (dir, store)
}

fn doc_with_body(cx: &mut Cx, body: &str) -> Doc {
    Doc::new(
        DocKind::new("report"),
        DocId::new("doc-1"),
        cx.factory().string(body.to_owned()).unwrap(),
        vec![ExternalRef::new(
            "local",
            "doc-1.md",
            Some("rev-1".to_owned()),
            None,
        )],
    )
}

#[test]
fn save_and_load_doc_snapshot() {
    let (_dir, store) = store();
    let mut cx = cx();
    let doc = doc_with_body(&mut cx, "body text");

    store.save_doc(&doc).unwrap();
    let loaded = store.load_doc(&doc.id).unwrap().unwrap();

    assert_eq!(loaded.id, doc.id);
    assert_eq!(loaded.kind, doc.kind);
    assert_eq!(loaded.origin, doc.origin);
    assert_eq!(
        value_expr(&mut cx, &loaded.body),
        Expr::String("body text".to_owned())
    );
}

#[test]
fn project_commit_then_undo_last_returns_inverse_edit() {
    let (_dir, store) = store();
    let mut cx = cx();
    let doc_id = DocId::new("doc-1");
    let edit = Edit::new(
        doc_id.clone(),
        "office/body",
        cx.factory().string("new body".to_owned()).unwrap(),
        cx.factory().string("old body".to_owned()).unwrap(),
    );

    let seq = store.project_commit(&doc_id, &edit, 42).unwrap();
    let undo = store.undo_last(&doc_id).unwrap().unwrap();

    assert_eq!(seq, 42);
    assert_eq!(undo.doc, doc_id);
    assert_eq!(undo.domain, "office/body");
    assert_eq!(
        value_expr(&mut cx, &undo.op),
        Expr::String("old body".to_owned())
    );
    assert_eq!(
        value_expr(&mut cx, &undo.inverse),
        Expr::String("new body".to_owned())
    );
}

#[test]
fn projected_edit_does_not_replace_saved_doc_body() {
    let (_dir, store) = store();
    let mut cx = cx();
    let doc = doc_with_body(&mut cx, "ledger body before edit");
    let edit = Edit::new(
        doc.id.clone(),
        "office/body",
        cx.factory()
            .string("projected body after edit".to_owned())
            .unwrap(),
        cx.factory()
            .string("ledger body before edit".to_owned())
            .unwrap(),
    );

    store.save_doc(&doc).unwrap();
    store.project_commit(&doc.id, &edit, 7).unwrap();
    let loaded = store.load_doc(&doc.id).unwrap().unwrap();

    assert_eq!(
        value_expr(&mut cx, &loaded.body),
        Expr::String("ledger body before edit".to_owned())
    );
}

#[test]
fn evidence_links_reopen_ordered_without_payload_data() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("docs.sqlite");
    let subject = DocId::new("site/powerproject/projects/p-1/tasks/17");
    let mail_body = "private mail body must not be stored";
    let project_data = "project item payload must not be stored";
    let rows = vec![
        Evidence::new(
            subject.clone(),
            ExternalRef::new(
                "site/msgraph",
                "messages/msg-1",
                Some("change-key-1".to_owned()),
                Some("https://graph.example/messages/msg-1".to_owned()),
            ),
            LinkRole::SourceDocument,
            30,
            Some("mail-etag-1".to_owned()),
        ),
        Evidence::new(
            subject.clone(),
            ExternalRef::new(
                "site/sharepoint",
                "sites/site-1/drive/items/file-9",
                Some("sharepoint-etag-9".to_owned()),
                Some("https://sharepoint.example/file-9".to_owned()),
            ),
            LinkRole::AccountingSupport,
            10,
            None,
        ),
        Evidence::new(
            subject.clone(),
            ExternalRef::new(
                "site/dalux",
                "items/issue-4",
                Some("2026-07-13T10:00:00Z".to_owned()),
                Some("https://dalux.example/items/issue-4".to_owned()),
            ),
            LinkRole::ProjectIssue,
            20,
            Some("dalux-updated-at".to_owned()),
        ),
        Evidence::new(
            subject.clone(),
            ExternalRef::new("ledger", "voucher/2026/0007", None, None),
            LinkRole::AccountingSupport,
            40,
            Some("voucher-digest".to_owned()),
        ),
    ];

    {
        let store = DocStore::create(&path).unwrap();
        for row in &rows {
            evidence::attach(&store, row).unwrap();
        }
    }

    let store = DocStore::create(&path).unwrap();
    let loaded = evidence::evidence_for(&store, &subject).unwrap();

    assert_eq!(
        loaded
            .iter()
            .map(|row| row.captured_at_seq)
            .collect::<Vec<_>>(),
        vec![10, 20, 30, 40]
    );
    assert_eq!(loaded[0].evidence.backend, "site/sharepoint");
    assert_eq!(loaded[1].evidence.backend, "site/dalux");
    assert_eq!(loaded[2].evidence.backend, "site/msgraph");
    assert_eq!(loaded[3].evidence.backend, "ledger");
    assert!(loaded.iter().all(
        |row| row.evidence.external_id != mail_body && row.evidence.external_id != project_data
    ));

    let bytes = std::fs::read(&path).unwrap();
    assert!(
        !bytes
            .windows(mail_body.len())
            .any(|window| window == mail_body.as_bytes())
    );
    assert!(
        !bytes
            .windows(project_data.len())
            .any(|window| window == project_data.as_bytes())
    );
}

fn value_expr(cx: &mut Cx, value: &Value) -> Expr {
    value.object().as_expr(cx).unwrap()
}

#[test]
fn legacy_fixture_is_a_complete_behavior_oracle() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("legacy.sqlite");
    std::fs::write(&path, LEGACY_FIXTURE).unwrap();
    let store = DocStore::create(&path).unwrap();
    let mut cx = cx();

    let doc = store.load_doc(&DocId::new("doc-legacy")).unwrap().unwrap();
    assert_eq!(doc.kind.as_str(), "report");
    assert_eq!(
        value_expr(&mut cx, &doc.body),
        Expr::String("legacy body".into())
    );

    let undo = store.undo_last(&doc.id).unwrap().unwrap();
    assert_eq!(undo.domain, "office/body");
    assert_eq!(
        value_expr(&mut cx, &undo.op),
        Expr::String("old body".into())
    );
    assert_eq!(
        value_expr(&mut cx, &undo.inverse),
        Expr::String("new body".into())
    );

    let evidence = evidence::evidence_for(&store, &doc.id).unwrap();
    assert_eq!(evidence.len(), 1);
    assert_eq!(evidence[0].predicate(), "office/source-document");
    assert_eq!(evidence[0].evidence.backend, "legacy");
    assert_eq!(evidence[0].captured_at_seq, 41);
}

#[test]
fn read_only_legacy_adoption_is_byte_exact_and_unstamped() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("legacy.sqlite");
    std::fs::write(&path, LEGACY_FIXTURE).unwrap();
    let before = std::fs::read(&path).unwrap();
    let store = DocStore::open_read_only(&path).unwrap();
    assert!(store.load_doc(&DocId::new("doc-legacy")).unwrap().is_some());
    drop(store);
    assert_eq!(std::fs::read(&path).unwrap(), before);
    assert!(
        !before
            .windows("__sim_relation_attestation".len())
            .any(|w| w == b"__sim_relation_attestation")
    );
    let manifest = crate::store::legacy_adoption_manifest().unwrap();
    assert_ne!(manifest.logical_schema, manifest.physical_schema);
}