helen 0.1.0

Repository review gate.
Documentation
//! Path-derived context for elenchus reviews.

use std::fmt::Write as _;

/// Maximum changed paths rendered inline before the prompt points to the diff.
const MAX_CHANGED_PATHS_IN_PROMPT: usize = 80;

/// Bounded reviewer context derived from the changed paths.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct ReviewContext {
    /// All changed paths from git output.
    paths: Vec<ChangedPath>,
    /// Source-record areas selected from changed paths.
    areas: Vec<ReviewArea>,
}

impl ReviewContext {
    /// Builds review context from `git diff --name-only` output.
    pub(super) fn from_changed_files(changed_files: &str) -> Self {
        let paths = changed_files
            .lines()
            .filter_map(ChangedPath::new)
            .collect::<Vec<_>>();
        let areas = REVIEW_AREAS
            .iter()
            .copied()
            .filter(|area| paths.iter().any(|path| area.matches_path(path.as_str())))
            .collect::<Vec<_>>();

        Self { paths, areas }
    }

    /// Formats changed paths for the prompt.
    pub(super) fn changed_files_for_prompt(&self) -> String {
        let mut files = String::new();
        for path in self.paths.iter().take(MAX_CHANGED_PATHS_IN_PROMPT) {
            let _result = writeln!(&mut files, "- {}", path.prompt_text());
        }
        if files.is_empty() {
            return String::from("- (none)");
        }

        let omitted = self.paths.len().saturating_sub(MAX_CHANGED_PATHS_IN_PROMPT);
        if omitted > 0 {
            let _result = writeln!(
                &mut files,
                "- ... {omitted} more changed path(s); inspect the saved diff snapshot for the full list."
            );
        }
        files
    }

    /// Formats source-record guidance for the prompt.
    pub(super) fn source_records_for_prompt(&self) -> String {
        if self.areas.is_empty() {
            return String::from(
                "No source-record hint was selected. Inspect the diff and nearby source first.",
            );
        }

        let mut guidance = format!(
            "Review-context hints selected from all {} changed path(s). Inspect the diff first; use these only when they help judge a touched invariant.\n",
            self.paths.len()
        );
        for area in &self.areas {
            let _result = writeln!(
                &mut guidance,
                "- {}: records: {}; invariants: {}",
                area.title(),
                area.records().join(", "),
                area.invariants()
            );
        }
        guidance
    }
}

/// A changed repository path from git output.
#[derive(Clone, Debug, Eq, PartialEq)]
struct ChangedPath {
    /// Raw path text used for area matching.
    path: String,
    /// Escaped path text safe to embed in the review prompt.
    prompt_text: String,
}

impl ChangedPath {
    /// Accepts only non-empty path lines without NUL bytes.
    fn new(path: &str) -> Option<Self> {
        if path.is_empty() || path.contains('\0') {
            return None;
        }

        Some(Self {
            path: path.to_owned(),
            prompt_text: escape_path_for_prompt(path),
        })
    }

    /// Borrows the path text exactly as git rendered it.
    fn as_str(&self) -> &str {
        &self.path
    }

    /// Borrows the prompt-safe path text.
    fn prompt_text(&self) -> &str {
        &self.prompt_text
    }
}

/// Escapes control characters before placing a path in reviewer instructions.
fn escape_path_for_prompt(path: &str) -> String {
    let mut escaped = String::with_capacity(path.len());
    for character in path.chars() {
        for escaped_character in character.escape_default() {
            escaped.push(escaped_character);
        }
    }
    escaped
}

/// Review areas with stable prompt ordering.
const REVIEW_AREAS: &[ReviewArea] = &[
    ReviewArea::Elenchus,
    ReviewArea::PluginRuntime,
    ReviewArea::EditorCore,
    ReviewArea::Ecs,
    ReviewArea::Vim,
    ReviewArea::Filesystem,
    ReviewArea::Config,
    ReviewArea::SupplyChain,
    ReviewArea::Tests,
    ReviewArea::ProgressNotes,
];

/// Source-record categories relevant to changed paths.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ReviewArea {
    /// Elenchus command, wrapper, or operator policy.
    Elenchus,
    /// Plugin runtime, policy, host, ABI, ECS publication, or plugin records.
    PluginRuntime,
    /// Pure editor text, buffer, rendering, and backend-adapter semantics.
    EditorCore,
    /// ECS owner-boundary requests, scheduling, and rejections.
    Ecs,
    /// Vim grammar, modes, motions, commands, and Bevy Vim adapter behavior.
    Vim,
    /// Filesystem helper policy, path normalization, and disclosure boundaries.
    Filesystem,
    /// Configuration parsing and authority defaults.
    Config,
    /// Dependency, lockfile, and Wasm-loading policy.
    SupplyChain,
    /// Test files and inline test modules.
    Tests,
    /// Progress notes, TODOs, and archived working records.
    ProgressNotes,
}

impl ReviewArea {
    /// Returns true when this area should be hinted for a changed path.
    fn matches_path(self, path: &str) -> bool {
        match self {
            Self::Elenchus => {
                path.starts_with("src/elenchus/")
                    || path.starts_with("helen/src/elenchus/")
                    || path == "tests/elenchus_flow.rs"
                    || path == "helen/tests/elenchus_flow.rs"
                    || path == "scripts/elenchus.sh"
                    || path == "helen/scripts/elenchus.sh"
                    || path == "AGENTS.md"
            }
            Self::PluginRuntime => is_plugin_runtime_path(path),
            Self::EditorCore => {
                path.starts_with("src/buffer/")
                    || path.starts_with("src/domain/")
                    || path.starts_with("src/editor/")
                    || path.starts_with("src/render/")
                    || path.starts_with("src/text_stream/")
                    || path == "docs/arch.md"
                    || path == "docs/target_arch.md"
            }
            Self::Ecs => path.starts_with("src/ecs/"),
            Self::Vim => path.starts_with("src/vim/") || path.starts_with("src/adapters/bevy/vim/"),
            Self::Filesystem => {
                path.starts_with("src/fs_utils/")
                    || path == "docs/authority.md"
                    || path == "docs/supply-chain.md"
            }
            Self::Config => path.starts_with("src/config/"),
            Self::SupplyChain => {
                matches!(path, "Cargo.toml" | "Cargo.lock" | "docs/supply-chain.md")
            }
            Self::Tests => path.starts_with("tests/") || path.contains("/tests.rs"),
            Self::ProgressNotes => {
                path == "docs/next_steps.md"
                    || path == "docs/elenchus_helen_extraction.md"
                    || path.ends_with("_todos.md")
                    || path.ends_with("_notes.md")
                    || path.starts_with("archive/")
            }
        }
    }

    /// Human-readable prompt title.
    const fn title(self) -> &'static str {
        match self {
            Self::Elenchus => "Elenchus gate",
            Self::PluginRuntime => "Plugin/Wasm boundary",
            Self::EditorCore => "Editor core",
            Self::Ecs => "ECS owner boundary",
            Self::Vim => "Vim semantics",
            Self::Filesystem => "Filesystem authority",
            Self::Config => "Configuration",
            Self::SupplyChain => "Supply chain",
            Self::Tests => "Tests",
            Self::ProgressNotes => "Progress notes",
        }
    }

    /// Source records to consult only when needed.
    const fn records(self) -> &'static [&'static str] {
        match self {
            Self::Elenchus => &[
                "AGENTS.md",
                ".codex/review-policy.md",
                "src/elenchus/",
                "helen/src/elenchus/",
            ],
            Self::PluginRuntime => &[
                "docs/arch.md",
                "docs/target_arch.md",
                "docs/authority.md",
                "docs/plugins/runtime.md",
                "docs/plugins/security.md",
                "docs/plugins/testing.md",
                "docs/supply-chain.md",
            ],
            Self::EditorCore | Self::Ecs => &["docs/arch.md", "docs/target_arch.md"],
            Self::Vim => &["docs/arch.md"],
            Self::Filesystem => &["docs/arch.md", "docs/authority.md", "docs/supply-chain.md"],
            Self::Config => &["docs/arch.md", "docs/authority.md"],
            Self::SupplyChain => &["docs/supply-chain.md"],
            Self::Tests => &["docs/plugins/testing.md when plugin behavior is touched"],
            Self::ProgressNotes => &["the changed note file", "the source records it cites"],
        }
    }

    /// Area-specific invariant reminder.
    const fn invariants(self) -> &'static str {
        match self {
            Self::Elenchus => {
                "preserve the gate contract: clean index, exact diff snapshot, local checks, read-only review, risk pause, and commit only after pass"
            }
            Self::PluginRuntime => {
                "untrusted guests, default-deny capabilities, typed proof values, bounded resources, explicit commit points, redacted diagnostics"
            }
            Self::EditorCore => {
                "pure Rust semantics first, UTF-8 and revision invariants owned by buffer/text types, Bevy only adapts owner decisions"
            }
            Self::Ecs => {
                "typed requests cross into owner systems; stale targets produce typed redacted rejections"
            }
            Self::Vim => {
                "grammar and modal semantics stay pure and deterministic before Bevy dispatch"
            }
            Self::Filesystem => {
                "normalize and validate paths at trust boundaries; keep raw paths out of status and diagnostics unless explicitly intended"
            }
            Self::Config => {
                "config is an authority boundary; defaults must fail closed and validation should produce typed proofs"
            }
            Self::SupplyChain => {
                "dependency and Wasm-loading changes need reviewed trust, pinning, and default-deny behavior"
            }
            Self::Tests => {
                "tests should protect externally visible behavior and boundary invariants, not implementation trivia"
            }
            Self::ProgressNotes => {
                "notes must accurately summarize the diff and checks without creating a second architecture source"
            }
        }
    }
}

/// Returns true for plugin behavior paths, excluding progress notes.
fn is_plugin_runtime_path(path: &str) -> bool {
    if path.ends_with("_todos.md") || path.ends_with("_notes.md") {
        return false;
    }

    path.starts_with("src/plugin/")
        || path.starts_with("src/ecs/plugin")
        || matches!(
            path,
            "src/ecs/events/plugin.rs"
                | "src/ecs/events/edit.rs"
                | "docs/arch.md"
                | "docs/target_arch.md"
                | "docs/authority.md"
                | "docs/supply-chain.md"
        )
        || path.starts_with("docs/plugins/")
        || path.starts_with("tests/goldens/plugin/")
}

#[cfg(test)]
mod tests {
    //! Tests for review context selection.

    use super::ReviewContext;
    use std::fmt::Write as _;

    #[test]
    fn elenchus_progress_note_does_not_trigger_plugin_runtime_context() {
        let context = ReviewContext::from_changed_files(
            "src/elenchus/review.rs\nsrc/plugin/refactor_todos.md\n",
        );
        let guidance = context.source_records_for_prompt();

        assert!(guidance.contains("Elenchus gate"));
        assert!(guidance.contains("Progress notes"));
        assert!(!guidance.contains("Plugin/Wasm boundary"));
    }

    #[test]
    fn helen_elenchus_paths_select_elenchus_context() {
        let context = ReviewContext::from_changed_files(
            "helen/src/elenchus/mod.rs\nhelen/scripts/elenchus.sh\nhelen/tests/elenchus_flow.rs\n",
        );
        let guidance = context.source_records_for_prompt();

        assert!(guidance.contains("Elenchus gate"));
    }

    #[test]
    fn plugin_runtime_path_selects_plugin_source_records() {
        let context = ReviewContext::from_changed_files("src/plugin/runtime/wasmtime.rs\n");
        let guidance = context.source_records_for_prompt();

        assert!(guidance.contains("Plugin/Wasm boundary"));
        assert!(guidance.contains("docs/plugins/runtime.md"));
        assert!(guidance.contains("default-deny capabilities"));
    }

    #[test]
    fn changed_files_are_rendered_without_source_hints_for_unclassified_paths() {
        let context = ReviewContext::from_changed_files("README.md\n");

        assert_eq!(context.changed_files_for_prompt(), "- README.md\n");
        assert_eq!(
            context.source_records_for_prompt(),
            "No source-record hint was selected. Inspect the diff and nearby source first."
        );
    }

    #[test]
    fn changed_files_prompt_is_bounded_but_context_uses_all_paths() {
        let mut changed_files = String::new();
        for index in 0..80 {
            let _result = writeln!(&mut changed_files, "notes/{index}.md");
        }
        changed_files.push_str("src/plugin/runtime/wasmtime.rs\n");

        let context = ReviewContext::from_changed_files(&changed_files);
        let files = context.changed_files_for_prompt();
        let guidance = context.source_records_for_prompt();

        assert!(files.contains("- notes/79.md\n"));
        assert!(!files.contains("src/plugin/runtime/wasmtime.rs"));
        assert!(files.contains(
            "- ... 1 more changed path(s); inspect the saved diff snapshot for the full list.\n"
        ));
        assert!(guidance.contains("selected from all 81 changed path(s)"));
        assert!(guidance.contains("Plugin/Wasm boundary"));
    }

    #[test]
    fn changed_path_prompt_escapes_control_characters() {
        let context = ReviewContext::from_changed_files("src/elenchus/review.rs\tcopy\n");

        assert_eq!(
            context.changed_files_for_prompt(),
            "- src/elenchus/review.rs\\tcopy\n"
        );
    }
}