crtx-reflect 0.1.1

Reflection orchestration, prompts, candidate parsing, and schema validation.
Documentation
use cortex_reflect::{
    parse_principle_candidates, parse_reflection, principle_candidate_batch_json_schema,
    quarantine_record, session_reflection_json_schema, MemoryType, ReflectError,
    REFLECTION_QUARANTINE_OPERATION,
};
use cortex_store::migrate::apply_pending;
use cortex_store::repo::AuditRepo;
use rusqlite::Connection;

const VALID_REFLECTION: &str = r#"{
  "trace_id": "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "episode_candidates": [
    {
      "summary": "User corrected reflection behavior",
      "source_event_ids": ["evt_01ARZ3NDEKTSV4RRFFQ69G5FAV"],
      "domains": ["agents"],
      "entities": ["Cortex"],
      "candidate_meaning": null,
      "confidence": 0.7
    }
  ],
  "memory_candidates": [
    {
      "memory_type": "correction",
      "claim": "Invalid reflection JSON must be quarantined.",
      "source_episode_indexes": [0],
      "applies_when": ["parsing model output"],
      "does_not_apply_when": ["input is trusted typed data"],
      "confidence": 0.8,
      "initial_salience": {
        "reusability": 0.9,
        "consequence": 0.7,
        "emotional_charge": 0.1
      }
    }
  ],
  "contradictions": [],
  "doctrine_suggestions": []
}"#;

#[test]
fn valid_reflection_parses() {
    let reflection = parse_reflection(VALID_REFLECTION).expect("valid reflection parses");

    assert_eq!(reflection.episode_candidates.len(), 1);
    assert_eq!(reflection.memory_candidates.len(), 1);
    assert_eq!(
        reflection.memory_candidates[0].memory_type,
        MemoryType::Correction
    );
}

#[test]
fn invalid_json_is_quarantine_ready() {
    let raw = r#"{"trace_id":"trc_01ARZ3NDEKTSV4RRFFQ69G5FAV""#;
    let err = parse_reflection(raw).expect_err("invalid JSON is rejected");

    assert!(matches!(err, ReflectError::InvalidJson { .. }));
    assert_eq!(err.quarantine_payload(), raw);
    assert_eq!(err.quarantine_reason(), "invalid_json");
}

#[test]
fn invalid_reflection_writes_one_quarantine_audit_row() {
    let pool = Connection::open_in_memory().expect("open in-memory sqlite");
    apply_pending(&pool).expect("apply store migrations");
    let raw = r#"{"trace_id":"trc_01ARZ3NDEKTSV4RRFFQ69G5FAV""#;
    let err = parse_reflection(raw).expect_err("invalid JSON is rejected");

    let entry = quarantine_record(err.quarantine_payload(), err.quarantine_reason(), &pool)
        .expect("append quarantine audit row");
    let entries = AuditRepo::new(&pool)
        .list_by_target_ref(&entry.target_ref)
        .expect("read quarantine audit rows");

    assert_eq!(entries.len(), 1);
    assert_eq!(entries[0].operation, REFLECTION_QUARANTINE_OPERATION);
    assert_eq!(entries[0].reason, "invalid_json");
    assert!(entries[0].target_ref.starts_with("reflection_output:"));
    assert_eq!(entries[0].after_hash.len(), 64);
    assert_ne!(entries[0].source_refs_json.to_string(), raw);
}

#[test]
fn invalid_schema_is_quarantine_ready() {
    let raw = VALID_REFLECTION.replace(
        "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV",
    );
    let err = parse_reflection(&raw).expect_err("wrong source ID type is rejected");

    assert!(matches!(err, ReflectError::InvalidSchema { .. }));
    assert_eq!(err.quarantine_payload(), raw);
    assert_eq!(err.quarantine_reason(), "invalid_schema");
    assert!(err.detail().contains("EventId") || err.detail().contains("expected prefix"));
}

#[test]
fn out_of_range_confidence_is_quarantine_ready() {
    let raw = VALID_REFLECTION.replace("\"confidence\": 0.8", "\"confidence\": 1.8");
    let err = parse_reflection(&raw).expect_err("out-of-range confidence is rejected");

    assert!(matches!(err, ReflectError::InvalidSchema { .. }));
    assert_eq!(err.quarantine_payload(), raw);
    assert!(err.detail().contains("confidence"));
}

#[test]
fn missing_episode_index_is_quarantine_ready() {
    let raw = VALID_REFLECTION.replace(
        "\"source_episode_indexes\": [0]",
        "\"source_episode_indexes\": [1]",
    );
    let err = parse_reflection(&raw).expect_err("missing episode index is rejected");

    assert!(matches!(err, ReflectError::InvalidSchema { .. }));
    assert_eq!(err.quarantine_payload(), raw);
    assert!(err.detail().contains("source_episode_indexes"));
}

#[test]
fn principle_candidate_batch_parses() {
    let raw = r#"{
      "candidate_principles": [
        {
          "statement": "Prefer evidence-bound parser failures.",
          "supporting_memory_ids": ["mem_01ARZ3NDEKTSV4RRFFQ69G5FAV"],
          "contradicting_memory_ids": [],
          "domains_observed": ["agents"],
          "applies_when": ["building parser boundaries"],
          "does_not_apply_when": ["runtime evidence is unavailable"],
          "alternative_interpretations": ["This is only a local parser preference."],
          "confidence": 0.6,
          "overgeneralisation_risk": 0.2
        }
      ]
    }"#;

    let batch = parse_principle_candidates(raw).expect("valid principles parse");

    assert_eq!(batch.candidate_principles.len(), 1);
}

#[test]
fn schema_export_matches_checked_in_json_schema() {
    let expected: serde_json::Value =
        serde_json::from_str(include_str!("../session_reflection.schema.json"))
            .expect("checked-in SessionReflection schema is valid JSON");

    assert_eq!(session_reflection_json_schema(), expected);
}

#[test]
fn principle_schema_export_contains_candidate_principles() {
    let schema = principle_candidate_batch_json_schema();

    assert_eq!(schema["title"], "PrincipleCandidateBatch");
    assert!(schema["properties"]
        .as_object()
        .expect("properties object")
        .contains_key("candidate_principles"));
}