crtx 0.1.1

CLI for the Cortex supervisory memory substrate.
//! `cortex principles ...` — Phase 2 principle candidate surface.
//!
//! Principle extraction is read-only at the CLI boundary: it prints candidate
//! JSON from active memories and never writes doctrine.

use clap::{Args, Subcommand};
use cortex_reflect::{
    extract_deterministic_candidates, AcceptedMemory, PrincipleCandidateBatch,
    PrincipleExtractionWindow,
};

use crate::cmd::open_default_store;
use crate::exit::Exit;

/// `cortex principles ...` subcommands.
#[derive(Debug, Subcommand)]
pub enum PrinciplesSub {
    /// Extract principle candidates without writing doctrine.
    Extract(ExtractArgs),
}

/// `cortex principles extract` arguments.
#[derive(Debug, Args)]
pub struct ExtractArgs {
    /// Window of accepted memories to inspect, for example `30d`.
    #[arg(long, value_name = "WINDOW")]
    pub window: String,

    /// Model or adapter selector. Replay remains the intended CI default.
    #[arg(long, value_name = "MODEL")]
    pub model: String,
}

/// Run a `cortex principles ...` command.
pub fn run(sub: PrinciplesSub) -> Exit {
    match sub {
        PrinciplesSub::Extract(args) => extract(args),
    }
}

fn extract(args: ExtractArgs) -> Exit {
    if args.window.trim().is_empty() {
        eprintln!("cortex principles extract: --window must not be empty");
        return Exit::Usage;
    }
    if args.model.trim().is_empty() {
        eprintln!("cortex principles extract: --model must not be empty");
        return Exit::Usage;
    }
    if args.model != "replay" {
        eprintln!(
            "cortex principles extract: unsupported --model `{}`. Only `replay` is wired for store-backed Phase 2 extraction.",
            args.model
        );
        return Exit::Usage;
    }

    let pool = match open_default_store("principles extract") {
        Ok(pool) => pool,
        Err(exit) => return exit,
    };
    let repo = cortex_store::repo::MemoryRepo::new(&pool);
    let memories = match repo.list_by_status("active") {
        Ok(memories) => memories,
        Err(err) => {
            eprintln!("cortex principles extract: failed to read active memories: {err}");
            return Exit::Internal;
        }
    };
    let window = PrincipleExtractionWindow::new(
        memories
            .into_iter()
            .map(|memory| AcceptedMemory {
                id: memory.id,
                claim: memory.claim,
                domains: string_array(&memory.domains_json),
                applies_when: string_array(&memory.applies_when_json),
                does_not_apply_when: string_array(&memory.does_not_apply_when_json),
            })
            .collect(),
    );
    let batch = PrincipleCandidateBatch {
        candidate_principles: extract_deterministic_candidates(&window),
    };

    match serde_json::to_string_pretty(&batch) {
        Ok(serialized) => {
            println!("{serialized}");
            Exit::Ok
        }
        Err(err) => {
            eprintln!("cortex principles extract: failed to serialize candidates: {err}");
            Exit::Internal
        }
    }
}

fn string_array(value: &serde_json::Value) -> Vec<String> {
    value
        .as_array()
        .into_iter()
        .flatten()
        .filter_map(|value| value.as_str().map(ToString::to_string))
        .collect()
}