chappi-core 0.1.0

Typed cognitive graph, patch, reentry, projection, and measurement engine for Chappi
Documentation
use std::fmt::Write as _;

use chappi_core::{
    BudgetUnit, CapsuleBudget, CognitiveState, Language, ProjectionFormat, build_capsule, project,
    project_capsule, state_hash,
};
use chappi_syntax::parse_document;
use serde_json::{Value, json};

fn decode(source: &str) -> CognitiveState {
    CognitiveState::try_from(parse_document(source).expect("fixture syntax should parse"))
        .expect("fixture semantics should decode")
}

fn hash_hex(state: &CognitiveState) -> String {
    let mut output = String::with_capacity(64);
    for byte in state_hash(state) {
        write!(output, "{byte:02x}").expect("writing to String cannot fail");
    }
    output
}

fn projection_state() -> CognitiveState {
    decode(
        r#"(chappi 1)
(entity self self persistent 100 person (name "Self"))
(entity other self persistent 90 person (name "Other"))
(claim claim self working 80 likely (text "same line"))
(goal goal self working 75 committed (ship v1))
(relationship relationship self persistent 70 other trusted (knows since-childhood))
(perspective perspective self working 65 other plausible (believes busy))
(affect affect self transient 60 other fondness strong (feels warm))
(rel about perspective other)
(focus work 0 self evoked none (fg claim) (bg other) (residue goal) (trigger assigned))
"#,
    )
}

#[test]
fn renders_deterministic_english_and_japanese_lines_with_explicit_perspectives() {
    let state = projection_state();

    let english = project(&state, ProjectionFormat::Text(Language::En));
    let japanese = project(&state, ProjectionFormat::Text(Language::Ja));

    assert!(english.starts_with("Chappi projection v1 state_hash="));
    assert!(english.contains("kind=relationship"));
    assert!(english.contains("owner=self"));
    assert!(english.contains("target=other"));
    assert!(english.contains("kind=perspective"));
    assert!(english.contains("perspective_target=other"));
    assert!(english.contains("payload=(knows since-childhood)"));
    assert!(japanese.starts_with("Chappi 投影 v1 状態ハッシュ="));
    assert!(japanese.contains("種別=relationship"));
    assert!(japanese.contains("所有者=self"));
    assert!(japanese.contains("対象=other"));
    assert!(japanese.contains("視点対象=other"));
    assert_eq!(english.lines().count(), japanese.lines().count());
    assert!(!english.contains("span="));
}

#[test]
fn emits_compact_versioned_json_with_exact_typed_fields_and_no_spans() {
    let state = projection_state();
    let output = project(&state, ProjectionFormat::Json);
    let value: Value = serde_json::from_str(&output).expect("projection should be valid JSON");

    assert_eq!(value["schema"], "chappi.projection");
    assert_eq!(value["version"], 1);
    assert_eq!(value["state_hash"], hash_hex(&state));
    assert_eq!(
        value["nodes"][0],
        json!({
            "id": "affect",
            "revision": 0,
            "owner": "self",
            "lifetime": "transient",
            "salience": 60,
            "kind": "affect",
            "target": "other",
            "affect_kind": "fondness",
            "intensity": "strong",
            "payload": "(feels warm)"
        })
    );
    assert_eq!(value["relations"][0]["kind"], "about");
    assert_eq!(value["relations"][0]["source"], "perspective");
    assert_eq!(value["focus"][0]["previous_sequence"], Value::Null);
    assert_eq!(value["focus"][0]["trigger"], "(trigger assigned)");
    assert!(!output.contains("span"));
    assert!(!output.contains('\n'));
}

#[test]
fn projects_capsules_with_the_source_hash_and_only_selected_content() {
    let state = projection_state();
    let capsule = build_capsule(
        &state,
        CapsuleBudget {
            limit: 0,
            unit: BudgetUnit::Bytes,
        },
    );

    let output = project_capsule(&capsule, ProjectionFormat::Json);
    let value: Value = serde_json::from_str(&output).expect("projection should be valid JSON");

    assert_eq!(value["state_hash"], hash_hex(&state));
    assert_eq!(
        value["nodes"]
            .as_array()
            .expect("nodes should be an array")
            .iter()
            .map(|node| node["id"].as_str().expect("ID should be text"))
            .collect::<Vec<_>>(),
        ["goal", "self"]
    );
    assert_eq!(value["relations"], json!([]));
    assert_eq!(value["focus"], json!([]));
    assert!(!output.contains("claim"));
}

#[test]
fn projection_output_is_stable_across_semantic_input_order_and_spans() {
    let ordered = projection_state();
    let reordered = decode(
        r#"; reordered and respanned
(focus work 0 self evoked none (fg claim) (bg other) (residue goal) (trigger assigned))
(rel about perspective other)
(affect affect self transient 60 other fondness strong (feels warm))
(perspective perspective self working 65 other plausible (believes busy))
(relationship relationship self persistent 70 other trusted (knows since-childhood))
(goal goal self working 75 committed (ship v1))
(claim claim self working 80 likely (text "same line"))
(entity other self persistent 90 person (name "Other"))
(entity self self persistent 100 person (name "Self"))
(chappi 1)
"#,
    );

    assert_eq!(
        project(&ordered, ProjectionFormat::Json),
        project(&reordered, ProjectionFormat::Json)
    );
    assert_eq!(
        project(&ordered, ProjectionFormat::Text(Language::En)),
        project(&reordered, ProjectionFormat::Text(Language::En))
    );
}