lazyspec 0.8.0

A little TUI & CLI for project documentation.
Documentation
mod common;

fn setup() -> (common::TestFixture, lazyspec::engine::store::Store) {
    let fixture = common::TestFixture::new();
    fixture.write_doc(
        "docs/rfcs/RFC-001-auth.md",
        "---\ntitle: \"Auth Redesign\"\ntype: rfc\nstatus: accepted\nauthor: jkaloger\ndate: 2026-03-01\ntags: [security]\nrelated: []\n---\n\nBody.\n",
    );
    fixture.write_doc(
        "docs/stories/STORY-001-auth.md",
        "---\ntitle: \"Auth Story\"\ntype: story\nstatus: draft\nauthor: jkaloger\ndate: 2026-03-02\ntags: []\nrelated:\n- implements: docs/rfcs/RFC-001-auth.md\n---\n\nBody.\n",
    );
    fixture.write_doc(
        "docs/iterations/ITERATION-001-sprint.md",
        "---\ntitle: \"Sprint 1\"\ntype: iteration\nstatus: draft\nauthor: agent\ndate: 2026-03-03\ntags: []\nrelated:\n- implements: docs/stories/STORY-001-auth.md\n---\n\nBody.\n",
    );
    let store = fixture.store();
    (fixture, store)
}

#[test]
fn status_json_has_documents_and_validation() {
    let (fixture, store) = setup();
    let output = lazyspec::cli::status::run_json(&store, &fixture.config());
    let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();

    assert!(parsed["documents"].is_array());
    assert!(parsed["validation"].is_object());
    assert!(parsed["validation"]["errors"].is_array());
    assert!(parsed["validation"]["warnings"].is_array());
}

#[test]
fn status_json_includes_all_documents() {
    let (fixture, store) = setup();
    let output = lazyspec::cli::status::run_json(&store, &fixture.config());
    let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();

    let docs = parsed["documents"].as_array().unwrap();
    assert_eq!(docs.len(), 3);

    let titles: Vec<&str> = docs.iter().map(|d| d["title"].as_str().unwrap()).collect();
    assert!(titles.contains(&"Auth Redesign"));
    assert!(titles.contains(&"Auth Story"));
    assert!(titles.contains(&"Sprint 1"));
}

#[test]
fn status_json_documents_use_full_schema() {
    let (fixture, store) = setup();
    let output = lazyspec::cli::status::run_json(&store, &fixture.config());
    let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();

    let doc = &parsed["documents"][0];
    assert!(doc["path"].is_string());
    assert!(doc["title"].is_string());
    assert!(doc["type"].is_string());
    assert!(doc["status"].is_string());
    assert!(doc["author"].is_string());
    assert!(doc["date"].is_string());
    assert!(doc["tags"].is_array());
    assert!(doc["related"].is_array());
}

#[test]
fn status_human_grouped_by_type() {
    let (_fixture, store) = setup();
    let output = lazyspec::cli::status::run_human(&store);

    assert!(output.contains("RFC"));
    assert!(output.contains("STORY"));
    assert!(output.contains("ITERATION"));
    assert!(output.contains("Auth Redesign"));
    assert!(output.contains("Auth Story"));
    assert!(output.contains("Sprint 1"));
}

#[test]
fn status_json_includes_parse_errors() {
    let fixture = common::TestFixture::new();
    fixture.write_rfc("RFC-001-good.md", "Good RFC", "draft");
    fixture.write_doc(
        "docs/rfcs/RFC-002-broken.md",
        "---\ntitle: \"Broken\"\ntype: rfc\nauthor: test\ndate: 2026-01-01\ntags: []\n---\n",
    );

    let store = fixture.store();
    let output = lazyspec::cli::status::run_json(&store, &fixture.config());
    let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();

    let parse_errors = parsed["parse_errors"].as_array().unwrap();
    assert!(!parse_errors.is_empty());
    assert!(parse_errors[0]["path"].is_string());
    assert!(parse_errors[0]["error"].is_string());
}

#[test]
fn status_empty_project() {
    let fixture = common::TestFixture::new();

    let store = fixture.store();

    let json_output = lazyspec::cli::status::run_json(&store, &fixture.config());
    let parsed: serde_json::Value = serde_json::from_str(&json_output).unwrap();
    assert_eq!(parsed["documents"].as_array().unwrap().len(), 0);

    let human_output = lazyspec::cli::status::run_human(&store);
    assert!(human_output.is_empty() || human_output.trim().is_empty());
}