truth-mirror 0.7.1

Truthfulness gate and adversarial reviewer harness for AI coding agents.
Documentation
use super::types::MemorySkillCandidate;

pub fn render_candidate_preview(candidate: &MemorySkillCandidate) -> String {
    format!(
        "# {}\n\n- id: {}\n- status: {}\n- kind: {}\n- source: {} {}\n- occurrences: {}\n\n{}\n",
        markdown_inline(&candidate.title),
        candidate.id,
        candidate.status,
        candidate.candidate_kind.as_str(),
        candidate.source_commit,
        candidate.truth_label,
        candidate.occurrence_count,
        markdown_paragraph(&candidate.description)
    )
}

pub fn render_skill(candidate: &MemorySkillCandidate) -> String {
    let mut output = String::new();
    output.push_str("---\n");
    output.push_str(&format!("name: {}\n", yaml_double_quoted(&candidate.slug)));
    output.push_str(&format!(
        "description: {}\n",
        yaml_double_quoted(&candidate.description)
    ));
    output.push_str("---\n\n");
    output.push_str(&format!("# {}\n\n", markdown_inline(&candidate.title)));
    output.push_str("## When to Use\n\n");
    for item in &candidate.when_to_use {
        output.push_str(&format!("- {}\n", markdown_inline(item)));
    }
    output.push_str("\n## Procedure\n\n");
    for (index, step) in candidate.procedure_steps.iter().enumerate() {
        output.push_str(&format!("{}. {}\n", index + 1, markdown_inline(step)));
    }
    output.push_str("\n## Pitfalls\n\n");
    for item in &candidate.pitfalls {
        output.push_str(&format!("- {}\n", markdown_inline(item)));
    }
    output.push_str("\n## Verification\n\n");
    for item in &candidate.verification_steps {
        output.push_str(&format!("- {}\n", markdown_inline(item)));
    }
    output.push_str("\n## Evidence\n\n");
    output.push_str(&format!("- source commit: {}\n", candidate.source_commit));
    output.push_str(&format!(
        "- ledger: {}\n",
        candidate.ledger_entry_ref.display()
    ));
    output.push_str(&format!(
        "- review run: {}\n",
        candidate.source_run_ref.display()
    ));
    for occurrence in &candidate.occurrence_refs {
        output.push_str(&format!("- occurrence: {}\n", occurrence.display()));
    }
    output
}

fn yaml_double_quoted(value: &str) -> String {
    let mut quoted = String::from("\"");
    for character in value.chars() {
        match character {
            '\\' => quoted.push_str("\\\\"),
            '"' => quoted.push_str("\\\""),
            '\n' => quoted.push_str("\\n"),
            '\r' => quoted.push_str("\\r"),
            '\t' => quoted.push_str("\\t"),
            other if (other as u32) < 0x20 => {
                quoted.push_str(&format!("\\u{:04x}", other as u32));
            }
            other => quoted.push(other),
        }
    }
    quoted.push('"');
    quoted
}

fn markdown_inline(value: &str) -> String {
    markdown_text(value, true)
}

fn markdown_paragraph(value: &str) -> String {
    markdown_text(value, false)
}

fn markdown_text(value: &str, single_line: bool) -> String {
    let mut output = String::new();
    let mut last_was_space = false;
    for character in value.chars() {
        match character {
            '\\' | '`' | '*' | '_' | '[' | ']' | '<' | '>' | '#' | '~' => {
                output.push('\\');
                output.push(character);
                last_was_space = false;
            }
            '\n' | '\r' | '\u{2028}' | '\u{2029}' if single_line => {
                if !last_was_space {
                    output.push(' ');
                    last_was_space = true;
                }
            }
            '\n' | '\r' | '\u{2028}' | '\u{2029}' => {
                if !output.ends_with('\n') {
                    output.push('\n');
                }
                last_was_space = false;
            }
            '\t' => {
                if !last_was_space {
                    output.push(' ');
                    last_was_space = true;
                }
            }
            other if (other as u32) < 0x20 => {
                output.push_str(&format!("\\u{:04x}", other as u32));
                last_was_space = false;
            }
            other => {
                output.push(other);
                last_was_space = other.is_whitespace();
            }
        }
    }
    let output = output.trim();
    if single_line {
        output.to_owned()
    } else {
        escape_list_prefixes(output)
    }
}

fn escape_list_prefixes(value: &str) -> String {
    value
        .lines()
        .map(escape_list_prefix)
        .collect::<Vec<_>>()
        .join("\n")
}

fn escape_list_prefix(line: &str) -> String {
    let trimmed = line.trim_start_matches(' ');
    let indent_len = line.len() - trimmed.len();
    if trimmed.starts_with("- ") || trimmed.starts_with("+ ") {
        return format!("{}\\{}", &line[..indent_len], trimmed);
    }
    if is_markdown_rule_line(trimmed) {
        return format!("{}\\{}", &line[..indent_len], trimmed);
    }
    let digit_count = trimmed.bytes().take_while(u8::is_ascii_digit).count();
    if digit_count > 0 {
        let marker = trimmed.as_bytes().get(digit_count).copied();
        if let Some(marker @ (b'.' | b')')) = marker {
            return format!(
                "{}{}\\{}{}",
                &line[..indent_len],
                &trimmed[..digit_count],
                marker as char,
                &trimmed[digit_count + 1..]
            );
        }
    }
    line.to_owned()
}

fn is_markdown_rule_line(trimmed: &str) -> bool {
    let marker = trimmed.as_bytes().first().copied();
    matches!(marker, Some(b'-' | b'='))
        && !trimmed.is_empty()
        && trimmed.as_bytes().iter().all(|byte| Some(*byte) == marker)
}

#[cfg(test)]
mod tests {
    use crate::{
        config::MemorySkillCandidateKind,
        ledger::Verdict,
        memory_skill::{
            CANDIDATE_SCHEMA_VERSION, CandidateStatus, EvidenceRef, MemorySkillCandidate,
        },
    };

    use super::{render_candidate_preview, render_skill};

    #[test]
    fn candidate_preview_sanitizes_markdown_control_text() {
        let ledger_ref = EvidenceRef::ledger("abc123");
        let candidate = MemorySkillCandidate {
            schema_version: CANDIDATE_SCHEMA_VERSION,
            id: "msk_1".to_owned(),
            fingerprint: "sha256:v1:test".to_owned(),
            learning_key: "test".to_owned(),
            status: CandidateStatus::Pending,
            candidate_kind: MemorySkillCandidateKind::HowToSkill,
            scope: "project".to_owned(),
            source_commit: "abc123".to_owned(),
            source_claim: "CLAIM: test | verified: cargo test | evidence: tests:test".to_owned(),
            truth_label: Verdict::Pass,
            ledger_entry_ref: ledger_ref.clone(),
            source_run_ref: EvidenceRef::run("run-1"),
            occurrence_count: 1,
            occurrence_refs: vec![ledger_ref.clone()],
            slug: "test".to_owned(),
            title: "Title\u{2028}# injected `code`".to_owned(),
            description:
                "line one\n-\n=\n- injected `item`\n+ plus\n---\n===\n2. numbered\n3) paren ~~gone~~\u{0007}"
                    .to_owned(),
            when_to_use: Vec::new(),
            procedure_steps: Vec::new(),
            pitfalls: Vec::new(),
            verification_steps: Vec::new(),
            evidence: vec![ledger_ref],
            created_at_unix: 100,
        };

        let preview = render_candidate_preview(&candidate);

        assert!(preview.starts_with("# Title \\# injected \\`code\\`"));
        assert!(preview.contains(
            "line one\n\\-\n\\=\n\\- injected \\`item\\`\n\\+ plus\n\\---\n\\===\n2\\. numbered\n3\\) paren \\~\\~gone\\~\\~\\u0007"
        ));
    }

    #[test]
    fn rendered_skill_sanitizes_list_items() {
        let ledger_ref = EvidenceRef::ledger("abc123");
        let candidate = MemorySkillCandidate {
            schema_version: CANDIDATE_SCHEMA_VERSION,
            id: "msk_1".to_owned(),
            fingerprint: "sha256:v1:test".to_owned(),
            learning_key: "test".to_owned(),
            status: CandidateStatus::Pending,
            candidate_kind: MemorySkillCandidateKind::HowToSkill,
            scope: "project".to_owned(),
            source_commit: "abc123".to_owned(),
            source_claim: "CLAIM: test | verified: cargo test | evidence: tests:test".to_owned(),
            truth_label: Verdict::Pass,
            ledger_entry_ref: ledger_ref.clone(),
            source_run_ref: EvidenceRef::run("run-1"),
            occurrence_count: 1,
            occurrence_refs: vec![ledger_ref.clone()],
            slug: "test".to_owned(),
            title: "Title\n# injected".to_owned(),
            description: "description".to_owned(),
            when_to_use: vec!["when\n- injected".to_owned()],
            procedure_steps: vec!["step one\n2. injected `code`".to_owned()],
            pitfalls: vec!["pitfall\n# injected".to_owned()],
            verification_steps: vec!["verify\n- injected".to_owned()],
            evidence: vec![ledger_ref],
            created_at_unix: 100,
        };

        let skill = render_skill(&candidate);

        assert!(skill.contains("# Title \\# injected"));
        assert!(skill.contains("- when - injected"));
        assert!(skill.contains("1. step one 2. injected \\`code\\`"));
        assert!(!skill.contains("\n2. injected"));
    }
}