sillok 0.5.1

A structured CLI chronicle for agentic daily work logs.
Documentation
use sillok::domain::archive::Archive;
use sillok::domain::event::{ChronicleEvent, EventKind, RecordStatus, WorkContext};
use sillok::domain::id::ChronicleId;
use sillok::domain::time::{DayKey, Timestamp};
use sillok::domain::view::ChronicleView;

fn context() -> WorkContext {
    WorkContext {
        cwd: Some("/tmp/sillok-test".to_string()),
        git_root: None,
        git_branch: None,
        git_head: None,
        git_remote: None,
    }
}

fn archive() -> Archive {
    Archive::new(Timestamp::from_millis(1), "test".to_string(), context())
}

#[test]
fn derives_day_task_tree() -> Result<(), Box<dyn std::error::Error>> {
    let mut archive = archive();
    let day_id = ChronicleId::new_v7();
    let task_id = ChronicleId::new_v7();
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(2),
        Timestamp::from_millis(2),
        "test".to_string(),
        context(),
        EventKind::DayOpened {
            day_id,
            day_key: DayKey {
                date: "2026-05-13".to_string(),
                timezone: "local".to_string(),
            },
        },
    ));
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(3),
        Timestamp::from_millis(3),
        "test".to_string(),
        context(),
        EventKind::TaskRecorded {
            task_id,
            day_id,
            parent_id: day_id,
            text: "implemented storage".to_string(),
            purpose: None,
            tags: vec!["rust".to_string()],
            status: RecordStatus::Completed,
        },
    ));

    let view = ChronicleView::build(&archive)?;
    let tree = view.tree(day_id)?;
    assert_eq!(tree.children.len(), 1);
    assert_eq!(tree.children[0].record.record_id, task_id);
    Ok(())
}

#[test]
fn query_uses_created_time_and_hides_retracted_records() -> Result<(), Box<dyn std::error::Error>> {
    let mut archive = archive();
    let day_id = ChronicleId::new_v7();
    let task_id = ChronicleId::new_v7();
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(2),
        Timestamp::from_millis(2),
        "test".to_string(),
        context(),
        EventKind::DayOpened {
            day_id,
            day_key: DayKey {
                date: "2026-05-13".to_string(),
                timezone: "local".to_string(),
            },
        },
    ));
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(3),
        Timestamp::from_millis(3),
        "test".to_string(),
        context(),
        EventKind::TaskRecorded {
            task_id,
            day_id,
            parent_id: day_id,
            text: "temporary".to_string(),
            purpose: None,
            tags: Vec::new(),
            status: RecordStatus::Completed,
        },
    ));
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(4),
        Timestamp::from_millis(4),
        "test".to_string(),
        context(),
        EventKind::TaskRetracted {
            record_id: task_id,
            reason: "mistake".to_string(),
        },
    ));

    let view = ChronicleView::build(&archive)?;
    let records = view.query(
        Timestamp::from_millis(0),
        Timestamp::from_millis(10),
        None,
        None,
        None,
    );
    assert_eq!(records.len(), 1);
    assert_eq!(records[0].record_id, day_id);
    Ok(())
}

#[test]
fn relinking_replaces_the_previous_parent_bucket() -> Result<(), Box<dyn std::error::Error>> {
    let mut archive = archive();
    let day_id = ChronicleId::new_v7();
    let first_parent_id = ChronicleId::new_v7();
    let second_parent_id = ChronicleId::new_v7();
    let child_id = ChronicleId::new_v7();
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(2),
        Timestamp::from_millis(2),
        "test".to_string(),
        context(),
        EventKind::DayOpened {
            day_id,
            day_key: DayKey {
                date: "2026-05-13".to_string(),
                timezone: "local".to_string(),
            },
        },
    ));
    for (task_id, text) in [
        (first_parent_id, "first parent"),
        (second_parent_id, "second parent"),
        (child_id, "child"),
    ] {
        archive.push(ChronicleEvent::new(
            Timestamp::from_millis(3),
            Timestamp::from_millis(3),
            "test".to_string(),
            context(),
            EventKind::TaskRecorded {
                task_id,
                day_id,
                parent_id: day_id,
                text: text.to_string(),
                purpose: None,
                tags: Vec::new(),
                status: RecordStatus::Open,
            },
        ));
    }
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(4),
        Timestamp::from_millis(4),
        "test".to_string(),
        context(),
        EventKind::TaskLinked {
            child_id,
            parent_id: first_parent_id,
        },
    ));
    archive.push(ChronicleEvent::new(
        Timestamp::from_millis(5),
        Timestamp::from_millis(5),
        "test".to_string(),
        context(),
        EventKind::TaskLinked {
            child_id,
            parent_id: second_parent_id,
        },
    ));

    let view = ChronicleView::build(&archive)?;
    let first_tree = view.tree(first_parent_id)?;
    let second_tree = view.tree(second_parent_id)?;

    assert_eq!(first_tree.children.len(), 0);
    assert_eq!(second_tree.children.len(), 1);
    assert_eq!(second_tree.children[0].record.record_id, child_id);
    Ok(())
}