cordance-emit 0.1.1

Cordance target emitters: AGENTS.md, CLAUDE.md, .cursor/rules, .codex, axiom harness-target.
Documentation
//! `.cursor/rules/*.mdc` emitter.

use camino::Utf8PathBuf;
use cordance_core::pack::CordancePack;

use crate::agents_md::{
    commands_body, doctrine_pointers_body, forbidden_surfaces_body, resolve_axiom_info,
};
use crate::{EmitError, TargetEmitter};

pub struct CursorEmitter;

fn mdc_file(description: &str, content: &str) -> Vec<u8> {
    format!("---\ndescription: {description}\nglobs: [\"**/*\"]\n---\n{content}\n").into_bytes()
}

const fn cortex_boundary_content() -> &'static str {
    "Cordance writes to Cortex only via `cordance-cortex-receipt-v1-candidate` receipts.\n\
     Never write directly to the cortex repo or modify cortex storage.\n\
     The operator hands receipts to Cortex's own acceptance flow."
}

/// Build the authority section using portable relative paths.
///
/// `axiom_source` is the raw string from `cordance.toml` (e.g. `"../pai-axiom"`).
///
/// Both inputs are target-controlled, so they are sanitised via
/// [`cordance_core::fence::sanitise_fenced_value`] before interpolation. See
/// `agents_md::axiom_load_order_body` for the threat model.
fn authority_content(version: &str, axiom_source: &str) -> String {
    let version = cordance_core::fence::sanitise_fenced_value(version);
    let axiom_source = cordance_core::fence::sanitise_fenced_value(axiom_source);
    let axiom_source_win = axiom_source.replace('/', "\\");
    format!(
        "## Authority Order\n\
         \n\
         Load axiom before acting on any multi-step task:\n\
         \n\
         Axiom source (configure in `cordance.toml` → `[axiom].source`): `{axiom_source}`\n\
         Algorithm version: `{version}`\n\
         \n\
         **POSIX / WSL:**\n\
         - `{axiom_source}/PAI/Algorithm/{version}.md`\n\
         - `{axiom_source}/PAI/CONTEXT_ROUTING.md`\n\
         \n\
         **Native Windows (replace `/` with `\\`):**\n\
         - `{axiom_source_win}\\PAI\\Algorithm\\{version}.md`\n\
         - `{axiom_source_win}\\PAI\\CONTEXT_ROUTING.md`\n\
         \n\
         Adjust `cordance.toml` → `[axiom].source` if axiom lives at a different path."
    )
}

impl TargetEmitter for CursorEmitter {
    fn name(&self) -> &'static str {
        "cursor"
    }

    fn render(&self, pack: &CordancePack) -> Result<Vec<(Utf8PathBuf, Vec<u8>)>, EmitError> {
        let (version, axiom_source) = resolve_axiom_info(pack);
        let auth = authority_content(&version, &axiom_source);
        let commands = commands_body(pack);
        let doctrine = doctrine_pointers_body(pack);
        let forbidden = forbidden_surfaces_body();

        let files: Vec<(Utf8PathBuf, Vec<u8>)> = vec![
            (
                ".cursor/rules/authority.mdc".into(),
                mdc_file("Doctrine authority order", &auth),
            ),
            (
                ".cursor/rules/commands.mdc".into(),
                mdc_file("Project build and test commands", &commands),
            ),
            (
                ".cursor/rules/doctrine-pointers.mdc".into(),
                mdc_file("Engineering doctrine pointers", &doctrine),
            ),
            (
                ".cursor/rules/forbidden-surfaces.mdc".into(),
                mdc_file("Blocked surfaces — never import or commit", forbidden),
            ),
            (
                ".cursor/rules/cortex-boundary.mdc".into(),
                mdc_file("Cortex non-write boundary", cortex_boundary_content()),
            ),
        ];

        Ok(files)
    }
}