truth-mirror 0.9.1

Truthfulness gate and adversarial reviewer harness for AI coding agents.
Documentation
use std::path::Path;

use crate::config::MemorySkillConfig;

use super::{
    scan::{scan_memory_skill_advisory, scan_memory_skill_candidate},
    store::MemorySkillStore,
    types::{MemorySkillAdvisory, MemorySkillCandidate, MemorySkillError},
};

const MAX_REINJECTION_ITEMS: usize = 20;
// Scan beyond the display cap so a small number of unsafe or stale records does
// not hide legitimate entries from the same source class.
const MAX_REINJECTION_SCAN_ITEMS: usize = MAX_REINJECTION_ITEMS * 4;

pub fn reinjection_section(
    state_dir: &Path,
    config: &crate::config::TruthMirrorConfig,
) -> Result<String, MemorySkillError> {
    if !config.memory_skill.effective_enabled(&config.skills) {
        return Ok(String::new());
    }
    let store = MemorySkillStore::new(state_dir, &config.memory_skill)?;
    let candidates = store.pending_candidates()?;
    let advisories = store
        .read_advisories()?
        .into_iter()
        .filter(|advisory| !advisory.dismissed)
        .collect::<Vec<_>>();
    if candidates.is_empty() && advisories.is_empty() {
        return Ok(String::new());
    }

    let mut items = Vec::new();
    let accepted_candidates = scan_and_collect(
        &candidates,
        render_candidate_item,
        |candidate| candidate.id.as_str(),
        scan_memory_skill_candidate,
        &config.memory_skill,
        &mut items,
    )?;
    let accepted_advisories = scan_and_collect(
        &advisories,
        render_advisory_item,
        |advisory| advisory.id.as_str(),
        scan_memory_skill_advisory,
        &config.memory_skill,
        &mut items,
    )?;
    let has_omitted_backlog =
        candidates.len() > accepted_candidates || advisories.len() > accepted_advisories;

    if items.is_empty() && !has_omitted_backlog {
        return Ok(String::new());
    }

    let mut output = String::new();
    output.push_str("\nTruth Mirror pending memory/skill candidates:\n");
    for item in items {
        output.push_str(&item);
    }
    if candidates.len() > accepted_candidates {
        output.push_str(&format!(
            "- ... {} more pending candidates omitted; run `truth-mirror memory-skill list`.\n",
            candidates.len() - accepted_candidates
        ));
    }
    if advisories.len() > accepted_advisories {
        output.push_str(&format!(
            "- ... {} more suggested candidates omitted; run `truth-mirror memory-skill list`.\n",
            advisories.len() - accepted_advisories
        ));
    }
    Ok(output)
}

fn scan_and_collect<T>(
    entries: &[T],
    render: impl Fn(&T) -> String,
    id_of: impl Fn(&T) -> &str,
    scan: impl Fn(&T, &MemorySkillConfig) -> Result<(), MemorySkillError>,
    config: &MemorySkillConfig,
    items: &mut Vec<String>,
) -> Result<usize, MemorySkillError> {
    let mut accepted = 0;
    for (scanned_items, entry) in entries.iter().enumerate() {
        if items.len() >= MAX_REINJECTION_ITEMS || scanned_items >= MAX_REINJECTION_SCAN_ITEMS {
            break;
        }
        if scan_reinjection_item(entry, config, id_of(entry), &scan)? {
            let item = render(entry);
            items.push(item);
            accepted += 1;
        }
    }
    Ok(accepted)
}

fn render_candidate_item(candidate: &MemorySkillCandidate) -> String {
    format!(
        "- {} {} {}\n  source: {} {}\n  occurrences: {}\n  action: human review required outside the agent runtime\n  inspect: `truth-mirror memory-skill show {}`\n",
        candidate.id,
        candidate.candidate_kind.as_str(),
        candidate.slug,
        candidate.source_commit,
        candidate.truth_label,
        candidate.occurrence_count,
        candidate.id
    )
}

fn render_advisory_item(advisory: &MemorySkillAdvisory) -> String {
    format!(
        "- {} {} {} (suggested)\n  source: {} {}\n  occurrences: {}\n  action: human review required outside the agent runtime\n  inspect: `truth-mirror memory-skill list`\n",
        advisory.id,
        advisory.candidate_kind.as_str(),
        advisory.slug,
        advisory.source_commit,
        advisory.truth_label,
        advisory.occurrence_count
    )
}

fn scan_reinjection_item<T>(
    entry: &T,
    config: &MemorySkillConfig,
    id: &str,
    scan: &impl Fn(&T, &MemorySkillConfig) -> Result<(), MemorySkillError>,
) -> Result<bool, MemorySkillError> {
    match scan(entry, config) {
        Ok(()) => Ok(true),
        Err(MemorySkillError::ScanRejected { reason }) => {
            tracing::warn!(
                item_id = id,
                reason = %reason,
                "skipping unsafe memory-skill reinjection item"
            );
            Ok(false)
        }
        Err(error) => Err(error),
    }
}