//! Intent-driven methodology validation harness.
//!
//! This module implements the comprehensive validation suite that enforces
//! Decapod's contracts, invariants, and methodology gates.

use crate::core::assets;
use crate::core::broker::DbBroker;
use crate::core::capsule_policy::{self, POLICY_SCHEMA_VERSION};
use crate::core::context_capsule::DeterministicContextCapsule;
use crate::core::error;
use crate::core::migration;
use crate::core::output;
use crate::core::plan_governance;
use crate::core::project_specs::{
    LOCAL_PROJECT_SPECS, LOCAL_PROJECT_SPECS_ARCHITECTURE, LOCAL_PROJECT_SPECS_DIR,
    LOCAL_PROJECT_SPECS_INTENT, LOCAL_PROJECT_SPECS_INTERFACES, LOCAL_PROJECT_SPECS_MANIFEST,
    LOCAL_PROJECT_SPECS_MANIFEST_SCHEMA, LOCAL_PROJECT_SPECS_OPERATIONS,
    LOCAL_PROJECT_SPECS_SECURITY, LOCAL_PROJECT_SPECS_SEMANTICS, LOCAL_PROJECT_SPECS_VALIDATION,
    hash_text, read_specs_manifest, repo_signal_fingerprint,
};
use crate::core::scaffold::DECAPOD_GITIGNORE_RULES;
use crate::core::store::{Store, StoreKind};
use crate::core::workunit::{self, WorkUnitManifest, WorkUnitStatus};
use crate::plugins::aptitude::{SkillCard, SkillResolution};
use crate::plugins::internalize::{self, DeterminismClass, InternalizationManifest, ReplayClass};
use crate::{db, primitives, todo};
use fancy_regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};

fn is_inside_git_work_tree(repo_root: &Path) -> bool {
    std::process::Command::new("git")
        .args(["rev-parse", "--is-inside-work-tree"])
        .current_dir(repo_root)
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
}

fn container_signal_reasons(repo_root: &Path) -> Vec<&'static str> {
    [
        (
            std::env::var("DECAPOD_CONTAINER").ok().as_deref() == Some("1"),
            "DECAPOD_CONTAINER=1",
        ),
        (repo_root.join(".dockerenv").exists(), ".dockerenv marker"),
        (
            repo_root.join(".devcontainer").exists(),
            ".devcontainer marker",
        ),
        (
            std::env::var("DOCKER_CONTAINER").is_ok(),
            "DOCKER_CONTAINER env",
        ),
    ]
    .into_iter()
    .filter_map(|(signal, name)| signal.then_some(name))
    .collect()
}

fn auto_remediable_validation_message(code: &str, message: &str, agent_action: &str) -> String {
    format!(
        "AUTOREMEDIABLE_VALIDATION_ERROR code={} severity=transient auto_remediable=true audience=agent agent_action=\"{}\" user_note=\"Recoverable validation issue; the agent should take this action or report the concrete blocker.\"\n{}",
        code, agent_action, message
    )
}

/// Spawn a validation gate in a rayon scope with timing and error capture.
///
/// Replaces ~10 lines of boilerplate per gate with a single invocation.
macro_rules! gate {
    ($_scope:expr, $timings:expr, $ctx:expr, $name:literal, $body:expr) => {{
        let start = Instant::now();
        if let Err(e) = $body {
            fail(&format!("gate error: {e}"), $ctx);
        }
        $timings.lock().unwrap().push(($name, start.elapsed()));
    }};
}

struct ValidationContext {
    pass_count: AtomicU32,
    fail_count: AtomicU32,
    warn_count: AtomicU32,
    fails: Mutex<Vec<String>>,
    warns: Mutex<Vec<String>>,
    repo_files_cache: Mutex<Vec<(PathBuf, Vec<PathBuf>)>>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ValidationGateTiming {
    pub name: String,
    pub elapsed_ms: u64,
}

#[derive(Debug, Clone, Serialize)]
pub struct ValidationReport {
    pub status: String,
    pub elapsed_ms: u64,
    pub pass_count: u32,
    pub fail_count: u32,
    pub warn_count: u32,
    pub failures: Vec<String>,
    pub warnings: Vec<String>,
    pub gate_timings: Vec<ValidationGateTiming>,
}

impl ValidationContext {
    fn new() -> Self {
        Self {
            pass_count: AtomicU32::new(0),
            fail_count: AtomicU32::new(0),
            warn_count: AtomicU32::new(0),
            fails: Mutex::new(Vec::new()),
            warns: Mutex::new(Vec::new()),
            repo_files_cache: Mutex::new(Vec::new()),
        }
    }
}

fn collect_repo_files(
    root: &Path,
    out: &mut Vec<PathBuf>,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    // Check cache first — this is called 3 times on the same root during validation.
    let cached = {
        let cache = ctx.repo_files_cache.lock().unwrap();
        cache
            .iter()
            .find(|(k, _)| k == root)
            .map(|(_, v)| v.clone())
    };
    if let Some(files) = cached {
        out.extend(files);
        return Ok(());
    }

    fn recurse(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), error::DecapodError> {
        if !dir.is_dir() {
            return Ok(());
        }

        let name = dir.file_name().and_then(|s| s.to_str()).unwrap_or("");
        // Skip VCS/build/runtime directories that are not authoritative sources
        // for validation rules and can be very large in active agent workspaces.
        if matches!(
            name,
            ".git"
                | "target"
                | ".decapod"
                | "artifacts"
                | "node_modules"
                | ".venv"
                | ".mypy_cache"
                | ".pytest_cache"
        ) {
            return Ok(());
        }

        for entry in fs::read_dir(dir).map_err(error::DecapodError::IoError)? {
            let entry = entry.map_err(error::DecapodError::IoError)?;
            let path = entry.path();
            if path.is_dir() {
                recurse(&path, out)?;
            } else if path.is_file() {
                out.push(path);
            }
        }
        Ok(())
    }

    let start = out.len();
    recurse(root, out)?;
    // Cache the result for subsequent calls with the same root.
    ctx.repo_files_cache
        .lock()
        .unwrap()
        .push((root.to_path_buf(), out[start..].to_vec()));
    Ok(())
}

fn validate_no_legacy_namespaces(
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Namespace Purge Gate");

    let mut files = Vec::new();
    collect_repo_files(decapod_dir, &mut files, ctx)?;

    let needles = [
        [".".to_string(), "globex".to_string()].concat(),
        [".".to_string(), "codex".to_string()].concat(),
    ];
    let mut offenders: Vec<(PathBuf, String)> = Vec::new();

    for path in files {
        // Skip obvious binaries.
        if path.extension().is_some_and(|e| e == "db") {
            continue;
        }
        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
        let is_texty = matches!(
            ext,
            "md" | "rs" | "toml" | "json" | "jsonl" | "yml" | "yaml" | "sh" | "lock"
        );
        if !is_texty {
            continue;
        }
        let content = match fs::read_to_string(&path) {
            Ok(c) => c,
            Err(_) => continue,
        };
        for n in needles.iter() {
            if content.contains(n) {
                offenders.push((path.clone(), n.clone()));
            }
        }
    }

    if offenders.is_empty() {
        pass(
            "No legacy namespace references found in repo text sources",
            ctx,
        );
    } else {
        let mut msg = String::from("Forbidden legacy namespace references found:");
        for (p, n) in offenders.iter().take(12) {
            msg.push_str(&format!(" {}({})", p.display(), n));
        }
        if offenders.len() > 12 {
            msg.push_str(&format!(" ... ({} total)", offenders.len()));
        }
        fail(&msg, ctx);
    }
    Ok(())
}

fn validate_embedded_self_contained(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Embedded Self-Contained Gate");

    // Only validate the embedded constitution source in the decapod repo itself.
    if !repo_root.join("assets/constitution.json").exists() {
        skip("No assets/constitution.json found (project repo)", ctx);
        return Ok(());
    }

    let ids = assets::list_docs();
    let mut offenders: Vec<String> = Vec::new();

    for id in ids {
        let content = match assets::get_embedded_doc(&id) {
            Some(c) => c,
            None => continue,
        };

        // Check for .decapod/ references that aren't documenting override behavior
        if content.contains(".decapod/") {
            // Allow legitimate documentation patterns, counting legitimate references (not just lines).
            let mut legitimate_ref_count = 0usize;
            for line in content.lines() {
                let refs_on_line = line.matches(".decapod/").count();
                if refs_on_line == 0 {
                    continue;
                }
                let is_legitimate_line = line.contains("<repo>")
                    || line.contains("store:")
                    || line.contains("directory")
                    || line.contains("override")
                    || line.contains("Override")
                    || line.contains("OVERRIDE.md")
                    || line.contains("Location:")
                    || line.contains("primarily contain")
                    || line.contains(".decapod/context/")
                    || line.contains(".decapod/memory/")
                    || line.contains("intended as")
                    || line.contains(".decapod/knowledge/")
                    || line.contains(".decapod/data/")
                    || line.contains(".decapod/workspaces/")
                    || line.contains(".decapod/generated/")
                    || line.contains(".decapod/generated/specs/")
                    || line.contains(".decapod/generated/policy/")
                    || line.contains(".decapod/policy/")
                    || line.contains("repo-scoped");
                if is_legitimate_line {
                    legitimate_ref_count += refs_on_line;
                }
            }

            let total_decapod_refs = content.matches(".decapod/").count();
            if total_decapod_refs > legitimate_ref_count {
                offenders.push(id);
            }
        }
    }

    if offenders.is_empty() {
        pass(
            "Embedded constitution files contain no invalid .decapod/ references",
            ctx,
        );
    } else {
        let mut msg =
            String::from("Embedded constitution files contain invalid .decapod/ references:");
        for id in offenders.iter().take(8) {
            msg.push_str(&format!(" {}", id));
        }
        if offenders.len() > 8 {
            msg.push_str(&format!(" ... ({} total)", offenders.len()));
        }
        fail(&msg, ctx);
    }
    Ok(())
}

fn pass(_message: &str, ctx: &ValidationContext) {
    ctx.pass_count.fetch_add(1, Ordering::Relaxed);
}

fn fail(message: &str, ctx: &ValidationContext) {
    ctx.fail_count.fetch_add(1, Ordering::Relaxed);
    ctx.fails.lock().unwrap().push(message.to_string());
}

fn skip(_message: &str, ctx: &ValidationContext) {
    ctx.pass_count.fetch_add(1, Ordering::Relaxed);
}

fn warn(message: &str, ctx: &ValidationContext) {
    ctx.warn_count.fetch_add(1, Ordering::Relaxed);
    ctx.warns.lock().unwrap().push(message.to_string());
}

fn info(_message: &str) {}

fn count_tasks_in_db(db_path: &Path) -> Result<i64, error::DecapodError> {
    let conn = db::db_connect_for_validate(&db_path.to_string_lossy())?;
    let count: i64 = conn
        .query_row("SELECT COUNT(*) FROM tasks", [], |row| row.get(0))
        .map_err(error::DecapodError::RusqliteError)?;
    Ok(count)
}

fn fetch_tasks_fingerprint(db_path: &Path) -> Result<String, error::DecapodError> {
    let conn = db::db_connect_for_validate(&db_path.to_string_lossy())?;
    let mut stmt = conn
        .prepare("SELECT id,title,status,updated_at,dir_path,scope,priority FROM tasks ORDER BY id")
        .map_err(error::DecapodError::RusqliteError)?;
    let rows = stmt
        .query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, String>(0)?,
                "title": row.get::<_, String>(1)?,
                "status": row.get::<_, String>(2)?,
                "updated_at": row.get::<_, String>(3)?,
                "dir_path": row.get::<_, String>(4)?,
                "scope": row.get::<_, String>(5)?,
                "priority": row.get::<_, String>(6)?,
            }))
        })
        .map_err(error::DecapodError::RusqliteError)?;

    let mut out = Vec::new();
    for r in rows {
        out.push(r.map_err(error::DecapodError::RusqliteError)?);
    }
    Ok(serde_json::to_string(&out).unwrap())
}

fn validate_user_store_blank_slate(ctx: &ValidationContext) -> Result<(), error::DecapodError> {
    info("Store: user (blank-slate semantics)");
    let tmp_root = std::env::temp_dir().join(format!(
        "decapod_validate_user_{}",
        crate::core::ulid::new_ulid()
    ));
    fs::create_dir_all(&tmp_root).map_err(error::DecapodError::IoError)?;

    todo::initialize_todo_db(&tmp_root)?;
    let db_path = tmp_root.join("todo.db");
    let n = count_tasks_in_db(&db_path)?;

    if n == 0 {
        pass("User store starts empty (no automatic seeding)", ctx);
    } else {
        fail(
            &format!(
                "User store is not empty on fresh init ({} task(s) found)",
                n
            ),
            ctx,
        );
    }
    Ok(())
}

fn validate_repo_store_dogfood(
    store: &Store,
    ctx: &ValidationContext,
    _decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Store: repo (dogfood backlog semantics)");

    let events = store.root.join("todo.events.jsonl");
    if !events.is_file() {
        fail("Repo store missing todo.events.jsonl", ctx);
        return Ok(());
    }
    let content = fs::read_to_string(&events).map_err(error::DecapodError::IoError)?;
    let add_count = content
        .lines()
        .filter(|l| l.contains("\"event_type\":\"task.add\""))
        .count();

    // Fresh setup has 0 events but is valid.
    pass(
        &format!(
            "Repo backlog event log present ({} task.add events)",
            add_count
        ),
        ctx,
    );

    let db_path = store.root.join("todo.db");
    if !db_path.is_file() {
        fail("Repo store missing todo.db", ctx);
        return Ok(());
    }

    // Broker log integrity check
    let broker = DbBroker::new(&store.root);
    let replay_report = broker.verify_replay()?;
    if replay_report.divergences.is_empty() {
        pass("Audit log integrity verified (no pending event gaps)", ctx);
    } else {
        warn(
            &format!(
                "Audit log contains {} potential crash divergence(s); historical pending entries detected. Run `decapod data broker verify` for details.",
                replay_report.divergences.len(),
            ),
            ctx,
        );
    }

    let tmp_root = std::env::temp_dir().join(format!(
        "decapod_validate_repo_{}",
        crate::core::ulid::new_ulid()
    ));
    fs::create_dir_all(&tmp_root).map_err(error::DecapodError::IoError)?;
    let tmp_db = tmp_root.join("todo.db");
    let _events = todo::rebuild_db_from_events(&events, &tmp_db)?;

    let fp_a = fetch_tasks_fingerprint(&db_path)?;
    let fp_b = fetch_tasks_fingerprint(&tmp_db)?;
    if fp_a == fp_b {
        pass(
            "Repo todo.db matches deterministic rebuild from todo.events.jsonl",
            ctx,
        );
    } else {
        fail(
            "Repo todo.db does NOT match rebuild from todo.events.jsonl",
            ctx,
        );
    }

    Ok(())
}

fn validate_repo_map(
    ctx: &ValidationContext,
    _decapod_dir: &Path, // decapod_dir is no longer used for filesystem constitution checks
) -> Result<(), error::DecapodError> {
    info("Repo Map");

    // We no longer check for a filesystem directory for constitution.
    // Instead, we verify embedded docs.
    pass(
        "Methodology constitution checks will verify embedded docs.",
        ctx,
    );

    let required_specs = ["specs/INTENT", "specs/SYSTEM"];
    let required_methodology = ["methodology/ARCHITECTURE"];
    for r in required_specs {
        if crate::core::assets::get_doc(r).is_some() {
            pass(&format!("Constitution doc {} present (embedded)", r), ctx);
        } else {
            fail(&format!("Constitution doc {} missing (embedded)", r), ctx);
        }
    }
    for r in required_methodology {
        if crate::core::assets::get_doc(r).is_some() {
            pass(&format!("Constitution doc {} present (embedded)", r), ctx);
        } else {
            fail(&format!("Constitution doc {} missing (embedded)", r), ctx);
        }
    }
    Ok(())
}

fn validate_docs_templates_bucket(
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Entrypoint Gate");

    // Entrypoints MUST be in the project root
    let required = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"];
    for a in required {
        let p = decapod_dir.join(a);
        if p.is_file() {
            pass(&format!("Root entrypoint {} present", a), ctx);
        } else {
            fail(
                &format!("Root entrypoint {} missing from project root", a),
                ctx,
            );
        }
    }

    if decapod_dir.join(".decapod").join("README.md").is_file() {
        pass(".decapod/README.md present", ctx);
    } else {
        fail(".decapod/README.md missing", ctx);
    }

    // NEGATIVE GATE: Decapod docs MUST NOT be copied into the project
    let forbidden_docs = decapod_dir.join(".decapod").join("docs");
    if forbidden_docs.exists() {
        fail(
            "Decapod internal docs were copied into .decapod/docs/ (Forbidden)",
            ctx,
        );
    } else {
        pass(
            "Decapod internal docs correctly excluded from project repo",
            ctx,
        );
    }

    // NEGATIVE GATE: projects/<id> MUST NOT exist
    let forbidden_projects = decapod_dir.join(".decapod").join("projects");
    if forbidden_projects.exists() {
        fail("Legacy .decapod/projects/ directory found (Forbidden)", ctx);
    } else {
        pass(".decapod/projects/ correctly absent", ctx);
    }

    Ok(())
}

fn validate_entrypoint_invariants(
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Four Invariants Gate");

    // Check AGENTS.md for the four invariants
    let agents_path = decapod_dir.join("AGENTS.md");
    if !agents_path.is_file() {
        fail("AGENTS.md missing, cannot check invariants", ctx);
        return Ok(());
    }

    let content = fs::read_to_string(&agents_path).map_err(error::DecapodError::IoError)?;
    let normalized = content.to_ascii_lowercase();

    // Exact invariant strings (tamper detection)
    let exact_invariants = [
        ("core/decapod", "Router pointer to core/DECAPOD"),
        ("cargo install decapod", "Version update gate language"),
        ("decapod validate", "Validation gate language"),
        (
            r#"decapod rpc --op constitution.get --params '{"section":"core/decapod"}'"#,
            "Core constitution RPC mandate language",
        ),
        ("stop if", "Stop-if-missing behavior"),
        ("docker git workspaces", "Docker workspace mandate language"),
        (
            "decapod todo claim --id <task-id>",
            "Task claim-before-work mandate language",
        ),
        (
            "request elevated permissions before docker/container workspace commands",
            "Elevated-permissions mandate language",
        ),
        (
            "decapod_session_password",
            "Per-agent session password mandate language",
        ),
        ("via decapod cli", "Jail rule: .decapod access is CLI-only"),
        (
            "interface abstraction boundary",
            "Control-plane opacity language",
        ),
        (
            "strict dependency: you are strictly bound to the decapod control plane",
            "Agent dependency enforcement language",
        ),
        ("✅", "Four invariants checklist format"),
    ];

    let mut all_present = true;
    for (marker, description) in exact_invariants {
        let present = if marker == "✅" {
            content.contains(marker)
        } else {
            normalized.contains(marker)
        };
        if present {
            pass(&format!("Invariant present: {}", description), ctx);
        } else {
            fail(&format!("Invariant missing: {}", description), ctx);
            all_present = false;
        }
    }

    // Check for legacy router names (must not exist)
    let legacy_routers = ["MAESTRO.md", "GLOBEX.md", "CODEX.md\" as router"];
    for legacy in legacy_routers {
        if content.contains(legacy) {
            fail(
                &format!("AGENTS.md contains legacy router reference: {}", legacy),
                ctx,
            );
            all_present = false;
        }
    }

    // Line count check (AGENTS.md should be thin: max 100 lines for universal contract)
    let line_count = content.lines().count();
    const MAX_AGENTS_LINES: usize = 100;
    if line_count <= MAX_AGENTS_LINES {
        pass(
            &format!(
                "AGENTS.md is thin ({} lines ≤ {})",
                line_count, MAX_AGENTS_LINES
            ),
            ctx,
        );
    } else {
        fail(
            &format!(
                "AGENTS.md exceeds line limit ({} lines > {})",
                line_count, MAX_AGENTS_LINES
            ),
            ctx,
        );
        all_present = false;
    }

    // Check that agent-specific files defer to AGENTS.md and are thin
    const MAX_AGENT_SPECIFIC_LINES: usize = 70;
    for agent_file in ["CLAUDE.md", "GEMINI.md", "CODEX.md"] {
        let agent_path = decapod_dir.join(agent_file);
        if !agent_path.is_file() {
            fail(&format!("{} missing from project root", agent_file), ctx);
            all_present = false;
            continue;
        }

        let agent_content =
            fs::read_to_string(&agent_path).map_err(error::DecapodError::IoError)?;

        // Must defer to AGENTS.md
        if agent_content.contains("See `AGENTS.md`") || agent_content.contains("AGENTS.md") {
            pass(&format!("{} defers to AGENTS.md", agent_file), ctx);
        } else {
            fail(&format!("{} does not reference AGENTS.md", agent_file), ctx);
            all_present = false;
        }

        if agent_content.contains("core/DECAPOD") {
            pass(&format!("{} references core/DECAPOD", agent_file), ctx);
        } else {
            fail(
                &format!("{} missing canonical router reference (.json)", agent_file),
                ctx,
            );
            all_present = false;
        }

        // Must use RPC constitution access, never the docs CLI or direct constitution/* file paths.
        if agent_content.contains("decapod docs")
            || agent_content.contains("docs show")
            || agent_content.contains("(constitution/")
        {
            fail(
                &format!(
                    "{} references docs CLI or direct constitution paths; use constitution.get RPC",
                    agent_file
                ),
                ctx,
            );
            all_present = false;
        } else if agent_content.contains("constitution.get") {
            pass(
                &format!("{} references constitution.get RPC", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!("{} missing constitution.get RPC reference", agent_file),
                ctx,
            );
            all_present = false;
        }

        // Must include explicit jail rule for .decapod access
        if agent_content.contains(".decapod files are accessed only via decapod CLI") {
            pass(
                &format!("{} includes .decapod CLI-only jail rule", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!("{} missing .decapod CLI-only jail rule marker", agent_file),
                ctx,
            );
            all_present = false;
        }

        // Must include Docker git workspace mandate
        if agent_content.contains("Docker git workspaces") {
            pass(
                &format!("{} includes Docker workspace mandate", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!("{} missing Docker workspace mandate marker", agent_file),
                ctx,
            );
            all_present = false;
        }

        // Must include elevated-permissions mandate for container workspace commands
        if agent_content
            .contains("request elevated permissions before Docker/container workspace commands")
        {
            pass(
                &format!("{} includes elevated-permissions mandate", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!("{} missing elevated-permissions mandate marker", agent_file),
                ctx,
            );
            all_present = false;
        }

        // Must include per-agent session password mandate
        if agent_content.contains("DECAPOD_SESSION_PASSWORD") {
            pass(
                &format!("{} includes per-agent session password mandate", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!(
                    "{} missing per-agent session password mandate marker",
                    agent_file
                ),
                ctx,
            );
            all_present = false;
        }

        // Must include claim-before-work mandate
        if agent_content.contains("decapod todo claim --id <task-id>") {
            pass(
                &format!("{} includes claim-before-work mandate", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!("{} missing claim-before-work mandate marker", agent_file),
                ctx,
            );
            all_present = false;
        }

        // Must include task creation before claim mandate
        if agent_content.contains("decapod todo add \"<task>\"") {
            pass(
                &format!("{} includes task creation mandate", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!("{} missing task creation mandate marker", agent_file),
                ctx,
            );
            all_present = false;
        }

        // Must include canonical Decapod workspace path mandate
        if agent_content.contains(".decapod/workspaces") {
            pass(
                &format!("{} includes canonical workspace path mandate", agent_file),
                ctx,
            );
        } else {
            fail(
                &format!(
                    "{} missing canonical workspace path marker (`.decapod/workspaces`)",
                    agent_file
                ),
                ctx,
            );
            all_present = false;
        }

        if agent_content.contains(".claude/worktrees") {
            let mut has_forbidden_positive_reference = false;
            for line in agent_content.lines() {
                if !line.contains(".claude/worktrees") {
                    continue;
                }
                let lower = line.to_ascii_lowercase();
                let is_negative_context = lower.contains("never")
                    || lower.contains("forbid")
                    || lower.contains("non-canonical")
                    || lower.contains("must not")
                    || lower.contains("do not");
                if !is_negative_context {
                    has_forbidden_positive_reference = true;
                    break;
                }
            }
            if has_forbidden_positive_reference {
                fail(
                    &format!(
                        "{} references forbidden non-canonical worktree path `.claude/worktrees`",
                        agent_file
                    ),
                    ctx,
                );
                all_present = false;
            } else {
                pass(
                    &format!(
                        "{} explicitly forbids `.claude/worktrees` non-canonical path",
                        agent_file
                    ),
                    ctx,
                );
            }
        }

        // Must include core constitution ingestion mandate
        if agent_content
            .to_ascii_lowercase()
            .contains(r#"decapod rpc --op constitution.get --params '{"section":"core/decapod"}'"#)
        {
            pass(
                &format!(
                    "{} includes core constitution ingestion mandate",
                    agent_file
                ),
                ctx,
            );
        } else {
            fail(
                &format!(
                    "{} missing core constitution ingestion mandate marker",
                    agent_file
                ),
                ctx,
            );
            all_present = false;
        }

        // Must include explicit update command in startup sequence
        if agent_content.contains("cargo install decapod") {
            pass(&format!("{} includes version update step", agent_file), ctx);
        } else {
            fail(
                &format!(
                    "{} missing version update step (`cargo install decapod`)",
                    agent_file
                ),
                ctx,
            );
            all_present = false;
        }

        // Must be thin (max 50 lines for agent-specific shims)
        let agent_lines = agent_content.lines().count();
        if agent_lines <= MAX_AGENT_SPECIFIC_LINES {
            pass(
                &format!(
                    "{} is thin ({} lines ≤ {})",
                    agent_file, agent_lines, MAX_AGENT_SPECIFIC_LINES
                ),
                ctx,
            );
        } else {
            fail(
                &format!(
                    "{} exceeds line limit ({} lines > {})",
                    agent_file, agent_lines, MAX_AGENT_SPECIFIC_LINES
                ),
                ctx,
            );
            all_present = false;
        }

        // Must not contain duplicated contracts (check for common duplication markers)
        let duplication_markers = [
            "## Lifecycle States", // Contract details belong in constitution
            "## Validation Rules", // Contract details belong in constitution
            "### Proof Gates",     // Contract details belong in constitution
            "## Store Model",      // Contract details belong in constitution
        ];
        for marker in duplication_markers {
            if agent_content.contains(marker) {
                fail(
                    &format!(
                        "{} contains duplicated contract details ({})",
                        agent_file, marker
                    ),
                    ctx,
                );
                all_present = false;
            }
        }
    }

    if all_present {
        pass("All entrypoint files follow thin waist architecture", ctx);
    }

    Ok(())
}

fn validate_interface_contract_bootstrap(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Interface Contract Bootstrap Gate");

    // This gate applies to the decapod repository where assets/constitution.json is present.
    // Project repos initialized by `decapod init` should not fail on missing embedded source.
    if !repo_root.join("assets/constitution.json").exists() {
        skip(
            "No assets/constitution.json found (project repo); skipping interface bootstrap checks",
            ctx,
        );
        return Ok(());
    }

    let risk_policy_id = "interfaces/RISK_POLICY_GATE";
    let context_pack_id = "interfaces/AGENT_CONTEXT_PACK";
    for (id, label) in [
        (risk_policy_id, "RISK_POLICY_GATE interface"),
        (context_pack_id, "AGENT_CONTEXT_PACK interface"),
    ] {
        if assets::get_embedded_doc(id).is_some() {
            pass(
                &format!("{} present in embedded assets: {}", label, id),
                ctx,
            );
        } else {
            fail(
                &format!("{} missing from embedded assets: {}", label, id),
                ctx,
            );
        }
    }

    if let Some(content) = assets::get_merged_doc(repo_root, risk_policy_id) {
        for marker in [
            "Authority:",
            "Layer: Interfaces",
            "Binding: Yes",
            "Scope:",
            "Non-goals:",
            "## 3. Current-Head SHA Discipline",
            "## 6. Browser Evidence Manifest (UI/Critical Flows)",
            "## 8. Truth Labels and Upgrade Path",
            "## 10. Contract Example (JSON)",
            "## Core Router",
        ] {
            if content.contains(marker) {
                pass(
                    &format!("RISK_POLICY_GATE includes marker: {}", marker),
                    ctx,
                );
            } else {
                fail(&format!("RISK_POLICY_GATE missing marker: {}", marker), ctx);
            }
        }
    }

    if let Some(content) = assets::get_merged_doc(repo_root, context_pack_id) {
        for marker in [
            "Authority:",
            "Layer: Interfaces",
            "Binding: Yes",
            "Scope:",
            "Non-goals:",
            "## 2. Deterministic Load Order",
            "## 3. Mutation Authority",
            "## 4. Memory Distillation Contract",
            "## 8. Truth Labels and Upgrade Path",
            "## Core Router",
        ] {
            if content.contains(marker) {
                pass(
                    &format!("AGENT_CONTEXT_PACK includes marker: {}", marker),
                    ctx,
                );
            } else {
                fail(
                    &format!("AGENT_CONTEXT_PACK missing marker: {}", marker),
                    ctx,
                );
            }
        }
    }

    Ok(())
}

fn extract_md_version(content: &str) -> Option<String> {
    for line in content.lines() {
        let line = line.trim();
        if let Some(rest) = line.strip_prefix("- v") {
            let v_and_rest = rest.trim();
            if !v_and_rest.is_empty() {
                // Extract version number, assuming it's the first word before the colon
                return v_and_rest.split(':').next().map(|s| s.trim().to_string());
            }
        }
    }
    None
}

fn validate_health_purity(
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Health Purity Gate");
    let mut files = Vec::new();
    collect_repo_files(decapod_dir, &mut files, ctx)?;

    let forbidden =
        Regex::new(r"(?i)\(health:\s*(VERIFIED|ASSERTED|STALE|CONTRADICTED)\)").unwrap();
    let mut offenders = Vec::new();

    let generated_path = decapod_dir.join(".decapod").join("generated");

    for path in files {
        if path.extension().is_some_and(|e| e == "md") {
            // Skip files in the generated artifacts directory
            if path.starts_with(&generated_path) {
                continue;
            }

            let content = fs::read_to_string(&path).unwrap_or_default();
            if forbidden.is_match(&content).unwrap_or(false) {
                offenders.push(path);
            }
        }
    }

    if offenders.is_empty() {
        pass(
            "No manual health status values found in authoritative docs",
            ctx,
        );
    } else {
        fail(
            &format!(
                "Manual health values found in non-generated files: {:?}",
                offenders
            ),
            ctx,
        );
    }
    Ok(())
}

fn validate_project_scoped_state(
    store: &Store,
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Project-Scoped State Gate");
    if store.kind != StoreKind::Repo {
        skip("Not in repo mode; skipping state scoping check", ctx);
        return Ok(());
    }

    // Check if any .db or .jsonl files exist outside .decapod/ in the project root
    let mut offenders = Vec::new();
    for entry in fs::read_dir(decapod_dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.is_file() {
            let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
            if matches!(ext, "db" | "jsonl") {
                offenders.push(path);
            }
        }
    }

    if offenders.is_empty() {
        pass("All state is correctly scoped within .decapod/", ctx);
    } else {
        fail(
            &format!(
                "Found Decapod state files outside .decapod/: {:?}",
                offenders
            ),
            ctx,
        );
    }
    Ok(())
}

fn validate_generated_artifact_whitelist(
    store: &Store,
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Generated Artifact Whitelist Gate");

    if store.kind != StoreKind::Repo {
        skip(
            "Not in repo mode; skipping generated artifact whitelist check",
            ctx,
        );
        return Ok(());
    }

    let gitignore_path = decapod_dir.join(".gitignore");
    let gitignore = fs::read_to_string(&gitignore_path).map_err(error::DecapodError::IoError)?;
    for rule in DECAPOD_GITIGNORE_RULES {
        if gitignore.lines().any(|line| line.trim() == *rule) {
            pass(&format!("Gitignore contains required rule '{}'", rule), ctx);
        } else {
            fail(
                &format!(
                    "Missing .gitignore rule '{}' for generated/data whitelist enforcement",
                    rule
                ),
                ctx,
            );
        }
    }

    let output = std::process::Command::new("git")
        .arg("-C")
        .arg(decapod_dir)
        .args(["ls-files", ".decapod/generated", ".decapod/data"])
        .output();

    let output = match output {
        Ok(o) if o.status.success() => o,
        Ok(_) | Err(_) => {
            warn(
                "Unable to evaluate tracked generated artifacts via git ls-files; skipping tracked whitelist check",
                ctx,
            );
            return Ok(());
        }
    };

    let allowed_tracked = [
        ".decapod/generated/Dockerfile",
        ".decapod/data/knowledge.promotions.jsonl",
        ".decapod/generated/specs/.manifest",
        ".decapod/generated/specs/.manifest.json",
        ".decapod/generated/policy/context_capsule_policy.json",
        ".decapod/generated/artifacts/provenance/kcr_trend.jsonl",
    ];
    let mut offenders = Vec::new();
    for line in String::from_utf8_lossy(&output.stdout).lines() {
        let path = line.trim();
        if path.is_empty() {
            continue;
        }
        let is_allowed_exact = allowed_tracked.iter().any(|allowed| allowed == &path);
        let is_allowed_context_json = path.starts_with(".decapod/generated/context/")
            && path.ends_with(".json")
            && !path.contains("/../");
        let is_allowed_provenance_json = path
            .starts_with(".decapod/generated/artifacts/provenance/")
            && path.ends_with(".json")
            && !path.contains("/../");
        let is_allowed_specs_md = path.starts_with(".decapod/generated/specs/")
            && path.ends_with(".md")
            && !path.contains("/../");
        if !is_allowed_exact
            && !is_allowed_context_json
            && !is_allowed_provenance_json
            && !is_allowed_specs_md
        {
            offenders.push(path.to_string());
        }
    }

    if offenders.is_empty() {
        pass(
            "Tracked generated artifacts are restricted to the whitelist",
            ctx,
        );
    } else {
        fail(
            &format!(
                "Tracked non-whitelisted generated artifacts found: {:?}. Keep generated files ignored unless explicitly allowlisted.",
                offenders
            ),
            ctx,
        );
    }

    Ok(())
}

fn validate_project_config_toml(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Project Config Gate");
    let config_path = repo_root.join(".decapod").join("config.toml");
    if !config_path.exists() {
        warn(
            "Missing .decapod/config.toml; rerun `decapod init` to scaffold repo context configuration.",
            ctx,
        );
        return Ok(());
    }
    let raw = fs::read_to_string(&config_path).map_err(error::DecapodError::IoError)?;
    let value: toml::Value = toml::from_str(&raw).map_err(|e| {
        error::DecapodError::ValidationError(format!("Invalid .decapod/config.toml syntax: {}", e))
    })?;
    let schema_version = value
        .get("schema_version")
        .and_then(|v| v.as_str())
        .unwrap_or("");
    if schema_version == "1.0.0" {
        pass("Project config schema_version is valid (1.0.0)", ctx);
    } else {
        fail(
            "Project config schema_version must be 1.0.0 in .decapod/config.toml",
            ctx,
        );
    }
    if value.get("repo").is_some() && value.get("init").is_some() {
        pass(
            "Project config contains required [repo] and [init] tables",
            ctx,
        );
    } else {
        fail(
            "Project config missing required [repo] or [init] table",
            ctx,
        );
    }
    let repo_table = value.get("repo").and_then(|v| v.as_table());
    let has_intent_anchor = repo_table
        .and_then(|t| t.get("product_summary"))
        .and_then(|v| v.as_str())
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false);
    if has_intent_anchor {
        pass(
            "Project config captures repo.product_summary intent anchor",
            ctx,
        );
    } else {
        fail(
            "Project config missing repo.product_summary (intent anchor).",
            ctx,
        );
    }

    let has_architecture_direction = repo_table
        .and_then(|t| {
            t.get("architecture_direction")
                .or_else(|| t.get("architecture_intent"))
        })
        .and_then(|v| v.as_str())
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false);
    if has_architecture_direction {
        pass("Project config captures repo.architecture_direction", ctx);
    } else {
        fail("Project config missing repo.architecture_direction.", ctx);
    }

    let has_done_criteria = repo_table
        .and_then(|t| t.get("done_criteria"))
        .and_then(|v| v.as_str())
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false);
    if has_done_criteria {
        pass(
            "Project config captures repo.done_criteria proof target",
            ctx,
        );
    } else {
        warn(
            "Project config missing repo.done_criteria; init should capture explicit done evidence.",
            ctx,
        );
    }
    Ok(())
}

fn validate_project_specs_docs(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Project Specs Architecture Gate");

    let specs_dir = repo_root.join(LOCAL_PROJECT_SPECS_DIR);
    if !specs_dir.exists() {
        warn(
            "Project specs directory missing (.decapod/generated/specs/). Run `decapod init --force` to scaffold intent/architecture docs.",
            ctx,
        );
        return Ok(());
    }

    for spec in LOCAL_PROJECT_SPECS {
        let path = repo_root.join(spec.path);
        let file = spec.path;
        if path.exists() {
            pass(&format!("Project specs file present: {}", file), ctx);
        } else if matches!(
            file,
            LOCAL_PROJECT_SPECS_SEMANTICS
                | LOCAL_PROJECT_SPECS_OPERATIONS
                | LOCAL_PROJECT_SPECS_SECURITY
        ) {
            warn(
                &format!(
                    "Recommended project spec missing (scaffold-v2+): {}. Run `decapod init --force` to add the expanded spec surface.",
                    file
                ),
                ctx,
            );
        } else {
            fail(
                &format!("Missing required project specs file: {}", file),
                ctx,
            );
        }
    }

    let manifest_path = repo_root.join(LOCAL_PROJECT_SPECS_MANIFEST);
    let manifest = read_specs_manifest(repo_root)?;
    if manifest.is_none() {
        warn(
            &format!(
                "TASK: Project specs manifest missing at {}. Run `decapod init --force` to generate scaffold metadata, then hydrate `.decapod/generated/specs/*.md`.",
                manifest_path.display()
            ),
            ctx,
        );
    }
    if let Some(manifest) = manifest {
        if manifest.schema_version == LOCAL_PROJECT_SPECS_MANIFEST_SCHEMA {
            pass("Project specs manifest schema is current", ctx);
        } else {
            warn(
                &format!(
                    "TASK: Project specs manifest schema mismatch (found {}, expected {}). Re-run `decapod init --force` then refresh specs.",
                    manifest.schema_version, LOCAL_PROJECT_SPECS_MANIFEST_SCHEMA
                ),
                ctx,
            );
        }

        let mut untouched_templates = Vec::new();
        for entry in &manifest.files {
            let path = repo_root.join(&entry.path);
            if !path.exists() {
                continue;
            }
            let body = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
            let current_hash = hash_text(&body);
            if current_hash == entry.template_hash {
                untouched_templates.push(entry.path.clone());
            }
        }
        if untouched_templates.is_empty() {
            pass(
                "Project specs are not raw scaffold templates (content evolved)",
                ctx,
            );
        } else {
            warn(
                &format!(
                    "TASK: Generated specs still match scaffold template for {:?}. Hydrate these docs with repo-specific details before implementation promotion.",
                    untouched_templates
                ),
                ctx,
            );
        }

        let current_repo_fp = repo_signal_fingerprint(repo_root)?;
        if current_repo_fp == manifest.repo_signal_fingerprint {
            pass(
                "Project specs manifest repo-signal fingerprint is current",
                ctx,
            );
        } else {
            warn(
                "TASK: Significant repo surfaces changed since specs scaffold/hydration. Review and update INTENT/ARCHITECTURE/INTERFACES/VALIDATION accordingly.",
                ctx,
            );
        }
    }

    let architecture_path = repo_root.join(LOCAL_PROJECT_SPECS_ARCHITECTURE);
    if architecture_path.exists() {
        let architecture =
            fs::read_to_string(&architecture_path).map_err(error::DecapodError::IoError)?;
        let required_new = [
            "# Architecture",
            "## Direction",
            "## Current Facts",
            "## Topology",
            "## Execution Path",
            "## Concurrency and Runtime Model",
            "## Deployment Topology",
            "## Data and Contracts",
            "## Delivery Plan",
            "## Risks and Mitigations",
        ];
        let required_legacy = [
            "# Architecture",
            "## Integrated Surface",
            "## Implementation Strategy",
            "## System Topology",
            "## Service Contracts",
            "## Delivery Plan",
            "## Risks and Mitigations",
        ];
        let has_new = required_new.iter().all(|s| architecture.contains(s));
        let has_legacy = required_legacy.iter().all(|s| architecture.contains(s));
        if has_new || has_legacy {
            pass(
                "Architecture spec contains required engineering sections",
                ctx,
            );
        } else {
            fail(
                "Architecture spec missing required section groups (expected new or legacy scaffold structure).",
                ctx,
            );
        }

        if architecture.contains("```mermaid") || architecture.contains("```text") {
            pass(
                "Architecture spec contains required topology diagram block",
                ctx,
            );
        } else {
            fail(
                "Architecture spec missing topology diagram block (`mermaid` or `text` fenced block)",
                ctx,
            );
        }
        if architecture.contains(
            "Describe the architecture in 5-8 dense sentences focused on deployment reality, system boundaries, and operational risks.",
        ) {
            fail(
                "Architecture spec still has placeholder executive summary; derive architecture from explicit intent.",
                ctx,
            );
        } else {
            pass("Architecture spec has non-placeholder executive summary", ctx);
        }

        let dense_line_count = architecture
            .lines()
            .filter(|line| !line.trim().is_empty())
            .count();
        if dense_line_count >= 35 {
            pass("Architecture spec meets minimum density threshold", ctx);
        } else {
            fail(
                "Architecture spec is too sparse (<35 non-empty lines); expand it to an engineer-ready overview",
                ctx,
            );
        }
    }

    let intent_path = repo_root.join(LOCAL_PROJECT_SPECS_INTENT);
    if intent_path.exists() {
        let intent = fs::read_to_string(intent_path).map_err(error::DecapodError::IoError)?;
        let required_intent_sections = [
            "# Intent",
            "## Product Outcome",
            "## Scope",
            "## Constraints",
            "## Acceptance Criteria",
        ];
        let mut missing = Vec::new();
        for section in required_intent_sections {
            if !intent.contains(section) {
                missing.push(section);
            }
        }
        if missing.is_empty() {
            pass("Intent spec contains required planning sections", ctx);
        } else {
            fail(
                &format!("Intent spec missing required sections: {:?}", missing),
                ctx,
            );
        }
        if intent.contains("Define the user-visible outcome in one paragraph.") {
            fail(
                "Intent spec still has placeholder product outcome; capture explicit intent before implementation.",
                ctx,
            );
        } else if intent.contains("against explicit user intent with proof-backed completion.") {
            warn(
                "TASK: Intent outcome still reads as generic scaffold text; replace it with explicit user/problem outcome.",
                ctx,
            );
        } else {
            pass("Intent spec has non-placeholder product outcome", ctx);
        }
    }

    let interfaces_path = repo_root.join(LOCAL_PROJECT_SPECS_INTERFACES);
    if interfaces_path.exists() {
        let interfaces =
            fs::read_to_string(&interfaces_path).map_err(error::DecapodError::IoError)?;
        for section in [
            "# Interfaces",
            "## Inbound Contracts",
            "## Outbound Dependencies",
            "## Data Ownership",
            "## Failure Semantics",
        ] {
            if !interfaces.contains(section) {
                fail(
                    &format!("Interfaces spec missing required section: {}", section),
                    ctx,
                );
            }
        }
        pass("Interfaces spec contains required contract sections", ctx);
    }

    let validation_path = repo_root.join(LOCAL_PROJECT_SPECS_VALIDATION);
    if validation_path.exists() {
        let validation =
            fs::read_to_string(&validation_path).map_err(error::DecapodError::IoError)?;
        for section in [
            "# Validation",
            "## Proof Surfaces",
            "## Promotion Gates",
            "## Evidence Artifacts",
            "## Regression Guardrails",
        ] {
            if !validation.contains(section) {
                fail(
                    &format!("Validation spec missing required section: {}", section),
                    ctx,
                );
            }
        }
        pass("Validation spec contains required proof/gate sections", ctx);
        if validation.contains("Add repository-specific test command(s) here.") {
            warn(
                "TASK: Validation spec still has placeholder test command guidance; add concrete test/integration commands.",
                ctx,
            );
        }
    }

    let semantics_path = repo_root.join(LOCAL_PROJECT_SPECS_SEMANTICS);
    if semantics_path.exists() {
        let semantics =
            fs::read_to_string(&semantics_path).map_err(error::DecapodError::IoError)?;
        for section in ["# Semantics", "## State Machines", "## Invariants"] {
            if !semantics.contains(section) {
                fail(
                    &format!("Semantics spec missing required section: {}", section),
                    ctx,
                );
            }
        }
        pass("Semantics spec contains required sections", ctx);
    }

    let operations_path = repo_root.join(LOCAL_PROJECT_SPECS_OPERATIONS);
    if operations_path.exists() {
        let operations =
            fs::read_to_string(&operations_path).map_err(error::DecapodError::IoError)?;
        for section in [
            "# Operations",
            "## Service Level Objectives",
            "## Monitoring",
            "## Incident Response",
        ] {
            if !operations.contains(section) {
                fail(
                    &format!("Operations spec missing required section: {}", section),
                    ctx,
                );
            }
        }
        pass("Operations spec contains required sections", ctx);
    }

    let security_path = repo_root.join(LOCAL_PROJECT_SPECS_SECURITY);
    if security_path.exists() {
        let security = fs::read_to_string(&security_path).map_err(error::DecapodError::IoError)?;
        for section in [
            "# Security",
            "## Threat Model",
            "## Authentication",
            "## Authorization",
            "## Data Classification",
        ] {
            if !security.contains(section) {
                fail(
                    &format!("Security spec missing required section: {}", section),
                    ctx,
                );
            }
        }
        pass("Security spec contains required sections", ctx);
    }

    Ok(())
}

fn validate_machine_contract(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Machine Contract Drift Detection Gate");

    let binary_path =
        std::env::current_exe().map_err(|e| error::DecapodError::ValidationError(e.to_string()))?;
    let capabilities_output = std::process::Command::new(&binary_path)
        .current_dir(repo_root)
        .args(["capabilities", "--format", "json"])
        .output()
        .map_err(|e| {
            error::DecapodError::ValidationError(format!("Failed to run capabilities: {}", e))
        })?;

    if !capabilities_output.status.success() {
        pass(
            "Could not verify machine contract (capabilities failed)",
            ctx,
        );
        return Ok(());
    }

    let capabilities_json: serde_json::Value =
        serde_json::from_str(&String::from_utf8_lossy(&capabilities_output.stdout)).map_err(
            |e| error::DecapodError::ValidationError(format!("Invalid capabilities JSON: {}", e)),
        )?;

    let interlock_codes = capabilities_json["interlock_codes"]
        .as_array()
        .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
        .unwrap_or_default();

    let required_interlock = [
        "workspace_required",
        "verification_required",
        "store_boundary_violation",
    ];

    let mut missing_interlock = Vec::new();
    for code in required_interlock {
        if !interlock_codes.contains(&code) {
            missing_interlock.push(code);
        }
    }

    if missing_interlock.is_empty() {
        pass("Machine contract interlock codes match binary", ctx);
    } else {
        fail(
            &format!(
                "Binary capabilities missing interlock codes: {:?}. Binary and specs are out of sync.",
                missing_interlock
            ),
            ctx,
        );
    }

    let capabilities_list = capabilities_json["capabilities"]
        .as_array()
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v["name"].as_str())
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();

    let required_caps = [
        "daemonless",
        "deterministic",
        "context.resolve",
        "validate.run",
        "workspace.ensure",
        "preflight.check",
        "impact.predict",
    ];
    let mut missing_caps = Vec::new();
    for cap in required_caps {
        if !capabilities_list.contains(&cap) {
            missing_caps.push(cap);
        }
    }

    if missing_caps.is_empty() {
        pass("Machine contract capabilities match binary", ctx);
    } else {
        warn(
            &format!(
                "Binary capabilities missing expected capabilities: {:?}",
                missing_caps
            ),
            ctx,
        );
    }

    Ok(())
}

fn validate_spec_drift(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Spec Drift Detection Gate (Hygiene)");

    let interfaces_path = repo_root.join(LOCAL_PROJECT_SPECS_INTERFACES);
    if !interfaces_path.exists() {
        pass("No INTERFACES.md to check for hygiene", ctx);
        return Ok(());
    }

    let interfaces = fs::read_to_string(&interfaces_path).map_err(error::DecapodError::IoError)?;

    warn(
        "Spec markdown drift checks are hygiene-only. Use validate_machine_contract for authoritative governance.",
        ctx,
    );

    let key_sections = ["# Interfaces", "## Inbound Contracts", "## Data Ownership"];

    let mut missing_sections = Vec::new();
    for section in key_sections {
        if !interfaces.contains(section) {
            missing_sections.push(section);
        }
    }

    if missing_sections.is_empty() {
        pass("INTERFACES.md has structural sections", ctx);
    } else {
        warn(
            &format!("INTERFACES.md missing sections: {:?}", missing_sections),
            ctx,
        );
    }

    for (path, name, sections) in [
        (
            LOCAL_PROJECT_SPECS_SEMANTICS,
            "SEMANTICS.md",
            vec!["# Semantics", "## State Machines", "## Invariants"],
        ),
        (
            LOCAL_PROJECT_SPECS_OPERATIONS,
            "OPERATIONS.md",
            vec![
                "# Operations",
                "## Service Level Objectives",
                "## Monitoring",
                "## Incident Response",
            ],
        ),
        (
            LOCAL_PROJECT_SPECS_SECURITY,
            "SECURITY.md",
            vec![
                "# Security",
                "## Threat Model",
                "## Authentication",
                "## Authorization",
                "## Data Classification",
            ],
        ),
    ] {
        let path = repo_root.join(path);
        if !path.exists() {
            warn(
                &format!(
                    "{} missing (hygiene check only). Run `decapod init --force` to scaffold it.",
                    name
                ),
                ctx,
            );
            continue;
        }
        let body = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
        let missing = sections
            .iter()
            .filter(|section| !body.contains(**section))
            .copied()
            .collect::<Vec<_>>();
        if missing.is_empty() {
            pass(&format!("{} has structural sections", name), ctx);
        } else {
            warn(&format!("{} missing sections: {:?}", name, missing), ctx);
        }
    }

    Ok(())
}

fn validate_workunit_manifests_if_present(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Work Unit Manifest Gate");

    let workunits_dir = repo_root
        .join(".decapod")
        .join("governance")
        .join("workunits");
    if !workunits_dir.exists() {
        skip("No workunit manifests found; skipping workunit gate", ctx);
        return Ok(());
    }

    let mut files = 0usize;
    for entry in fs::read_dir(&workunits_dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        files += 1;
        let raw = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
        let parsed: WorkUnitManifest = serde_json::from_str(&raw).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid workunit manifest {}: {}",
                path.display(),
                e
            ))
        })?;
        let _ = parsed.canonical_json_bytes().map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "workunit canonicalization failed for {}: {}",
                path.display(),
                e
            ))
        })?;
        if parsed.status == WorkUnitStatus::Verified {
            workunit::validate_verified_manifest(&parsed).map_err(|e| {
                error::DecapodError::ValidationError(format!(
                    "invalid VERIFIED workunit manifest: {} ({})",
                    e,
                    path.display()
                ))
            })?;
            workunit::verify_capsule_policy_lineage_for_task(repo_root, &parsed).map_err(|e| {
                error::DecapodError::ValidationError(format!(
                    "invalid VERIFIED workunit manifest: {} ({})",
                    e,
                    path.display()
                ))
            })?;
        }
    }

    pass(
        &format!(
            "Workunit manifest schema check passed for {} file(s)",
            files
        ),
        ctx,
    );
    Ok(())
}

fn validate_context_capsules_if_present(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Context Capsule Gate");

    let capsules_dir = repo_root.join(".decapod").join("generated").join("context");
    if !capsules_dir.exists() {
        skip(
            "No context capsules found; skipping context capsule gate",
            ctx,
        );
        return Ok(());
    }

    let mut files = 0usize;
    for entry in fs::read_dir(&capsules_dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        files += 1;
        let raw = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
        let parsed: DeterministicContextCapsule = serde_json::from_str(&raw).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid context capsule {}: {}",
                path.display(),
                e
            ))
        })?;
        let expected = parsed.computed_hash_hex().map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "context capsule hash computation failed for {}: {}",
                path.display(),
                e
            ))
        })?;
        if parsed.capsule_hash != expected {
            fail(
                &format!(
                    "Context capsule hash mismatch in {} (expected {}, got {})",
                    path.display(),
                    expected,
                    parsed.capsule_hash
                ),
                ctx,
            );
        }
    }

    pass(
        &format!("Context capsule integrity checked for {} file(s)", files),
        ctx,
    );
    Ok(())
}

fn validate_context_capsule_policy_contract(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Context Capsule Policy Gate");
    let (policy, path) = match capsule_policy::load_policy_contract(repo_root) {
        Ok(v) => v,
        Err(error::DecapodError::ValidationError(msg))
            if msg.starts_with("CAPSULE_POLICY_MISSING:") =>
        {
            warn(
                "Context capsule policy contract missing; run `decapod init --force` to scaffold .decapod/generated/policy/context_capsule_policy.json",
                ctx,
            );
            return Ok(());
        }
        Err(e) => return Err(e),
    };
    if policy.schema_version != POLICY_SCHEMA_VERSION {
        fail(
            &format!(
                "Context capsule policy schema mismatch at {} (actual={}, expected={})",
                path.display(),
                policy.schema_version,
                POLICY_SCHEMA_VERSION
            ),
            ctx,
        );
    }
    if !policy.tiers.contains_key(&policy.default_risk_tier) {
        fail(
            &format!(
                "Context capsule policy default_risk_tier '{}' is not declared in tiers",
                policy.default_risk_tier
            ),
            ctx,
        );
    }
    for (tier, rule) in &policy.tiers {
        if rule.allowed_scopes.is_empty() {
            fail(
                &format!(
                    "Context capsule policy tier '{}' has no allowed_scopes (fail closed)",
                    tier
                ),
                ctx,
            );
        }
        if rule.max_limit == 0 {
            fail(
                &format!(
                    "Context capsule policy tier '{}' has max_limit=0 (invalid)",
                    tier
                ),
                ctx,
            );
        }
    }
    pass(
        &format!(
            "Context capsule policy contract parsed and validated ({})",
            path.display()
        ),
        ctx,
    );
    Ok(())
}

fn validate_knowledge_promotions_if_present(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Knowledge Promotion Ledger Gate");

    let ledger = repo_root
        .join(".decapod")
        .join("data")
        .join("knowledge.promotions.jsonl");
    if !ledger.exists() {
        skip(
            "No knowledge promotion ledger found; skipping promotion ledger gate",
            ctx,
        );
        return Ok(());
    }

    let raw = fs::read_to_string(&ledger).map_err(error::DecapodError::IoError)?;
    for (idx, line) in raw.lines().enumerate() {
        if line.trim().is_empty() {
            continue;
        }
        let v: serde_json::Value = serde_json::from_str(line).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid promotion ledger line {} in {}: {}",
                idx + 1,
                ledger.display(),
                e
            ))
        })?;
        for key in [
            "event_id",
            "ts",
            "source_entry_id",
            "target_class",
            "evidence_refs",
            "approved_by",
            "actor",
            "reason",
        ] {
            if v.get(key).is_none() {
                fail(
                    &format!(
                        "Knowledge promotion ledger missing '{}' on line {} ({})",
                        key,
                        idx + 1,
                        ledger.display()
                    ),
                    ctx,
                );
            }
        }

        if v.get("target_class").and_then(|x| x.as_str()) != Some("procedural") {
            fail(
                &format!(
                    "Knowledge promotion ledger requires target_class='procedural' on line {} ({})",
                    idx + 1,
                    ledger.display()
                ),
                ctx,
            );
        }

        let evidence_ok = v
            .get("evidence_refs")
            .and_then(|x| x.as_array())
            .map(|arr| {
                !arr.is_empty()
                    && arr
                        .iter()
                        .all(|item| item.as_str().map(|s| !s.trim().is_empty()).unwrap_or(false))
            })
            .unwrap_or(false);
        if !evidence_ok {
            fail(
                &format!(
                    "Knowledge promotion ledger evidence_refs must be a non-empty string array on line {} ({})",
                    idx + 1,
                    ledger.display()
                ),
                ctx,
            );
        }

        for key in ["approved_by", "actor", "reason"] {
            let non_empty = v
                .get(key)
                .and_then(|x| x.as_str())
                .map(|s| !s.trim().is_empty())
                .unwrap_or(false);
            if !non_empty {
                fail(
                    &format!(
                        "Knowledge promotion ledger '{}' must be a non-empty string on line {} ({})",
                        key,
                        idx + 1,
                        ledger.display()
                    ),
                    ctx,
                );
            }
        }
    }

    pass("Knowledge promotion ledger schema check passed", ctx);
    Ok(())
}

fn validate_skill_cards_if_present(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Skill Card Artifact Gate");

    let dir = repo_root.join(".decapod").join("skills");
    if !dir.exists() {
        skip("No skill cards found; skipping skill card gate", ctx);
        return Ok(());
    }

    let mut files = 0usize;
    for entry in fs::read_dir(&dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        files += 1;
        let raw = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
        let parsed: SkillCard = serde_json::from_str(&raw).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid skill card {}: {}",
                path.display(),
                e
            ))
        })?;
        if parsed.kind != "skill_card" || parsed.schema_version != "1.0.0" {
            fail(
                &format!(
                    "skill card {} has invalid kind/schema_version",
                    path.display()
                ),
                ctx,
            );
            continue;
        }
        let mut normalized = parsed.clone();
        let expected = parsed.card_hash.clone();
        normalized.card_hash.clear();
        normalized.generated_at.clear();
        let canonical = serde_json::to_vec(&normalized).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "skill card canonicalization failed for {}: {}",
                path.display(),
                e
            ))
        })?;
        let actual = {
            use sha2::{Digest, Sha256};
            let mut hasher = Sha256::new();
            hasher.update(&canonical);
            format!("{:x}", hasher.finalize())
        };
        if actual != expected {
            fail(
                &format!(
                    "skill card hash mismatch in {} (expected {}, got {})",
                    path.display(),
                    expected,
                    actual
                ),
                ctx,
            );
        }
    }

    pass(
        &format!("Skill card integrity checked for {} file(s)", files),
        ctx,
    );
    Ok(())
}

fn validate_skill_resolutions_if_present(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Skill Resolution Artifact Gate");

    let dir = repo_root.join(".decapod").join("generated").join("skills");
    if !dir.exists() {
        skip(
            "No skill resolution artifacts found; skipping skill resolution gate",
            ctx,
        );
        return Ok(());
    }

    let mut files = 0usize;
    for entry in fs::read_dir(&dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        files += 1;
        let raw = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
        let parsed: SkillResolution = serde_json::from_str(&raw).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid skill resolution {}: {}",
                path.display(),
                e
            ))
        })?;
        if parsed.kind != "skill_resolution" || parsed.schema_version != "1.0.0" {
            fail(
                &format!(
                    "skill resolution {} has invalid kind/schema_version",
                    path.display()
                ),
                ctx,
            );
            continue;
        }
        let mut normalized = parsed.clone();
        let expected = parsed.resolution_hash.clone();
        normalized.resolution_hash.clear();
        normalized.generated_at.clear();
        let canonical = serde_json::to_vec(&normalized).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "skill resolution canonicalization failed for {}: {}",
                path.display(),
                e
            ))
        })?;
        let actual = {
            use sha2::{Digest, Sha256};
            let mut hasher = Sha256::new();
            hasher.update(&canonical);
            format!("{:x}", hasher.finalize())
        };
        if actual != expected {
            fail(
                &format!(
                    "skill resolution hash mismatch in {} (expected {}, got {})",
                    path.display(),
                    expected,
                    actual
                ),
                ctx,
            );
        }
    }

    pass(
        &format!("Skill resolution integrity checked for {} file(s)", files),
        ctx,
    );
    Ok(())
}

fn validate_internalization_artifacts_if_present(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Internalization Artifact Gate");

    let artifacts_dir = repo_root
        .join(".decapod")
        .join("generated")
        .join("artifacts")
        .join("internalizations");
    if !artifacts_dir.exists() {
        skip(
            "No internalization artifacts found; skipping internalization gate",
            ctx,
        );
        return Ok(());
    }

    let mut files = 0usize;
    for entry in fs::read_dir(&artifacts_dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let manifest_path = path.join("manifest.json");
        if !manifest_path.exists() {
            fail(
                &format!(
                    "Internalization artifact is missing manifest.json ({})",
                    path.display()
                ),
                ctx,
            );
            continue;
        }

        files += 1;
        let raw = fs::read_to_string(&manifest_path).map_err(error::DecapodError::IoError)?;
        let manifest: InternalizationManifest = serde_json::from_str(&raw).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid internalization manifest {}: {}",
                manifest_path.display(),
                e
            ))
        })?;

        if manifest.schema_version != internalize::SCHEMA_VERSION {
            fail(
                &format!(
                    "Internalization manifest schema mismatch in {} (actual={}, expected={})",
                    manifest_path.display(),
                    manifest.schema_version,
                    internalize::SCHEMA_VERSION
                ),
                ctx,
            );
        }
        if manifest.base_model_id.trim().is_empty() {
            fail(
                &format!(
                    "Internalization manifest missing base_model_id ({})",
                    manifest_path.display()
                ),
                ctx,
            );
        }
        if manifest.capabilities_contract.permitted_tools.is_empty() {
            fail(
                &format!(
                    "Internalization manifest must declare permitted_tools ({})",
                    manifest_path.display()
                ),
                ctx,
            );
        }
        if manifest.replay_recipe.mode == ReplayClass::Replayable
            && manifest.determinism_class != DeterminismClass::Deterministic
        {
            fail(
                &format!(
                    "Internalization manifest claims replayable despite non-deterministic profile ({})",
                    manifest_path.display()
                ),
                ctx,
            );
        }
        if manifest.determinism_class == DeterminismClass::BestEffort
            && (manifest.binary_hash.trim().is_empty()
                || manifest.runtime_fingerprint.trim().is_empty())
        {
            fail(
                &format!(
                    "Best-effort internalization manifest must include binary_hash and runtime_fingerprint ({})",
                    manifest_path.display()
                ),
                ctx,
            );
        }

        let inspect =
            internalize::inspect_internalization(&repo_root.join(".decapod"), &manifest.id)
                .map_err(|e| {
                    error::DecapodError::ValidationError(format!(
                        "internalization inspect failed for {}: {}",
                        manifest_path.display(),
                        e
                    ))
                })?;
        if !inspect.integrity.adapter_hash_valid {
            fail(
                &format!(
                    "Internalization adapter hash mismatch ({})",
                    manifest_path.display()
                ),
                ctx,
            );
        }
        if inspect.integrity.source_verification == "mismatch" {
            fail(
                &format!(
                    "Internalization source hash mismatch ({})",
                    manifest_path.display()
                ),
                ctx,
            );
        }
        if !inspect.integrity.replayable_claim_valid {
            fail(
                &format!(
                    "Internalization replay metadata is inconsistent ({})",
                    manifest_path.display()
                ),
                ctx,
            );
        }
    }

    let sessions_dir = repo_root
        .join(".decapod")
        .join("generated")
        .join("sessions");
    if sessions_dir.exists() {
        for session_entry in fs::read_dir(&sessions_dir).map_err(error::DecapodError::IoError)? {
            let session_entry = session_entry.map_err(error::DecapodError::IoError)?;
            let mounts_dir = session_entry.path().join("internalize_mounts");
            if !mounts_dir.exists() {
                continue;
            }
            for mount_entry in fs::read_dir(&mounts_dir).map_err(error::DecapodError::IoError)? {
                let mount_entry = mount_entry.map_err(error::DecapodError::IoError)?;
                let mount_path = mount_entry.path();
                if mount_path.extension().and_then(|s| s.to_str()) != Some("json") {
                    continue;
                }
                let raw = fs::read_to_string(&mount_path).map_err(error::DecapodError::IoError)?;
                let mount: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
                    error::DecapodError::ValidationError(format!(
                        "invalid internalization mount lease {}: {}",
                        mount_path.display(),
                        e
                    ))
                })?;
                let lease_expires_at = mount
                    .get("lease_expires_at")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                if lease_expires_at.is_empty() {
                    fail(
                        &format!(
                            "Internalization mount missing lease_expires_at ({})",
                            mount_path.display()
                        ),
                        ctx,
                    );
                    continue;
                }
                if lease_expires_at < internalize::now_iso8601().as_str() {
                    fail(
                        &format!(
                            "Internalization mount lease expired but still present ({})",
                            mount_path.display()
                        ),
                        ctx,
                    );
                }
            }
        }
    }

    pass(
        &format!(
            "Internalization artifact contract checked for {} artifact(s)",
            files
        ),
        ctx,
    );
    Ok(())
}

fn validate_schema_determinism(
    ctx: &ValidationContext,
    _decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Schema Determinism Gate");
    let run_schema = || -> Result<String, error::DecapodError> {
        let snapshot = crate::deterministic_schema_envelope();
        serde_json::to_string(&snapshot).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "schema determinism serialization failed: {}",
                e
            ))
        })
    };

    // Run sequentially: parallel execution causes non-determinism due to shared state
    let s1 = run_schema()?;
    let s2 = run_schema()?;

    if s1 == s2 && !s1.is_empty() {
        pass("Schema output is deterministic", ctx);
    } else {
        fail("Schema output is non-deterministic or empty", ctx);
    }
    Ok(())
}

fn validate_database_schema_versions(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Database Schema Version Gate");
    if !matches!(store.kind, StoreKind::Repo) {
        skip(
            "Database schema version gate applies to repo store only",
            ctx,
        );
        return Ok(());
    }
    let checks = migration::check_versioned_db_schema_expectations(&store.root)?;
    for check in checks {
        if !check.exists {
            fail(
                &format!(
                    "Versioned database {} is missing (expected schema_version={})",
                    check.db_name, check.expected_version
                ),
                ctx,
            );
            continue;
        }
        match check.actual_version {
            Some(actual) if actual == check.expected_version => {
                pass(
                    &format!(
                        "{} schema_version matches expected {}",
                        check.db_name, check.expected_version
                    ),
                    ctx,
                );
            }
            Some(actual) => {
                fail(
                    &format!(
                        "{} schema_version mismatch: actual={}, expected={}",
                        check.db_name, actual, check.expected_version
                    ),
                    ctx,
                );
            }
            None => {
                fail(
                    &format!(
                        "{} missing readable schema_version in meta table (expected {})",
                        check.db_name, check.expected_version
                    ),
                    ctx,
                );
            }
        }
    }
    Ok(())
}

fn validate_eval_gate_if_required(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Eval Gate Requirement");
    let failures = crate::plugins::eval::validate_eval_gate_if_required(&store.root)?;
    if failures.is_empty() {
        pass("Eval gate requirement satisfied or not configured", ctx);
    } else {
        for failure in failures {
            fail(&failure, ctx);
        }
    }
    Ok(())
}

fn validate_health_cache_integrity(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Health Cache Non-Authoritative Gate");
    let db_path = store.root.join("health.db");
    if !db_path.exists() {
        skip("health.db not found; skipping health integrity check", ctx);
        return Ok(());
    }

    let conn = db::db_connect_for_validate(&db_path.to_string_lossy())?;

    // Check if any health_cache entries exist without corresponding proof_events
    let orphaned: i64 = conn.query_row(
        "SELECT COUNT(*) FROM health_cache hc LEFT JOIN proof_events pe ON hc.claim_id = pe.claim_id WHERE pe.event_id IS NULL",
        [],
        |row| row.get(0),
    ).map_err(error::DecapodError::RusqliteError)?;

    if orphaned == 0 {
        pass("No orphaned health cache entries (integrity pass)", ctx);
    } else {
        warn(
            &format!(
                "Found {} health cache entries without proof events (might be manual writes)",
                orphaned
            ),
            ctx,
        );
    }
    Ok(())
}

fn validate_risk_map(store: &Store, ctx: &ValidationContext) -> Result<(), error::DecapodError> {
    info("Risk Map Gate");
    let map_path = store.root.join("RISKMAP.json");
    if map_path.exists() {
        pass("Risk map (blast-radius) is present", ctx);
    } else {
        warn("Risk map missing (run `decapod riskmap init`)", ctx);
    }
    Ok(())
}

fn validate_risk_map_violations(
    store: &Store,
    ctx: &ValidationContext,
    pre_read_broker: Option<&str>,
) -> Result<(), error::DecapodError> {
    info("Zone Violation Gate");
    let fallback;
    let content = match pre_read_broker {
        Some(c) => c,
        None => {
            let audit_log = store.root.join("broker.events.jsonl");
            if !audit_log.exists() {
                return Ok(());
            }
            fallback = fs::read_to_string(audit_log)?;
            &fallback
        }
    };
    {
        let mut offenders = Vec::new();
        for line in content.lines() {
            if line.contains("\".decapod/\"") && line.contains("\"op\":\"todo.add\"") {
                offenders.push(line.to_string());
            }
        }
        if offenders.is_empty() {
            pass("No risk zone violations detected in audit log", ctx);
        } else {
            fail(
                &format!("Detected operations in protected zones: {:?}", offenders),
                ctx,
            );
        }
    }
    Ok(())
}

fn validate_policy_integrity(
    store: &Store,
    ctx: &ValidationContext,
    pre_read_broker: Option<&str>,
) -> Result<(), error::DecapodError> {
    info("Policy Integrity Gates");
    let db_path = store.root.join("policy.db");
    if !db_path.exists() {
        skip("policy.db not found; skipping policy check", ctx);
        return Ok(());
    }

    let _conn = db::db_connect_for_validate(&db_path.to_string_lossy())?;

    let fallback;
    let content_opt = match pre_read_broker {
        Some(c) => Some(c),
        None => {
            let audit_log = store.root.join("broker.events.jsonl");
            if audit_log.exists() {
                fallback = fs::read_to_string(audit_log)?;
                Some(fallback.as_str())
            } else {
                None
            }
        }
    };
    if let Some(content) = content_opt {
        let mut offenders = Vec::new();
        for line in content.lines() {
            if line.contains("\"op\":\"policy.approve\"")
                && line.contains("\"db_id\":\"health.db\"")
            {
                offenders.push(line.to_string());
            }
        }
        if offenders.is_empty() {
            pass(
                "Approval isolation verified (no direct health mutations)",
                ctx,
            );
        } else {
            fail(
                &format!(
                    "Policy approval directly mutated health state: {:?}",
                    offenders
                ),
                ctx,
            );
        }
    }

    Ok(())
}

#[derive(Debug, Deserialize)]
struct RecursiveImprovementPass {
    schema_version: String,
    id: String,
    observed_deficiency: String,
    parent_task_ref: Option<String>,
    parent_spec_ref: Option<String>,
    constitutional_authority: String,
    allowed_changes: Vec<String>,
    forbidden_changes: Vec<String>,
    touched_paths: Vec<String>,
    proof_required: Vec<String>,
    stop_condition: String,
    risk_level: String,
    requires_user_approval: bool,
    user_approval_ref: Option<String>,
    mutates_parent_intent: bool,
    expands_scope: bool,
    weakens_governance: bool,
}

fn non_empty(s: &str) -> bool {
    !s.trim().is_empty()
}

fn vague_proof(proof: &str) -> bool {
    let normalized = proof.trim().to_ascii_lowercase();
    normalized.is_empty()
        || matches!(
            normalized.as_str(),
            "proof"
                | "verify"
                | "validation"
                | "tests"
                | "check"
                | "review"
                | "manual review"
                | "looks good"
                | "looks clean"
                | "green"
        )
        || normalized.contains("todo")
        || normalized.contains("tbd")
        || normalized.contains("some test")
}

fn vague_stop_condition(stop_condition: &str) -> bool {
    let normalized = stop_condition.trim().to_ascii_lowercase();
    normalized.is_empty()
        || matches!(
            normalized.as_str(),
            "none"
                | "n/a"
                | "until done"
                | "until good"
                | "until clean"
                | "until it looks good"
                | "when good"
                | "when clean"
                | "open ended"
                | "infinite"
        )
        || normalized.contains("forever")
}

fn forbidden_path_touched(forbidden: &[String], touched: &[String]) -> Option<(String, String)> {
    for path in touched {
        let p = path.trim().trim_start_matches("./");
        for rule in forbidden {
            let r = rule.trim().trim_start_matches("./");
            if r.is_empty() {
                continue;
            }
            let prefix = r.trim_end_matches('*').trim_end_matches('/');
            if p == prefix || p.starts_with(&format!("{prefix}/")) {
                return Some((path.clone(), rule.clone()));
            }
        }
    }
    None
}

fn validate_recursive_pass(pass: &RecursiveImprovementPass) -> Result<(), String> {
    if pass.schema_version != "recursive-improvement-pass.v1" {
        return Err("schema_version must be recursive-improvement-pass.v1".to_string());
    }
    if !non_empty(&pass.id) {
        return Err("id is required".to_string());
    }
    if !non_empty(&pass.observed_deficiency) {
        return Err("observed_deficiency is required".to_string());
    }
    let has_parent_task = pass.parent_task_ref.as_deref().is_some_and(non_empty);
    let has_parent_spec = pass.parent_spec_ref.as_deref().is_some_and(non_empty);
    if !has_parent_task && !has_parent_spec {
        return Err("parent task/spec reference is required".to_string());
    }
    if !non_empty(&pass.constitutional_authority) {
        return Err("constitutional authority is required".to_string());
    }
    let authority = pass.constitutional_authority.trim();
    if !authority.starts_with("claim.") && !authority.contains('/') && !authority.contains(".md") {
        return Err(
            "constitutional authority must cite a claim id or constitution document".to_string(),
        );
    }
    if pass.allowed_changes.is_empty() || !pass.allowed_changes.iter().all(|s| non_empty(s)) {
        return Err("allowed_changes must name bounded mutation scope".to_string());
    }
    if pass.forbidden_changes.is_empty() || !pass.forbidden_changes.iter().all(|s| non_empty(s)) {
        return Err("forbidden_changes must name forbidden mutation scope".to_string());
    }
    if pass.proof_required.is_empty() || pass.proof_required.iter().any(|p| vague_proof(p)) {
        return Err("proof_required must contain concrete proof gates".to_string());
    }
    if vague_stop_condition(&pass.stop_condition) {
        return Err("stop_condition is required and must prevent infinite polishing".to_string());
    }
    if !matches!(
        pass.risk_level.as_str(),
        "low" | "medium" | "high" | "critical"
    ) {
        return Err("risk_level must be one of low, medium, high, critical".to_string());
    }
    if pass.requires_user_approval && !pass.user_approval_ref.as_deref().is_some_and(non_empty) {
        return Err(
            "user_approval_ref is required when requires_user_approval is true".to_string(),
        );
    }
    if pass.mutates_parent_intent {
        return Err("recursive pass must not mutate parent intent".to_string());
    }
    if pass.expands_scope {
        return Err("recursive pass must not expand scope".to_string());
    }
    if pass.weakens_governance {
        return Err(
            "recursive pass must not weaken constitution, repo rules, proof gates, or boundaries"
                .to_string(),
        );
    }
    if let Some((path, rule)) = forbidden_path_touched(&pass.forbidden_changes, &pass.touched_paths)
    {
        return Err(format!(
            "recursive pass touched forbidden path '{}' matching '{}'",
            path, rule
        ));
    }
    Ok(())
}

fn validate_recursive_improvement_passes_if_present(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Recursive Improvement Pass Gate");

    let passes_dir = repo_root
        .join(".decapod")
        .join("governance")
        .join("recursive_passes");
    if !passes_dir.exists() {
        skip(
            "No recursive improvement pass artifacts found; skipping recursive pass gate",
            ctx,
        );
        return Ok(());
    }

    let mut files = 0usize;
    for entry in fs::read_dir(&passes_dir).map_err(error::DecapodError::IoError)? {
        let entry = entry.map_err(error::DecapodError::IoError)?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        files += 1;
        let raw = fs::read_to_string(&path).map_err(error::DecapodError::IoError)?;
        let parsed: RecursiveImprovementPass = serde_json::from_str(&raw).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid recursive improvement pass {}: {}",
                path.display(),
                e
            ))
        })?;
        validate_recursive_pass(&parsed).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid recursive improvement pass: {} ({})",
                e,
                path.display()
            ))
        })?;
    }

    pass(
        &format!(
            "Recursive improvement pass schema check passed for {} file(s)",
            files
        ),
        ctx,
    );
    Ok(())
}

fn validate_knowledge_integrity(
    store: &Store,
    ctx: &ValidationContext,
    pre_read_broker: Option<&str>,
) -> Result<(), error::DecapodError> {
    info("Knowledge Integrity Gate");
    let db_path = store.root.join("knowledge.db");
    if !db_path.exists() {
        skip(
            "knowledge.db not found; skipping knowledge integrity check",
            ctx,
        );
        return Ok(());
    }

    let query_missing_provenance = |conn: &rusqlite::Connection| -> Result<i64, rusqlite::Error> {
        conn.query_row(
            "SELECT COUNT(*) FROM knowledge WHERE provenance IS NULL OR provenance = ''",
            [],
            |row| row.get(0),
        )
    };

    let mut conn = db::db_connect_for_validate(&db_path.to_string_lossy())?;
    let missing_provenance: i64 = match query_missing_provenance(&conn) {
        Ok(v) => v,
        Err(rusqlite::Error::SqliteFailure(_, Some(msg)))
            if msg.contains("no such table: knowledge") =>
        {
            // Self-heal schema drift/partial bootstrap before validating integrity.
            db::initialize_knowledge_db(&store.root)?;
            conn = db::db_connect_for_validate(&db_path.to_string_lossy())?;
            query_missing_provenance(&conn).map_err(error::DecapodError::RusqliteError)?
        }
        Err(e) => return Err(error::DecapodError::RusqliteError(e)),
    };

    if missing_provenance == 0 {
        pass(
            "Knowledge provenance verified (all entries have pointers)",
            ctx,
        );
    } else {
        fail(
            &format!(
                "Found {} knowledge entries missing mandatory provenance",
                missing_provenance
            ),
            ctx,
        );
    }

    let procedural_missing_event_provenance: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM knowledge
             WHERE id LIKE 'procedural/%'
               AND (provenance IS NULL OR provenance = '' OR provenance NOT LIKE 'event:%')",
            [],
            |row| row.get(0),
        )
        .map_err(error::DecapodError::RusqliteError)?;
    if procedural_missing_event_provenance == 0 {
        pass(
            "Knowledge promotion firewall verified (procedural entries carry event provenance)",
            ctx,
        );
    } else {
        fail(
            &format!(
                "Found {} procedural knowledge entries without event-backed provenance",
                procedural_missing_event_provenance
            ),
            ctx,
        );
    }

    let event_ids = load_knowledge_promotion_event_ids(&store.root)?;
    let mut stmt = conn
        .prepare(
            "SELECT provenance FROM knowledge
             WHERE id LIKE 'procedural/%' AND provenance LIKE 'event:%'",
        )
        .map_err(error::DecapodError::RusqliteError)?;
    let rows = stmt
        .query_map([], |row| row.get::<_, String>(0))
        .map_err(error::DecapodError::RusqliteError)?;
    let mut missing_event_refs = 0usize;
    for row in rows {
        let prov = row.map_err(error::DecapodError::RusqliteError)?;
        let event_id = prov.trim_start_matches("event:");
        if !event_ids.contains(event_id) {
            missing_event_refs += 1;
        }
    }
    if missing_event_refs == 0 {
        pass("Knowledge promotion firewall ledger linkage verified", ctx);
    } else {
        fail(
            &format!(
                "Found {} procedural knowledge entries referencing missing promotion events",
                missing_event_refs
            ),
            ctx,
        );
    }

    let fallback;
    let content_opt = match pre_read_broker {
        Some(c) => Some(c),
        None => {
            let audit_log = store.root.join("broker.events.jsonl");
            if audit_log.exists() {
                fallback = fs::read_to_string(audit_log)?;
                Some(fallback.as_str())
            } else {
                None
            }
        }
    };
    if let Some(content) = content_opt {
        let mut offenders = Vec::new();
        for line in content.lines() {
            if line.contains("\"op\":\"knowledge.add\"") && line.contains("\"db_id\":\"health.db\"")
            {
                offenders.push(line.to_string());
            }
        }
        if offenders.is_empty() {
            pass("No direct health promotion from knowledge detected", ctx);
        } else {
            fail(
                &format!(
                    "Knowledge system directly mutated health state: {:?}",
                    offenders
                ),
                ctx,
            );
        }
    }

    Ok(())
}

fn load_knowledge_promotion_event_ids(
    store_root: &Path,
) -> Result<HashSet<String>, error::DecapodError> {
    let ledger = store_root.join("knowledge.promotions.jsonl");
    if !ledger.exists() {
        return Ok(HashSet::new());
    }

    let raw = fs::read_to_string(&ledger).map_err(error::DecapodError::IoError)?;
    let mut ids = HashSet::new();
    for (idx, line) in raw.lines().enumerate() {
        if line.trim().is_empty() {
            continue;
        }
        let v: serde_json::Value = serde_json::from_str(line).map_err(|e| {
            error::DecapodError::ValidationError(format!(
                "invalid promotion ledger line {} in {}: {}",
                idx + 1,
                ledger.display(),
                e
            ))
        })?;
        if let Some(id) = v.get("event_id").and_then(|x| x.as_str()) {
            ids.insert(id.to_string());
        }
    }
    Ok(ids)
}

fn validate_lineage_hard_gate(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Lineage Hard Gate");
    let todo_events = store.root.join("todo.events.jsonl");
    let federation_db = store.root.join("federation.db");
    let todo_db = store.root.join("todo.db");

    // Fast path: if any required file is missing, skip entirely
    if !todo_events.exists() || !federation_db.exists() || !todo_db.exists() {
        skip("lineage inputs missing; skipping", ctx);
        return Ok(());
    }

    // Quick check: if todo events is empty or very small, skip
    if let Ok(metadata) = fs::metadata(&todo_events)
        && metadata.len() < 100
    {
        skip("todo.events.jsonl too small; skipping", ctx);
        return Ok(());
    }

    let content = match fs::read_to_string(&todo_events) {
        Ok(c) => c,
        Err(_) => {
            skip("cannot read todo.events.jsonl; skipping", ctx);
            return Ok(());
        }
    };

    // Fast path: if no intent: prefix events, skip the expensive part
    if !content.contains("intent:") {
        pass("no intent-tagged events found; skipping", ctx);
        return Ok(());
    }

    let mut add_candidates = Vec::new();
    let mut done_candidates = Vec::new();
    for line in content.lines() {
        let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
            continue;
        };
        let event_type = v.get("event_type").and_then(|x| x.as_str()).unwrap_or("");
        let task_id = v.get("task_id").and_then(|x| x.as_str()).unwrap_or("");
        if task_id.is_empty() {
            continue;
        }
        let intent_ref = v
            .get("payload")
            .and_then(|p| p.get("intent_ref"))
            .and_then(|x| x.as_str())
            .unwrap_or("");
        // Hard gate only applies to new intent-tagged events.
        if !intent_ref.starts_with("intent:") {
            continue;
        }
        if event_type == "task.add" {
            add_candidates.push(task_id.to_string());
        } else if event_type == "task.done" {
            done_candidates.push(task_id.to_string());
        }
    }

    // Fast path: no candidates to check
    if add_candidates.is_empty() && done_candidates.is_empty() {
        pass("no intent-tagged task events to validate", ctx);
        return Ok(());
    }

    let conn = db::db_connect_for_validate(&federation_db.to_string_lossy())?;
    let todo_conn = db::db_connect_for_validate(&todo_db.to_string_lossy())?;
    let mut violations = Vec::new();

    for task_id in add_candidates {
        let exists: i64 = todo_conn
            .query_row(
                "SELECT COUNT(*) FROM tasks WHERE id = ?1",
                rusqlite::params![task_id.clone()],
                |row| row.get(0),
            )
            .map_err(error::DecapodError::RusqliteError)?;
        if exists == 0 {
            continue;
        }
        let source = format!("event:{}", task_id);
        let commitment_count: i64 = conn
            .query_row(
                "SELECT COUNT(*) FROM nodes n JOIN sources s ON s.node_id = n.id WHERE s.source = ?1 AND n.node_type = 'commitment'",
                rusqlite::params![source],
                |row| row.get(0),
            )
            .map_err(error::DecapodError::RusqliteError)?;
        if commitment_count == 0 {
            violations.push(format!(
                "task.add {} missing commitment lineage node",
                task_id
            ));
        }
    }

    for task_id in done_candidates {
        let exists: i64 = todo_conn
            .query_row(
                "SELECT COUNT(*) FROM tasks WHERE id = ?1",
                rusqlite::params![task_id.clone()],
                |row| row.get(0),
            )
            .map_err(error::DecapodError::RusqliteError)?;
        if exists == 0 {
            continue;
        }
        let source = format!("event:{}", task_id);
        let commitment_count: i64 = conn
            .query_row(
                "SELECT COUNT(*) FROM nodes n JOIN sources s ON s.node_id = n.id WHERE s.source = ?1 AND n.node_type = 'commitment'",
                rusqlite::params![source.clone()],
                |row| row.get(0),
            )
            .map_err(error::DecapodError::RusqliteError)?;
        let decision_count: i64 = conn
            .query_row(
                "SELECT COUNT(*) FROM nodes n JOIN sources s ON s.node_id = n.id WHERE s.source = ?1 AND n.node_type = 'decision'",
                rusqlite::params![source],
                |row| row.get(0),
            )
            .map_err(error::DecapodError::RusqliteError)?;
        if commitment_count == 0 || decision_count == 0 {
            violations.push(format!(
                "task.done {} missing commitment/decision lineage nodes",
                task_id
            ));
        }
    }

    if violations.is_empty() {
        pass(
            "Intent-tagged task.add/task.done events have commitment+proof lineage",
            ctx,
        );
    } else {
        fail(&format!("Lineage gate violations: {:?}", violations), ctx);
    }
    Ok(())
}

fn validate_repomap_determinism(
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Repo Map Determinism Gate");
    use crate::core::repomap;
    let dir1 = decapod_dir.to_path_buf();
    let dir2 = decapod_dir.to_path_buf();
    let h1 =
        std::thread::spawn(move || serde_json::to_string(&repomap::generate_map(&dir1)).unwrap());
    let h2 =
        std::thread::spawn(move || serde_json::to_string(&repomap::generate_map(&dir2)).unwrap());

    let m1 = h1
        .join()
        .map_err(|_| error::DecapodError::ValidationError("repomap thread panicked".into()))?;
    let m2 = h2
        .join()
        .map_err(|_| error::DecapodError::ValidationError("repomap thread panicked".into()))?;

    if m1 == m2 && !m1.is_empty() {
        pass("Repo map output is deterministic", ctx);
    } else {
        fail("Repo map output is non-deterministic or empty", ctx);
    }
    Ok(())
}

fn validate_watcher_audit(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Watcher Audit Gate");
    let audit_log = store.root.join("watcher.events.jsonl");
    if audit_log.exists() {
        pass("Watcher audit trail present", ctx);
    } else {
        warn(
            "Watcher audit trail missing (run `decapod govern watcher run`)",
            ctx,
        );
    }
    Ok(())
}

fn validate_watcher_purity(
    store: &Store,
    ctx: &ValidationContext,
    pre_read_broker: Option<&str>,
) -> Result<(), error::DecapodError> {
    info("Watcher Purity Gate");
    let fallback;
    let content_opt = match pre_read_broker {
        Some(c) => Some(c),
        None => {
            let audit_log = store.root.join("broker.events.jsonl");
            if audit_log.exists() {
                fallback = fs::read_to_string(audit_log)?;
                Some(fallback.as_str())
            } else {
                None
            }
        }
    };
    if let Some(content) = content_opt {
        let mut offenders = Vec::new();
        for line in content.lines() {
            if line.contains("\"actor\":\"watcher\"") {
                offenders.push(line.to_string());
            }
        }
        if offenders.is_empty() {
            pass("Watcher purity verified (read-only checks only)", ctx);
        } else {
            fail(
                &format!(
                    "Watcher subsystem attempted brokered mutations: {:?}",
                    offenders
                ),
                ctx,
            );
        }
    }
    Ok(())
}

fn validate_archive_integrity(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Archive Integrity Gate");
    let db_path = store.root.join("archive.db");
    if !db_path.exists() {
        skip("archive.db not found; skipping archive check", ctx);
        return Ok(());
    }

    use crate::archive;
    let failures = archive::verify_archives(store)?;
    if failures.is_empty() {
        pass(
            "All session archives verified (content and hash match)",
            ctx,
        );
    } else {
        fail(
            &format!("Archive integrity failures detected: {:?}", failures),
            ctx,
        );
    }
    Ok(())
}

fn validate_control_plane_contract(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Control Plane Contract Gate");

    // Check that all database mutations went through the broker
    // by verifying event log consistency
    let data_dir = &store.root;
    let mut violations = Vec::new();

    // Check for broker audit trail presence
    let broker_log = data_dir.join("broker.events.jsonl");
    if !broker_log.exists() {
        // First run - no broker log yet, this is OK
        pass("No broker events yet (first run)", ctx);
        return Ok(());
    }

    // Check that critical databases have corresponding broker events
    let todo_db = data_dir.join("todo.db");
    if todo_db.exists() {
        let todo_events = data_dir.join("todo.events.jsonl");
        if !todo_events.exists() {
            violations.push("todo.db exists but todo.events.jsonl is missing".to_string());
        }
    }

    let federation_db = data_dir.join("federation.db");
    if federation_db.exists() {
        let federation_events = data_dir.join("federation.events.jsonl");
        if !federation_events.exists() {
            violations
                .push("federation.db exists but federation.events.jsonl is missing".to_string());
        }
    }

    // Check for direct SQLite write patterns in process list (best effort).
    // Bound the probe to keep validate responsive in active workspaces.
    #[cfg(target_os = "linux")]
    {
        use std::process::Command;
        if let Ok(output) = Command::new("timeout")
            .args(["3s", "lsof", "+D", data_dir.to_string_lossy().as_ref()])
            .output()
            && output.status.success()
        {
            let stdout = String::from_utf8_lossy(&output.stdout);
            for line in stdout.lines() {
                if line.contains("sqlite") && !line.contains("decapod") {
                    violations.push(format!("External SQLite process accessing store: {}", line));
                }
            }
        }
    }

    if violations.is_empty() {
        pass(
            "Control plane contract honored (all mutations brokered)",
            ctx,
        );
    } else {
        fail(
            &format!(
                "Control plane contract violations detected: {:?}",
                violations
            ),
            ctx,
        );
    }

    Ok(())
}

fn validate_canon_mutation(
    store: &Store,
    ctx: &ValidationContext,
    pre_read_broker: Option<&str>,
) -> Result<(), error::DecapodError> {
    info("Canon Mutation Gate");
    let fallback;
    let content_opt = match pre_read_broker {
        Some(c) => Some(c),
        None => {
            let audit_log = store.root.join("broker.events.jsonl");
            if audit_log.exists() {
                fallback = fs::read_to_string(audit_log)?;
                Some(fallback.as_str())
            } else {
                None
            }
        }
    };
    if let Some(content) = content_opt {
        let mut offenders = Vec::new();
        for line in content.lines() {
            if line.contains("\"op\":\"write\"")
                && (line.contains(".md\"") || line.contains(".json\""))
                && !line.contains("\"actor\":\"decapod\"")
                && !line.contains("\"actor\":\"scaffold\"")
            {
                offenders.push(line.to_string());
            }
        }
        if offenders.is_empty() {
            pass("No unauthorized canon mutations detected", ctx);
        } else {
            warn(
                &format!(
                    "Detected direct mutations to canonical documents: {:?}",
                    offenders
                ),
                ctx,
            );
        }
    }
    Ok(())
}

fn validate_heartbeat_invocation_gate(
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Heartbeat Invocation Gate");

    let lib_rs = decapod_dir.join("src").join("lib.rs");
    let todo_rs = decapod_dir.join("src").join("plugins").join("todo.rs");
    if lib_rs.exists() && todo_rs.exists() {
        let lib_content = fs::read_to_string(&lib_rs).unwrap_or_default();
        let todo_content = fs::read_to_string(&todo_rs).unwrap_or_default();

        let code_markers = [
            (
                lib_content.contains("should_auto_clock_in(&cli.command)")
                    && lib_content.contains("todo::clock_in_agent_presence(&project_store)?"),
                "Top-level command dispatch auto-clocks heartbeat",
            ),
            (
                lib_content
                    .contains("Command::Todo(todo_cli) => !todo::is_heartbeat_command(todo_cli)"),
                "Decorator excludes explicit todo heartbeat to prevent duplicates",
            ),
            (
                todo_content.contains("pub fn clock_in_agent_presence")
                    && todo_content.contains("record_heartbeat"),
                "TODO plugin exposes reusable clock-in helper",
            ),
        ];

        for (ok, msg) in code_markers {
            if ok {
                pass(msg, ctx);
            } else {
                fail(msg, ctx);
            }
        }
    } else {
        skip(
            "Heartbeat wiring source files absent; skipping code-level heartbeat checks",
            ctx,
        );
    }

    let doc_markers = [
        (
            crate::core::assets::get_doc("core/DECAPOD")
                .unwrap_or_default()
                .contains("invocation heartbeat"),
            "Router documents invocation heartbeat contract",
        ),
        (
            crate::core::assets::get_doc("interfaces/CONTROL_PLANE")
                .unwrap_or_default()
                .contains("invocation heartbeat"),
            "Control-plane interface documents invocation heartbeat",
        ),
        (
            crate::core::assets::get_doc("plugins/TODO")
                .unwrap_or_default()
                .contains("auto-clocks liveness"),
            "TODO plugin documents automatic liveness clock-in",
        ),
        (
            crate::core::assets::get_doc("plugins/REFLEX")
                .unwrap_or_default()
                .contains("todo.heartbeat.autoclaim"),
            "REFLEX plugin documents heartbeat autoclaim action",
        ),
    ];

    for (ok, msg) in doc_markers {
        if ok {
            pass(msg, ctx);
        } else {
            fail(msg, ctx);
        }
    }

    Ok(())
}

fn validate_federation_gates(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Federation Gates");

    let results = crate::plugins::federation::validate_federation(&store.root)?;

    for (gate_name, passed, message) in results {
        if passed {
            pass(&format!("[{}] {}", gate_name, message), ctx);
        } else {
            // Federation gates are advisory (warn) rather than hard-fail because the
            // two-phase DB+JSONL write design can produce transient drift that does
            // not indicate data loss.
            warn(&format!("[{}] {}", gate_name, message), ctx);
        }
    }

    Ok(())
}

fn validate_markdown_primitives_roundtrip_gate(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("Markdown Primitive Round-Trip Gate");
    match primitives::validate_roundtrip_gate(store) {
        Ok(()) => {
            pass(
                "Markdown primitives export and round-trip validation pass",
                ctx,
            );
        }
        Err(err) => {
            fail(
                &format!("Markdown primitive round-trip failed: {}", err),
                ctx,
            );
        }
    }
    Ok(())
}

/// Validates that tooling requirements are satisfied.
/// This gate ensures formatting, linting, and type checking pass before promotion.
fn validate_git_workspace_context(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Git Workspace Context Gate");

    // Allow bypass for testing/CI environments
    if std::env::var("DECAPOD_VALIDATE_SKIP_GIT_GATES").is_ok() {
        skip(
            "Git workspace gates skipped (DECAPOD_VALIDATE_SKIP_GIT_GATES set)",
            ctx,
        );
        return Ok(());
    }

    // Exempt read-only schema commands (data schema, lcm schema, map schema)
    let args: Vec<String> = std::env::args().collect();
    let is_schema_command = args.iter().any(|a| {
        a == "schema"
            || (a == "lcm"
                && args
                    .iter()
                    .skip_while(|x| *x != "lcm")
                    .nth(1)
                    .is_some_and(|x| x == "schema"))
            || (a == "map"
                && args
                    .iter()
                    .skip_while(|x| *x != "map")
                    .nth(1)
                    .is_some_and(|x| x == "schema"))
    });
    if is_schema_command {
        skip(
            "Schema command exempted from workspace requirement (read-only)",
            ctx,
        );
        return Ok(());
    }

    if !is_inside_git_work_tree(repo_root) {
        skip(
            "Git workspace gates skipped: initialized project is not a git repository",
            ctx,
        );
        return Ok(());
    }

    let git_dir = repo_root.join(".git");
    let is_worktree = git_dir.is_file() && {
        let content = std::fs::read_to_string(&git_dir).unwrap_or_default();
        content.contains("gitdir:")
    };

    let is_isolated = is_worktree || repo_root.to_string_lossy().contains(".decapod/workspaces");

    if is_isolated {
        pass("Running in isolated workspace (.decapod/workspaces/)", ctx);
    } else {
        fail(
            "Not running in isolated git worktree - must use .decapod/workspaces/ to prevent disrupting the root repository",
            ctx,
        );
    }

    let container_reasons = container_signal_reasons(repo_root);
    if !container_reasons.is_empty() {
        pass(
            &format!(
                "Container-detected: (signals: {})",
                container_reasons.join(", ")
            ),
            ctx,
        );
    } else {
        fail(
            &auto_remediable_validation_message(
                "container_workspace_required",
                "Not running in container workspace - git-tracked work must execute in Docker-isolated workspace (claim.git.container_workspace_required)",
                "Agent: rerun through `decapod workspace ensure`, or continue inside a Decapod-created container workspace.",
            ),
            ctx,
        );
    }

    validate_commit_often_gate(ctx, repo_root)?;

    Ok(())
}

fn validate_commit_often_gate(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    let max_dirty_files = std::env::var("DECAPOD_COMMIT_OFTEN_MAX_DIRTY_FILES")
        .ok()
        .and_then(|v| v.parse::<usize>().ok())
        .filter(|v| *v > 0)
        .unwrap_or(6);

    let status_output = std::process::Command::new("git")
        .args(["status", "--porcelain"])
        .current_dir(repo_root)
        .output()
        .map_err(error::DecapodError::IoError)?;

    if !status_output.status.success() {
        warn("Commit-often gate skipped: unable to read git status", ctx);
        return Ok(());
    }

    let dirty_count = String::from_utf8_lossy(&status_output.stdout)
        .lines()
        .filter(|line| !line.trim().is_empty())
        .count();

    if dirty_count == 0 {
        pass("Commit-often gate: working tree is clean", ctx);
        return Ok(());
    }

    if dirty_count > max_dirty_files {
        fail(
            &format!(
                "Commit-often mandate violation: {} dirty file(s) exceed limit {}. Commit incremental changes before continuing.",
                dirty_count, max_dirty_files
            ),
            ctx,
        );
    } else {
        pass(
            &format!(
                "Commit-often gate: {} dirty file(s) within limit {}",
                dirty_count, max_dirty_files
            ),
            ctx,
        );
    }

    Ok(())
}

fn validate_plan_governed_execution_gate(
    store: &Store,
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Plan-Governed Execution Gate");

    // Test harnesses and isolated fixture repos explicitly bypass git gates.
    // Keep plan-governed promotion checks out of that mode to preserve stable
    // verification replay fixtures that are not modeled as full workspaces.
    if std::env::var("DECAPOD_VALIDATE_SKIP_GIT_GATES").is_ok() {
        skip(
            "Plan-governed execution gate skipped (DECAPOD_VALIDATE_SKIP_GIT_GATES set)",
            ctx,
        );
        return Ok(());
    }

    let plan = plan_governance::load_plan(repo_root)?;
    if let Some(plan) = plan {
        if plan.state != plan_governance::PlanState::Approved
            && plan.state != plan_governance::PlanState::Done
        {
            fail(
                &format!(
                    "NEEDS_PLAN_APPROVAL: plan state is {:?}; execution/promotion requires APPROVED or DONE",
                    plan.state
                ),
                ctx,
            );
        } else {
            pass("Plan artifact state allows governed execution", ctx);
        }

        if plan.intent.trim().is_empty()
            || !plan.unknowns.is_empty()
            || !plan.human_questions.is_empty()
        {
            fail(
                "NEEDS_HUMAN_INPUT: governed plan has unresolved intent/unknowns/questions",
                ctx,
            );
        } else {
            pass("Plan intent and unknowns are resolved", ctx);
        }
    } else {
        let done_count = plan_governance::count_done_todos(&store.root)?;
        if done_count > 0 {
            fail(
                &format!(
                    "NEEDS_PLAN_APPROVAL: {} done TODO(s) exist but governed PLAN artifact is missing",
                    done_count
                ),
                ctx,
            );
        } else {
            pass(
                "No governed plan artifact present; gate is advisory until first done TODO",
                ctx,
            );
        }
    }

    let unverified = plan_governance::collect_unverified_done_todos(&store.root)?;
    if !unverified.is_empty() {
        fail(
            &format!(
                "PROOF_HOOK_FAILED: {} done TODO(s) are CLAIMED but not VERIFIED: {}",
                unverified.len(),
                output::preview_messages(&unverified, 4, 80)
            ),
            ctx,
        );
    } else {
        pass("Done TODOs are proof-verified", ctx);
    }

    Ok(())
}

fn validate_git_protected_branch(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Git Protected Branch Gate");

    // Allow bypass for testing/CI environments
    if std::env::var("DECAPOD_VALIDATE_SKIP_GIT_GATES").is_ok() {
        skip(
            "Git protected branch gate skipped (DECAPOD_VALIDATE_SKIP_GIT_GATES set)",
            ctx,
        );
        return Ok(());
    }

    if !is_inside_git_work_tree(repo_root) {
        skip(
            "Git protected branch gate skipped: initialized project is not a git repository",
            ctx,
        );
        return Ok(());
    }

    let current_branch = {
        let output = std::process::Command::new("git")
            .args(["rev-parse", "--abbrev-ref", "HEAD"])
            .current_dir(repo_root)
            .output();
        output
            .ok()
            .and_then(|o| {
                if o.status.success() {
                    Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
                } else {
                    None
                }
            })
            .unwrap_or_else(|| "unknown".to_string())
    };

    let is_protected = is_protected_git_branch(&current_branch);

    if is_protected {
        fail(
            &format!(
                "Currently on protected branch '{}' - implementation work must happen in working branch, not directly on protected refs (claim.git.no_direct_main_push)",
                current_branch
            ),
            ctx,
        );
    } else {
        pass(
            &format!("On working branch '{}' (not protected)", current_branch),
            ctx,
        );
    }

    if is_protected && git_origin_exists(repo_root) {
        let ahead_behind = std::process::Command::new("git")
            .args(["rev-list", "--left-right", "--count", "HEAD...origin/HEAD"])
            .current_dir(repo_root)
            .output();

        if let Ok(out) = ahead_behind
            && out.status.success()
        {
            let counts = String::from_utf8_lossy(&out.stdout);
            if let Some((ahead, _behind)) = parse_ahead_behind_counts(&counts) {
                if ahead > 0 {
                    let output = std::process::Command::new("git")
                        .args(["rev-list", "--format=%s", "-n1", "HEAD"])
                        .current_dir(repo_root)
                        .output();
                    let commit_msg = output
                        .ok()
                        .and_then(|o| {
                            if o.status.success() {
                                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
                            } else {
                                None
                            }
                        })
                        .unwrap_or_else(|| "unknown".to_string());

                    fail(
                        &format!(
                            "Protected branch has {} unpushed commit(s) - direct push to protected branch detected (commit: {})",
                            ahead, commit_msg
                        ),
                        ctx,
                    );
                } else {
                    pass("No unpushed commits to protected branches", ctx);
                }
            }
        }
    } else if git_origin_exists(repo_root) {
        match git_upstream_ref(repo_root) {
            Some(upstream) => {
                let output = std::process::Command::new("git")
                    .args(["rev-list", "--left-right", "--count"])
                    .arg(format!("HEAD...{}", upstream))
                    .current_dir(repo_root)
                    .output();
                if let Ok(out) = output
                    && out.status.success()
                {
                    let counts = String::from_utf8_lossy(&out.stdout);
                    if let Some((ahead, behind)) = parse_ahead_behind_counts(&counts) {
                        pass(
                            &format!(
                                "Working branch divergence from upstream '{}': ahead {}, behind {}; protected branch direct-push check not applicable",
                                upstream, ahead, behind
                            ),
                            ctx,
                        );
                    }
                }
            }
            None => pass(
                "Working branch has no upstream; protected branch direct-push check not applicable",
                ctx,
            ),
        }
    }

    Ok(())
}

fn is_protected_git_branch(branch: &str) -> bool {
    matches!(branch, "master" | "main" | "production" | "stable") || branch.starts_with("release/")
}

fn git_origin_exists(repo_root: &Path) -> bool {
    std::process::Command::new("git")
        .args(["remote", "get-url", "origin"])
        .current_dir(repo_root)
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

fn git_upstream_ref(repo_root: &Path) -> Option<String> {
    let output = std::process::Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
        .current_dir(repo_root)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let upstream = String::from_utf8_lossy(&output.stdout).trim().to_string();
    (!upstream.is_empty()).then_some(upstream)
}

fn parse_ahead_behind_counts(counts: &str) -> Option<(u32, u32)> {
    let mut parts = counts.split_whitespace();
    let ahead = parts.next()?.parse().ok()?;
    let behind = parts.next()?.parse().ok()?;
    Some((ahead, behind))
}

fn validate_tooling_gate(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("Tooling Validation Gate");

    let tooling_enabled = std::env::var("DECAPOD_VALIDATE_ENABLE_TOOLING_GATES")
        .ok()
        .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
        .unwrap_or(false);
    if !tooling_enabled {
        skip(
            "Tooling validation gates disabled by default (set DECAPOD_VALIDATE_ENABLE_TOOLING_GATES=1 to enable)",
            ctx,
        );
        return Ok(());
    }

    if std::env::var("DECAPOD_VALIDATE_SKIP_TOOLING_GATES").is_ok() {
        skip(
            "Tooling validation gates skipped (DECAPOD_VALIDATE_SKIP_TOOLING_GATES set)",
            ctx,
        );
        return Ok(());
    }

    let mut has_failures = false;
    let mut has_tooling = false;

    let cargo_toml = repo_root.join("Cargo.toml");
    if cargo_toml.exists() {
        has_tooling = true;
        let fmt_available = cargo_subcommand_available("fmt");
        let clippy_available = cargo_subcommand_available("clippy");
        if !fmt_available {
            fail(
                &auto_remediable_validation_message(
                    "rustfmt_unavailable",
                    "Rust formatter is unavailable; `cargo fmt --version` did not succeed.",
                    "Agent: enter the repo's Nix/Rust toolchain with rustfmt available, then retry validation.",
                ),
                ctx,
            );
            has_failures = true;
        }
        if !clippy_available {
            fail(
                &auto_remediable_validation_message(
                    "clippy_unavailable",
                    "Rust clippy is unavailable; `cargo clippy --version` did not succeed.",
                    "Agent: enter the repo's Nix/Rust toolchain with clippy available, then retry validation.",
                ),
                ctx,
            );
            has_failures = true;
        }

        let fmt_handle = fmt_available.then(|| {
            let root_fmt = repo_root.to_path_buf();
            std::thread::spawn(move || {
                std::process::Command::new("cargo")
                    .args(["fmt", "--all", "--", "--check"])
                    .current_dir(&root_fmt)
                    .output()
            })
        });

        let clippy_handle = clippy_available.then(|| {
            let root_clippy = repo_root.to_path_buf();
            std::thread::spawn(move || {
                std::process::Command::new("cargo")
                    .args([
                        "clippy",
                        "--all-targets",
                        "--all-features",
                        "--",
                        "-D",
                        "warnings",
                    ])
                    .current_dir(&root_clippy)
                    .output()
            })
        });

        if let Some(fmt_handle) = fmt_handle {
            match fmt_handle.join().expect("fmt thread panicked") {
                Ok(output) => {
                    if output.status.success() {
                        pass("Rust code formatting passes (cargo fmt)", ctx);
                    } else {
                        fail(
                            &auto_remediable_validation_message(
                                "cargo_fmt_failed",
                                &format!(
                                    "Rust code formatting failed - run `cargo fmt --all`.\nstderr:\n{}",
                                    String::from_utf8_lossy(&output.stderr).trim()
                                ),
                                "Agent: run `cargo fmt --all`, then retry validation.",
                            ),
                            ctx,
                        );
                        has_failures = true;
                    }
                }
                Err(e) => {
                    fail(
                        &auto_remediable_validation_message(
                            "cargo_fmt_execution_failed",
                            &format!("Failed to run cargo fmt: {}", e),
                            "Agent: switch to a complete Rust toolchain, then retry validation.",
                        ),
                        ctx,
                    );
                    has_failures = true;
                }
            }
        }

        if let Some(clippy_handle) = clippy_handle {
            match clippy_handle.join().expect("clippy thread panicked") {
                Ok(output) => {
                    if output.status.success() {
                        pass("Rust linting passes (cargo clippy)", ctx);
                    } else {
                        fail(
                            &auto_remediable_validation_message(
                                "cargo_clippy_failed",
                                &format!(
                                    "Rust linting failed - run `cargo clippy --all-targets --all-features`.\nstderr:\n{}",
                                    String::from_utf8_lossy(&output.stderr).trim()
                                ),
                                "Agent: fix lint failures; for local linker configuration, clear RUSTFLAGS and set CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=cc before retrying.",
                            ),
                            ctx,
                        );
                        has_failures = true;
                    }
                }
                Err(e) => {
                    fail(
                        &auto_remediable_validation_message(
                            "cargo_clippy_execution_failed",
                            &format!("Failed to run cargo clippy: {}", e),
                            "Agent: switch to a complete Rust toolchain, then retry validation.",
                        ),
                        ctx,
                    );
                    has_failures = true;
                }
            }
        }
    }

    let pyproject = repo_root.join("pyproject.toml");
    let requirements = repo_root.join("requirements.txt");
    if pyproject.exists() || requirements.exists() {
        has_tooling = true;

        if std::process::Command::new("which")
            .arg("ruff")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
        {
            let root_ruff = repo_root.to_path_buf();
            let ruff_handle = std::thread::spawn(move || {
                std::process::Command::new("ruff")
                    .args(["check", ".", "--output-format=concise"])
                    .current_dir(&root_ruff)
                    .output()
            });

            match ruff_handle.join().expect("ruff thread panicked") {
                Ok(output) => {
                    if output.status.success() {
                        pass("Python linting passes (ruff)", ctx);
                    } else {
                        fail("Python linting failed - fix ruff violations", ctx);
                        has_failures = true;
                    }
                }
                Err(e) => {
                    warn(&format!("ruff not available: {}", e), ctx);
                }
            }
        } else {
            skip("ruff not installed; skipping Python linting", ctx);
        }
    }

    let shell_check = repo_root.join(".shellcheckrc");
    let shell_files_exist = std::fs::read_dir(repo_root)
        .into_iter()
        .flatten()
        .filter_map(|e| e.ok())
        .any(|e| {
            let p = e.path();
            p.is_file() && p.extension().map(|s| s == "sh").unwrap_or(false)
        });

    if shell_check.exists() || shell_files_exist {
        has_tooling = true;

        if std::process::Command::new("which")
            .arg("shellcheck")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
        {
            let repo_root_clone = repo_root.to_path_buf();
            let shellcheck_handle = std::thread::spawn(move || {
                std::process::Command::new("shellcheck")
                    .args(["--enable=all"])
                    .current_dir(repo_root_clone)
                    .output()
            });

            match shellcheck_handle
                .join()
                .expect("shellcheck thread panicked")
            {
                Ok(output) => {
                    if output.status.success() {
                        pass("Shell script linting passes (shellcheck)", ctx);
                    } else {
                        fail(
                            "Shell script linting failed - fix shellcheck violations",
                            ctx,
                        );
                        has_failures = true;
                    }
                }
                Err(e) => {
                    warn(&format!("shellcheck failed: {}", e), ctx);
                }
            }
        } else {
            skip("shellcheck not installed; skipping shell linting", ctx);
        }
    }

    let yaml_check = repo_root.join(".yamllint");
    let yaml_files_exist = std::fs::read_dir(repo_root)
        .into_iter()
        .flatten()
        .filter_map(|e| e.ok())
        .any(|e| {
            let p = e.path();
            p.is_file()
                && p.extension()
                    .map(|s| s == "yaml" || s == "yml")
                    .unwrap_or(false)
        });

    if yaml_check.exists() || yaml_files_exist {
        has_tooling = true;

        if std::process::Command::new("which")
            .arg("yamllint")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
        {
            let repo_root_clone = repo_root.to_path_buf();
            let yamllint_handle = std::thread::spawn(move || {
                std::process::Command::new("yamllint")
                    .arg(".")
                    .current_dir(repo_root_clone)
                    .output()
            });

            match yamllint_handle.join().expect("yamllint thread panicked") {
                Ok(output) => {
                    if output.status.success() {
                        pass("YAML linting passes (yamllint)", ctx);
                    } else {
                        fail("YAML linting failed - fix yamllint violations", ctx);
                        has_failures = true;
                    }
                }
                Err(e) => {
                    warn(&format!("yamllint failed: {}", e), ctx);
                }
            }
        } else {
            skip("yamllint not installed; skipping YAML linting", ctx);
        }
    }

    let dockerfile_exists = std::fs::read_dir(repo_root)
        .into_iter()
        .flatten()
        .filter_map(|e| e.ok())
        .any(|e| {
            e.path()
                .file_name()
                .and_then(|n| n.to_str())
                .map(|n| n.to_lowercase() == "dockerfile")
                .unwrap_or(false)
        });

    if dockerfile_exists {
        has_tooling = true;

        if std::process::Command::new("which")
            .arg("hadolint")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
        {
            let repo_root_clone = repo_root.to_path_buf();
            let hadolint_handle = std::thread::spawn(move || {
                std::process::Command::new("hadolint")
                    .args(["Dockerfile"])
                    .current_dir(repo_root_clone)
                    .output()
            });

            match hadolint_handle.join().expect("hadolint thread panicked") {
                Ok(output) => {
                    if output.status.success() {
                        pass("Dockerfile linting passes (hadolint)", ctx);
                    } else {
                        fail("Dockerfile linting failed - fix hadolint violations", ctx);
                        has_failures = true;
                    }
                }
                Err(e) => {
                    warn(&format!("hadolint failed: {}", e), ctx);
                }
            }
        } else {
            skip("hadolint not installed; skipping Dockerfile linting", ctx);
        }
    }

    if !has_tooling {
        skip(
            "No recognized project files found; skipping tooling validation",
            ctx,
        );
    } else if !has_failures {
        pass(
            "All toolchain validations pass - project is ready for promotion",
            ctx,
        );
    }

    Ok(())
}

fn cargo_subcommand_available(subcommand: &str) -> bool {
    std::process::Command::new("cargo")
        .arg(subcommand)
        .arg("--version")
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
}

fn validate_state_commit_gate(
    ctx: &ValidationContext,
    repo_root: &Path,
) -> Result<(), error::DecapodError> {
    info("STATE_COMMIT Validation Gate");

    // Policy knob: configurable CI job name (can be set via env var)
    let required_ci_job = std::env::var("DECAPOD_STATE_COMMIT_CI_JOB")
        .unwrap_or_else(|_| "state_commit_golden_vectors".to_string());

    info(&format!(
        "STATE_COMMIT: required_ci_job = {}",
        required_ci_job
    ));

    // Check for v1 golden directory (versioned)
    let golden_v1_dir = repo_root
        .join("tests")
        .join("golden")
        .join("state_commit")
        .join("v1");
    if !golden_v1_dir.exists() {
        skip(
            "No tests/golden/state_commit/v1 directory found; skipping STATE_COMMIT validation",
            ctx,
        );
        return Ok(());
    }

    // Check for required v1 golden files
    let required_files = ["scope_record_hash.txt", "state_commit_root.txt"];
    let mut has_golden = true;
    for file in &required_files {
        if !golden_v1_dir.join(file).exists() {
            fail(
                &format!("Missing golden file: tests/golden/state_commit/v1/{}", file),
                ctx,
            );
            has_golden = false;
        }
    }

    // Immutability check: v1 files should not change
    // In v1, these are the canonical golden vectors
    if has_golden {
        pass("STATE_COMMIT v1 golden vectors present", ctx);

        // Verify the expected hashes match v1 protocol
        let expected_scope_hash =
            "41d7e3729b6f4512887fb3cb6f10140942b600041e0d88308b0177e06ebb4b93";
        let expected_root = "28591ac86e52ffac76d5fc3aceeceda5d8592708a8d7fcb75371567fdc481492";

        if let Ok(actual_hash) =
            std::fs::read_to_string(golden_v1_dir.join("scope_record_hash.txt"))
            && actual_hash.trim() != expected_scope_hash
        {
            fail(
                &format!(
                    "STATE_COMMIT v1 scope_record_hash changed! Expected {}, got {}. This requires a SPEC_VERSION bump to v2.",
                    expected_scope_hash,
                    actual_hash.trim()
                ),
                ctx,
            );
        }

        if let Ok(actual_root) =
            std::fs::read_to_string(golden_v1_dir.join("state_commit_root.txt"))
            && actual_root.trim() != expected_root
        {
            fail(
                &format!(
                    "STATE_COMMIT v1 state_commit_root changed! Expected {}, got {}. This requires a SPEC_VERSION bump to v2.",
                    expected_root,
                    actual_root.trim()
                ),
                ctx,
            );
        }
    }

    Ok(())
}

fn validate_obligations(store: &Store, ctx: &ValidationContext) -> Result<(), error::DecapodError> {
    // Initialize the DB to ensure tables exist
    crate::core::obligation::initialize_obligation_db(&store.root)?;

    let obligations = crate::core::obligation::list_obligations(store)?;
    let mut met_count = 0;
    for ob in obligations {
        // If an obligation is marked Met, we MUST verify it still holds
        if ob.status == crate::core::obligation::ObligationStatus::Met {
            let (status, reason) = crate::core::obligation::verify_obligation(store, &ob.id)?;
            if status != crate::core::obligation::ObligationStatus::Met {
                fail(
                    &format!("Obligation {} failed verification: {}", ob.id, reason),
                    ctx,
                );
            } else {
                met_count += 1;
            }
        }
    }
    pass(
        &format!(
            "Obligation Graph Validation Gate ({} met nodes verified)",
            met_count
        ),
        ctx,
    );
    Ok(())
}

fn validate_lcm_immutability(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("LCM Immutability Gate");
    let ledger_path = store.root.join(crate::core::schemas::LCM_EVENTS_NAME);
    if !ledger_path.exists() {
        pass("No LCM ledger yet; gate trivially passes", ctx);
        return Ok(());
    }

    let failures = crate::plugins::lcm::validate_ledger_integrity(&store.root)?;
    if failures.is_empty() {
        pass("LCM ledger integrity verified", ctx);
    } else {
        for f in &failures {
            fail(&format!("LCM immutability: {}", f), ctx);
        }
    }
    Ok(())
}

fn validate_lcm_rebuild_gate(
    store: &Store,
    ctx: &ValidationContext,
) -> Result<(), error::DecapodError> {
    info("LCM Rebuild Gate");
    let ledger_path = store.root.join(crate::core::schemas::LCM_EVENTS_NAME);
    if !ledger_path.exists() {
        pass("No LCM ledger yet; rebuild gate trivially passes", ctx);
        return Ok(());
    }

    let result = crate::plugins::lcm::rebuild_index(store, true)?;
    if result.get("status").and_then(|v| v.as_str()) == Some("success") {
        pass("LCM index rebuild successful", ctx);
    } else {
        let errors = result
            .get("errors")
            .and_then(|v| v.as_array())
            .map(|a| {
                a.iter()
                    .filter_map(|e| e.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            })
            .unwrap_or_default();
        fail(&format!("LCM rebuild failed: {}", errors), ctx);
    }
    Ok(())
}

fn validate_gatekeeper_gate(
    ctx: &ValidationContext,
    decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Gatekeeper Safety Gate");

    // Get staged files from git (if in a git repo)
    let output = std::process::Command::new("git")
        .args(["diff", "--cached", "--name-only"])
        .current_dir(decapod_dir)
        .output();

    let staged_paths: Vec<PathBuf> = match output {
        Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
            .lines()
            .filter(|l| !l.is_empty())
            .map(PathBuf::from)
            .collect(),
        _ => {
            skip(
                "Git not available or not in a repo; skipping gatekeeper gate",
                ctx,
            );
            return Ok(());
        }
    };

    if staged_paths.is_empty() {
        pass("No staged files; gatekeeper gate trivially passes", ctx);
        return Ok(());
    }

    let config = crate::core::gatekeeper::GatekeeperConfig::default();
    let result = crate::core::gatekeeper::run_gatekeeper(decapod_dir, &staged_paths, 0, &config)?;

    if result.passed {
        pass(
            &format!(
                "Gatekeeper: {} staged file(s) passed safety checks",
                staged_paths.len()
            ),
            ctx,
        );
    } else {
        let secret_count = result
            .violations
            .iter()
            .filter(|v| v.kind == crate::core::gatekeeper::ViolationKind::SecretDetected)
            .count();
        let blocked_count = result
            .violations
            .iter()
            .filter(|v| v.kind == crate::core::gatekeeper::ViolationKind::PathBlocked)
            .count();
        let dangerous_count = result
            .violations
            .iter()
            .filter(|v| v.kind == crate::core::gatekeeper::ViolationKind::DangerousPattern)
            .count();

        let mut parts = Vec::new();
        if secret_count > 0 {
            parts.push(format!("{} secret(s)", secret_count));
        }
        if blocked_count > 0 {
            parts.push(format!("{} blocked path(s)", blocked_count));
        }
        if dangerous_count > 0 {
            parts.push(format!("{} dangerous pattern(s)", dangerous_count));
        }
        fail(&format!("Gatekeeper violations: {}", parts.join(", ")), ctx);
    }

    Ok(())
}

/// Evaluates a set of mandates and returns any active blockers.
pub fn evaluate_mandates(
    project_root: &Path,
    store: &Store,
    mandates: &[crate::core::docs::Mandate],
) -> Vec<crate::core::rpc::Blocker> {
    use crate::core::rpc::{Blocker, BlockerKind};
    let mut blockers = Vec::new();

    for mandate in mandates {
        match mandate.check_tag.as_str() {
            "gate.worktree.no_master" => {
                let status = crate::core::workspace::get_workspace_status(project_root);
                if let Ok(s) = status
                    && s.git.is_protected
                {
                    blockers.push(Blocker {
                        kind: BlockerKind::ProtectedBranch,
                        message: format!("Mandate Violation: {}", mandate.fragment.title),
                        resolve_hint: "Run `decapod workspace ensure` to create a working branch."
                            .to_string(),
                    });
                }
            }
            "gate.worktree.isolated" => {
                let status = crate::core::workspace::get_workspace_status(project_root);
                if let Ok(s) = status
                    && !s.git.in_worktree
                {
                    blockers.push(Blocker {
                        kind: BlockerKind::WorkspaceRequired,
                        message: format!("Mandate Violation: {}", mandate.fragment.title),
                        resolve_hint:
                            "Run `decapod workspace ensure` to create an isolated git worktree."
                                .to_string(),
                    });
                }
            }
            "gate.session.active" => {
                // This is usually handled by the RPC kernel session check,
                // but we can add a blocker if we want more detail.
            }
            "gate.todo.active_task" => {
                let agent_id =
                    std::env::var("DECAPOD_AGENT_ID").unwrap_or_else(|_| "unknown".to_string());
                if agent_id != "unknown" {
                    let mut active_tasks = crate::core::todo::list_tasks(
                        &store.root,
                        Some("open".to_string()),
                        None,
                        None,
                        None,
                        None,
                    );
                    if let Ok(ref mut tasks) = active_tasks {
                        let pre_filter_count = tasks.len();
                        let debug_info = if !tasks.is_empty() {
                            format!(
                                "First task assigned to: '{}', My ID: '{}'",
                                tasks[0].assigned_to, agent_id
                            )
                        } else {
                            format!(
                                "No tasks found. My ID: '{}', Root: '{}'",
                                agent_id,
                                project_root.display()
                            )
                        };

                        tasks.retain(|t| t.assigned_to == agent_id);
                        if tasks.is_empty() {
                            blockers.push(Blocker {
                                kind: BlockerKind::MissingProof,
                                message: format!("Mandate Violation: {} (Pre-filter: {}, {})", mandate.fragment.title, pre_filter_count, debug_info),
                                resolve_hint: "You MUST create and claim a `todo` before starting work. Run `decapod todo add \"...\"` then `decapod todo claim --id <id>`.".to_string(),
                            });
                        }
                    }
                }
            }
            "gate.validation.pass" => {
                // Future: check a 'last_validated' marker in the store
            }
            _ => {}
        }
    }

    blockers
}

/// Co-Player Policy Tightening Gate
///
/// Validates that the coplayer policy derivation function only tightens
/// constraints as reliability decreases. This is a structural invariant:
/// no snapshot should produce a policy that is looser than a less-reliable one.
fn validate_coplayer_policy_tightening(
    ctx: &ValidationContext,
    _decapod_dir: &Path,
) -> Result<(), error::DecapodError> {
    info("Co-Player Policy Tightening Gate");

    use crate::core::coplayer::{CoPlayerSnapshot, derive_policy};

    // Test the invariant: unknown → high → medium → low reliability
    // Each step must be equal or tighter than the next.
    let profiles = vec![
        ("unknown", 0.0, 0),
        ("high", 0.5, 20),
        ("medium", 0.8, 20),
        ("low", 0.95, 100),
    ];

    let mut prev_policy = None;
    let mut all_valid = true;

    for (risk, reliability, total) in &profiles {
        let snap = CoPlayerSnapshot {
            agent_id: format!("gate-test-{}", risk),
            reliability_score: *reliability,
            total_ops: *total,
            successful_ops: (*total as f64 * reliability) as usize,
            failed_ops: *total - (*total as f64 * reliability) as usize,
            last_active: "gate-test".to_string(),
            common_ops: vec![],
            risk_profile: risk.to_string(),
        };

        let policy = derive_policy(&snap);

        // Validation is ALWAYS required
        if !policy.require_validation {
            fail(
                &format!(
                    "Co-player policy for '{}' does not require validation (MUST always be true)",
                    risk
                ),
                ctx,
            );
            all_valid = false;
        }

        // Check tightening: diff limits must be <= previous (less reliable) agent's limits
        if let Some(prev) = &prev_policy {
            let prev: &crate::core::coplayer::CoPlayerPolicy = prev;
            // More reliable agents may have larger diff limits, never smaller
            if policy.max_diff_lines < prev.max_diff_lines {
                // This is expected: more reliable = looser (larger diff limit)
                // The INVARIANT is the reverse must not happen:
                // less reliable must not have LARGER limits than more reliable
            }
        }

        prev_policy = Some(policy);
    }

    if all_valid {
        pass("Co-player policies only tighten constraints", ctx);
    }

    Ok(())
}

pub fn run_validation(
    store: &Store,
    decapod_dir: &Path,
    _home_dir: &Path,
    _verbose: bool,
) -> Result<ValidationReport, error::DecapodError> {
    let total_start = Instant::now();

    let ctx = ValidationContext::new();

    // Pre-read broker.events.jsonl once for gates that need it
    let broker_events_path = store.root.join("broker.events.jsonl");
    let broker_content: Option<String> = if broker_events_path.exists() {
        fs::read_to_string(&broker_events_path).ok()
    } else {
        None
    };

    // Store validations — run sequentially since they set up state
    match store.kind {
        StoreKind::User => {
            let start = Instant::now();
            validate_user_store_blank_slate(&ctx)?;
            let _ = start;
        }
        StoreKind::Repo => {
            let start = Instant::now();
            validate_repo_store_dogfood(store, &ctx, decapod_dir)?;
            let _ = start;
        }
    }

    // Run remaining gates in parallel for bounded wall-clock validation time.
    let timings: Mutex<Vec<(&str, Duration)>> = Mutex::new(Vec::new());
    {
        let _s = ();
        let ctx = &ctx;
        let timings = &timings;
        let broker = broker_content.as_deref();

        gate!(
            s,
            timings,
            ctx,
            "validate_repo_map",
            validate_repo_map(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_no_legacy_namespaces",
            validate_no_legacy_namespaces(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_embedded_self_contained",
            validate_embedded_self_contained(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_docs_templates_bucket",
            validate_docs_templates_bucket(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_entrypoint_invariants",
            validate_entrypoint_invariants(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_interface_contract_bootstrap",
            validate_interface_contract_bootstrap(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_health_purity",
            validate_health_purity(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_project_scoped_state",
            validate_project_scoped_state(store, ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_generated_artifact_whitelist",
            validate_generated_artifact_whitelist(store, ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_project_config_toml",
            validate_project_config_toml(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_project_specs_docs",
            validate_project_specs_docs(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_spec_drift",
            validate_spec_drift(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_machine_contract",
            validate_machine_contract(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_workunit_manifests_if_present",
            validate_workunit_manifests_if_present(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_recursive_improvement_passes_if_present",
            validate_recursive_improvement_passes_if_present(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_context_capsule_policy_contract",
            validate_context_capsule_policy_contract(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_context_capsules_if_present",
            validate_context_capsules_if_present(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_knowledge_promotions_if_present",
            validate_knowledge_promotions_if_present(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_skill_cards_if_present",
            validate_skill_cards_if_present(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_skill_resolutions_if_present",
            validate_skill_resolutions_if_present(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_internalization_artifacts_if_present",
            validate_internalization_artifacts_if_present(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_eval_gate_if_required",
            validate_eval_gate_if_required(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_schema_determinism",
            validate_schema_determinism(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_database_schema_versions",
            validate_database_schema_versions(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_health_cache_integrity",
            validate_health_cache_integrity(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_risk_map",
            validate_risk_map(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_risk_map_violations",
            validate_risk_map_violations(store, ctx, broker)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_policy_integrity",
            validate_policy_integrity(store, ctx, broker)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_knowledge_integrity",
            validate_knowledge_integrity(store, ctx, broker)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_lineage_hard_gate",
            validate_lineage_hard_gate(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_repomap_determinism",
            validate_repomap_determinism(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_watcher_audit",
            validate_watcher_audit(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_watcher_purity",
            validate_watcher_purity(store, ctx, broker)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_archive_integrity",
            validate_archive_integrity(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_control_plane_contract",
            validate_control_plane_contract(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_canon_mutation",
            validate_canon_mutation(store, ctx, broker)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_heartbeat_invocation_gate",
            validate_heartbeat_invocation_gate(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_markdown_primitives_roundtrip_gate",
            validate_markdown_primitives_roundtrip_gate(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_federation_gates",
            validate_federation_gates(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_git_workspace_context",
            validate_git_workspace_context(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_git_protected_branch",
            validate_git_protected_branch(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_tooling_gate",
            validate_tooling_gate(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_state_commit_gate",
            validate_state_commit_gate(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_obligations",
            validate_obligations(store, ctx)
        );

        gate!(
            s,
            timings,
            ctx,
            "validate_gatekeeper_gate",
            validate_gatekeeper_gate(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_coplayer_policy_tightening",
            validate_coplayer_policy_tightening(ctx, decapod_dir)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_lcm_immutability",
            validate_lcm_immutability(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_lcm_rebuild_gate",
            validate_lcm_rebuild_gate(store, ctx)
        );
        gate!(
            s,
            timings,
            ctx,
            "validate_plan_governed_execution_gate",
            validate_plan_governed_execution_gate(store, ctx, decapod_dir)
        );
    }

    let elapsed = total_start.elapsed();
    let pass_count = ctx.pass_count.load(Ordering::Relaxed);
    let fail_count = ctx.fail_count.load(Ordering::Relaxed);
    let warn_count = ctx.warn_count.load(Ordering::Relaxed);
    let fails = ctx.fails.lock().unwrap().clone();
    let warns = ctx.warns.lock().unwrap().clone();
    let fail_total = (fails.len() as u32).max(fail_count);
    let warn_total = (warns.len() as u32).max(warn_count);
    let mut gate_timings = timings.into_inner().unwrap();
    gate_timings.sort_by(|a, b| b.1.cmp(&a.1));

    Ok(ValidationReport {
        status: if fail_total > 0 { "fail" } else { "ok" }.to_string(),
        elapsed_ms: elapsed.as_millis() as u64,
        pass_count,
        fail_count: fail_total,
        warn_count: warn_total,
        failures: fails,
        warnings: warns,
        gate_timings: gate_timings
            .into_iter()
            .map(|(name, elapsed)| ValidationGateTiming {
                name: name.to_string(),
                elapsed_ms: elapsed.as_millis() as u64,
            })
            .collect(),
    })
}

pub fn render_validation_report(report: &ValidationReport, verbose: bool) {
    use crate::core::ansi::AnsiExt;

    let intent_content = crate::core::assets::get_doc("specs/INTENT").unwrap_or_default();
    let intent_version =
        extract_md_version(&intent_content).unwrap_or_else(|| "unknown".to_string());

    println!(
        "{} {}",
        "▶".bright_green().bold(),
        "validate".bright_cyan().bold()
    );
    println!(
        "  {} intent_version={}",
        "spec".bright_cyan(),
        intent_version.bright_white()
    );
    println!(
        "  {} {}",
        "gate".bright_magenta().bold(),
        "Four Invariants Gate".bright_white()
    );

    if verbose {
        println!(
            "  {} {}",
            "gates".bright_magenta().bold(),
            "timings".bright_white()
        );
        for gate in &report.gate_timings {
            println!(
                "  {} [{}] {}ms",
                "✓".bright_green(),
                gate.name.bright_cyan(),
                gate.elapsed_ms
            );
        }
    }

    println!(
        "  {} pass={} fail={} warn={} ({:.2}s)",
        "summary".bright_cyan().bold(),
        report.pass_count.to_string().bright_green(),
        report.fail_count.to_string().bright_red(),
        report.warn_count.to_string().bright_yellow(),
        report.elapsed_ms as f64 / 1000.0
    );

    if !report.failures.is_empty() && verbose {
        println!(
            "  {} {}",
            "issues".bright_red().bold(),
            output::preview_messages(&report.failures, 10, 160)
        );
    } else if let Some(first_failure) = report.failures.first() {
        println!(
            "  {} {} found; first item: {}",
            "issues".bright_yellow().bold(),
            report.failures.len().to_string().bright_red(),
            output::compact_line(first_failure, 120)
        );
        println!(
            "  {} run `decapod validate -v` or `decapod validate --format json` for the full list",
            "details".bright_blue().bold()
        );
    }

    if !report.warnings.is_empty() && verbose {
        println!(
            "  {} {}",
            "warnings".bright_yellow().bold(),
            output::preview_messages(&report.warnings, 10, 160)
        );
    } else if !report.warnings.is_empty() {
        println!(
            "  {} {} hidden; use `-v` for warning details",
            "warnings".bright_yellow().bold(),
            report.warnings.len().to_string().bright_yellow()
        );
    }

    if report.fail_count == 0 {
        println!(
            "{} {}",
            "✓".bright_green().bold(),
            "validation passed".bright_green().bold()
        );
    } else {
        println!(
            "{} {}",
            "!".bright_yellow().bold(),
            "validation needs attention".bright_yellow().bold()
        );
    }
}

#[cfg(test)]
mod tests {
    use super::{is_protected_git_branch, parse_ahead_behind_counts};

    #[test]
    fn protected_branch_matching_is_limited_to_protected_refs() {
        assert!(is_protected_git_branch("master"));
        assert!(is_protected_git_branch("main"));
        assert!(is_protected_git_branch("release/2026.05"));
        assert!(!is_protected_git_branch("agent/codex/fix"));
        assert!(!is_protected_git_branch("feature/main-cleanup"));
    }

    #[test]
    fn parses_git_ahead_behind_counts() {
        assert_eq!(parse_ahead_behind_counts("3\t1\n"), Some((3, 1)));
        assert_eq!(parse_ahead_behind_counts("0 12"), Some((0, 12)));
        assert_eq!(parse_ahead_behind_counts("bad 12"), None);
    }
}
