// === m1nd-mcp/src/surgical_handlers.rs ===
//
// surgical_context and apply tool handlers.
//
// surgical_context: returns complete context for an LLM to edit code surgically --
//   reads the target file, fetches graph neighbours (callers, callees, importers),
//   and packages provenance so the editor has everything it needs in one call.
//
// apply: writes LLM-edited code back to a file and triggers an incremental
//   re-ingest so the graph stays coherent with the changed source.
//
// Pattern: identical to layer_handlers.rs -- parse typed input -> call engine -> return output.

use crate::daemon_handlers::{make_daemon_alert, DaemonAlertSeed};
use crate::protocol::{layers, surgical};
use crate::scope::{normalize_path_text, normalize_scope_path};
use crate::session::{EditPreviewState, SessionState};
use m1nd_core::error::{M1ndError, M1ndResult};
use m1nd_core::types::{EdgeIdx, NodeId, NodeType};
use std::collections::{HashMap, HashSet, VecDeque};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Instant, SystemTime, UNIX_EPOCH};

fn proactive_severity_rank(value: &str) -> u8 {
    match value {
        "critical" => 0,
        "warning" => 1,
        _ => 2,
    }
}

fn push_proactive_insight(
    insights: &mut Vec<surgical::ProactiveInsight>,
    insight: surgical::ProactiveInsight,
) {
    insights.push(insight);
}

fn current_repo_prefix(external_id: &str) -> Option<&str> {
    external_id.split_once("::file::").map(|(prefix, _)| prefix)
}

fn likely_test_companions(path: &Path) -> Vec<String> {
    let mut companions = Vec::new();
    let Some(parent) = path.parent() else {
        return companions;
    };
    let Some(stem) = path.file_stem().and_then(|value| value.to_str()) else {
        return companions;
    };
    let ext = path
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or_default();

    let candidates: Vec<PathBuf> = match ext {
        "py" => vec![
            parent.join(format!("test_{}.py", stem)),
            parent.join(format!("{}_test.py", stem)),
            parent.join("tests").join(format!("test_{}.py", stem)),
        ],
        "ts" | "tsx" | "js" | "jsx" => vec![
            parent.join(format!("{}.test.{}", stem, ext)),
            parent.join(format!("{}.spec.{}", stem, ext)),
            parent
                .join("__tests__")
                .join(format!("{}.test.{}", stem, ext)),
        ],
        "go" => vec![parent.join(format!("{}_test.go", stem))],
        "rs" => vec![
            parent.join(format!("{}_test.rs", stem)),
            parent
                .parent()
                .unwrap_or(parent)
                .join("tests")
                .join(format!("{}.rs", stem)),
        ],
        _ => Vec::new(),
    };

    for candidate in candidates {
        if candidate.exists() {
            companions.push(candidate.to_string_lossy().to_string());
        }
    }
    companions
}

fn resolve_external_id_for_node(
    graph: &m1nd_core::graph::Graph,
    node_id: NodeId,
) -> Option<String> {
    graph.id_to_node.iter().find_map(|(interned, &nid)| {
        if nid == node_id {
            Some(graph.strings.resolve(*interned).to_string())
        } else {
            None
        }
    })
}

fn is_file_node(graph: &m1nd_core::graph::Graph, node_id: NodeId) -> bool {
    graph
        .nodes
        .node_type
        .get(node_id.as_usize())
        .is_some_and(|node_type| matches!(node_type, NodeType::File))
}

fn build_proactive_insights(
    state: &SessionState,
    file_path: &str,
    external_id: Option<&str>,
    heuristic_summary: Option<&surgical::SurgicalHeuristicSummary>,
    verification: Option<&surgical::VerificationReport>,
) -> Vec<surgical::ProactiveInsight> {
    let mut insights = Vec::new();

    if let Some(summary) = heuristic_summary {
        if summary.antibody_hits > 0 {
            push_proactive_insight(
                &mut insights,
                surgical::ProactiveInsight {
                    severity: "warning".into(),
                    kind: "antibody_recurrence".into(),
                    message: format!(
                        "{} recurring antibody pattern(s) already point at this edit surface.",
                        summary.antibody_hits
                    ),
                    confidence: (0.45 + (summary.antibody_hits.min(3) as f32 * 0.15)).min(0.95),
                    evidence: vec![format!("antibody_hits={}", summary.antibody_hits)],
                    suggested_tool: Some("antibody_scan".into()),
                    suggested_target: external_id.map(|value| value.to_string()),
                },
            );
        }

        if summary.heuristic_signals.trust_risk_multiplier > 1.1 || summary.risk_score >= 0.45 {
            push_proactive_insight(
                &mut insights,
                surgical::ProactiveInsight {
                    severity: if summary.heuristic_signals.trust_risk_multiplier > 1.35 {
                        "critical".into()
                    } else {
                        "warning".into()
                    },
                    kind: "trust_drop".into(),
                    message: format!(
                        "This file sits in a low-trust zone (trust {:.2}, risk multiplier {:.2}).",
                        summary.heuristic_signals.trust_score,
                        summary.heuristic_signals.trust_risk_multiplier
                    ),
                    confidence: (summary.risk_score + 0.2).min(0.95),
                    evidence: vec![
                        format!("trust_score={:.2}", summary.heuristic_signals.trust_score),
                        format!(
                            "trust_risk_multiplier={:.2}",
                            summary.heuristic_signals.trust_risk_multiplier
                        ),
                    ],
                    suggested_tool: Some("heuristics_surface".into()),
                    suggested_target: Some(file_path.to_string()),
                },
            );
        }

        if summary.heuristic_signals.tremor_observation_count >= 3
            || summary
                .heuristic_signals
                .tremor_magnitude
                .is_some_and(|value| value > 0.0)
        {
            push_proactive_insight(
                &mut insights,
                surgical::ProactiveInsight {
                    severity: "warning".into(),
                    kind: "tremor_hotspot".into(),
                    message: format!(
                        "This file shows tremor activity ({} observations).",
                        summary.heuristic_signals.tremor_observation_count
                    ),
                    confidence: 0.7,
                    evidence: vec![
                        format!(
                            "tremor_observation_count={}",
                            summary.heuristic_signals.tremor_observation_count
                        ),
                        summary
                            .heuristic_signals
                            .tremor_magnitude
                            .map(|value| format!("tremor_magnitude={value:.2}"))
                            .unwrap_or_else(|| "tremor_magnitude=unknown".into()),
                    ],
                    suggested_tool: Some("tremor".into()),
                    suggested_target: external_id.map(|value| value.to_string()),
                },
            );
        }

        if let Some(external_id) = external_id {
            let prefix = current_repo_prefix(external_id).unwrap_or_default();
            if let Some(cross_repo_target) = summary.top_affected.iter().find(|target| {
                current_repo_prefix(target).is_some_and(|target_prefix| {
                    !target_prefix.is_empty() && target_prefix != prefix
                })
            }) {
                push_proactive_insight(
                    &mut insights,
                    surgical::ProactiveInsight {
                        severity: "warning".into(),
                        kind: "cross_repo_contract_risk".into(),
                        message: "This edit appears to reverberate into a federated repo boundary."
                            .into(),
                        confidence: 0.72,
                        evidence: vec![cross_repo_target.clone()],
                        suggested_tool: Some("why".into()),
                        suggested_target: Some(cross_repo_target.clone()),
                    },
                );
            }

            let graph = state.graph.read();
            if let Some((file_node, _)) = find_nodes_for_file(&graph, file_path)
                .into_iter()
                .find(|(node_id, ext)| is_file_node(&graph, *node_id) && ext.starts_with("file::"))
            {
                let co_changes = state.temporal.co_change.predict(file_node, 1);
                if let Some(partner) = co_changes.first() {
                    if let Some(target_ext_id) =
                        resolve_external_id_for_node(&graph, partner.target)
                    {
                        push_proactive_insight(
                            &mut insights,
                            surgical::ProactiveInsight {
                                severity: "info".into(),
                                kind: "co_change_prediction".into(),
                                message: "Historical co-change suggests another file likely needs attention."
                                    .into(),
                                confidence: partner.strength.get().clamp(0.0, 1.0),
                                evidence: vec![
                                    format!("co_change_strength={:.2}", partner.strength.get()),
                                    target_ext_id.clone(),
                                ],
                                suggested_tool: Some("batch_view".into()),
                                suggested_target: Some(target_ext_id),
                            },
                        );
                    }
                }
            }
        }
    }

    let companion_tests = likely_test_companions(Path::new(file_path));
    let tests_run = verification
        .and_then(|report| report.tests_run)
        .unwrap_or(0);
    if !companion_tests.is_empty() && tests_run == 0 {
        push_proactive_insight(
            &mut insights,
            surgical::ProactiveInsight {
                severity: "warning".into(),
                kind: "untouched_test_companion".into(),
                message: "A nearby test companion exists but was not exercised by this write."
                    .into(),
                confidence: 0.78,
                evidence: companion_tests.clone(),
                suggested_tool: Some("batch_view".into()),
                suggested_target: companion_tests.first().cloned(),
            },
        );
    }

    if let Some(report) = verification {
        if report.layer_violations.iter().any(|entry| {
            let lower = entry.to_lowercase();
            lower.contains("schema") || lower.contains("proto") || lower.contains("openapi")
        }) {
            push_proactive_insight(
                &mut insights,
                surgical::ProactiveInsight {
                    severity: "warning".into(),
                    kind: "schema_contract_drift".into(),
                    message: "The verification surface hints at contract/schema drift introduced by this write.".into(),
                    confidence: 0.76,
                    evidence: report
                        .layer_violations
                        .iter()
                        .filter(|entry| {
                            let lower = entry.to_lowercase();
                            lower.contains("schema") || lower.contains("proto") || lower.contains("openapi")
                        })
                        .take(3)
                        .cloned()
                        .collect(),
                    suggested_tool: Some("federate_auto".into()),
                    suggested_target: Some(file_path.to_string()),
                },
            );
        }
    }

    insights.sort_by(|a, b| {
        proactive_severity_rank(&a.severity)
            .cmp(&proactive_severity_rank(&b.severity))
            .then_with(|| b.confidence.total_cmp(&a.confidence))
    });
    insights.truncate(3);
    insights
}

pub(crate) fn daemon_proactive_insights_for_file(
    state: &SessionState,
    file_path: &str,
    verification: Option<&surgical::VerificationReport>,
) -> Vec<surgical::ProactiveInsight> {
    let graph = state.graph.read();
    let file_node = find_nodes_for_file(&graph, file_path)
        .into_iter()
        .find(|(node_id, ext)| is_file_node(&graph, *node_id) && ext.starts_with("file::"))
        .map(|(_, ext)| ext);
    drop(graph);

    let heuristic_summary = file_node
        .as_deref()
        .map(|external_id| build_surgical_heuristic_summary(state, external_id, file_path));
    build_proactive_insights(
        state,
        file_path,
        file_node.as_deref(),
        heuristic_summary.as_ref(),
        verification,
    )
}

/// Map a `m1nd_core` NodeType to the antibody `node_type` string vocabulary
/// (the set understood by `ab_str_to_node_type`). Returns `None` for
/// `Custom(_)`, which the antibody matcher treats as unrecognized/lenient — in
/// that case we omit `node_type` and lean on `label_contains` instead so we
/// never emit a constraint that is silently dropped.
fn antibody_node_type_str(nt: &NodeType) -> Option<&'static str> {
    match nt {
        NodeType::File => Some("file"),
        NodeType::Directory => Some("directory"),
        NodeType::Function => Some("function"),
        NodeType::Class => Some("class"),
        NodeType::Struct => Some("struct"),
        NodeType::Enum => Some("enum"),
        NodeType::Type => Some("type"),
        NodeType::Module => Some("module"),
        NodeType::Reference => Some("reference"),
        NodeType::Concept => Some("concept"),
        NodeType::Material => Some("material"),
        NodeType::Process => Some("process"),
        NodeType::Product => Some("product"),
        NodeType::Supplier => Some("supplier"),
        NodeType::Regulatory => Some("regulatory"),
        NodeType::System => Some("system"),
        NodeType::Cost => Some("cost"),
        NodeType::Custom(_) => None,
    }
}

/// Pick a discriminating `label_contains` fragment from a node label. For symbol
/// labels (function/struct/... names) we want a token that generalizes across
/// the codebase, not the whole verbose id. Strips any `file::`/`::`-segmented
/// id down to its last segment, then takes a meaningful identifier token. Returns
/// `None` when nothing usable remains (e.g. an empty or path-only label).
fn antibody_label_fragment(label: &str) -> Option<String> {
    let last = label.rsplit("::").next().unwrap_or(label).trim();
    // Drop a leading "file::"-style path tail or empty values.
    let last = last.trim_start_matches("file::").trim();
    if last.is_empty() || last.contains('/') {
        return None;
    }
    // Prefer a single identifier-ish token (split on common separators) that is
    // long enough to be discriminating but short enough to generalize.
    let token = last
        .split(['(', ' ', '<', '['])
        .next()
        .unwrap_or(last)
        .trim();
    if token.len() < 3 {
        return None;
    }
    Some(token.to_string())
}

/// Build a `proposed_antibody` ProactiveInsight from a flagged finding the agent
/// just fixed by editing `node_id`. Consumes the finding (so each fix proposes
/// once) and derives a MINIMAL VIABLE antibody template from the edited node's
/// live structural signature (seed node = node_type + a discriminating label
/// fragment). The insight carries a ready-to-run `antibody_create` call: the
/// complete `arguments` object is embedded as a compact JSON string in
/// `evidence[0]` so the agent can send it as-is. This is a PROPOSAL only — it
/// never writes to the antibody store.
///
/// Honest degradation: if no usable structural signature can be derived from the
/// node (e.g. a file-level finding whose only label is a path), the proposal
/// still emits a syntactically valid `antibody_create` call seeded with whatever
/// is known plus a `refine` note in the message — it never fabricates an edge or
/// relation that may not exist in the graph (which would produce a non-matching
/// antibody). Returns `None` when there is no flagged finding for this
/// `(agent_id, node_id)`.
fn build_proposed_antibody_insight(
    state: &mut SessionState,
    agent_id: &str,
    node_id: &str,
) -> Option<surgical::ProactiveInsight> {
    let mark = state.take_finding(agent_id, node_id)?;

    // Resolve the edited node's live structural signature from the graph.
    let (node_type_str, label_fragment): (Option<&'static str>, Option<String>) = {
        let graph = state.graph.read();
        let mut found: Option<NodeId> = None;
        for (interned, &nid) in &graph.id_to_node {
            if graph.strings.resolve(*interned) == node_id {
                found = Some(nid);
                break;
            }
        }
        if let Some(nid) = found {
            let idx = nid.as_usize();
            let nt = antibody_node_type_str(&graph.nodes.node_type[idx]);
            let label = graph.strings.resolve(graph.nodes.label[idx]).to_string();
            (nt, antibody_label_fragment(&label))
        } else {
            (None, antibody_label_fragment(node_id))
        }
    };

    // Assemble the single seed pattern node. We deliberately emit ONLY the seed
    // node (no fabricated edges) so the antibody is guaranteed to match at least
    // structurally-similar nodes; edges/relations would risk a non-matching
    // pattern unless mined from the real graph vocabulary.
    let mut seed = serde_json::Map::new();
    seed.insert("role".to_string(), serde_json::json!("site"));
    let mut have_constraint = false;
    if let Some(nt) = node_type_str {
        seed.insert("node_type".to_string(), serde_json::json!(nt));
        have_constraint = true;
    }
    if let Some(frag) = &label_fragment {
        seed.insert("label_contains".to_string(), serde_json::json!(frag));
        have_constraint = true;
    }

    // Specificity floor honesty: a seed with neither a node_type nor a label
    // fragment would be below MIN_SPECIFICITY and rejected by antibody_create.
    // In that degraded case fall back to a structural node_type only when we have
    // one, else there is genuinely nothing to seed from — surface a refine-only
    // proposal that the agent must complete by hand.
    let needs_refine = label_fragment.is_none() || node_type_str.is_none();

    let antibody_name = format!("{}-recurrence", mark.kind);
    let description = if needs_refine {
        format!(
            "Auto-proposed from a fixed `{}` finding at {}. Seed is structural-only — refine the pattern (add a discriminating label_contains and/or an edge) before relying on it.",
            mark.kind, node_id
        )
    } else {
        format!(
            "Auto-proposed from a fixed `{}` finding so the next audit catches the same structural bug elsewhere.",
            mark.kind
        )
    };

    let pattern = serde_json::json!({
        "nodes": [ serde_json::Value::Object(seed) ],
        "edges": []
    });
    let arguments = serde_json::json!({
        "agent_id": agent_id,
        "action": "create",
        "name": antibody_name,
        "description": description,
        "severity": mark.severity,
        "pattern": pattern,
    });
    let arguments_str = serde_json::to_string(&arguments).unwrap_or_else(|_| "{}".to_string());

    let message = if !have_constraint {
        format!(
            "You just fixed a `{}` finding here. No structural signature could be derived from this node — refine the proposed antibody_create pattern (node_type + label_contains) so the next agent's audit catches this bug elsewhere.",
            mark.kind
        )
    } else if needs_refine {
        format!(
            "You just fixed a `{}` finding here. Proposed a ready-to-run antibody_create (structural seed) — refine its label_contains/edges, then send it so the next audit catches this bug elsewhere.",
            mark.kind
        )
    } else {
        format!(
            "You just fixed a `{}` finding here. Proposed a ready-to-run antibody_create so the next agent's audit catches the same structural bug elsewhere — send it as-is to seed the antibody.",
            mark.kind
        )
    };

    let severity = match mark.severity.as_str() {
        "critical" => "critical",
        "info" => "info",
        _ => "warning",
    };

    Some(surgical::ProactiveInsight {
        severity: severity.to_string(),
        kind: "proposed_antibody".to_string(),
        message,
        confidence: if needs_refine { 0.55 } else { 0.8 },
        evidence: vec![
            // evidence[0] is the COMPLETE, ready-to-run antibody_create arguments
            // object as a compact JSON string — the agent sends it as-is.
            arguments_str,
            format!("finding_kind={}", mark.kind),
            format!("finding_severity={}", mark.severity),
            format!("flagged_node={}", node_id),
        ],
        suggested_tool: Some("antibody_create".to_string()),
        suggested_target: Some(node_id.to_string()),
    })
}

/// Collect candidate external node ids for `file_path` and, for each that has a
/// flagged finding recorded by `agent_id` this session, emit a `proposed_antibody`
/// insight. `primary_ids` (e.g. `updated_node_ids`) are checked first; the rest
/// are gathered from the graph so a symbol-level finding still joins a file-level
/// write. Consumes each matched finding (proposes once).
fn propose_antibodies_for_write(
    state: &mut SessionState,
    agent_id: &str,
    file_path: &str,
    primary_ids: &[String],
) -> Vec<surgical::ProactiveInsight> {
    // Build the candidate id set: explicit updated ids + every graph node for the
    // written file. Dedup while preserving the primary-first order.
    let mut candidates: Vec<String> = Vec::new();
    let mut seen: HashSet<String> = HashSet::new();
    for id in primary_ids {
        if seen.insert(id.clone()) {
            candidates.push(id.clone());
        }
    }
    {
        let graph = state.graph.read();
        for (_, ext_id) in find_nodes_for_file(&graph, file_path) {
            if seen.insert(ext_id.clone()) {
                candidates.push(ext_id);
            }
        }
    }

    let mut out = Vec::new();
    for id in &candidates {
        if let Some(insight) = build_proposed_antibody_insight(state, agent_id, id) {
            out.push(insight);
        }
    }
    out
}

pub(crate) fn persist_daemon_alerts_from_insights(
    state: &mut SessionState,
    proactive_insights: &[surgical::ProactiveInsight],
    default_file_path: Option<&str>,
    default_node_id: Option<&str>,
) -> usize {
    if !state.daemon_state.active || proactive_insights.is_empty() {
        return 0;
    }

    let tick_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|value| value.as_millis() as u64)
        .unwrap_or(0);
    state.daemon_state.last_tick_ms = Some(tick_ms);

    let emitted_count = proactive_insights.iter().take(3).count();
    for insight in proactive_insights.iter().take(3) {
        let alert_file_path = insight
            .suggested_target
            .as_deref()
            .and_then(|target| target.strip_prefix("file::").or(Some(target)))
            .map(str::to_string)
            .or_else(|| default_file_path.map(str::to_string));
        let alert_node_id = insight
            .suggested_target
            .as_deref()
            .filter(|target| target.starts_with("file::"))
            .map(str::to_string)
            .or_else(|| default_node_id.map(str::to_string));
        let alert = make_daemon_alert(DaemonAlertSeed {
            severity: insight.severity.clone(),
            kind: insight.kind.clone(),
            message: insight.message.clone(),
            confidence: insight.confidence,
            evidence: insight.evidence.clone(),
            suggested_tool: insight.suggested_tool.clone(),
            suggested_target: insight.suggested_target.clone(),
            file_path: alert_file_path,
            node_id: alert_node_id,
        });
        state.record_daemon_alert(alert);
    }

    if let Err(error) = state.persist_daemon_state() {
        eprintln!(
            "[m1nd] WARNING: daemon state persist failed during apply: {}",
            error
        );
    }
    if let Err(error) = state.persist_daemon_alerts() {
        eprintln!(
            "[m1nd] WARNING: daemon alert persist failed during apply: {}",
            error
        );
    }
    emitted_count
}

fn surgical_dampened_trust_factor(raw_factor: f32) -> f32 {
    1.0 + (raw_factor - 1.0) * 0.2
}

fn surgical_dampened_tremor_factor(alert: Option<&m1nd_core::tremor::TremorAlert>) -> f32 {
    1.0 + alert.map_or(0.0, |value| value.magnitude.min(1.0) * 0.1)
}

fn surgical_antibody_hits(state: &SessionState, external_id: &str, file_path: &str) -> usize {
    state
        .antibodies
        .iter()
        .filter(|antibody| {
            antibody.enabled
                && antibody
                    .source_nodes
                    .iter()
                    .any(|source| source == external_id || source.ends_with(file_path))
        })
        .count()
}

fn surgical_heuristic_reason(
    trust_factor: f32,
    tremor_factor: f32,
    tremor_observation_count: usize,
    antibody_hits: usize,
    blast_risk: &str,
) -> String {
    let mut parts = Vec::new();
    if trust_factor > 1.01 {
        parts.push("low-trust risk prior".to_string());
    } else if trust_factor < 0.99 {
        parts.push("high-trust damping".to_string());
    }
    if tremor_factor > 1.01 && tremor_observation_count > 0 {
        parts.push("tremor acceleration".to_string());
    }
    if antibody_hits > 0 {
        parts.push(format!("immune-memory recurrence x{}", antibody_hits));
    }
    if blast_risk != "low" {
        parts.push(format!("{} blast radius", blast_risk));
    }
    if parts.is_empty() {
        "neutral heuristics".to_string()
    } else {
        parts.join(" + ")
    }
}

pub(crate) fn build_surgical_heuristic_summary(
    state: &SessionState,
    external_id: &str,
    file_path: &str,
) -> surgical::SurgicalHeuristicSummary {
    build_surgical_heuristic_summary_with_extra_paths(state, external_id, file_path, &[])
}

fn build_surgical_heuristic_summary_with_extra_paths(
    state: &SessionState,
    external_id: &str,
    file_path: &str,
    extra_file_paths: &[&str],
) -> surgical::SurgicalHeuristicSummary {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|duration| duration.as_secs_f64())
        .unwrap_or(0.0);
    let mut aliases = surgical_external_id_aliases(state, external_id, file_path);
    for extra_path in extra_file_paths {
        for alias in surgical_external_id_aliases(state, external_id, extra_path) {
            if !aliases.contains(&alias) {
                aliases.push(alias);
            }
        }
    }
    let trust = aliases
        .iter()
        .map(|alias| state.trust_ledger.compute_trust(alias, now))
        .max_by(|left, right| left.risk_multiplier.total_cmp(&right.risk_multiplier))
        .unwrap_or_else(|| state.trust_ledger.compute_trust(external_id, now));
    let raw_trust_factor = aliases
        .iter()
        .map(|alias| {
            state
                .trust_ledger
                .adjust_prior(1.0, std::slice::from_ref(alias), false, now)
        })
        .fold(1.0f32, f32::max);
    let trust_factor = surgical_dampened_trust_factor(raw_trust_factor);

    let tremor_alias = aliases
        .iter()
        .max_by_key(|alias| state.tremor_registry.observation_count(alias))
        .map(String::as_str)
        .unwrap_or(external_id);
    let tremor_observation_count = state.tremor_registry.observation_count(tremor_alias);
    let tremor_alert = if tremor_observation_count < 3 {
        None
    } else {
        state
            .tremor_registry
            .analyze(
                m1nd_core::tremor::TremorWindow::All,
                0.0,
                1,
                Some(tremor_alias),
                now,
                0,
            )
            .tremors
            .into_iter()
            .next()
    };
    let tremor_factor = surgical_dampened_tremor_factor(tremor_alert.as_ref());

    let antibody_hits = surgical_antibody_hits(state, external_id, file_path);
    let antibody_factor = 1.0 + (antibody_hits.min(3) as f32 * 0.05);

    let (blast_radius_files, top_affected) = {
        let graph = state.graph.read();
        compute_blast_radius(&graph, file_path)
    };
    let blast_radius_risk = blast_radius_risk(blast_radius_files).to_string();
    let blast_factor = match blast_radius_risk.as_str() {
        "high" => 1.15,
        "medium" => 1.05,
        _ => 1.0,
    };

    let heuristic_factor = trust_factor * tremor_factor * antibody_factor * blast_factor;
    let trust_risk = ((trust.risk_multiplier - 1.0) / 2.0).clamp(0.0, 1.0);
    let tremor_risk = tremor_alert
        .as_ref()
        .map(|alert| alert.magnitude.clamp(0.0, 1.0))
        .unwrap_or(0.0);
    let antibody_risk = (antibody_hits.min(3) as f32 / 3.0).clamp(0.0, 1.0);
    let blast_risk_score = match blast_radius_risk.as_str() {
        "high" => 1.0,
        "medium" => 0.5,
        _ => 0.0,
    };
    let risk_score =
        (trust_risk * 0.4 + tremor_risk * 0.25 + antibody_risk * 0.15 + blast_risk_score * 0.2)
            .min(1.0);
    let risk_level = if risk_score >= 0.75 || blast_radius_risk == "high" {
        "high"
    } else if risk_score >= 0.35 || blast_radius_risk == "medium" {
        "medium"
    } else {
        "low"
    };

    surgical::SurgicalHeuristicSummary {
        risk_level: risk_level.to_string(),
        risk_score,
        blast_radius_files,
        blast_radius_risk: blast_radius_risk.clone(),
        top_affected,
        antibody_hits,
        heuristic_signals: crate::protocol::layers::HeuristicSignals {
            heuristic_factor,
            trust_score: trust.trust_score,
            trust_risk_multiplier: trust.risk_multiplier,
            trust_tier: format!("{:?}", trust.tier),
            tremor_magnitude: tremor_alert.as_ref().map(|alert| alert.magnitude),
            tremor_observation_count,
            tremor_risk_level: tremor_alert
                .as_ref()
                .map(|alert| format!("{:?}", alert.risk_level)),
            reason: surgical_heuristic_reason(
                trust_factor,
                tremor_factor,
                tremor_observation_count,
                antibody_hits,
                &blast_radius_risk,
            ),
        },
    }
}

fn surgical_external_id_aliases(
    state: &SessionState,
    external_id: &str,
    file_path: &str,
) -> Vec<String> {
    let mut aliases = Vec::new();
    let mut candidates = vec![
        external_id.to_string(),
        format!("file::{file_path}"),
        format!("file::{}", normalize_path_text(file_path)),
    ];
    for raw in [
        external_id.strip_prefix("file::").unwrap_or(external_id),
        file_path,
    ] {
        let stripped = strip_windows_extended_path_prefix_raw(raw);
        candidates.push(format!("file::{stripped}"));
    }
    for raw in [external_id, file_path] {
        if let Some(scope) = normalize_scope_path(Some(raw), &state.ingest_roots) {
            candidates.push(format!("file::{scope}"));
        }
    }
    push_graph_path_aliases(state, file_path, &mut candidates);
    push_heuristic_memory_aliases(state, external_id, file_path, &mut candidates);
    for root in &state.ingest_roots {
        let root_norm = normalize_path_text(root).trim_end_matches('/').to_string();
        let file_norm = normalize_path_text(file_path);
        if !root_norm.is_empty() {
            let root_prefix = format!("{root_norm}/");
            let file_cmp;
            let root_cmp;
            #[cfg(windows)]
            {
                file_cmp = file_norm.to_ascii_lowercase();
                root_cmp = root_prefix.to_ascii_lowercase();
            }
            #[cfg(not(windows))]
            {
                file_cmp = file_norm.clone();
                root_cmp = root_prefix.clone();
            }
            if file_cmp.starts_with(&root_cmp) {
                candidates.push(format!("file::{}", &file_norm[root_prefix.len()..]));
            }
        }
        if let Ok(rel) = Path::new(file_path).strip_prefix(Path::new(root)) {
            candidates.push(format!(
                "file::{}",
                normalize_path_text(&rel.to_string_lossy())
            ));
        }
    }
    for candidate in candidates {
        if !candidate.is_empty() && !aliases.contains(&candidate) {
            aliases.push(candidate);
        }
    }
    aliases
}

fn push_graph_path_aliases(state: &SessionState, file_path: &str, candidates: &mut Vec<String>) {
    let graph = state.graph.read();
    for (index, provenance) in graph.nodes.provenance.iter().enumerate() {
        let Some(source_path) = provenance.source_path else {
            continue;
        };
        let Some(path_text) = graph.strings.try_resolve(source_path) else {
            continue;
        };
        if !surgical_paths_match(path_text, file_path) {
            continue;
        }

        let node_id = NodeId::new(index as u32);
        candidates.push(resolve_external_id(&graph, node_id));
        candidates.push(format!("file::{path_text}"));
        candidates.push(format!("file::{}", normalize_path_text(path_text)));
        if let Some(scope) = normalize_scope_path(Some(path_text), &state.ingest_roots) {
            candidates.push(format!("file::{scope}"));
        }
    }
}

fn push_heuristic_memory_aliases(
    state: &SessionState,
    external_id: &str,
    file_path: &str,
    candidates: &mut Vec<String>,
) {
    for alias in state.trust_ledger.external_ids() {
        if surgical_alias_targets_file(alias, external_id, file_path) {
            candidates.push(alias.to_string());
        }
    }
    for alias in state.tremor_registry.external_ids() {
        if surgical_alias_targets_file(alias, external_id, file_path) {
            candidates.push(alias.to_string());
        }
    }
}

fn surgical_alias_targets_file(alias: &str, external_id: &str, file_path: &str) -> bool {
    if surgical_id_text_matches(alias, external_id) {
        return true;
    }

    let alias_path = alias.strip_prefix("file::").unwrap_or(alias);
    let external_path = external_id.strip_prefix("file::").unwrap_or(external_id);
    surgical_paths_match(alias_path, file_path) || surgical_paths_match(alias_path, external_path)
}

fn surgical_id_text_matches(left: &str, right: &str) -> bool {
    #[cfg(windows)]
    {
        left.eq_ignore_ascii_case(right)
    }
    #[cfg(not(windows))]
    {
        left == right
    }
}

fn strip_windows_extended_path_prefix_raw(value: &str) -> String {
    if let Some(rest) = value.strip_prefix(r"\\?\UNC\") {
        return format!(r"\\{rest}");
    }
    if let Some(rest) = value.strip_prefix(r"\\?\") {
        return rest.to_string();
    }
    value.to_string()
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Resolve a file_path input to a concrete path.
/// Absolute inputs pass through unchanged.
/// Relative inputs prefer the most recent ingest root that already contains
/// the file, then fall back to the newest ingest root for new paths.
fn resolve_file_path(file_path: &str, ingest_roots: &[String]) -> PathBuf {
    let p = Path::new(file_path);
    if p.is_absolute() {
        p.to_path_buf()
    } else {
        let mut matched_paths: Vec<PathBuf> = Vec::new();
        for root in ingest_roots.iter().rev() {
            let candidate = Path::new(root).join(file_path);
            if candidate.exists() {
                matched_paths.push(candidate);
            }
        }

        if let Some(resolved) = matched_paths.first() {
            if matched_paths.len() > 1 {
                eprintln!(
                    "[m1nd] WARNING: ambiguous relative path '{}' matched {} ingest roots; using most recent match {}",
                    file_path,
                    matched_paths.len(),
                    resolved.display()
                );
            }
            resolved.clone()
        } else if let Some(root) = ingest_roots.last() {
            Path::new(root).join(file_path)
        } else {
            p.to_path_buf()
        }
    }
}

/// Deny-list: m1nd state files that must never be overwritten by apply/apply_batch.
const DENIED_FILENAMES: &[&str] = &[
    "graph_snapshot.json",
    "graph.json",
    "plasticity_state.json",
    "plasticity.json",
    "antibodies.json",
    "tremor_state.json",
    "trust_state.json",
];

/// Validate that a path is within allowed workspace roots.
/// Returns Ok(canonical_path) or Err if path traversal is detected.
///
/// BUG FIX (E4): When ingest_roots is empty, REFUSE all writes instead of
/// allowing any path. At least one ingest must happen before any apply.
///
/// BUG FIX (E3): Deny-list prevents overwriting m1nd's own state files.
fn validate_path_safety(resolved: &Path, ingest_roots: &[String]) -> M1ndResult<PathBuf> {
    // BUG FIX (E4): Block all writes when no ingest roots configured
    if ingest_roots.is_empty() {
        return Err(invalid_params_with_hint(
            "m1nd_apply",
            format!(
                "path {} cannot be written: no ingest roots configured",
                resolved.display()
            ),
            "Run ingest on the workspace first, then retry apply/apply_batch inside that ingested root.",
        ));
    }

    // Canonicalize the resolved path (follows symlinks, resolves ..)
    // For new files that don't exist yet, canonicalize the parent directory
    let canonical = if resolved.exists() {
        resolved.canonicalize().map_err(|e| {
            invalid_params_with_hint(
                "m1nd_apply",
                format!("cannot resolve path {}: {}", resolved.display(), e),
                "Pass an absolute file path or a path relative to an ingested workspace root.",
            )
        })?
    } else {
        // File doesn't exist yet: canonicalize parent + append filename
        let parent = resolved.parent().unwrap_or(Path::new("."));
        let filename = resolved.file_name().unwrap_or_default();
        let parent_canonical = parent
            .canonicalize()
            .map_err(|e| {
                invalid_params_with_hint(
                    "m1nd_apply",
                    format!("cannot resolve parent directory {}: {}", parent.display(), e),
                    "Create the parent directory under an ingested workspace root, or choose a file_path that already lives inside one.",
                )
            })?;
        parent_canonical.join(filename)
    };

    // BUG FIX (E3): Deny-list for m1nd state files
    if let Some(filename) = canonical.file_name().and_then(|f| f.to_str()) {
        if DENIED_FILENAMES.contains(&filename) {
            return Err(invalid_params_with_hint(
                "m1nd_apply",
                format!(
                    "path {} is a protected m1nd state file and cannot be overwritten",
                    resolved.display()
                ),
                "Write to a source file in your workspace instead. m1nd runtime files like graph/plasticity state are intentionally protected.",
            ));
        }
    }

    // Check that canonical path starts with at least one ingest root
    for root in ingest_roots {
        if let Ok(root_canonical) = Path::new(root).canonicalize() {
            if canonical.starts_with(&root_canonical) {
                return Ok(canonical);
            }
        }
    }

    Err(invalid_params_with_hint(
        "m1nd_apply",
        format!(
            "path {} is outside allowed workspace roots",
            resolved.display()
        ),
        "Retry with a file_path under one of the ingested workspace roots, or ingest the intended workspace before writing.",
    ))
}

/// Simple line-based diff summary: count added and removed lines.
fn diff_summary(old: &str, new: &str) -> (i32, i32) {
    let old_lines: Vec<&str> = old.lines().collect();
    let new_lines: Vec<&str> = new.lines().collect();
    let old_set: HashSet<&str> = old_lines.iter().copied().collect();
    let new_set: HashSet<&str> = new_lines.iter().copied().collect();

    let removed = old_lines.iter().filter(|l| !new_set.contains(**l)).count() as i32;
    let added = new_lines.iter().filter(|l| !old_set.contains(**l)).count() as i32;
    (added, removed)
}

fn content_hash(content: &str) -> String {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    content.hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

fn invalid_params(tool: &str, detail: impl Into<String>) -> M1ndError {
    M1ndError::InvalidParams {
        tool: tool.into(),
        detail: detail.into(),
    }
}

fn invalid_params_with_hint(
    tool: &str,
    detail: impl Into<String>,
    hint: impl AsRef<str>,
) -> M1ndError {
    let detail = detail.into();
    let hint = hint.as_ref();
    invalid_params(tool, format!("{detail} Hint: {hint}"))
}

fn invalid_params_with_hint_and_example(
    tool: &str,
    detail: impl Into<String>,
    hint: impl AsRef<str>,
    example: impl AsRef<str>,
) -> M1ndError {
    let detail = detail.into();
    let hint = hint.as_ref();
    let example = example.as_ref();
    invalid_params(tool, format!("{detail} Hint: {hint} Example: {example}"))
}

fn unified_diff_preview(old: &str, new: &str) -> String {
    if old == new {
        return "".to_string();
    }
    let old_lines: Vec<&str> = old.lines().collect();
    let new_lines: Vec<&str> = new.lines().collect();
    let mut out = String::new();
    out.push_str("--- source\n+++ candidate\n");
    let max_len = old_lines.len().max(new_lines.len());
    for i in 0..max_len {
        match (old_lines.get(i), new_lines.get(i)) {
            (Some(a), Some(b)) if a == b => {}
            (Some(a), Some(b)) => {
                out.push_str(&format!("-{}\n+{}\n", a, b));
            }
            (Some(a), None) => {
                out.push_str(&format!("-{}\n", a));
            }
            (None, Some(b)) => {
                out.push_str(&format!("+{}\n", b));
            }
            (None, None) => {}
        }
        if out.lines().count() > 120 {
            out.push_str("... (truncated)\n");
            break;
        }
    }
    out
}

/// Extract symbols from file content (lightweight heuristic parser).
/// Works for Rust, Python, TypeScript/JavaScript, Go.
fn extract_symbols(content: &str, file_path: &str) -> Vec<surgical::SurgicalSymbol> {
    let ext = Path::new(file_path)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");

    let lines: Vec<&str> = content.lines().collect();
    let mut symbols = Vec::new();

    match ext {
        "rs" => extract_rust_symbols(&lines, &mut symbols),
        "py" => extract_python_symbols(&lines, &mut symbols),
        "ts" | "tsx" | "js" | "jsx" => extract_ts_symbols(&lines, &mut symbols),
        "go" => extract_go_symbols(&lines, &mut symbols),
        _ => {} // Unknown language, no symbol extraction
    }

    symbols
}

fn extract_rust_symbols(lines: &[&str], symbols: &mut Vec<surgical::SurgicalSymbol>) {
    let mut i = 0;
    while i < lines.len() {
        let trimmed = lines[i].trim();
        let line_num = (i + 1) as u32;

        // Match: pub fn, fn, pub struct, struct, pub enum, enum, pub trait, trait, impl
        let (name, sym_type) = if let Some(rest) = trimmed
            .strip_prefix("pub fn ")
            .or_else(|| trimmed.strip_prefix("pub(crate) fn "))
            .or_else(|| trimmed.strip_prefix("pub(super) fn "))
        {
            (extract_identifier(rest), "function")
        } else if let Some(rest) = trimmed.strip_prefix("fn ") {
            if !trimmed.starts_with("fn ")
                || trimmed.contains("//")
                    && trimmed.find("//").unwrap() < trimmed.find("fn").unwrap_or(0)
            {
                i += 1;
                continue;
            }
            (extract_identifier(rest), "function")
        } else if let Some(rest) = trimmed
            .strip_prefix("pub struct ")
            .or_else(|| trimmed.strip_prefix("pub(crate) struct "))
        {
            (extract_identifier(rest), "struct")
        } else if let Some(rest) = trimmed.strip_prefix("struct ") {
            (extract_identifier(rest), "struct")
        } else if let Some(rest) = trimmed
            .strip_prefix("pub enum ")
            .or_else(|| trimmed.strip_prefix("pub(crate) enum "))
        {
            (extract_identifier(rest), "enum")
        } else if let Some(rest) = trimmed.strip_prefix("enum ") {
            (extract_identifier(rest), "enum")
        } else if let Some(rest) = trimmed
            .strip_prefix("pub trait ")
            .or_else(|| trimmed.strip_prefix("pub(crate) trait "))
        {
            (extract_identifier(rest), "trait")
        } else if let Some(rest) = trimmed.strip_prefix("impl ") {
            (extract_identifier(rest), "impl")
        } else {
            i += 1;
            continue;
        };

        if name.is_empty() {
            i += 1;
            continue;
        }

        // Find the end of this symbol: track brace depth
        let line_end = find_brace_end(lines, i);
        let excerpt = build_excerpt(lines, i, line_end);

        symbols.push(surgical::SurgicalSymbol {
            name,
            symbol_type: sym_type.to_string(),
            line_start: line_num,
            line_end: (line_end + 1) as u32,
            excerpt: Some(excerpt),
        });

        i = line_end + 1;
    }
}

fn extract_python_symbols(lines: &[&str], symbols: &mut Vec<surgical::SurgicalSymbol>) {
    for (i, line) in lines.iter().enumerate() {
        let trimmed = line.trim();
        let line_num = (i + 1) as u32;

        let (name, sym_type) = if let Some(rest) = trimmed.strip_prefix("def ") {
            (extract_identifier(rest), "function")
        } else if let Some(rest) = trimmed.strip_prefix("class ") {
            (extract_identifier(rest), "class")
        } else if let Some(rest) = trimmed.strip_prefix("async def ") {
            (extract_identifier(rest), "function")
        } else {
            continue;
        };

        if name.is_empty() {
            continue;
        }

        // Find end by indentation: next line at same or lower indent level
        let base_indent = line.len() - line.trim_start().len();
        let mut end = i;
        #[allow(clippy::needless_range_loop)]
        for j in (i + 1)..lines.len() {
            let next = lines[j];
            if next.trim().is_empty() {
                continue;
            }
            let next_indent = next.len() - next.trim_start().len();
            if next_indent <= base_indent {
                break;
            }
            end = j;
        }

        let excerpt = build_excerpt(lines, i, end);
        symbols.push(surgical::SurgicalSymbol {
            name,
            symbol_type: sym_type.to_string(),
            line_start: line_num,
            line_end: (end + 1) as u32,
            excerpt: Some(excerpt),
        });
    }
}

fn extract_ts_symbols(lines: &[&str], symbols: &mut Vec<surgical::SurgicalSymbol>) {
    let mut i = 0;
    while i < lines.len() {
        let trimmed = lines[i].trim();
        let line_num = (i + 1) as u32;

        let (name, sym_type) = if trimmed.contains("function ") {
            let after = trimmed.split("function ").nth(1).unwrap_or("");
            (extract_identifier(after), "function")
        } else if trimmed.contains("class ") {
            let after = trimmed.split("class ").nth(1).unwrap_or("");
            (extract_identifier(after), "class")
        } else if trimmed.starts_with("export ") && trimmed.contains("const ") {
            let after = trimmed.split("const ").nth(1).unwrap_or("");
            (extract_identifier(after), "const")
        } else if trimmed.starts_with("interface ") || trimmed.starts_with("export interface ") {
            let after = trimmed.split("interface ").nth(1).unwrap_or("");
            (extract_identifier(after), "interface")
        } else {
            i += 1;
            continue;
        };

        if name.is_empty() {
            i += 1;
            continue;
        }

        let line_end = find_brace_end(lines, i);
        let excerpt = build_excerpt(lines, i, line_end);

        symbols.push(surgical::SurgicalSymbol {
            name,
            symbol_type: sym_type.to_string(),
            line_start: line_num,
            line_end: (line_end + 1) as u32,
            excerpt: Some(excerpt),
        });

        i = line_end + 1;
    }
}

fn extract_go_symbols(lines: &[&str], symbols: &mut Vec<surgical::SurgicalSymbol>) {
    let mut i = 0;
    while i < lines.len() {
        let trimmed = lines[i].trim();
        let line_num = (i + 1) as u32;

        let (name, sym_type) = if let Some(rest) = trimmed.strip_prefix("func ") {
            (extract_identifier(rest), "function")
        } else if let Some(rest) = trimmed.strip_prefix("type ") {
            let ident = extract_identifier(rest);
            let remainder = rest.get(ident.len()..).unwrap_or("").trim();
            if remainder.starts_with("struct") {
                (ident, "struct")
            } else if remainder.starts_with("interface") {
                (ident, "interface")
            } else {
                (ident, "type")
            }
        } else {
            i += 1;
            continue;
        };

        if name.is_empty() {
            i += 1;
            continue;
        }

        let line_end = find_brace_end(lines, i);
        let excerpt = build_excerpt(lines, i, line_end);

        symbols.push(surgical::SurgicalSymbol {
            name,
            symbol_type: sym_type.to_string(),
            line_start: line_num,
            line_end: (line_end + 1) as u32,
            excerpt: Some(excerpt),
        });

        i = line_end + 1;
    }
}

/// Extract an identifier from the start of a string.
fn extract_identifier(s: &str) -> String {
    s.chars()
        .take_while(|c| c.is_alphanumeric() || *c == '_')
        .collect()
}

/// Find the line index where a brace-delimited block ends.
/// Returns the line index of the closing brace.
fn find_brace_end(lines: &[&str], start: usize) -> usize {
    let mut depth: i32 = 0;
    let mut found_open = false;

    #[allow(clippy::needless_range_loop)]
    for i in start..lines.len() {
        for ch in lines[i].chars() {
            if ch == '{' {
                depth += 1;
                found_open = true;
            } else if ch == '}' {
                depth -= 1;
                if found_open && depth == 0 {
                    return i;
                }
            }
        }
    }

    // If no closing brace found, return end of file or start + reasonable range
    (start + 50).min(lines.len().saturating_sub(1))
}

/// Build an excerpt from lines (first 20 lines of the symbol).
fn build_excerpt(lines: &[&str], start: usize, end: usize) -> String {
    let max_lines = 20;
    let actual_end = (start + max_lines).min(end + 1).min(lines.len());
    let excerpt_lines: Vec<&str> = lines[start..actual_end].to_vec();
    let mut result = excerpt_lines.join("\n");
    if actual_end <= end {
        result.push_str("\n    // ... (truncated)");
    }
    result
}

/// Collect graph neighbours of a node within a given BFS radius.
/// Returns (callers, callees, test_neighbours).
fn collect_neighbours(
    state: &SessionState,
    node: NodeId,
    radius: u32,
    include_tests: bool,
) -> (
    Vec<surgical::SurgicalNeighbour>,
    Vec<surgical::SurgicalNeighbour>,
    Vec<surgical::SurgicalNeighbour>,
) {
    let graph = state.graph.read();
    let n = graph.num_nodes() as usize;
    let idx = node.as_usize();

    if idx >= n || !graph.finalized {
        return (vec![], vec![], vec![]);
    }

    let mut callers = Vec::new();
    let mut callees = Vec::new();
    let mut tests = Vec::new();

    // BFS: collect nodes at each radius level
    let mut visited = HashSet::new();
    visited.insert(node);
    let mut current_frontier = vec![node];

    for _depth in 0..radius {
        let mut next_frontier = Vec::new();

        for &frontier_node in &current_frontier {
            let fi = frontier_node.as_usize();
            if fi >= n {
                continue;
            }

            // Forward edges (callees): this node -> target
            let out_range = graph.csr.out_range(frontier_node);
            for edge_pos in out_range {
                let target = graph.csr.targets[edge_pos];
                if visited.contains(&target) {
                    continue;
                }
                visited.insert(target);
                next_frontier.push(target);

                let ti = target.as_usize();
                if ti >= n {
                    continue;
                }

                let label = graph.strings.resolve(graph.nodes.label[ti]).to_string();
                let relation = graph
                    .strings
                    .resolve(graph.csr.relations[edge_pos])
                    .to_string();
                let weight = graph
                    .csr
                    .read_weight(m1nd_core::types::EdgeIdx::new(edge_pos as u32))
                    .get();

                let prov = graph.resolve_node_provenance(target);
                let file_path = prov.source_path.clone().unwrap_or_default();

                let neighbour = surgical::SurgicalNeighbour {
                    node_id: resolve_external_id(&graph, target),
                    label: label.clone(),
                    file_path: file_path.clone(),
                    relation: relation.clone(),
                    edge_weight: weight,
                };

                // Classify: test file or callee
                let is_test = include_tests
                    && (relation.contains("test")
                        || label.contains("test")
                        || file_path.contains("test")
                        || file_path.contains("spec"));

                if is_test {
                    tests.push(neighbour);
                } else {
                    callees.push(neighbour);
                }
            }

            // Reverse edges (callers): source -> this node
            let in_range = graph.csr.in_range(frontier_node);
            for rev_pos in in_range {
                let source = graph.csr.rev_sources[rev_pos];
                if visited.contains(&source) {
                    continue;
                }
                visited.insert(source);
                next_frontier.push(source);

                let si = source.as_usize();
                if si >= n {
                    continue;
                }

                let label = graph.strings.resolve(graph.nodes.label[si]).to_string();
                let fwd_idx = graph.csr.rev_edge_idx[rev_pos];
                let relation = graph
                    .strings
                    .resolve(graph.csr.relations[fwd_idx.as_usize()])
                    .to_string();
                let weight = graph.csr.read_weight(fwd_idx).get();

                let prov = graph.resolve_node_provenance(source);
                let file_path = prov.source_path.clone().unwrap_or_default();

                let neighbour = surgical::SurgicalNeighbour {
                    node_id: resolve_external_id(&graph, source),
                    label: label.clone(),
                    file_path: file_path.clone(),
                    relation: relation.clone(),
                    edge_weight: weight,
                };

                let is_test = include_tests
                    && (relation.contains("test")
                        || label.contains("test")
                        || file_path.contains("test")
                        || file_path.contains("spec"));

                if is_test {
                    tests.push(neighbour);
                } else {
                    callers.push(neighbour);
                }
            }
        }

        current_frontier = next_frontier;
    }

    // Sort by edge weight descending for relevance
    callers.sort_by(|a, b| {
        b.edge_weight
            .partial_cmp(&a.edge_weight)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    callees.sort_by(|a, b| {
        b.edge_weight
            .partial_cmp(&a.edge_weight)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
    tests.sort_by(|a, b| {
        b.edge_weight
            .partial_cmp(&a.edge_weight)
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    (callers, callees, tests)
}

/// Resolve the external string ID for a NodeId.
fn resolve_external_id(graph: &m1nd_core::graph::Graph, node: NodeId) -> String {
    for (interned, &nid) in &graph.id_to_node {
        if nid == node {
            return graph.strings.resolve(*interned).to_string();
        }
    }
    format!("node_{}", node.as_usize())
}

/// Find graph nodes whose provenance source_path matches the given file path.
fn find_nodes_for_file(graph: &m1nd_core::graph::Graph, file_path: &str) -> Vec<(NodeId, String)> {
    let n = graph.num_nodes() as usize;
    let mut results = Vec::new();

    for i in 0..n {
        let prov = &graph.nodes.provenance[i];
        if let Some(sp) = prov.source_path {
            if let Some(path_str) = graph.strings.try_resolve(sp) {
                if surgical_paths_match(path_str, file_path) {
                    let nid = NodeId::new(i as u32);
                    let ext_id = resolve_external_id(graph, nid);
                    results.push((nid, ext_id));
                }
            }
        }
    }

    results
}

fn surgical_paths_match(left: &str, right: &str) -> bool {
    let left_norm = surgical_path_compare_text(left);
    let right_norm = surgical_path_compare_text(right);

    let left_cmp;
    let right_cmp;
    #[cfg(windows)]
    {
        left_cmp = left_norm.to_ascii_lowercase();
        right_cmp = right_norm.to_ascii_lowercase();
    }
    #[cfg(not(windows))]
    {
        left_cmp = left_norm;
        right_cmp = right_norm;
    }

    left_cmp == right_cmp || left_cmp.ends_with(&right_cmp) || right_cmp.ends_with(&left_cmp)
}

fn surgical_path_compare_text(value: &str) -> String {
    let value = value.strip_prefix("file::").unwrap_or(value);
    normalize_path_text(&strip_windows_extended_path_prefix_raw(value))
}

// ---------------------------------------------------------------------------
// Layer C: BFS blast radius (2-hop reachability via CSR edges)
// ---------------------------------------------------------------------------

/// BFS outward from seed nodes through the CSR graph, collecting all reachable
/// nodes within `max_hops`. Returns unique NodeIds reached (excluding seeds).
fn bfs_reachable(
    graph: &m1nd_core::graph::Graph,
    seeds: &[NodeId],
    max_hops: u32,
) -> HashSet<NodeId> {
    let n = graph.num_nodes() as usize;
    let mut visited: HashSet<NodeId> = seeds.iter().copied().collect();
    let mut queue: VecDeque<(NodeId, u32)> = seeds.iter().map(|&nid| (nid, 0)).collect();
    let mut reachable: HashSet<NodeId> = HashSet::new();

    while let Some((node, depth)) = queue.pop_front() {
        if depth >= max_hops {
            continue;
        }
        let idx = node.as_usize();
        if idx >= n || !graph.finalized {
            continue;
        }

        // Forward edges: node -> targets
        let out_range = graph.csr.out_range(node);
        for edge_pos in out_range {
            let target = graph.csr.targets[edge_pos];
            if !visited.contains(&target) {
                visited.insert(target);
                reachable.insert(target);
                queue.push_back((target, depth + 1));
            }
        }

        // Reverse edges: sources -> node (nodes that depend on this one)
        let in_range = graph.csr.in_range(node);
        for rev_pos in in_range {
            let source = graph.csr.rev_sources[rev_pos];
            if !visited.contains(&source) {
                visited.insert(source);
                reachable.insert(source);
                queue.push_back((source, depth + 1));
            }
        }
    }

    reachable
}

/// Compute Layer C blast radius for a single file: BFS 2-hop from file's nodes,
/// then filter to only OTHER file-level nodes. Returns (reachable_count, top_affected_ids).
fn compute_blast_radius(graph: &m1nd_core::graph::Graph, file_path: &str) -> (usize, Vec<String>) {
    let file_nodes = find_nodes_for_file(graph, file_path);
    if file_nodes.is_empty() {
        return (0, Vec::new());
    }

    let seeds: Vec<NodeId> = file_nodes.iter().map(|(nid, _)| *nid).collect();
    let seed_set: HashSet<NodeId> = seeds.iter().copied().collect();
    let reachable = bfs_reachable(graph, &seeds, 2);

    // Filter to file-level nodes that belong to OTHER files
    let n = graph.num_nodes() as usize;
    let mut other_file_nodes: Vec<String> = Vec::new();

    for &nid in &reachable {
        let idx = nid.as_usize();
        if idx >= n {
            continue;
        }
        // Only count File-type nodes (not functions/structs within the same file)
        if graph.nodes.node_type[idx] != NodeType::File {
            continue;
        }
        // Exclude the file's own nodes
        if seed_set.contains(&nid) {
            continue;
        }
        let ext_id = resolve_external_id(graph, nid);
        other_file_nodes.push(ext_id);
    }

    let count = other_file_nodes.len();
    other_file_nodes.truncate(5); // top 5 for reporting
    (count, other_file_nodes)
}

fn blast_radius_risk(count: usize) -> &'static str {
    if count <= 3 {
        "low"
    } else if count <= 10 {
        "medium"
    } else {
        "high"
    }
}

// ---------------------------------------------------------------------------
// Layer D: Affected test execution
// ---------------------------------------------------------------------------

/// Run a command with a timeout. Returns Ok(output) or Err on timeout/spawn failure.
/// Uses spawn + poll loop with try_wait, killing after `timeout_secs`.
fn run_command_with_timeout(
    mut cmd: Command,
    timeout_secs: u64,
) -> Result<std::process::Output, String> {
    let mut child = cmd
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| format!("spawn failed: {}", e))?;

    let deadline = Instant::now() + std::time::Duration::from_secs(timeout_secs);
    let poll_interval = std::time::Duration::from_millis(100);

    loop {
        match child.try_wait() {
            Ok(Some(_status)) => {
                // Child exited — collect output
                return child
                    .wait_with_output()
                    .map_err(|e| format!("wait_with_output failed: {}", e));
            }
            Ok(None) => {
                // Still running — check timeout
                if Instant::now() >= deadline {
                    let _ = child.kill();
                    let _ = child.wait(); // reap zombie
                    return Err(format!("command timed out after {}s", timeout_secs));
                }
                std::thread::sleep(poll_interval);
            }
            Err(e) => {
                return Err(format!("try_wait failed: {}", e));
            }
        }
    }
}

/// Detect and run tests for modified files. Returns (tests_run, tests_passed, tests_failed, output_on_failure).
fn run_affected_tests(
    modified_paths: &[PathBuf],
) -> (Option<u32>, Option<u32>, Option<u32>, Option<String>) {
    let mut total_run: u32 = 0;
    let mut total_passed: u32 = 0;
    let mut total_failed: u32 = 0;
    let mut failure_output: Option<String> = None;
    let mut any_tests_found = false;

    for path in modified_paths {
        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
        match ext {
            "rs" => {
                if let Some((run, passed, failed, output)) = run_rust_tests(path) {
                    any_tests_found = true;
                    total_run += run;
                    total_passed += passed;
                    total_failed += failed;
                    if failed > 0 && failure_output.is_none() {
                        failure_output = output;
                    }
                }
            }
            "go" => {
                if let Some((run, passed, failed, output)) = run_go_tests(path) {
                    any_tests_found = true;
                    total_run += run;
                    total_passed += passed;
                    total_failed += failed;
                    if failed > 0 && failure_output.is_none() {
                        failure_output = output;
                    }
                }
            }
            "py" => {
                if let Some((run, passed, failed, output)) = run_python_tests(path) {
                    any_tests_found = true;
                    total_run += run;
                    total_passed += passed;
                    total_failed += failed;
                    if failed > 0 && failure_output.is_none() {
                        failure_output = output;
                    }
                }
            }
            _ => {}
        }
    }

    if any_tests_found {
        (
            Some(total_run),
            Some(total_passed),
            Some(total_failed),
            failure_output,
        )
    } else {
        (None, None, None, None)
    }
}

/// Detect Rust tests: check for #[cfg(test)] in the file or companion _test.rs.
/// Runs `cargo test --lib -p <package> -- <filter>` with 30s timeout.
fn run_rust_tests(path: &Path) -> Option<(u32, u32, u32, Option<String>)> {
    let content = std::fs::read_to_string(path).ok()?;
    let has_inline_tests = content.contains("#[cfg(test)]");
    let stem = path.file_stem()?.to_str()?;
    let parent = path.parent()?;
    let test_file = parent.join(format!("{}_test.rs", stem));
    let has_test_file = test_file.exists();

    if !has_inline_tests && !has_test_file {
        return None;
    }

    // Find the Cargo.toml to determine the package name
    let package = find_cargo_package(path)?;

    // Build the test filter from the file stem
    let filter = stem.replace('-', "_");

    let mut cmd = Command::new("cargo");
    cmd.args(["test", "--lib", "-p", &package, "--", &filter])
        .current_dir(find_cargo_workspace(path)?)
        .env("RUST_BACKTRACE", "0");

    match run_command_with_timeout(cmd, 30) {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout);
            let stderr = String::from_utf8_lossy(&out.stderr);
            let combined = format!("{}{}", stdout, stderr);
            let (run, passed, failed) = parse_cargo_test_output(&combined);
            let fail_output = if failed > 0 {
                Some(combined.chars().take(500).collect())
            } else {
                None
            };
            Some((run, passed, failed, fail_output))
        }
        Err(_) => {
            // Timeout or spawn failure — report as 1 failed test
            Some((
                1,
                0,
                1,
                Some("cargo test timed out (30s limit)".to_string()),
            ))
        }
    }
}

/// Parse cargo test output for pass/fail counts.
/// Looks for lines like: "test result: ok. 5 passed; 0 failed; 0 ignored"
fn parse_cargo_test_output(output: &str) -> (u32, u32, u32) {
    for line in output.lines() {
        if line.starts_with("test result:") {
            let mut passed = 0u32;
            let mut failed = 0u32;
            for part in line.split(';') {
                let trimmed = part.trim();
                if let Some(num_str) = trimmed.strip_suffix(" passed") {
                    let num_str = num_str.trim();
                    // "test result: ok. 5 passed" — extract the number
                    if let Some(n) = num_str.split_whitespace().last() {
                        passed = n.parse().unwrap_or(0);
                    }
                } else if let Some(num_str) = trimmed.strip_suffix(" failed") {
                    let num_str = num_str.trim();
                    if let Some(n) = num_str.split_whitespace().last() {
                        failed = n.parse().unwrap_or(0);
                    }
                }
            }
            return (passed + failed, passed, failed);
        }
    }
    (0, 0, 0)
}

/// Find the cargo package name by walking up to find Cargo.toml and parsing [package] name.
fn find_cargo_package(path: &Path) -> Option<String> {
    let mut dir = path.parent()?;
    loop {
        let cargo_toml = dir.join("Cargo.toml");
        if cargo_toml.exists() {
            let content = std::fs::read_to_string(&cargo_toml).ok()?;
            // Simple parse: find `name = "..."` under [package]
            let mut in_package = false;
            for line in content.lines() {
                let trimmed = line.trim();
                if trimmed == "[package]" {
                    in_package = true;
                    continue;
                }
                if trimmed.starts_with('[') {
                    in_package = false;
                    continue;
                }
                if in_package {
                    if let Some(rest) = trimmed.strip_prefix("name") {
                        let rest = rest.trim_start();
                        if let Some(rest) = rest.strip_prefix('=') {
                            let name = rest.trim().trim_matches('"').trim_matches('\'');
                            return Some(name.to_string());
                        }
                    }
                }
            }
        }
        dir = dir.parent()?;
    }
}

/// Find the workspace root (directory with Cargo.toml containing [workspace]).
fn find_cargo_workspace(path: &Path) -> Option<PathBuf> {
    let mut dir = path.parent()?;
    loop {
        let cargo_toml = dir.join("Cargo.toml");
        if cargo_toml.exists() {
            if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
                if content.contains("[workspace]") {
                    return Some(dir.to_path_buf());
                }
            }
        }
        match dir.parent() {
            Some(p) if p != dir => dir = p,
            _ => break,
        }
    }
    // Fallback: use the first Cargo.toml parent
    let mut dir = path.parent()?;
    loop {
        if dir.join("Cargo.toml").exists() {
            return Some(dir.to_path_buf());
        }
        match dir.parent() {
            Some(p) if p != dir => dir = p,
            _ => return None,
        }
    }
}

fn find_cargo_project_root(path: &Path) -> Option<String> {
    let mut dir = path.parent()?;
    loop {
        if dir.join("Cargo.toml").exists() {
            return Some(dir.to_string_lossy().to_string());
        }
        match dir.parent() {
            Some(parent) if parent != dir => dir = parent,
            _ => return None,
        }
    }
}

fn available_python_command() -> Option<&'static str> {
    ["python3", "python"].into_iter().find(|command| {
        Command::new(command)
            .arg("--version")
            .output()
            .map(|output| output.status.success())
            .unwrap_or(false)
    })
}

/// Detect Go tests: find _test.go files in the same directory.
/// Runs `go test ./package/...` with 30s timeout.
fn run_go_tests(path: &Path) -> Option<(u32, u32, u32, Option<String>)> {
    let parent = path.parent()?;
    let stem = path.file_stem()?.to_str()?;
    let test_file = parent.join(format!("{}_test.go", stem));

    if !test_file.exists() {
        // Also check for any *_test.go in same dir
        let has_any_test = std::fs::read_dir(parent).ok()?.any(|entry| {
            entry
                .ok()
                .and_then(|e| e.file_name().to_str().map(|s| s.ends_with("_test.go")))
                .unwrap_or(false)
        });
        if !has_any_test {
            return None;
        }
    }

    let pkg_path = format!("./{}", parent.file_name()?.to_str()?);
    let mut cmd = Command::new("go");
    cmd.args(["test", &format!("{}/...", pkg_path)])
        .current_dir(parent.parent()?);

    match run_command_with_timeout(cmd, 30) {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout);
            let stderr = String::from_utf8_lossy(&out.stderr);
            let combined = format!("{}{}", stdout, stderr);
            let (run, passed, failed) = parse_go_test_output(&combined);
            let fail_output = if failed > 0 {
                Some(combined.chars().take(500).collect())
            } else {
                None
            };
            Some((run, passed, failed, fail_output))
        }
        Err(_) => Some((1, 0, 1, Some("go test timed out (30s limit)".to_string()))),
    }
}

/// Parse go test output. Looks for "ok" / "FAIL" lines and "--- PASS" / "--- FAIL".
fn parse_go_test_output(output: &str) -> (u32, u32, u32) {
    let mut passed = 0u32;
    let mut failed = 0u32;
    for line in output.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("--- PASS:") {
            passed += 1;
        } else if trimmed.starts_with("--- FAIL:") {
            failed += 1;
        }
    }
    (passed + failed, passed, failed)
}

/// Detect Python tests: find test_*.py or *_test.py nearby.
/// Runs `python3 -m pytest <test_file> -x --tb=short` with 30s timeout.
fn run_python_tests(path: &Path) -> Option<(u32, u32, u32, Option<String>)> {
    let parent = path.parent()?;
    let stem = path.file_stem()?.to_str()?;

    // Look for test files: test_{stem}.py or {stem}_test.py
    let test_file_a = parent.join(format!("test_{}.py", stem));
    let test_file_b = parent.join(format!("{}_test.py", stem));
    // Also check tests/ subdirectory
    let test_file_c = parent.join("tests").join(format!("test_{}.py", stem));

    let test_file = if test_file_a.exists() {
        test_file_a
    } else if test_file_b.exists() {
        test_file_b
    } else if test_file_c.exists() {
        test_file_c
    } else {
        return None;
    };

    let test_file_str = test_file.to_string_lossy().to_string();
    let mut cmd = Command::new("python3");
    cmd.args(["-m", "pytest", &test_file_str, "-x", "--tb=short"])
        .current_dir(parent);

    match run_command_with_timeout(cmd, 30) {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout);
            let stderr = String::from_utf8_lossy(&out.stderr);
            let combined = format!("{}{}", stdout, stderr);
            let (run, passed, failed) = parse_pytest_output(&combined);
            let fail_output = if failed > 0 {
                Some(combined.chars().take(500).collect())
            } else {
                None
            };
            Some((run, passed, failed, fail_output))
        }
        Err(_) => Some((1, 0, 1, Some("pytest timed out (30s limit)".to_string()))),
    }
}

/// Parse pytest output. Looks for summary line like "5 passed, 1 failed" or "3 passed".
fn parse_pytest_output(output: &str) -> (u32, u32, u32) {
    // pytest final line format: "= 5 passed, 2 failed in 1.23s ="
    // or "= 5 passed in 1.23s ="
    for line in output.lines().rev() {
        let trimmed = line.trim().trim_matches('=').trim();
        let mut passed = 0u32;
        let mut failed = 0u32;
        for part in trimmed.split(',') {
            let part = part.trim();
            if let Some(n) = part.strip_suffix(" passed") {
                passed = n.trim().parse().unwrap_or(0);
            } else if let Some(n) = part.strip_suffix(" failed") {
                failed = n.trim().parse().unwrap_or(0);
            }
        }
        if passed > 0 || failed > 0 {
            return (passed + failed, passed, failed);
        }
    }
    (0, 0, 0)
}

// ---------------------------------------------------------------------------
// m1nd.surgical_context
// ---------------------------------------------------------------------------

/// Handle m1nd.heuristics_surface.
///
/// Resolves a target by node_id or file_path and returns the unified
/// heuristic explanation surface used by surgical_context/apply_batch.
pub fn handle_heuristics_surface(
    state: &mut SessionState,
    input: surgical::HeuristicsSurfaceInput,
) -> M1ndResult<surgical::HeuristicsSurfaceOutput> {
    let start = Instant::now();

    let target = if let Some(node_id) = input.node_id.as_ref().filter(|value| !value.is_empty()) {
        let graph = state.graph.read();
        let node = graph
            .resolve_id(node_id)
            .ok_or_else(|| {
                invalid_params_with_hint_and_example(
                    "heuristics_surface",
                    format!("node not found: {}", node_id),
                    "Retry with a file-backed node_id, or pass file_path if you want file-level heuristics without resolving a node first.",
                    r#"{"agent_id":"dev","file_path":"src/auth.rs"}"#,
                )
            })?;
        let provenance = graph.resolve_node_provenance(node);
        let resolved_path = provenance
            .source_path
            .or_else(|| {
                node_id
                    .strip_prefix("file::")
                    .map(|value| value.to_string())
            })
            .ok_or_else(|| {
                invalid_params_with_hint_and_example(
                    "heuristics_surface",
                    format!("node has no file provenance: {}", node_id),
                    "Use file_path for file-level heuristics, or choose a node_id whose provenance points to a source file.",
                    r#"{"agent_id":"dev","file_path":"src/auth.rs"}"#,
                )
            })?;
        (node_id.clone(), resolved_path, "node_id".to_string())
    } else if let Some(file_path) = input.file_path.as_ref().filter(|value| !value.is_empty()) {
        let resolved_path = resolve_file_path(file_path, &state.ingest_roots)
            .to_string_lossy()
            .to_string();
        let node_id = {
            let graph = state.graph.read();
            find_nodes_for_file(&graph, &resolved_path)
                .into_iter()
                .find(|(nid, _)| {
                    let idx = nid.as_usize();
                    idx < graph.num_nodes() as usize
                        && graph.nodes.node_type[idx] == m1nd_core::types::NodeType::File
                })
                .or_else(|| {
                    find_nodes_for_file(&graph, &resolved_path)
                        .into_iter()
                        .next()
                })
                .map(|(_, ext_id)| ext_id)
        }
        .unwrap_or_else(|| format!("file::{}", resolved_path));
        (node_id, resolved_path, "file_path".to_string())
    } else {
        return Err(invalid_params_with_hint_and_example(
            "heuristics_surface",
            "provide node_id or file_path",
            "Pass exactly one target so m1nd knows which file or node to explain.",
            r#"{"agent_id":"dev","file_path":"src/auth.rs"}"#,
        ));
    };

    let heuristic_summary = build_surgical_heuristic_summary(state, &target.0, &target.1);
    state.track_agent(&input.agent_id);

    Ok(surgical::HeuristicsSurfaceOutput {
        node_id: target.0,
        file_path: target.1,
        resolved_by: target.2,
        heuristic_summary,
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
    })
}

/// Handle m1nd.surgical_context.
///
/// Returns everything an LLM needs to edit `file_path` surgically:
///   - full file contents
///   - graph context: callers, callees, importers, test files
///   - provenance: node_ids, line ranges
///   - optional focused symbol slice when `symbol` is provided
///
/// Steps:
///   1. Reading `input.file_path` from disk.
///   2. Finding all graph nodes whose provenance matches this file.
///   3. BFS to radius 1-2 to gather callers / callees / tests.
///   4. Optionally narrowing to a specific symbol via line-range extraction.
///   5. Returning a `SurgicalContextOutput` with all fields populated.
pub fn handle_surgical_context(
    state: &mut SessionState,
    input: surgical::SurgicalContextInput,
) -> M1ndResult<surgical::SurgicalContextOutput> {
    let start = Instant::now();

    // Step 1: Resolve and read the file
    let resolved_path = resolve_file_path(&input.file_path, &state.ingest_roots);
    let file_contents =
        std::fs::read_to_string(&resolved_path).map_err(|e| {
            invalid_params_with_hint(
                "m1nd_surgical_context",
                format!("cannot read file {}: {}", resolved_path.display(), e),
                "Pass an existing file_path under an ingested workspace root, or use view(auto_ingest=true) first if you are probing a file that is not in the graph yet.",
            )
        })?;

    let line_count = file_contents.lines().count() as u32;

    // Step 2: Extract symbols from file content
    let path_str = resolved_path.to_string_lossy().to_string();
    let symbols = extract_symbols(&file_contents, &path_str);

    // Step 3: Find graph nodes for this file
    let graph = state.graph.read();
    let file_nodes = find_nodes_for_file(&graph, &path_str);
    drop(graph);

    // Pick the primary node (prefer File-type node, otherwise first match)
    let primary_node: Option<(NodeId, String)> = {
        let graph = state.graph.read();
        let file_type_node = file_nodes.iter().find(|(nid, _)| {
            let idx = nid.as_usize();
            idx < graph.num_nodes() as usize
                && graph.nodes.node_type[idx] == m1nd_core::types::NodeType::File
        });
        file_type_node.or(file_nodes.first()).cloned()
    };

    let node_id_str = primary_node
        .as_ref()
        .map(|(_, ext)| ext.clone())
        .unwrap_or_default();

    // Step 4: Collect graph neighbours via BFS
    let (callers, callees, tests) = if let Some((nid, _)) = &primary_node {
        collect_neighbours(state, *nid, input.radius, input.include_tests)
    } else {
        // No graph node found -- also try collecting from all file nodes
        let mut all_callers = Vec::new();
        let mut all_callees = Vec::new();
        let mut all_tests = Vec::new();
        for (nid, _) in &file_nodes {
            let (c, d, t) = collect_neighbours(state, *nid, input.radius, input.include_tests);
            all_callers.extend(c);
            all_callees.extend(d);
            all_tests.extend(t);
        }
        (all_callers, all_callees, all_tests)
    };

    // Step 5: Focused symbol (if requested)
    let focused_symbol = input.symbol.as_ref().and_then(|sym_name| {
        symbols
            .iter()
            .find(|s| s.name.eq_ignore_ascii_case(sym_name) || s.name == *sym_name)
            .cloned()
    });

    let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
    let heuristic_summary = if node_id_str.is_empty() {
        None
    } else {
        Some(build_surgical_heuristic_summary(
            state,
            &node_id_str,
            &path_str,
        ))
    };

    // Track agent session
    state.track_agent(&input.agent_id);
    let mut visited_files = vec![path_str.clone()];
    visited_files.extend(callers.iter().map(|entry| entry.file_path.clone()));
    visited_files.extend(callees.iter().map(|entry| entry.file_path.clone()));
    visited_files.extend(tests.iter().map(|entry| entry.file_path.clone()));
    let mut visited_nodes = Vec::new();
    if !node_id_str.is_empty() {
        visited_nodes.push(node_id_str.clone());
    }
    visited_nodes.extend(callers.iter().map(|entry| entry.node_id.clone()));
    visited_nodes.extend(callees.iter().map(|entry| entry.node_id.clone()));
    visited_nodes.extend(tests.iter().map(|entry| entry.node_id.clone()));
    state.note_coverage(
        &input.agent_id,
        "surgical_context",
        visited_files,
        visited_nodes,
    );

    Ok(surgical::SurgicalContextOutput {
        file_path: path_str,
        file_contents,
        line_count,
        node_id: node_id_str,
        symbols,
        focused_symbol,
        callers,
        callees,
        tests,
        heuristic_summary,
        elapsed_ms,
    })
}

// ---------------------------------------------------------------------------
// m1nd.edit_preview / m1nd.edit_commit
// ---------------------------------------------------------------------------

pub fn handle_edit_preview(
    state: &mut SessionState,
    input: surgical::EditPreviewInput,
) -> M1ndResult<surgical::EditPreviewOutput> {
    let start = Instant::now();
    let resolved_path = resolve_file_path(&input.file_path, &state.ingest_roots);
    let validated_path = validate_path_safety(&resolved_path, &state.ingest_roots)?;

    let old_content = std::fs::read_to_string(&validated_path).unwrap_or_default();
    let file_exists = validated_path.exists();
    let line_count = old_content.lines().count();
    let source_hash = content_hash(&old_content);
    let (lines_added, lines_removed) = diff_summary(&old_content, &input.new_content);
    let unified_diff = unified_diff_preview(&old_content, &input.new_content);
    let bytes_written = input.new_content.len();
    let candidate_is_empty = input.new_content.is_empty();
    let candidate_equals_source = old_content == input.new_content;
    let preview_id = state.next_edit_preview_id(&input.agent_id);

    state.edit_previews.insert(
        preview_id.clone(),
        EditPreviewState {
            preview_id: preview_id.clone(),
            agent_id: input.agent_id.clone(),
            file_path: validated_path.to_string_lossy().to_string(),
            new_content: input.new_content.clone(),
            source_hash: source_hash.clone(),
            source_exists: file_exists,
            source_bytes: old_content.len(),
            source_line_count: line_count,
            lines_added,
            lines_removed,
            bytes_written,
            unified_diff: unified_diff.clone(),
            description: input.description.clone(),
            created_at_ms: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_millis() as u64)
                .unwrap_or(0),
        },
    );

    state.track_agent(&input.agent_id);

    Ok(surgical::EditPreviewOutput {
        preview_id,
        file_path: validated_path.to_string_lossy().to_string(),
        snapshot: surgical::SourceFileSnapshot {
            file_path: validated_path.to_string_lossy().to_string(),
            file_exists,
            content_hash: source_hash,
            bytes: old_content.len(),
            line_count,
        },
        diff: surgical::CandidateDiffReport {
            unified_diff,
            lines_added,
            lines_removed,
            bytes_written,
        },
        validation: surgical::PreviewValidationReport {
            source_changed: false,
            candidate_is_empty,
            candidate_equals_source,
            ready_to_commit: !candidate_equals_source,
        },
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
    })
}

pub fn handle_edit_commit(
    state: &mut SessionState,
    input: surgical::EditCommitInput,
) -> M1ndResult<surgical::EditCommitOutput> {
    let start = Instant::now();

    // Guard: confirm must be true
    if !input.confirm {
        return Err(invalid_params_with_hint_and_example(
            "edit_commit",
            "confirm must be true to commit",
            "Review the preview first, then resend the same preview_id with confirm=true.",
            r#"{"preview_id":"preview_dev_001","agent_id":"dev","confirm":true}"#,
        ));
    }

    // Garbage-collect expired previews (TTL = 5 min)
    let now_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0);
    const TTL_MS: u64 = 5 * 60 * 1000;
    state
        .edit_previews
        .retain(|_, v| now_ms.saturating_sub(v.created_at_ms) < TTL_MS);

    let preview = state
        .edit_previews
        .get(&input.preview_id)
        .cloned()
        .ok_or_else(|| {
            invalid_params_with_hint_and_example(
                "edit_commit",
                format!(
                    "preview_id not found or expired (TTL=5min): {}",
                    input.preview_id
                ),
                "Run edit_preview again for the same file to mint a fresh preview_id, then retry edit_commit.",
                r#"{"file_path":"src/auth.rs","agent_id":"dev","new_content":"..."}"#,
            )
        })?;

    if preview.agent_id != input.agent_id {
        return Err(invalid_params_with_hint(
            "edit_commit",
            "preview belongs to a different agent",
            "Retry with the same agent_id that created the preview, or create a new preview under this agent before committing.",
        ));
    }

    let current_content = std::fs::read_to_string(&preview.file_path).unwrap_or_default();
    let current_hash = content_hash(&current_content);
    if current_hash != preview.source_hash {
        return Err(invalid_params_with_hint_and_example(
            "edit_commit",
            "source_modified: file changed since preview was created",
            "Re-run edit_preview against the current on-disk file to refresh the source hash, then retry edit_commit.",
            r#"{"file_path":"src/auth.rs","agent_id":"dev","new_content":"..."}"#,
        ));
    }

    let apply_output = handle_apply(
        state,
        surgical::ApplyInput {
            file_path: preview.file_path.clone(),
            agent_id: input.agent_id.clone(),
            new_content: preview.new_content.clone(),
            description: preview.description.clone(),
            reingest: input.reingest,
        },
    )?;

    state.edit_previews.remove(&input.preview_id);
    state.track_agent(&input.agent_id);

    Ok(surgical::EditCommitOutput {
        preview_id: input.preview_id,
        file_path: apply_output.file_path,
        bytes_written: apply_output.bytes_written,
        lines_added: apply_output.lines_added,
        lines_removed: apply_output.lines_removed,
        reingested: apply_output.reingested,
        updated_node_ids: apply_output.updated_node_ids,
        proactive_insights: apply_output.proactive_insights,
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
    })
}

// ---------------------------------------------------------------------------
// apply
// ---------------------------------------------------------------------------

/// Handle apply.
///
/// Writes LLM-edited code back to `file_path` and triggers an incremental
/// re-ingest so the graph reflects the new source.
///
/// Steps:
///   1. Validating `input.file_path` is within the workspace root (no path traversal).
///   2. Reading old content for diff.
///   3. Atomically writing `input.new_content` to disk (write-then-rename).
///   4. If reingest: performing incremental re-ingest via single-file ingest.
///   5. Returning diff summary + updated node_ids in `ApplyOutput`.
pub fn handle_apply(
    state: &mut SessionState,
    input: surgical::ApplyInput,
) -> M1ndResult<surgical::ApplyOutput> {
    let start = Instant::now();

    // Step 1: Resolve and validate path
    let resolved_path = resolve_file_path(&input.file_path, &state.ingest_roots);
    let validated_path = validate_path_safety(&resolved_path, &state.ingest_roots)?;

    // Step 2: Read old content for diff (if file exists)
    let old_content = std::fs::read_to_string(&validated_path).unwrap_or_default();
    let (lines_added, lines_removed) = diff_summary(&old_content, &input.new_content);
    let bytes_written = input.new_content.len();

    // Step 3: Atomic write -- write to temp file, then rename
    let parent = validated_path.parent().unwrap_or(Path::new("."));
    let temp_path = parent.join(format!(".m1nd_apply_{}.tmp", std::process::id()));

    // Ensure parent directory exists
    if !parent.exists() {
        std::fs::create_dir_all(parent).map_err(|e| M1ndError::InvalidParams {
            tool: "m1nd_apply".into(),
            detail: format!(
                "cannot create directory {}: {} Hint: create or choose a parent directory inside an ingested workspace root, then retry apply.",
                parent.display(),
                e
            ),
        })?;
    }

    // Write to temp file
    std::fs::write(&temp_path, &input.new_content).map_err(|e| M1ndError::InvalidParams {
        tool: "m1nd_apply".into(),
        detail: format!(
            "cannot write temp file {}: {} Hint: check write permissions under the workspace root and retry apply.",
            temp_path.display(),
            e
        ),
    })?;

    // Rename (atomic on same filesystem)
    std::fs::rename(&temp_path, &validated_path).map_err(|e| {
        // Clean up temp file on rename failure
        let _ = std::fs::remove_file(&temp_path);
        M1ndError::InvalidParams {
            tool: "m1nd_apply".into(),
            detail: format!(
                "atomic rename failed {} -> {}: {} Hint: retry apply after checking filesystem permissions and free space in the workspace.",
                temp_path.display(),
                validated_path.display(),
                e
            ),
        }
    })?;

    // Step 4: Incremental re-ingest (if requested)
    let mut updated_node_ids = Vec::new();
    let reingested = if input.reingest {
        // Find existing nodes for this file before re-ingest
        {
            let graph = state.graph.read();
            let path_str = validated_path.to_string_lossy().to_string();
            let existing = find_nodes_for_file(&graph, &path_str);
            for (_, ext_id) in &existing {
                updated_node_ids.push(ext_id.clone());
            }
        }

        // Attempt incremental ingest via single-file code ingest
        let ingest_input = crate::protocol::IngestInput {
            path: validated_path.to_string_lossy().to_string(),
            agent_id: input.agent_id.clone(),
            mode: "merge".to_string(),
            incremental: true,
            adapter: "code".to_string(),
            namespace: None,
            include_dotfiles: false,
            dotfile_patterns: Vec::new(),
        };

        match crate::tools::handle_ingest(state, ingest_input) {
            Ok(result) => {
                // Extract any new node IDs from the ingest result
                if let Some(obj) = result.as_object() {
                    if let Some(nodes) = obj.get("nodes_created") {
                        if let Some(n) = nodes.as_u64() {
                            if n > 0 && updated_node_ids.is_empty() {
                                updated_node_ids
                                    .push(format!("file::{}", validated_path.to_string_lossy()));
                            }
                        }
                    }
                }
                true
            }
            Err(e) => {
                // Re-ingest failure is non-fatal -- file was already written successfully
                eprintln!(
                    "[m1nd] WARNING: apply re-ingest failed for {}: {}",
                    validated_path.display(),
                    e
                );
                false
            }
        }
    } else {
        false
    };

    let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;

    // Track agent session
    state.track_agent(&input.agent_id);

    let applied_file_path = validated_path.to_string_lossy().to_string();
    let mut proactive_insights =
        daemon_proactive_insights_for_file(state, &applied_file_path, None);
    // Antibody auto-propose on fix: if this agent's scan/audit previously flagged
    // a node in the written file, emit a `proposed_antibody` carrying a
    // ready-to-run antibody_create so the next agent's audit catches the same
    // structural bug elsewhere (compounding negative memory). Proposal only — no
    // silent write to the antibody store.
    {
        let mut proposed = propose_antibodies_for_write(
            state,
            &input.agent_id,
            &applied_file_path,
            &updated_node_ids,
        );
        if !proposed.is_empty() {
            // Keep the standard 3-insight cap, but never let the proposed_antibody
            // be silently dropped by truncation: prepend the proposals, sort the
            // remainder, and re-truncate so at least the proposals survive.
            let mut combined = proposed;
            combined.append(&mut proactive_insights);
            combined.sort_by(|a, b| {
                // proposed_antibody first, then by severity, then confidence.
                let a_prop = (a.kind != "proposed_antibody") as u8;
                let b_prop = (b.kind != "proposed_antibody") as u8;
                a_prop
                    .cmp(&b_prop)
                    .then_with(|| {
                        proactive_severity_rank(&a.severity)
                            .cmp(&proactive_severity_rank(&b.severity))
                    })
                    .then_with(|| b.confidence.total_cmp(&a.confidence))
            });
            combined.truncate(3);
            proactive_insights = combined;
        }
    }
    let _ = persist_daemon_alerts_from_insights(
        state,
        &proactive_insights,
        Some(&applied_file_path),
        updated_node_ids.first().map(String::as_str),
    );

    Ok(surgical::ApplyOutput {
        file_path: validated_path.to_string_lossy().to_string(),
        bytes_written,
        lines_added,
        lines_removed,
        reingested,
        updated_node_ids,
        proactive_insights,
        elapsed_ms,
    })
}

// ---------------------------------------------------------------------------
// m1nd.surgical_context_v2
// ---------------------------------------------------------------------------

/// Handle m1nd.surgical_context_v2.
///
/// Returns V1 surgical context for the primary file PLUS source code
/// of connected files (callers, callees, tests), sorted by edge_weight,
/// capped at max_connected_files, truncated at max_lines_per_file.
///
/// Steps:
///   1. Delegate to handle_surgical_context() for the primary file (V1 output).
///   2. Collect unique file paths from primary.callers + callees + tests.
///   3. Deduplicate by file_path (keep highest weight per path).
///   4. Sort by edge_weight descending, take top max_connected_files.
///   5. Read each connected file's source (truncate to max_lines_per_file).
///   6. Assemble SurgicalContextV2Output.
pub fn handle_surgical_context_v2(
    state: &mut SessionState,
    input: surgical::SurgicalContextV2Input,
) -> M1ndResult<surgical::SurgicalContextV2Output> {
    let start = Instant::now();

    // Step 1: Get V1 context for the primary file
    let v1_input = surgical::SurgicalContextInput {
        file_path: input.file_path.clone(),
        agent_id: input.agent_id.clone(),
        symbol: input.symbol.clone(),
        radius: input.radius,
        include_tests: input.include_tests,
    };
    let primary = handle_surgical_context(state, v1_input)?;

    // Step 2: Collect candidate files from neighbourhood
    // Use a HashMap to deduplicate by file_path, keeping highest weight
    let primary_path = primary.file_path.clone();
    let primary_node_id = primary.node_id.clone();
    let mut candidate_map: std::collections::HashMap<String, (String, String, String, f32)> =
        std::collections::HashMap::new(); // path -> (node_id, label, relation, weight)

    for caller in &primary.callers {
        if !caller.file_path.is_empty() && caller.file_path != primary_path {
            let entry = candidate_map.entry(caller.file_path.clone()).or_insert((
                caller.node_id.clone(),
                caller.label.clone(),
                "caller".to_string(),
                caller.edge_weight,
            ));
            if caller.edge_weight > entry.3 {
                *entry = (
                    caller.node_id.clone(),
                    caller.label.clone(),
                    "caller".to_string(),
                    caller.edge_weight,
                );
            }
        }
    }
    for callee in &primary.callees {
        if !callee.file_path.is_empty() && callee.file_path != primary_path {
            let entry = candidate_map.entry(callee.file_path.clone()).or_insert((
                callee.node_id.clone(),
                callee.label.clone(),
                "callee".to_string(),
                callee.edge_weight,
            ));
            if callee.edge_weight > entry.3 {
                *entry = (
                    callee.node_id.clone(),
                    callee.label.clone(),
                    "callee".to_string(),
                    callee.edge_weight,
                );
            }
        }
    }
    for test in &primary.tests {
        if !test.file_path.is_empty() && test.file_path != primary_path {
            let entry = candidate_map.entry(test.file_path.clone()).or_insert((
                test.node_id.clone(),
                test.label.clone(),
                "test".to_string(),
                test.edge_weight,
            ));
            if test.edge_weight > entry.3 {
                *entry = (
                    test.node_id.clone(),
                    test.label.clone(),
                    "test".to_string(),
                    test.edge_weight,
                );
            }
        }
    }

    // Also exclude primary node_id from connected set (circular guard)
    candidate_map.retain(|_, (nid, _, _, _)| *nid != primary_node_id);

    // Step 3: Prefer code-bearing proof files over docs/manifests/tests when
    // selecting a bounded connected set. This keeps v2 useful for edit prep
    // instead of spending precious slots on auxiliary surfaces first.
    let mut scored: Vec<(String, String, String, String, f32)> = candidate_map
        .into_iter()
        .map(|(path, (nid, label, rel, w))| (path, nid, label, rel, w))
        .collect();
    scored.sort_by(|a, b| {
        surgical_v2_file_kind_rank(&b.0)
            .cmp(&surgical_v2_file_kind_rank(&a.0))
            .then_with(|| surgical_v2_relation_rank(&b.3).cmp(&surgical_v2_relation_rank(&a.3)))
            .then_with(|| b.4.partial_cmp(&a.4).unwrap_or(std::cmp::Ordering::Equal))
            .then_with(|| a.0.cmp(&b.0))
    });
    let max_connected_files = if input.proof_focused {
        input.max_connected_files.clamp(1, 3)
    } else {
        input.max_connected_files
    };
    let max_lines = if input.proof_focused {
        input.max_lines_per_file.clamp(8, 25)
    } else {
        input.max_lines_per_file
    };
    let scored = surgical_v2_select_candidates(scored, max_connected_files, input.proof_focused);

    // Step 4: Read each connected file, build ConnectedFileSource
    let mut connected_files: Vec<surgical::ConnectedFileSource> = Vec::new();
    let mut total_lines = primary.line_count as usize;

    for (path, node_id, label, relation_type, edge_weight) in &scored {
        let resolved = resolve_file_path(path, &state.ingest_roots);
        match std::fs::read_to_string(&resolved) {
            Ok(content) => {
                let all_lines: Vec<&str> = content.lines().collect();
                let file_line_count = all_lines.len();
                let truncated = file_line_count > max_lines;
                let excerpt_lines = if truncated {
                    max_lines
                } else {
                    file_line_count
                };
                let source_excerpt: String = all_lines
                    .iter()
                    .take(excerpt_lines)
                    .cloned()
                    .collect::<Vec<&str>>()
                    .join("\n");

                total_lines += excerpt_lines;
                let heuristic_summary = if node_id.is_empty() {
                    None
                } else {
                    Some(build_surgical_heuristic_summary(
                        state,
                        node_id,
                        &resolved.to_string_lossy(),
                    ))
                };

                connected_files.push(surgical::ConnectedFileSource {
                    node_id: node_id.clone(),
                    label: label.clone(),
                    file_path: resolved.to_string_lossy().to_string(),
                    relation_type: relation_type.clone(),
                    edge_weight: *edge_weight,
                    source_excerpt,
                    excerpt_lines,
                    truncated,
                    heuristic_summary,
                });
            }
            Err(e) => {
                // Non-fatal: skip unreadable/binary files
                eprintln!(
                    "[m1nd] WARNING: surgical_context_v2 cannot read connected file {}: {}",
                    resolved.display(),
                    e
                );
            }
        }
    }

    // Cap the primary file's contents if needed
    let max_primary_lines = input.max_primary_file_lines.unwrap_or(400);
    let (primary_file_contents, primary_truncated) = {
        let all_lines: Vec<&str> = primary.file_contents.lines().collect();
        if all_lines.len() > max_primary_lines {
            let truncated_lines = all_lines.len() - max_primary_lines;
            let mut capped = all_lines[..max_primary_lines].join("\n");
            capped.push_str(&format!(
                "\n// ... [truncated {} of {} lines]",
                truncated_lines,
                all_lines.len()
            ));
            (capped, true)
        } else {
            (primary.file_contents.clone(), false)
        }
    };

    let (next_suggested_tool, next_suggested_target, next_step_hint) = surgical_v2_next_step(
        &primary.file_path,
        primary.heuristic_summary.as_ref(),
        &connected_files,
        input.proof_focused,
    );
    let proof_state = surgical_v2_proof_state(
        &primary_file_contents,
        primary.heuristic_summary.as_ref(),
        &connected_files,
        next_suggested_tool.as_deref(),
        input.proof_focused,
    );

    let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
    state.track_agent(&input.agent_id);
    let mut visited_files = vec![primary.file_path.clone()];
    visited_files.extend(
        connected_files
            .iter()
            .map(|entry| entry.file_path.clone())
            .collect::<Vec<_>>(),
    );
    let mut visited_nodes = Vec::new();
    if !primary.node_id.is_empty() {
        visited_nodes.push(primary.node_id.clone());
    }
    visited_nodes.extend(
        connected_files
            .iter()
            .map(|entry| entry.node_id.clone())
            .collect::<Vec<_>>(),
    );
    state.note_coverage(
        &input.agent_id,
        "surgical_context_v2",
        visited_files,
        visited_nodes,
    );

    // M1ND_PROOF_GATE: when this prover clears the primary target to edit, record
    // (agent_id, normalized target) so the write gate can later let a real edit
    // of that exact file through. Only the primary `file_path` is proved here —
    // connected files are context, not cleared edit targets.
    if proof_state == "ready_to_edit" {
        state.note_proof_ready(&input.agent_id, &primary.file_path, "surgical_context_v2");
    }

    Ok(surgical::SurgicalContextV2Output {
        file_path: primary.file_path,
        file_contents: primary_file_contents,
        line_count: primary.line_count,
        node_id: primary.node_id,
        symbols: primary.symbols,
        focused_symbol: primary.focused_symbol,
        connected_files,
        heuristic_summary: primary.heuristic_summary,
        next_suggested_tool,
        next_suggested_target,
        next_step_hint,
        proof_state,
        total_lines,
        elapsed_ms,
        primary_truncated,
    })
}

fn surgical_v2_next_step(
    file_path: &str,
    heuristic_summary: Option<&surgical::SurgicalHeuristicSummary>,
    connected_files: &[surgical::ConnectedFileSource],
    proof_focused: bool,
) -> (Option<String>, Option<String>, Option<String>) {
    let has_connected_proof = !connected_files.is_empty();
    let risky = heuristic_summary
        .map(|summary| summary.risk_score > 0.0 || summary.blast_radius_files > 0)
        .unwrap_or(false);

    if proof_focused || risky || has_connected_proof {
        let relation_list = connected_files
            .iter()
            .take(3)
            .map(|file| file.relation_type.as_str())
            .collect::<Vec<_>>()
            .join(", ");
        let hint = if relation_list.is_empty() {
            format!("Run validate_plan next before editing {}", file_path)
        } else {
            format!(
                "Run validate_plan next before editing {} because connected proof includes {}",
                file_path, relation_list
            )
        };
        return (
            Some("validate_plan".into()),
            Some(file_path.to_string()),
            Some(hint),
        );
    }

    (None, None, None)
}

fn surgical_v2_select_candidates(
    scored: Vec<(String, String, String, String, f32)>,
    max_connected_files: usize,
    proof_focused: bool,
) -> Vec<(String, String, String, String, f32)> {
    if !proof_focused || scored.len() <= max_connected_files {
        return scored.into_iter().take(max_connected_files).collect();
    }

    let mut selected = Vec::new();
    let mut used_relations = HashSet::new();

    for candidate in &scored {
        if used_relations.insert(candidate.3.clone()) {
            selected.push(candidate.clone());
            if selected.len() >= max_connected_files {
                return selected;
            }
        }
    }

    for candidate in scored {
        if selected.iter().any(|existing| existing.0 == candidate.0) {
            continue;
        }
        selected.push(candidate);
        if selected.len() >= max_connected_files {
            break;
        }
    }

    selected
}

fn surgical_v2_proof_state(
    file_contents: &str,
    heuristic_summary: Option<&surgical::SurgicalHeuristicSummary>,
    connected_files: &[surgical::ConnectedFileSource],
    next_suggested_tool: Option<&str>,
    proof_focused: bool,
) -> String {
    if file_contents.trim().is_empty() {
        return "blocked".into();
    }

    let risky = heuristic_summary
        .map(|summary| {
            summary.risk_level == "high"
                || summary.risk_score >= 0.35
                || summary.blast_radius_files > 0
        })
        .unwrap_or(false);

    if proof_focused || next_suggested_tool == Some("validate_plan") || risky {
        return "proving".into();
    }

    if !connected_files.is_empty() || heuristic_summary.is_some() {
        return "triaging".into();
    }

    "ready_to_edit".into()
}

fn surgical_v2_relation_rank(relation_type: &str) -> u8 {
    match relation_type {
        "caller" | "callee" => 3,
        "test" => 2,
        _ => 1,
    }
}

fn surgical_v2_file_kind_rank(path: &str) -> u8 {
    let path_lower = path.to_lowercase();
    let basename = Path::new(path)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("")
        .to_lowercase();

    if matches!(
        basename.as_str(),
        "cargo.toml" | "cargo.lock" | "package.json"
    ) || path_lower.ends_with(".md")
        || path_lower.contains("/docs/")
        || path_lower.contains("/target/")
        || path_lower.contains("/node_modules/")
        || path_lower.contains("/dist/")
    {
        return 0;
    }

    if path_lower.contains("/tests/")
        || basename.starts_with("test_")
        || path_lower.contains(".test.")
        || path_lower.contains(".spec.")
        || path_lower.ends_with("_test.rs")
    {
        return 2;
    }

    if matches!(
        Path::new(path)
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or(""),
        "rs" | "py" | "ts" | "tsx" | "js" | "jsx" | "go" | "java"
    ) {
        return 3;
    }

    1
}

// ---------------------------------------------------------------------------
// apply_batch
// ---------------------------------------------------------------------------

/// Handle apply_batch.
///
/// Writes multiple files atomically and triggers a single bulk re-ingest.
///
/// Steps:
///   1. Empty edits = fast-path no-op.
///   2. Resolve and validate all file paths (path safety check) BEFORE any writes.
///   3. Read old content for each file (for diff).
///   4. ATOMIC mode: write all files to unique temp files first.
///      If any temp write fails, clean up all temp files and return error.
///      Then rename all temp files to targets.
///   5. NON-ATOMIC mode: write each file independently via temp+rename.
///   6. Compute diffs per file.
///   7. If reingest: bulk re-ingest all modified files in one pass.
///   8. Assemble ApplyBatchOutput.
pub fn handle_apply_batch(
    state: &mut SessionState,
    input: surgical::ApplyBatchInput,
) -> M1ndResult<surgical::ApplyBatchOutput> {
    let start = Instant::now();
    let batch_id = format!(
        "batch-{}",
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|value| value.as_nanos())
            .unwrap_or(0)
    );
    let mut phases: Vec<surgical::ApplyBatchPhase> = Vec::new();
    let mut progress_events: Vec<surgical::ApplyBatchProgressEvent> = Vec::new();
    let phase_count = 5usize;
    let phase_names = ["validate", "write", "reingest", "verify", "done"];

    // Step 1: Empty edits = fast-path no-op
    if input.edits.is_empty() {
        let noop_event = surgical::ApplyBatchProgressEvent {
            batch_id: batch_id.clone(),
            event_type: "batch_completed".into(),
            phase: "done".into(),
            phase_index: 0,
            progress_pct: 100.0,
            current_file: None,
            next_phase: None,
            proof_state: Some("ready_to_edit".into()),
            next_suggested_tool: None,
            next_suggested_target: None,
            next_step_hint: None,
            proactive_insights: Vec::new(),
            elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
            message: "No edits were provided.".into(),
        };
        emit_apply_batch_progress(state, &noop_event);
        return Ok(surgical::ApplyBatchOutput {
            batch_id,
            all_succeeded: true,
            files_written: 0,
            files_total: 0,
            results: Vec::new(),
            reingested: false,
            total_bytes_written: 0,
            verification: None,
            proactive_insights: Vec::new(),
            next_suggested_tool: None,
            next_suggested_target: None,
            next_step_hint: None,
            proof_state: "ready_to_edit".into(),
            status_message: "apply_batch noop: no edits provided".into(),
            active_phase: "done".into(),
            completed_phase_count: 1,
            phase_count,
            remaining_phase_count: 0,
            progress_pct: 100.0,
            next_phase: None,
            progress_events: vec![noop_event],
            phases: vec![surgical::ApplyBatchPhase {
                phase: "done".into(),
                phase_index: 0,
                status: "completed".into(),
                files_completed: 0,
                files_total: 0,
                current_file: None,
                progress_pct: 100.0,
                next_phase: None,
                elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
                message: "No edits were provided.".into(),
            }],
            elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        });
    }

    // Step 2: Resolve and validate ALL paths upfront (fail-fast before any writes)
    let mut resolved_edits: Vec<(PathBuf, &surgical::BatchEditItem, String)> = Vec::new();
    for edit in &input.edits {
        let resolved = resolve_file_path(&edit.file_path, &state.ingest_roots);
        let validated = validate_path_safety(&resolved, &state.ingest_roots)?;
        // Read old content for diff (empty string if new file)
        let old_content = std::fs::read_to_string(&validated).unwrap_or_default();
        resolved_edits.push((validated, edit, old_content));
    }
    phases.push(surgical::ApplyBatchPhase {
        phase: "validate".into(),
        phase_index: 0,
        status: "completed".into(),
        files_completed: 0,
        files_total: input.edits.len(),
        current_file: resolved_edits
            .first()
            .map(|(path, _, _)| path.to_string_lossy().to_string()),
        progress_pct: 20.0,
        next_phase: Some(phase_names[1].into()),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: format!("Validated {} edit targets.", input.edits.len()),
    });
    let validate_event = surgical::ApplyBatchProgressEvent {
        batch_id: batch_id.clone(),
        event_type: "phase_completed".into(),
        phase: "validate".into(),
        phase_index: 0,
        progress_pct: 20.0,
        current_file: resolved_edits
            .first()
            .map(|(path, _, _)| path.to_string_lossy().to_string()),
        next_phase: Some(phase_names[1].into()),
        proof_state: None,
        next_suggested_tool: None,
        next_suggested_target: None,
        next_step_hint: None,
        proactive_insights: Vec::new(),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: format!("Validated {} edit targets.", input.edits.len()),
    };
    emit_apply_batch_progress(state, &validate_event);
    progress_events.push(validate_event);

    // Pre-write snapshot: capture graph nodes BEFORE writing (for verify graph-diff)
    let pre_nodes: std::collections::HashMap<String, HashSet<String>> = if input.verify {
        let graph = state.graph.read();
        resolved_edits
            .iter()
            .map(|(path, _, _)| {
                let path_str = path.to_string_lossy().to_string();
                let nodes: HashSet<String> = find_nodes_for_file(&graph, &path_str)
                    .into_iter()
                    .map(|(_, ext_id)| ext_id)
                    .collect();
                (path_str, nodes)
            })
            .collect()
    } else {
        std::collections::HashMap::new()
    };

    let mut results: Vec<surgical::BatchEditResult> = Vec::new();
    let mut total_bytes_written: usize = 0;

    if input.atomic {
        // --- ATOMIC MODE: all-or-nothing ---

        // Phase 1: Write all to unique temp files
        let mut temp_files: Vec<(PathBuf, PathBuf)> = Vec::new(); // (tmp_path, target_path)
        let pid = std::process::id();
        let batch_id = start.elapsed().as_nanos(); // unique per batch call

        for (i, (validated, edit, _old)) in resolved_edits.iter().enumerate() {
            let parent = validated.parent().unwrap_or(Path::new("."));

            // Ensure parent directory exists
            if !parent.exists() {
                if let Err(e) = std::fs::create_dir_all(parent) {
                    // Clean up temp files written so far
                    for (tmp, _) in &temp_files {
                        let _ = std::fs::remove_file(tmp);
                    }
                    return Err(M1ndError::InvalidParams {
                        tool: "m1nd_apply_batch".into(),
                        detail: format!(
                            "cannot create directory {}: {} Hint: create or choose a parent directory inside an ingested workspace root, then retry apply_batch.",
                            parent.display(),
                            e
                        ),
                    });
                }
            }

            // BUG FIX (B2): unique temp file per edit (pid + batch_id + index)
            let tmp_path = parent.join(format!(".m1nd_batch_{}_{}_{}_.tmp", pid, batch_id, i));

            match std::fs::write(&tmp_path, &edit.new_content) {
                Ok(_) => {
                    temp_files.push((tmp_path, validated.clone()));
                }
                Err(e) => {
                    // Clean up already-written temp files
                    for (tmp, _) in &temp_files {
                        let _ = std::fs::remove_file(tmp);
                    }
                    return Err(M1ndError::InvalidParams {
                        tool: "m1nd_apply_batch".into(),
                        detail: format!(
                            "atomic batch failed: cannot write temp file for {}: {} Hint: check write permissions under the workspace root and retry apply_batch.",
                            validated.display(),
                            e
                        ),
                    });
                }
            }
        }

        // Phase 2: Rename all temp files to targets (atomic per-file)
        let mut renamed_files: Vec<(PathBuf, String)> = Vec::new(); // (target, old_content for rollback)
        for (idx, (tmp_path, target_path)) in temp_files.iter().enumerate() {
            if let Err(e) = std::fs::rename(tmp_path, target_path) {
                // Rename failure: rollback already-renamed files by restoring old content
                for (rollback_target, old_content) in &renamed_files {
                    let _ = std::fs::write(rollback_target, old_content);
                }
                // Clean up remaining temp files
                for (tmp, _) in temp_files.iter().skip(idx) {
                    let _ = std::fs::remove_file(tmp);
                }
                return Err(M1ndError::InvalidParams {
                    tool: "m1nd_apply_batch".into(),
                    detail: format!(
                        "atomic rename failed {} -> {}: {} Hint: retry apply_batch after checking filesystem permissions and free space in the workspace.",
                        tmp_path.display(),
                        target_path.display(),
                        e
                    ),
                });
            }
            // Track for potential rollback
            renamed_files.push((
                target_path.clone(),
                resolved_edits[idx].2.clone(), // old_content
            ));
        }

        // Phase 3: Compute diffs for all successfully written files
        for (validated, edit, old_content) in &resolved_edits {
            let (added, removed) = diff_summary(old_content, &edit.new_content);
            let bytes = edit.new_content.len();
            total_bytes_written += bytes;

            // Build a simple unified diff string
            let diff_str = format!(
                "@@ -{},{} +{},{} @@\n{}{}",
                1,
                old_content.lines().count(),
                1,
                edit.new_content.lines().count(),
                old_content
                    .lines()
                    .take(3)
                    .map(|l| format!("-{}\n", l))
                    .collect::<String>(),
                edit.new_content
                    .lines()
                    .take(3)
                    .map(|l| format!("+{}\n", l))
                    .collect::<String>(),
            );

            results.push(surgical::BatchEditResult {
                file_path: validated.to_string_lossy().to_string(),
                success: true,
                diff: diff_str,
                lines_added: added,
                lines_removed: removed,
                error: None,
            });
        }
    } else {
        // --- NON-ATOMIC MODE: write each file independently ---
        let pid = std::process::id();
        let batch_id = start.elapsed().as_nanos();

        for (i, (validated, edit, old_content)) in resolved_edits.iter().enumerate() {
            let parent = validated.parent().unwrap_or(Path::new("."));

            // Ensure parent directory exists
            if !parent.exists() {
                let _ = std::fs::create_dir_all(parent);
            }

            // Unique temp file per edit (same fix as atomic)
            let tmp_path = parent.join(format!(".m1nd_batch_{}_{}_{}_.tmp", pid, batch_id, i));

            match std::fs::write(&tmp_path, &edit.new_content)
                .and_then(|_| std::fs::rename(&tmp_path, validated))
            {
                Ok(_) => {
                    let (added, removed) = diff_summary(old_content, &edit.new_content);
                    let bytes = edit.new_content.len();
                    total_bytes_written += bytes;

                    let diff_str = format!(
                        "@@ -{},{} +{},{} @@\n{}{}",
                        1,
                        old_content.lines().count(),
                        1,
                        edit.new_content.lines().count(),
                        old_content
                            .lines()
                            .take(3)
                            .map(|l| format!("-{}\n", l))
                            .collect::<String>(),
                        edit.new_content
                            .lines()
                            .take(3)
                            .map(|l| format!("+{}\n", l))
                            .collect::<String>(),
                    );

                    results.push(surgical::BatchEditResult {
                        file_path: validated.to_string_lossy().to_string(),
                        success: true,
                        diff: diff_str,
                        lines_added: added,
                        lines_removed: removed,
                        error: None,
                    });
                }
                Err(e) => {
                    let _ = std::fs::remove_file(&tmp_path);
                    results.push(surgical::BatchEditResult {
                        file_path: validated.to_string_lossy().to_string(),
                        success: false,
                        diff: String::new(),
                        lines_added: 0,
                        lines_removed: 0,
                        error: Some(e.to_string()),
                    });
                }
            }
        }
    }
    phases.push(surgical::ApplyBatchPhase {
        phase: "write".into(),
        phase_index: 1,
        status: if results.iter().all(|r| r.success) {
            "completed".into()
        } else {
            "failed".into()
        },
        files_completed: results.iter().filter(|r| r.success).count(),
        files_total: input.edits.len(),
        current_file: results.last().map(|result| result.file_path.clone()),
        progress_pct: 40.0,
        next_phase: Some(phase_names[2].into()),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: format!(
            "Wrote {} of {} files.",
            results.iter().filter(|r| r.success).count(),
            input.edits.len()
        ),
    });
    let write_event = surgical::ApplyBatchProgressEvent {
        batch_id: batch_id.clone(),
        event_type: "phase_completed".into(),
        phase: "write".into(),
        phase_index: 1,
        progress_pct: 40.0,
        current_file: results.last().map(|result| result.file_path.clone()),
        next_phase: Some(phase_names[2].into()),
        proof_state: None,
        next_suggested_tool: None,
        next_suggested_target: None,
        next_step_hint: None,
        proactive_insights: Vec::new(),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: format!(
            "Wrote {} of {} files.",
            results.iter().filter(|r| r.success).count(),
            input.edits.len()
        ),
    };
    emit_apply_batch_progress(state, &write_event);
    progress_events.push(write_event);

    // Step 7: Bulk re-ingest (single pass covering all successfully written files)
    let files_written = results.iter().filter(|r| r.success).count();
    let all_succeeded = files_written == input.edits.len();

    let reingested = if input.reingest && files_written > 0 {
        let successful_paths: Vec<String> = results
            .iter()
            .filter(|r| r.success)
            .map(|r| r.file_path.clone())
            .collect();

        let mut any_ingested = false;
        for path in &successful_paths {
            let ingest_input = crate::protocol::IngestInput {
                path: path.clone(),
                agent_id: input.agent_id.clone(),
                mode: "merge".to_string(),
                incremental: true,
                adapter: "code".to_string(),
                namespace: None,
                include_dotfiles: false,
                dotfile_patterns: Vec::new(),
            };

            match crate::tools::handle_ingest(state, ingest_input) {
                Ok(_) => {
                    any_ingested = true;
                }
                Err(e) => {
                    eprintln!(
                        "[m1nd] WARNING: apply_batch re-ingest failed for {}: {}",
                        path, e
                    );
                }
            }
        }
        any_ingested
    } else {
        false
    };
    phases.push(surgical::ApplyBatchPhase {
        phase: "reingest".into(),
        phase_index: 2,
        status: if input.reingest {
            if reingested {
                "completed".into()
            } else {
                "failed".into()
            }
        } else {
            "skipped".into()
        },
        files_completed: files_written,
        files_total: input.edits.len(),
        current_file: results
            .iter()
            .find(|result| result.success)
            .map(|result| result.file_path.clone()),
        progress_pct: 60.0,
        next_phase: Some(phase_names[3].into()),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: if input.reingest {
            if reingested {
                format!("Re-ingested {} written files.", files_written)
            } else {
                "Re-ingest did not complete successfully.".into()
            }
        } else {
            "Re-ingest skipped.".into()
        },
    });
    let reingest_event = surgical::ApplyBatchProgressEvent {
        batch_id: batch_id.clone(),
        event_type: "phase_completed".into(),
        phase: "reingest".into(),
        phase_index: 2,
        progress_pct: 60.0,
        current_file: results
            .iter()
            .find(|result| result.success)
            .map(|result| result.file_path.clone()),
        next_phase: Some(phase_names[3].into()),
        proof_state: None,
        next_suggested_tool: None,
        next_suggested_target: None,
        next_step_hint: None,
        proactive_insights: Vec::new(),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: if input.reingest {
            if reingested {
                format!("Re-ingested {} written files.", files_written)
            } else {
                "Re-ingest did not complete successfully.".into()
            }
        } else {
            "Re-ingest skipped.".into()
        },
    };
    emit_apply_batch_progress(state, &reingest_event);
    progress_events.push(reingest_event);

    // Step 8: Post-write verification via GRAPH DIFF (verify=true)
    // Compares pre-write graph nodes vs post-write to find what ACTUALLY changed structurally.
    // Also detects anti-patterns in the textual diff (todo!() → empty Ok(), etc.)
    let verification = if input.verify && all_succeeded && reingested {
        let verify_start = Instant::now();
        let mut high_impact_files = Vec::new();
        let mut total_affected = 0usize;
        let mut antibodies_triggered = Vec::new();
        let mut layer_violations = Vec::new();

        // Graph-diff: compare pre vs post nodes per file
        {
            let graph = state.graph.read();
            for result in &results {
                if !result.success {
                    continue;
                }

                // Post-write nodes (after re-ingest)
                let post_nodes: HashSet<String> = find_nodes_for_file(&graph, &result.file_path)
                    .into_iter()
                    .map(|(_, ext_id)| ext_id)
                    .collect();

                // Pre-write nodes (captured before writing)
                let empty_set = HashSet::new();
                let pre = pre_nodes.get(&result.file_path).unwrap_or(&empty_set);

                // Structural diff
                let added: Vec<&String> = post_nodes.difference(pre).collect();
                let removed: Vec<&String> = pre.difference(&post_nodes).collect();
                let changed_count = added.len() + removed.len();
                total_affected += changed_count;

                // Risk based on structural change, not file size
                let risk = if !removed.is_empty() || changed_count > 5 {
                    "high" // removing symbols or many changes = potentially breaking
                } else if changed_count > 0 {
                    "medium"
                } else {
                    "low" // content change within existing symbols
                };

                let mut top_affected: Vec<String> = Vec::new();
                for n in added.iter().take(3) {
                    top_affected.push(format!("+{}", n));
                }
                for n in removed.iter().take(3) {
                    top_affected.push(format!("-{}", n));
                }

                let agent_file_path = normalize_path_text(&result.file_path);
                let node_id = post_nodes
                    .iter()
                    .next()
                    .cloned()
                    .unwrap_or_else(|| format!("file::{agent_file_path}"));
                let requested_path_aliases: Vec<&str> = resolved_edits
                    .iter()
                    .filter_map(|(validated_path, edit, _)| {
                        let validated_path_str = validated_path.to_string_lossy();
                        if surgical_paths_match(&validated_path_str, &result.file_path) {
                            Some(edit.file_path.as_str())
                        } else {
                            None
                        }
                    })
                    .collect();
                let heuristic_summary = Some(build_surgical_heuristic_summary_with_extra_paths(
                    state,
                    &node_id,
                    &result.file_path,
                    &requested_path_aliases,
                ));
                let heuristics_surface_ref = Some(layers::HeuristicsSurfaceRef {
                    node_id: node_id.clone(),
                    file_path: agent_file_path.clone(),
                });

                high_impact_files.push(surgical::VerificationImpact {
                    file_path: agent_file_path,
                    node_id,
                    affected_count: changed_count,
                    risk: risk.to_string(),
                    top_affected,
                    heuristic_summary,
                    heuristics_surface_ref,
                });
            }
        }

        // Anti-pattern detection: compare old_content vs new_content directly
        // (The diff field can be unreliable for intra-line changes)
        //
        // Layer A: expanded trivial-return and stub detection.
        // Layer B: post-write compilation check (below this block).

        /// Count occurrences of ALL known trivial return patterns in source text.
        fn count_trivial_returns(src: &str) -> usize {
            const TRIVIAL_PATTERNS: &[&str] = &[
                "Vec::new()",
                "HashMap::new()",
                "BTreeMap::new()",
                "HashSet::new()",
                "String::new()",
                "String::from(\"\")",
                "Default::default()",
                "None",
                "Ok(())",
                "false",
                "true",
            ];
            const TRIVIAL_NUMERIC: &[&str] = &[
                "0,", "0;", "0)", "0\n", // plain 0
                "0.0", "0u8", "0i32", "0usize",
            ];
            let mut total = 0usize;
            for pat in TRIVIAL_PATTERNS {
                total += src.matches(pat).count();
            }
            for pat in TRIVIAL_NUMERIC {
                total += src.matches(pat).count();
            }
            // empty string literal: count `""` occurrences that are NOT `String::from("")`
            // (String::from("") already counted above)
            let empty_str_total = src.matches("\"\"").count();
            let string_from_empty = src.matches("String::from(\"\")").count();
            total += empty_str_total.saturating_sub(string_from_empty);
            total
        }

        /// Heuristic: does `new_code` contain real logic (function calls, control flow)?
        fn has_real_logic(new_code: &str) -> bool {
            // Look for function calls (word followed by parens with args)
            let has_fn_calls = new_code.contains("(") && !new_code.trim().is_empty();
            // Control flow keywords
            let has_control = new_code.contains("if ")
                || new_code.contains("match ")
                || new_code.contains("for ")
                || new_code.contains("while ")
                || new_code.contains("loop ")
                || new_code.contains(".map(")
                || new_code.contains(".filter(")
                || new_code.contains(".iter(")
                || new_code.contains(".and_then(")
                || new_code.contains(".unwrap_or(")
                || new_code.contains("await");
            has_fn_calls && has_control
        }

        for (validated_path, edit, old_content) in &resolved_edits {
            let new_content = &edit.new_content;
            let path_str = validated_path.to_string_lossy().to_string();

            // --- todo!() removal analysis (Layer A: improved stub detection) ---
            let old_todo_count = old_content.matches("todo!(").count();
            let new_todo_count = new_content.matches("todo!(").count();
            let todos_removed = old_todo_count.saturating_sub(new_todo_count);
            if todos_removed > 0 {
                // Count trivial returns in old vs new
                let old_trivial = count_trivial_returns(old_content);
                let new_trivial = count_trivial_returns(new_content);

                // Count net new lines (rough proxy for implementation size)
                let old_line_count = old_content.lines().count();
                let new_line_count = new_content.lines().count();
                let net_new_lines = new_line_count as i64 - old_line_count as i64;

                if new_trivial > old_trivial {
                    // More trivial returns appeared → SILENT OK
                    layer_violations.push(format!(
                        "SILENT OK: {} — {} todo!() replaced with trivial return (+{} trivial patterns detected: Vec::new/Default::default/None/0/Ok(())/etc)",
                        path_str, todos_removed, new_trivial - old_trivial
                    ));
                } else if net_new_lines <= 2 {
                    // todo!() removed but barely any new code → BROKEN
                    layer_violations.push(format!(
                        "BROKEN STUB: {} — {} todo!() removed but only {} net new lines (likely trivial replacement, not real implementation)",
                        path_str, todos_removed, net_new_lines
                    ));
                } else if net_new_lines > 5 && has_real_logic(new_content) {
                    // Substantial new code with real logic → SAFE (just note it)
                    layer_violations.push(format!(
                        "STUB FILLED: {} — {} todo!() stubs implemented with {} net new lines of real logic (verify correctness)",
                        path_str, todos_removed, net_new_lines
                    ));
                } else {
                    // Some code added but unclear quality → warn
                    layer_violations.push(format!(
                        "STUB FILLED (REVIEW): {} — {} todo!() removed, {} net new lines added but no clear control flow detected",
                        path_str, todos_removed, net_new_lines
                    ));
                }
            }

            // --- New .unwrap() calls ---
            let old_unwrap_count = old_content.matches(".unwrap()").count();
            let new_unwrap_count = new_content.matches(".unwrap()").count();
            if new_unwrap_count > old_unwrap_count {
                layer_violations.push(format!(
                    "NEW UNWRAP: {} — {} new .unwrap() calls added (potential panic points)",
                    path_str,
                    new_unwrap_count - old_unwrap_count
                ));
            }

            // --- Error handling removed ---
            let old_question_count =
                old_content.matches("?;").count() + old_content.matches("?)").count();
            let new_question_count =
                new_content.matches("?;").count() + new_content.matches("?)").count();
            if old_question_count > new_question_count + 2 {
                layer_violations.push(format!(
                    "ERROR HANDLING REMOVED: {} — {} fewer error propagation points (?; or ?) detected",
                    path_str, old_question_count - new_question_count
                ));
            }
        }

        // Antibody name/description match in diffs
        for ab in &state.antibodies {
            if !ab.enabled {
                continue;
            }
            for result in &results {
                if result.diff.contains(&ab.name) || result.diff.contains(&ab.description) {
                    antibodies_triggered.push(format!(
                        "MATCH {}: {} (in {})",
                        ab.id, ab.description, result.file_path
                    ));
                }
            }
        }

        // -----------------------------------------------------------------
        // Layer C: Real graph BFS impact (2-hop blast radius)
        // -----------------------------------------------------------------
        let mut blast_radius_entries: Vec<surgical::BlastRadiusEntry> = Vec::new();
        {
            let graph = state.graph.read();
            for result in &results {
                if !result.success {
                    continue;
                }
                let (reachable_count, top_affected_ids) =
                    compute_blast_radius(&graph, &result.file_path);
                let risk = blast_radius_risk(reachable_count);

                // Update the high_impact_files entry with real BFS data:
                // replace the top_affected with real reachable file node IDs
                if let Some(impact) = high_impact_files
                    .iter_mut()
                    .find(|f| surgical_paths_match(&f.file_path, &result.file_path))
                {
                    impact.top_affected = top_affected_ids.clone();
                    impact.affected_count = impact.affected_count.max(reachable_count);
                    // Upgrade risk if BFS says higher
                    if risk == "high" || (risk == "medium" && impact.risk == "low") {
                        impact.risk = risk.to_string();
                    }
                }

                // Also track the real affected node count
                total_affected = total_affected.max(total_affected + reachable_count);

                blast_radius_entries.push(surgical::BlastRadiusEntry {
                    file_path: result.file_path.clone(),
                    reachable_files: reachable_count,
                    risk: risk.to_string(),
                    top_affected: top_affected_ids,
                });
            }
        }

        // -----------------------------------------------------------------
        // Layer D: Affected test execution
        // -----------------------------------------------------------------
        let modified_paths: Vec<PathBuf> = resolved_edits
            .iter()
            .filter(|(_, _, _)| true)
            .map(|(path, _, _)| path.clone())
            .collect();

        let (tests_run, tests_passed, tests_failed, test_output) =
            run_affected_tests(&modified_paths);

        let tests_broken = tests_failed.unwrap_or(0) > 0;

        // -----------------------------------------------------------------
        // Layer B (compile): Post-write compilation check
        // -----------------------------------------------------------------
        let compile_check: Option<String> = {
            let extensions: HashSet<&str> = resolved_edits
                .iter()
                .filter_map(|(path, _, _)| path.extension().and_then(|e| e.to_str()))
                .collect();
            let ws_root = state
                .workspace_root
                .clone()
                .or_else(|| state.ingest_roots.last().cloned());

            if extensions.contains("rs") {
                let cargo_dir = resolved_edits
                    .iter()
                    .filter(|(p, _, _)| p.extension().and_then(|e| e.to_str()) == Some("rs"))
                    .find_map(|(p, _, _)| find_cargo_project_root(p))
                    .or_else(|| {
                        ws_root
                            .clone()
                            .filter(|dir| Path::new(dir).join("Cargo.toml").exists())
                    });
                if let Some(dir) = cargo_dir {
                    match std::process::Command::new("cargo")
                        .arg("check")
                        .arg("--message-format=short")
                        .current_dir(&dir)
                        .stdout(std::process::Stdio::piped())
                        .stderr(std::process::Stdio::piped())
                        .spawn()
                    {
                        Ok(child) => match child.wait_with_output() {
                            Ok(out) if out.status.success() => Some("ok".to_string()),
                            Ok(out) => {
                                let stderr = String::from_utf8_lossy(&out.stderr);
                                let t: String = stderr.chars().take(200).collect();
                                layer_violations
                                    .push(format!("COMPILE ERROR (cargo check): {}", t));
                                Some(format!("error: {}", t))
                            }
                            Err(e) => {
                                let m = format!("cargo check process error: {}", e);
                                layer_violations.push(format!("COMPILE ERROR: {}", m));
                                Some(format!("error: {}", m))
                            }
                        },
                        Err(e) => {
                            let m = format!("failed to spawn cargo: {}", e);
                            layer_violations.push(format!("COMPILE ERROR: {}", m));
                            Some(format!("error: {}", m))
                        }
                    }
                } else {
                    None
                }
            } else if extensions.contains("go") {
                if let Some(dir) = ws_root.clone() {
                    match std::process::Command::new("go")
                        .args(["build", "./..."])
                        .current_dir(&dir)
                        .stdout(std::process::Stdio::piped())
                        .stderr(std::process::Stdio::piped())
                        .output()
                    {
                        Ok(out) if out.status.success() => Some("ok".to_string()),
                        Ok(out) => {
                            let stderr = String::from_utf8_lossy(&out.stderr);
                            let t: String = stderr.chars().take(200).collect();
                            layer_violations.push(format!("COMPILE ERROR (go build): {}", t));
                            Some(format!("error: {}", t))
                        }
                        Err(e) => {
                            let m = format!("failed to spawn go: {}", e);
                            layer_violations.push(format!("COMPILE ERROR: {}", m));
                            Some(format!("error: {}", m))
                        }
                    }
                } else {
                    None
                }
            } else if extensions.contains("py") {
                if let Some(python_command) = available_python_command() {
                    let mut py_ok = true;
                    let mut py_err = String::new();
                    for (path, _, _) in resolved_edits.iter().filter(|(path, _, _)| {
                        path.extension().and_then(|e| e.to_str()) == Some("py")
                    }) {
                        let path_str = path.to_string_lossy();
                        match std::process::Command::new(python_command)
                            .args([
                                "-c",
                                "import ast, pathlib, sys; ast.parse(pathlib.Path(sys.argv[1]).read_text())",
                            ])
                            .arg(path)
                            .stdout(std::process::Stdio::piped())
                            .stderr(std::process::Stdio::piped())
                            .output()
                        {
                            Ok(out) if !out.status.success() => {
                                let stderr = String::from_utf8_lossy(&out.stderr);
                                let t: String = stderr.chars().take(200).collect();
                                if py_err.is_empty() {
                                    py_err = t.to_string();
                                }
                                layer_violations.push(format!(
                                    "PARSE ERROR ({} ast): {} — {}",
                                    python_command, path_str, t
                                ));
                                py_ok = false;
                            }
                            Ok(_) => {}
                            Err(e) => {
                                py_err = format!("failed to spawn {}: {}", python_command, e);
                                py_ok = false;
                            }
                        }
                    }

                    if py_ok {
                        Some("ok".to_string())
                    } else {
                        Some(format!("error: {}", py_err))
                    }
                } else {
                    None
                }
            } else if extensions.contains("ts") || extensions.contains("tsx") {
                if let Some(dir) = ws_root.clone() {
                    let tsconfig = Path::new(&dir).join("tsconfig.json");
                    if tsconfig.exists() {
                        match std::process::Command::new("tsc")
                            .arg("--noEmit")
                            .current_dir(&dir)
                            .stdout(std::process::Stdio::piped())
                            .stderr(std::process::Stdio::piped())
                            .output()
                        {
                            Ok(out) if out.status.success() => Some("ok".to_string()),
                            Ok(out) => {
                                let combined = format!(
                                    "{}{}",
                                    String::from_utf8_lossy(&out.stdout),
                                    String::from_utf8_lossy(&out.stderr)
                                );
                                let t: String = combined.chars().take(200).collect();
                                layer_violations.push(format!("TYPE ERROR (tsc --noEmit): {}", t));
                                Some(format!("error: {}", t))
                            }
                            Err(e) => {
                                let m = format!("failed to spawn tsc: {}", e);
                                layer_violations.push(format!("TYPE ERROR: {}", m));
                                Some(format!("error: {}", m))
                            }
                        }
                    } else {
                        None
                    }
                } else {
                    None
                }
            } else {
                None
            }
        };
        let compile_broken = compile_check
            .as_deref()
            .map(|s| s.starts_with("error"))
            .unwrap_or(false);

        // -----------------------------------------------------------------
        // Verdict (incorporates all layers: A+B+C+D + compile)
        // -----------------------------------------------------------------
        let has_violations = !layer_violations.is_empty();
        let has_antibodies = !antibodies_triggered.is_empty();
        let has_high_risk = high_impact_files.iter().any(|f| f.risk == "high");
        let has_bfs_high = blast_radius_entries.iter().any(|b| b.risk == "high");
        let has_high_heuristic_risk = high_impact_files.iter().any(|impact| {
            impact
                .heuristic_summary
                .as_ref()
                .map(|summary| {
                    summary.risk_level != "low"
                        || summary.risk_score >= 0.35
                        || summary.heuristic_signals.tremor_observation_count >= 3
                        || summary.heuristic_signals.trust_risk_multiplier > 1.1
                        || summary.antibody_hits > 0
                })
                .unwrap_or(false)
        });

        let verdict = if tests_broken || compile_broken || has_violations || has_antibodies {
            "BROKEN".to_string() // compile/test failure or violations = BROKEN
        } else if has_high_risk || has_bfs_high || has_high_heuristic_risk {
            "RISKY".to_string()
        } else {
            "SAFE".to_string()
        };

        Some(surgical::VerificationReport {
            verdict,
            high_impact_files,
            antibodies_triggered,
            layer_violations,
            total_affected_nodes: total_affected,
            blast_radius: blast_radius_entries,
            tests_run,
            tests_passed,
            tests_failed,
            test_output,
            compile_check,
            verify_elapsed_ms: verify_start.elapsed().as_secs_f64() * 1000.0,
        })
    } else {
        None
    };
    phases.push(surgical::ApplyBatchPhase {
        phase: "verify".into(),
        phase_index: 3,
        status: if input.verify {
            if verification.is_some() {
                "completed".into()
            } else {
                "skipped".into()
            }
        } else {
            "skipped".into()
        },
        files_completed: files_written,
        files_total: input.edits.len(),
        current_file: verification
            .as_ref()
            .and_then(|report| {
                report
                    .high_impact_files
                    .first()
                    .map(|impact| impact.file_path.clone())
            })
            .or_else(|| {
                results
                    .iter()
                    .find(|result| result.success)
                    .map(|r| r.file_path.clone())
            }),
        progress_pct: 80.0,
        next_phase: Some(phase_names[4].into()),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: if let Some(report) = verification.as_ref() {
            format!("Verification finished with verdict {}.", report.verdict)
        } else if input.verify {
            "Verification could not run because writes or re-ingest did not complete.".into()
        } else {
            "Verification skipped.".into()
        },
    });
    let verify_event = surgical::ApplyBatchProgressEvent {
        batch_id: batch_id.clone(),
        event_type: "phase_completed".into(),
        phase: "verify".into(),
        phase_index: 3,
        progress_pct: 80.0,
        current_file: verification
            .as_ref()
            .and_then(|report| {
                report
                    .high_impact_files
                    .first()
                    .map(|impact| impact.file_path.clone())
            })
            .or_else(|| {
                results
                    .iter()
                    .find(|result| result.success)
                    .map(|r| r.file_path.clone())
            }),
        next_phase: Some(phase_names[4].into()),
        proof_state: None,
        next_suggested_tool: None,
        next_suggested_target: None,
        next_step_hint: None,
        proactive_insights: Vec::new(),
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
        message: if let Some(report) = verification.as_ref() {
            format!("Verification finished with verdict {}.", report.verdict)
        } else if input.verify {
            "Verification could not run because writes or re-ingest did not complete.".into()
        } else {
            "Verification skipped.".into()
        },
    };
    emit_apply_batch_progress(state, &verify_event);
    progress_events.push(verify_event);

    let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
    state.track_agent(&input.agent_id);
    let status_message = if all_succeeded {
        if let Some(report) = verification.as_ref() {
            format!(
                "apply_batch completed: wrote {} files, verification verdict {}.",
                files_written, report.verdict
            )
        } else if reingested {
            format!(
                "apply_batch completed: wrote {} files and re-ingested successfully.",
                files_written
            )
        } else {
            format!("apply_batch completed: wrote {} files.", files_written)
        }
    } else {
        format!(
            "apply_batch finished with partial success: wrote {} of {} files.",
            files_written,
            input.edits.len()
        )
    };
    let proactive_insights = {
        let mut insights = Vec::new();
        for impact in verification
            .as_ref()
            .map(|report| report.high_impact_files.iter().collect::<Vec<_>>())
            .unwrap_or_default()
        {
            let mut per_file = build_proactive_insights(
                state,
                &impact.file_path,
                Some(&impact.node_id),
                impact.heuristic_summary.as_ref(),
                verification.as_ref(),
            );
            insights.append(&mut per_file);
        }
        if insights.is_empty() {
            for result in &results {
                if !result.success {
                    continue;
                }
                let graph = state.graph.read();
                let file_node = find_nodes_for_file(&graph, &result.file_path)
                    .into_iter()
                    .find(|(node_id, ext)| {
                        is_file_node(&graph, *node_id) && ext.starts_with("file::")
                    })
                    .map(|(_, ext)| ext);
                drop(graph);
                let heuristic_summary = file_node.as_deref().map(|external_id| {
                    build_surgical_heuristic_summary(state, external_id, &result.file_path)
                });
                let mut per_file = build_proactive_insights(
                    state,
                    &result.file_path,
                    file_node.as_deref(),
                    heuristic_summary.as_ref(),
                    verification.as_ref(),
                );
                insights.append(&mut per_file);
            }
        }
        insights.sort_by(|a, b| {
            proactive_severity_rank(&a.severity)
                .cmp(&proactive_severity_rank(&b.severity))
                .then_with(|| b.confidence.total_cmp(&a.confidence))
        });
        insights.truncate(3);
        insights
    };
    // Antibody auto-propose on fix (batch): for each successfully written file,
    // if this agent's scan/audit previously flagged a node in it, emit a
    // `proposed_antibody`. Done after the read-only insight block closes so we
    // hold `&mut state` for finding consumption. Proposed antibodies are kept
    // even if the standard cap is hit.
    let mut proactive_insights = proactive_insights;
    {
        let written_files: Vec<String> = results
            .iter()
            .filter(|result| result.success)
            .map(|result| result.file_path.clone())
            .collect();
        let mut proposed = Vec::new();
        for file_path in &written_files {
            let mut per_file = propose_antibodies_for_write(state, &input.agent_id, file_path, &[]);
            proposed.append(&mut per_file);
        }
        if !proposed.is_empty() {
            let mut combined = proposed;
            combined.append(&mut proactive_insights);
            combined.sort_by(|a, b| {
                let a_prop = (a.kind != "proposed_antibody") as u8;
                let b_prop = (b.kind != "proposed_antibody") as u8;
                a_prop
                    .cmp(&b_prop)
                    .then_with(|| {
                        proactive_severity_rank(&a.severity)
                            .cmp(&proactive_severity_rank(&b.severity))
                    })
                    .then_with(|| b.confidence.total_cmp(&a.confidence))
            });
            combined.truncate(3);
            proactive_insights = combined;
        }
    }
    let primary_file_path = results
        .iter()
        .find(|result| result.success)
        .map(|result| result.file_path.as_str());
    let primary_node_id = verification.as_ref().and_then(|report| {
        report
            .high_impact_files
            .first()
            .map(|impact| impact.node_id.as_str())
    });
    let _ = persist_daemon_alerts_from_insights(
        state,
        &proactive_insights,
        primary_file_path,
        primary_node_id,
    );
    phases.push(surgical::ApplyBatchPhase {
        phase: "done".into(),
        phase_index: 4,
        status: if all_succeeded {
            "completed".into()
        } else {
            "failed".into()
        },
        files_completed: files_written,
        files_total: input.edits.len(),
        current_file: None,
        progress_pct: 100.0,
        next_phase: None,
        elapsed_ms,
        message: status_message.clone(),
    });
    let (next_suggested_tool, next_suggested_target, next_step_hint, proof_state) =
        apply_batch_next_step(all_succeeded, reingested, verification.as_ref(), &results);

    let done_event = surgical::ApplyBatchProgressEvent {
        batch_id: batch_id.clone(),
        event_type: "batch_completed".into(),
        phase: "done".into(),
        phase_index: 4,
        progress_pct: 100.0,
        current_file: None,
        next_phase: None,
        proof_state: Some(proof_state.clone()),
        next_suggested_tool: next_suggested_tool.clone(),
        next_suggested_target: next_suggested_target.clone(),
        next_step_hint: next_step_hint.clone(),
        proactive_insights: proactive_insights.clone(),
        elapsed_ms,
        message: status_message.clone(),
    };
    emit_apply_batch_progress(state, &done_event);
    progress_events.push(done_event);

    Ok(surgical::ApplyBatchOutput {
        batch_id,
        all_succeeded,
        files_written,
        files_total: input.edits.len(),
        results,
        reingested,
        total_bytes_written,
        verification,
        proactive_insights,
        next_suggested_tool,
        next_suggested_target,
        next_step_hint,
        proof_state,
        status_message,
        active_phase: "done".into(),
        completed_phase_count: phases.len(),
        phase_count,
        remaining_phase_count: 0,
        progress_pct: ((phases.len() as f32 / phase_count as f32) * 100.0).min(100.0),
        next_phase: None,
        progress_events,
        phases,
        elapsed_ms,
    })
}

fn emit_apply_batch_progress(state: &SessionState, event: &surgical::ApplyBatchProgressEvent) {
    if let Some(sink) = state.apply_batch_progress_sink.as_ref() {
        sink(event);
    }
}

fn apply_batch_next_step(
    all_succeeded: bool,
    reingested: bool,
    verification: Option<&surgical::VerificationReport>,
    results: &[surgical::BatchEditResult],
) -> (Option<String>, Option<String>, Option<String>, String) {
    let first_written = results
        .iter()
        .find(|result| result.success)
        .map(|result| result.file_path.clone());
    let first_failed = results
        .iter()
        .find(|result| !result.success)
        .map(|result| result.file_path.clone());

    if !all_succeeded {
        let target = first_failed.or(first_written);
        return (
            Some("view".into()),
            target,
            Some("Inspect the failed or partial write target before retrying the batch.".into()),
            "blocked".into(),
        );
    }

    if let Some(report) = verification {
        let hotspot_target = report
            .high_impact_files
            .first()
            .map(|impact| impact.file_path.clone())
            .or_else(|| first_written.clone());
        return match report.verdict.as_str() {
            "BROKEN" => (
                Some("view".into()),
                hotspot_target,
                Some("Verification found a broken outcome; inspect the leading file before making more edits.".into()),
                "blocked".into(),
            ),
            "RISKY" => (
                Some("heuristics_surface".into()),
                hotspot_target,
                Some("Verification is risky; inspect the highest-impact file before continuing the edit loop.".into()),
                "proving".into(),
            ),
            _ => (
                None,
                first_written,
                Some("Batch verification came back safe; the edit set is ready for follow-up work if needed.".into()),
                "ready_to_edit".into(),
            ),
        };
    }

    if reingested {
        return (
            Some("validate_plan".into()),
            first_written,
            Some("The batch wrote successfully, but it still needs a verification pass before promotion.".into()),
            "triaging".into(),
        );
    }

    (
        Some("view".into()),
        first_written,
        Some("The batch wrote files, but re-ingest did not complete; inspect the touched file before trusting graph state.".into()),
        "blocked".into(),
    )
}

fn truncate_output(
    content: String,
    max_output_chars: Option<usize>,
    label: &str,
) -> (String, bool, Option<String>) {
    let Some(limit) = max_output_chars else {
        return (content, false, None);
    };
    if content.chars().count() <= limit {
        return (content, false, None);
    }

    let truncated_content: String = content.chars().take(limit).collect();
    let summary = format!(
        "{} output exceeded {} chars and was truncated inline. Refine scope or raise max_output_chars to inspect the full payload.",
        label, limit
    );
    (truncated_content, true, Some(summary))
}

fn summarize_file_excerpt(file_path: &str, total_lines: usize, lines_returned: usize) -> String {
    format!(
        "{} — returned {} of {} lines",
        file_path, lines_returned, total_lines
    )
}

// ---------------------------------------------------------------------------
// view — lightweight file reader
// ---------------------------------------------------------------------------

/// Handle view: fast file reading with line numbers.
/// No graph traversal — just read, format, return.
/// Auto-ingests the file if not in the graph.
pub fn handle_view(
    state: &mut SessionState,
    input: surgical::ViewInput,
) -> M1ndResult<surgical::ViewOutput> {
    let start = Instant::now();

    // Step 1: Resolve path
    let resolved_path = resolve_file_path(&input.file_path, &state.ingest_roots);

    // Step 2: Read file
    let raw_content =
        std::fs::read_to_string(&resolved_path).map_err(|e| M1ndError::InvalidParams {
            tool: "m1nd_view".into(),
            detail: format!("cannot read file {}: {}", resolved_path.display(), e),
        })?;

    let all_lines: Vec<&str> = raw_content.lines().collect();
    let total_lines = all_lines.len();

    // Step 3: Apply offset and limit
    let offset = input.offset.unwrap_or(0).min(total_lines);
    let remaining = total_lines.saturating_sub(offset);
    let limit = input.limit.unwrap_or(remaining).min(remaining);
    let slice = &all_lines[offset..offset + limit];

    // Step 4: Format with line numbers (1-based, like cat -n)
    let width = if total_lines > 0 {
        ((offset + limit) as f64).log10().floor() as usize + 1
    } else {
        1
    };
    let rendered = slice
        .iter()
        .enumerate()
        .map(|(i, line)| format!("{:>w$}  {}", offset + i + 1, line, w = width))
        .collect::<Vec<_>>()
        .join("\n");

    // Step 5: Auto-ingest if requested and file not in graph
    let mut auto_ingested = false;
    if input.auto_ingest {
        let path_str = resolved_path.to_string_lossy().to_string();
        let graph = state.graph.read();
        let existing = find_nodes_for_file(&graph, &path_str);
        drop(graph);

        if existing.is_empty() {
            let ingest_input = crate::protocol::IngestInput {
                path: path_str,
                agent_id: input.agent_id.clone(),
                mode: "merge".to_string(),
                incremental: true,
                adapter: "code".to_string(),
                namespace: None,
                include_dotfiles: false,
                dotfile_patterns: Vec::new(),
            };
            if crate::tools::handle_ingest(state, ingest_input).is_ok() {
                auto_ingested = true;
            }
        }
    }

    let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
    let file_path = resolved_path.to_string_lossy().to_string();
    let (content, truncated, inline_summary) =
        truncate_output(rendered, input.max_output_chars, &file_path);
    state.note_coverage(
        &input.agent_id,
        "view",
        vec![file_path.clone()],
        std::iter::empty::<String>(),
    );

    Ok(surgical::ViewOutput {
        file_path,
        content,
        total_lines,
        offset,
        lines_returned: limit,
        auto_ingested,
        truncated,
        inline_summary,
        elapsed_ms,
    })
}

/// Handle m1nd.batch_view: read multiple files or globs in one call.
pub fn handle_batch_view(
    state: &mut SessionState,
    input: surgical::BatchViewInput,
) -> M1ndResult<surgical::BatchViewOutput> {
    let start = Instant::now();
    let max_lines = input.max_lines_per_file.max(1);
    let mut requested_files: Vec<(String, std::path::PathBuf)> = Vec::new();

    for requested in &input.files {
        if requested.contains('*') || requested.contains('?') || requested.contains('[') {
            let pattern = if std::path::Path::new(requested).is_absolute() {
                requested.clone()
            } else if let Some(root) = state.ingest_roots.last() {
                format!("{}/{}", root.trim_end_matches('/'), requested)
            } else {
                requested.clone()
            };
            let entries = glob::glob(&pattern).map_err(|e| M1ndError::InvalidParams {
                tool: "batch_view".into(),
                detail: format!("invalid glob '{}': {}", requested, e),
            })?;
            for entry in entries.flatten() {
                if entry.is_file() {
                    requested_files.push((requested.clone(), entry));
                }
            }
        } else {
            requested_files.push((
                requested.clone(),
                resolve_file_path(requested, &state.ingest_roots),
            ));
        }
    }

    let mut seen = std::collections::BTreeSet::new();
    let mut entries = Vec::new();
    let mut total_lines = 0usize;
    let mut visited_files = Vec::new();

    for (requested, resolved_path) in requested_files {
        let file_path = resolved_path.to_string_lossy().to_string();
        if !resolved_path.is_file() || !seen.insert(file_path.clone()) {
            continue;
        }

        let raw_content =
            std::fs::read_to_string(&resolved_path).map_err(|e| M1ndError::InvalidParams {
                tool: "batch_view".into(),
                detail: format!("cannot read file {}: {}", resolved_path.display(), e),
            })?;
        let all_lines: Vec<&str> = raw_content.lines().collect();
        let total = all_lines.len();
        total_lines += total;
        let lines_returned = total.min(max_lines);
        let width = if lines_returned > 0 {
            (lines_returned as f64).log10().floor() as usize + 1
        } else {
            1
        };
        let rendered = all_lines[..lines_returned]
            .iter()
            .enumerate()
            .map(|(i, line)| format!("{:>w$}  {}", i + 1, line, w = width))
            .collect::<Vec<_>>()
            .join("\n");

        let mut auto_ingested = false;
        if input.auto_ingest {
            let graph = state.graph.read();
            let existing = find_nodes_for_file(&graph, &file_path);
            drop(graph);

            if existing.is_empty() {
                let ingest_input = crate::protocol::IngestInput {
                    path: file_path.clone(),
                    agent_id: input.agent_id.clone(),
                    mode: "merge".to_string(),
                    incremental: true,
                    adapter: "code".to_string(),
                    namespace: None,
                    include_dotfiles: false,
                    dotfile_patterns: Vec::new(),
                };
                if crate::tools::handle_ingest(state, ingest_input).is_ok() {
                    auto_ingested = true;
                }
            }
        }

        visited_files.push(file_path.clone());
        entries.push(surgical::BatchViewFileOutput {
            requested,
            file_path: file_path.clone(),
            total_lines: total,
            lines_returned,
            auto_ingested,
            truncated: total > lines_returned,
            summary: input
                .summary_mode
                .then(|| summarize_file_excerpt(&file_path, total, lines_returned)),
            content: rendered,
        });
    }

    let aggregated = entries
        .iter()
        .map(|entry| format!("===== {} =====\n{}\n", entry.file_path, entry.content))
        .collect::<Vec<_>>()
        .join("\n");
    let (_, truncated, inline_summary) =
        truncate_output(aggregated, input.max_output_chars, "batch_view");

    state.note_coverage(
        &input.agent_id,
        "batch_view",
        visited_files,
        std::iter::empty::<String>(),
    );

    Ok(surgical::BatchViewOutput {
        files_read: entries.len(),
        total_lines,
        entries,
        truncated,
        inline_summary,
        elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
    })
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::server::McpConfig;
    use m1nd_core::antibody::{Antibody, AntibodyPattern, AntibodySeverity};
    use m1nd_core::domain::DomainConfig;
    use m1nd_core::graph::{Graph, NodeProvenanceInput};
    use m1nd_core::types::{EdgeDirection, FiniteF32, NodeType};
    use std::sync::{Arc, Mutex};

    fn build_surgical_state(root: &std::path::Path, file_path: &str) -> SessionState {
        let runtime_dir = root.join("runtime");
        std::fs::create_dir_all(&runtime_dir).expect("runtime dir");

        let config = McpConfig {
            graph_source: runtime_dir.join("graph.json"),
            plasticity_state: runtime_dir.join("plasticity.json"),
            runtime_dir: Some(runtime_dir),
            ..Default::default()
        };

        let mut graph = Graph::new();
        let primary = graph
            .add_node(
                &format!("file::{}", file_path),
                "core.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add primary node");
        graph.set_node_provenance(
            primary,
            NodeProvenanceInput {
                source_path: Some(file_path),
                line_start: Some(1),
                line_end: Some(12),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );

        let impacted_path = root.join("src/dependent.rs");
        std::fs::create_dir_all(impacted_path.parent().expect("dependent parent"))
            .expect("mk dependent parent");
        std::fs::write(&impacted_path, "pub fn dependent() {}\n").expect("write dependent file");
        let impacted_str = impacted_path.to_string_lossy().to_string();

        let impacted = graph
            .add_node(
                &format!("file::{}", impacted_str),
                "dependent.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add impacted node");
        graph.set_node_provenance(
            impacted,
            NodeProvenanceInput {
                source_path: Some(&impacted_str),
                line_start: Some(1),
                line_end: Some(1),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );

        graph
            .add_edge(
                primary,
                impacted,
                "imports",
                FiniteF32::new(1.0),
                EdgeDirection::Forward,
                false,
                FiniteF32::new(0.8),
            )
            .expect("add edge");
        graph.finalize().expect("finalize graph");

        let mut state =
            SessionState::initialize(graph, &config, DomainConfig::code()).expect("init session");
        state.ingest_roots = vec![root.to_string_lossy().to_string()];
        state.workspace_root = Some(root.to_string_lossy().to_string());
        state
    }

    fn recent_test_timestamp() -> f64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("clock")
            .as_secs_f64()
    }

    #[test]
    fn heuristic_summary_resolves_windows_extended_path_aliases() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.py");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "def core():\n    return 1\n").expect("write primary");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let windows_raw = r"C:\repo\src\core.py";
        let windows_extended = r"\\?\C:\repo\src\core.py";
        let now = recent_test_timestamp();
        state
            .trust_ledger
            .record_defect(&format!("file::{windows_raw}"), now - 60.0);

        let summary = build_surgical_heuristic_summary(&state, "file::core.py", windows_extended);

        assert!(
            summary.risk_score > 0.0,
            "extended Windows path should resolve to the raw ledger path alias"
        );
    }

    #[test]
    fn heuristic_summary_resolves_memory_alias_when_graph_path_is_missing() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.py");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "def core():\n    return 1\n").expect("write primary");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let absolute_alias = format!("file::{primary_str}");
        let now = recent_test_timestamp();
        state
            .trust_ledger
            .record_defect(&absolute_alias, now - 120.0);
        state
            .trust_ledger
            .record_defect(&absolute_alias, now - 60.0);
        state
            .tremor_registry
            .record_observation(&absolute_alias, 1.0, 4, now - 50.0);
        state
            .tremor_registry
            .record_observation(&absolute_alias, 1.1, 4, now - 40.0);
        state
            .tremor_registry
            .record_observation(&absolute_alias, 1.2, 4, now - 30.0);
        {
            let mut graph = state.graph.write();
            *graph = Graph::new();
            graph.finalize().expect("empty graph finalize");
        }

        let summary = build_surgical_heuristic_summary(&state, "file::src/core.py", "src/core.py");

        assert!(
            summary.risk_score > 0.0,
            "relative file inputs should still resolve absolute trust/tremor memory"
        );
        assert!(summary.heuristic_signals.tremor_observation_count >= 3);
    }

    fn build_surgical_state_with_doc_noise(
        root: &std::path::Path,
        file_path: &str,
    ) -> SessionState {
        let runtime_dir = root.join("runtime");
        std::fs::create_dir_all(&runtime_dir).expect("runtime dir");

        let config = McpConfig {
            graph_source: runtime_dir.join("graph.json"),
            plasticity_state: runtime_dir.join("plasticity.json"),
            runtime_dir: Some(runtime_dir),
            ..Default::default()
        };

        let mut graph = Graph::new();
        let primary = graph
            .add_node(
                &format!("file::{}", file_path),
                "core.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add primary node");
        graph.set_node_provenance(
            primary,
            NodeProvenanceInput {
                source_path: Some(file_path),
                line_start: Some(1),
                line_end: Some(12),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );

        let code_path = root.join("src/dependent.rs");
        std::fs::create_dir_all(code_path.parent().expect("dependent parent"))
            .expect("mk dependent parent");
        std::fs::write(&code_path, "pub fn dependent() {}\n").expect("write dependent file");
        let code_str = code_path.to_string_lossy().to_string();
        let code = graph
            .add_node(
                &format!("file::{}", code_str),
                "dependent.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add code node");
        graph.set_node_provenance(
            code,
            NodeProvenanceInput {
                source_path: Some(&code_str),
                line_start: Some(1),
                line_end: Some(1),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );

        let doc_path = root.join("EXAMPLES.md");
        std::fs::write(&doc_path, "# examples\n").expect("write doc file");
        let doc_str = doc_path.to_string_lossy().to_string();
        let doc = graph
            .add_node(
                &format!("file::{}", doc_str),
                "EXAMPLES.md",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add doc node");
        graph.set_node_provenance(
            doc,
            NodeProvenanceInput {
                source_path: Some(&doc_str),
                line_start: Some(1),
                line_end: Some(1),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );

        graph
            .add_edge(
                primary,
                code,
                "imports",
                FiniteF32::new(1.0),
                EdgeDirection::Forward,
                false,
                FiniteF32::new(0.8),
            )
            .expect("add core->code edge");
        graph
            .add_edge(
                primary,
                doc,
                "documents",
                FiniteF32::new(1.0),
                EdgeDirection::Forward,
                false,
                FiniteF32::new(0.95),
            )
            .expect("add core->doc edge");
        graph.finalize().expect("finalize graph");

        let mut state =
            SessionState::initialize(graph, &config, DomainConfig::code()).expect("init session");
        state.ingest_roots = vec![root.to_string_lossy().to_string()];
        state.workspace_root = Some(root.to_string_lossy().to_string());
        state
    }

    #[test]
    fn test_extract_identifier() {
        assert_eq!(extract_identifier("handle_apply(state)"), "handle_apply");
        assert_eq!(extract_identifier("MyStruct {"), "MyStruct");
        assert_eq!(extract_identifier(""), "");
        // Alphanumeric sequences including leading digits are accepted
        // (the caller context -- e.g. `fn ` prefix -- ensures valid identifiers)
        assert_eq!(extract_identifier("123abc"), "123abc");
        assert_eq!(extract_identifier("(foo)"), "");
    }

    #[test]
    fn test_diff_summary() {
        let old = "line1\nline2\nline3";
        let new = "line1\nline2_modified\nline3\nline4";
        let (added, removed) = diff_summary(old, new);
        assert!(added > 0);
        assert!(removed > 0);
    }

    #[test]
    fn test_diff_summary_identical() {
        let content = "line1\nline2\nline3";
        let (added, removed) = diff_summary(content, content);
        assert_eq!(added, 0);
        assert_eq!(removed, 0);
    }

    #[test]
    fn test_find_brace_end_simple() {
        let lines = vec!["fn foo() {", "    bar();", "}"];
        assert_eq!(find_brace_end(&lines, 0), 2);
    }

    #[test]
    fn test_find_brace_end_nested() {
        let lines = vec![
            "fn foo() {",
            "    if true {",
            "        bar();",
            "    }",
            "}",
        ];
        assert_eq!(find_brace_end(&lines, 0), 4);
    }

    #[test]
    fn test_extract_rust_symbols_basic() {
        let content = "pub fn handle_apply(\n    state: &mut SessionState,\n) -> Result<()> {\n    todo!()\n}\n";
        let symbols = extract_symbols(content, "test.rs");
        assert_eq!(symbols.len(), 1);
        assert_eq!(symbols[0].name, "handle_apply");
        assert_eq!(symbols[0].symbol_type, "function");
    }

    #[test]
    fn test_extract_python_symbols() {
        let content = "def my_function():\n    pass\n\nclass MyClass:\n    pass\n";
        let symbols = extract_symbols(content, "test.py");
        assert_eq!(symbols.len(), 2);
        assert_eq!(symbols[0].name, "my_function");
        assert_eq!(symbols[0].symbol_type, "function");
        assert_eq!(symbols[1].name, "MyClass");
        assert_eq!(symbols[1].symbol_type, "class");
    }

    #[test]
    fn test_resolve_file_path_absolute() {
        let p = resolve_file_path("/absolute/path/file.rs", &[]);
        assert_eq!(p, PathBuf::from("/absolute/path/file.rs"));
    }

    #[test]
    fn test_resolve_file_path_relative_with_root() {
        let roots = vec!["/workspace".to_string()];
        let p = resolve_file_path("src/main.rs", &roots);
        assert_eq!(p, PathBuf::from("/workspace/src/main.rs"));
    }

    #[test]
    fn test_resolve_file_path_prefers_most_recent_matching_root() {
        let root1 = tempfile::tempdir().expect("root1");
        let root2 = tempfile::tempdir().expect("root2");
        let rel = "src/shared.rs";

        let path1 = root1.path().join(rel);
        let path2 = root2.path().join(rel);
        std::fs::create_dir_all(path1.parent().expect("parent1")).expect("mkdir root1");
        std::fs::create_dir_all(path2.parent().expect("parent2")).expect("mkdir root2");
        std::fs::write(&path1, "one").expect("write root1 file");
        std::fs::write(&path2, "two").expect("write root2 file");

        let roots = vec![
            root1.path().to_string_lossy().to_string(),
            root2.path().to_string_lossy().to_string(),
        ];

        let resolved = resolve_file_path(rel, &roots);
        assert_eq!(resolved, path2);
    }

    #[test]
    fn test_resolve_file_path_uses_newest_root_for_new_relative_paths() {
        let root1 = tempfile::tempdir().expect("root1");
        let root2 = tempfile::tempdir().expect("root2");

        let roots = vec![
            root1.path().to_string_lossy().to_string(),
            root2.path().to_string_lossy().to_string(),
        ];

        let resolved = resolve_file_path("src/new_file.rs", &roots);
        assert_eq!(resolved, root2.path().join("src/new_file.rs"));
    }

    #[test]
    fn test_validate_path_safety_no_ingest_roots_has_recovery_hint() {
        let err = validate_path_safety(Path::new("/tmp/example.rs"), &[]).expect_err("must fail");
        let msg = format!("{}", err);
        assert!(msg.contains("no ingest roots configured"));
        assert!(msg.contains("Hint:"));
        assert!(msg.contains("ingest"));
    }

    #[test]
    fn test_validate_path_safety_protected_file_has_recovery_hint() {
        let root = tempfile::tempdir().expect("tempdir");
        let protected = root.path().join("graph.json");
        std::fs::write(&protected, "{}").expect("write protected");
        let roots = vec![root.path().to_string_lossy().to_string()];

        let err = validate_path_safety(&protected, &roots).expect_err("must fail");
        let msg = format!("{}", err);
        assert!(msg.contains("protected m1nd state file"));
        assert!(msg.contains("Hint:"));
        assert!(msg.contains("workspace"));
    }

    #[test]
    fn test_validate_path_safety_outside_root_has_recovery_hint() {
        let root = tempfile::tempdir().expect("root");
        let other = tempfile::tempdir().expect("other");
        let path = other.path().join("src/outside.rs");
        std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
        std::fs::write(&path, "pub fn outside() {}\n").expect("write");
        let roots = vec![root.path().to_string_lossy().to_string()];

        let err = validate_path_safety(&path, &roots).expect_err("must fail");
        let msg = format!("{}", err);
        assert!(msg.contains("outside allowed workspace roots"));
        assert!(msg.contains("Hint:"));
        assert!(msg.contains("ingested workspace roots"));
    }

    #[test]
    fn test_build_excerpt_truncation() {
        let lines: Vec<&str> = (0..30).map(|_| "code line").collect();
        let excerpt = build_excerpt(&lines, 0, 29);
        assert!(excerpt.contains("truncated"));
    }

    #[test]
    fn test_build_excerpt_short() {
        let lines = vec!["line1", "line2", "line3"];
        let excerpt = build_excerpt(&lines, 0, 2);
        assert!(!excerpt.contains("truncated"));
        assert!(excerpt.contains("line1"));
        assert!(excerpt.contains("line3"));
    }

    #[test]
    fn test_surgical_context_surfaces_heuristic_summary_for_risky_file() {
        let temp = tempfile::tempdir().expect("tempdir");
        let file_path = temp.path().join("src/core.rs");
        std::fs::create_dir_all(file_path.parent().expect("parent")).expect("mk parent");
        std::fs::write(&file_path, "pub fn core() {\n    dependent();\n}\n").expect("write core");
        let file_path_str = file_path.to_string_lossy().to_string();

        let mut state = build_surgical_state(temp.path(), &file_path_str);
        let now = recent_test_timestamp();
        state
            .trust_ledger
            .record_defect(&format!("file::{}", file_path_str), now - 120.0);
        state
            .trust_ledger
            .record_defect(&format!("file::{}", file_path_str), now - 60.0);
        state.tremor_registry.record_observation(
            &format!("file::{}", file_path_str),
            0.8,
            4,
            now - 30.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", file_path_str),
            0.9,
            4,
            now - 20.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", file_path_str),
            1.0,
            4,
            now - 10.0,
        );
        state.antibodies.push(Antibody {
            id: "ab-surgical-risk".into(),
            name: "surgical hotspot".into(),
            description: "Tracks recurring failures in core.rs".into(),
            pattern: AntibodyPattern {
                nodes: vec![],
                edges: vec![],
                negative_edges: vec![],
            },
            severity: AntibodySeverity::Warning,
            match_count: 0,
            created_at: now - 5.0,
            last_match_at: None,
            created_by: "test".into(),
            source_query: "core defects".into(),
            source_nodes: vec![format!("file::{}", file_path_str)],
            enabled: true,
            specificity: 0.8,
        });

        let output = handle_surgical_context(
            &mut state,
            surgical::SurgicalContextInput {
                file_path: file_path_str.clone(),
                agent_id: "test".into(),
                symbol: None,
                radius: 1,
                include_tests: true,
            },
        )
        .expect("surgical context");

        let summary = output
            .heuristic_summary
            .expect("heuristic summary should be present");
        assert_eq!(summary.antibody_hits, 1);
        assert_eq!(summary.blast_radius_files, 1);
        assert_eq!(summary.blast_radius_risk, "low");
        assert!(summary.risk_score > 0.0);
        assert!(
            summary
                .heuristic_signals
                .reason
                .contains("immune-memory recurrence")
                || summary
                    .heuristic_signals
                    .reason
                    .contains("low-trust risk prior")
        );
    }

    #[test]
    fn test_heuristics_surface_resolves_file_path_target() {
        let temp = tempfile::tempdir().expect("tempdir");
        let file_path = temp.path().join("src/core.rs");
        std::fs::create_dir_all(file_path.parent().expect("parent")).expect("mk parent");
        std::fs::write(&file_path, "pub fn core() {}\n").expect("write core");
        let file_path_str = file_path.to_string_lossy().to_string();

        let mut state = build_surgical_state(temp.path(), &file_path_str);
        let now = recent_test_timestamp();
        state
            .trust_ledger
            .record_defect(&format!("file::{}", file_path_str), now - 60.0);
        state.tremor_registry.record_observation(
            &format!("file::{}", file_path_str),
            0.9,
            4,
            now - 30.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", file_path_str),
            1.0,
            4,
            now - 20.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", file_path_str),
            1.1,
            4,
            now - 10.0,
        );

        let output = handle_heuristics_surface(
            &mut state,
            surgical::HeuristicsSurfaceInput {
                agent_id: "test".into(),
                node_id: None,
                file_path: Some(file_path_str.clone()),
            },
        )
        .expect("heuristics surface");

        assert_eq!(output.file_path, file_path_str);
        assert_eq!(output.resolved_by, "file_path");
        assert!(output.heuristic_summary.risk_score > 0.0);
    }

    #[test]
    fn test_heuristics_surface_resolves_node_id_target() {
        let temp = tempfile::tempdir().expect("tempdir");
        let file_path = temp.path().join("src/core.rs");
        std::fs::create_dir_all(file_path.parent().expect("parent")).expect("mk parent");
        std::fs::write(&file_path, "pub fn core() {}\n").expect("write core");
        let file_path_str = file_path.to_string_lossy().to_string();

        let mut state = build_surgical_state(temp.path(), &file_path_str);
        let output = handle_heuristics_surface(
            &mut state,
            surgical::HeuristicsSurfaceInput {
                agent_id: "test".into(),
                node_id: Some(format!("file::{}", file_path_str)),
                file_path: None,
            },
        )
        .expect("heuristics surface");

        assert_eq!(output.node_id, format!("file::{}", file_path_str));
        assert_eq!(output.resolved_by, "node_id");
        assert_eq!(output.file_path, file_path_str);
    }

    #[test]
    fn test_heuristics_surface_missing_target_has_recovery_hint() {
        let temp = tempfile::tempdir().expect("tempdir");
        let file_path = temp.path().join("src/core.rs");
        std::fs::create_dir_all(file_path.parent().expect("parent")).expect("mk parent");
        std::fs::write(&file_path, "pub fn core() {}\n").expect("write core");
        let file_path_str = file_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(temp.path(), &file_path_str);

        let err = handle_heuristics_surface(
            &mut state,
            surgical::HeuristicsSurfaceInput {
                agent_id: "test".into(),
                node_id: None,
                file_path: None,
            },
        )
        .expect_err("missing target must fail");

        let msg = format!("{}", err);
        assert!(msg.contains("provide node_id or file_path"));
        assert!(msg.contains("Hint:"));
        assert!(msg.contains("Example:"));
    }

    #[test]
    fn test_surgical_context_v2_surfaces_connected_file_heuristics() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.rs");
        let dependent_path = root.join("src/dependent.rs");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "pub fn core() {\n    dependent();\n}\n")
            .expect("write primary");
        std::fs::write(&dependent_path, "pub fn dependent() {}\n").expect("write dependent");

        let primary_str = primary_path.to_string_lossy().to_string();
        let dependent_str = dependent_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let now = recent_test_timestamp();

        state
            .trust_ledger
            .record_defect(&format!("file::{}", dependent_str), now - 120.0);
        state
            .trust_ledger
            .record_defect(&format!("file::{}", dependent_str), now - 60.0);
        state.tremor_registry.record_observation(
            &format!("file::{}", dependent_str),
            1.1,
            5,
            now - 50.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", dependent_str),
            1.2,
            5,
            now - 40.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", dependent_str),
            1.3,
            5,
            now - 30.0,
        );

        let output = handle_surgical_context_v2(
            &mut state,
            surgical::SurgicalContextV2Input {
                file_path: primary_str,
                agent_id: "test".into(),
                symbol: None,
                radius: 1,
                include_tests: true,
                max_connected_files: 5,
                max_lines_per_file: 60,
                proof_focused: false,
                max_primary_file_lines: None,
            },
        )
        .expect("surgical context v2");

        let connected = output
            .connected_files
            .iter()
            .find(|file| file.file_path == dependent_str)
            .expect("dependent file should be connected");
        let summary = connected
            .heuristic_summary
            .as_ref()
            .expect("connected file heuristic summary");
        assert!(summary.risk_score > 0.0);
        assert!(summary.heuristic_signals.trust_risk_multiplier >= 1.0);
        assert!(summary.heuristic_signals.tremor_observation_count >= 3);
    }

    #[test]
    fn test_surgical_context_v2_prefers_code_over_doc_noise_when_slots_are_tight() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.rs");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "pub fn core() {\n    dependent();\n}\n")
            .expect("write primary");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state_with_doc_noise(root, &primary_str);

        let output = handle_surgical_context_v2(
            &mut state,
            surgical::SurgicalContextV2Input {
                file_path: primary_str,
                agent_id: "test".into(),
                symbol: None,
                radius: 1,
                include_tests: true,
                max_connected_files: 1,
                max_lines_per_file: 60,
                proof_focused: false,
                max_primary_file_lines: None,
            },
        )
        .expect("surgical context v2");

        assert_eq!(output.connected_files.len(), 1);
        assert!(
            output.connected_files[0]
                .file_path
                .ends_with("src/dependent.rs"),
            "code neighbor should outrank markdown noise when slots are limited"
        );
    }

    #[test]
    fn test_surgical_context_v2_proof_focused_compacts_connected_payload() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.rs");
        let caller_path = root.join("src/caller.rs");
        let callee_path = root.join("src/dependent.rs");
        let test_path = root.join("tests/core_test.rs");
        let doc_path = root.join("docs/reference.md");

        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::create_dir_all(test_path.parent().expect("test parent")).expect("mk test parent");
        std::fs::create_dir_all(doc_path.parent().expect("doc parent")).expect("mk doc parent");

        let long_source: String = (1..=80).map(|i| format!("line {}\n", i)).collect();
        std::fs::write(&primary_path, &long_source).expect("write primary");
        std::fs::write(&caller_path, &long_source).expect("write caller");
        std::fs::write(&callee_path, &long_source).expect("write callee");
        std::fs::write(&test_path, &long_source).expect("write test");
        std::fs::write(&doc_path, "# reference\n").expect("write doc");

        let runtime_dir = root.join("runtime");
        std::fs::create_dir_all(&runtime_dir).expect("runtime dir");
        let config = McpConfig {
            graph_source: runtime_dir.join("graph.json"),
            plasticity_state: runtime_dir.join("plasticity.json"),
            runtime_dir: Some(runtime_dir),
            ..Default::default()
        };

        let primary_str = primary_path.to_string_lossy().to_string();
        let caller_str = caller_path.to_string_lossy().to_string();
        let callee_str = callee_path.to_string_lossy().to_string();
        let test_str = test_path.to_string_lossy().to_string();
        let doc_str = doc_path.to_string_lossy().to_string();

        let mut graph = Graph::new();
        let primary = graph
            .add_node(
                &format!("file::{}", primary_str),
                "core.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add primary");
        let caller = graph
            .add_node(
                &format!("file::{}", caller_str),
                "caller.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add caller");
        let callee = graph
            .add_node(
                &format!("file::{}", callee_str),
                "dependent.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add callee");
        let test = graph
            .add_node(
                &format!("file::{}", test_str),
                "core_test.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add test");
        let doc = graph
            .add_node(
                &format!("file::{}", doc_str),
                "reference.md",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add doc");

        for (node, path) in [
            (primary, primary_str.as_str()),
            (caller, caller_str.as_str()),
            (callee, callee_str.as_str()),
            (test, test_str.as_str()),
            (doc, doc_str.as_str()),
        ] {
            graph.set_node_provenance(
                node,
                NodeProvenanceInput {
                    source_path: Some(path),
                    line_start: Some(1),
                    line_end: Some(80),
                    excerpt: None,
                    namespace: None,
                    canonical: true,
                },
            );
        }

        graph
            .add_edge(
                caller,
                primary,
                "imports",
                FiniteF32::new(1.0),
                EdgeDirection::Forward,
                false,
                FiniteF32::new(0.9),
            )
            .expect("add caller edge");
        graph
            .add_edge(
                primary,
                callee,
                "imports",
                FiniteF32::new(1.0),
                EdgeDirection::Forward,
                false,
                FiniteF32::new(0.85),
            )
            .expect("add callee edge");
        graph
            .add_edge(
                primary,
                test,
                "test-covers",
                FiniteF32::new(1.0),
                EdgeDirection::Forward,
                false,
                FiniteF32::new(0.8),
            )
            .expect("add test edge");
        graph
            .add_edge(
                primary,
                doc,
                "documents",
                FiniteF32::new(1.0),
                EdgeDirection::Forward,
                false,
                FiniteF32::new(0.95),
            )
            .expect("add doc edge");
        graph.finalize().expect("finalize graph");

        let mut state =
            SessionState::initialize(graph, &config, DomainConfig::code()).expect("init session");
        state.ingest_roots = vec![root.to_string_lossy().to_string()];
        state.workspace_root = Some(root.to_string_lossy().to_string());

        let output = handle_surgical_context_v2(
            &mut state,
            surgical::SurgicalContextV2Input {
                file_path: primary_str.clone(),
                agent_id: "test".into(),
                symbol: None,
                radius: 1,
                include_tests: true,
                max_connected_files: 8,
                max_lines_per_file: 60,
                proof_focused: true,
                max_primary_file_lines: None,
            },
        )
        .expect("surgical context v2");

        assert_eq!(output.connected_files.len(), 3);
        assert!(
            output
                .connected_files
                .iter()
                .all(|file| file.excerpt_lines <= 25),
            "proof-focused mode should cap connected excerpts aggressively"
        );
        assert!(output
            .connected_files
            .iter()
            .any(|file| file.relation_type == "caller"));
        assert!(output
            .connected_files
            .iter()
            .any(|file| file.relation_type == "callee"));
        assert!(output
            .connected_files
            .iter()
            .any(|file| file.relation_type == "test"));
        assert!(
            output
                .connected_files
                .iter()
                .all(|file| !file.file_path.ends_with("reference.md")),
            "proof-focused mode should keep proof files over documentation noise"
        );
        assert_eq!(output.next_suggested_tool.as_deref(), Some("validate_plan"));
        assert_eq!(
            output.next_suggested_target.as_deref(),
            Some(primary_str.as_str())
        );
        assert_eq!(output.proof_state, "proving");
        assert!(output
            .next_step_hint
            .as_deref()
            .is_some_and(|hint| hint.contains("Run validate_plan next before editing")));
    }

    #[test]
    fn test_apply_batch_verification_surfaces_heuristic_summary_for_modified_file() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.rs");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "pub fn core() -> i32 {\n    1\n}\n").expect("write primary");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let now = recent_test_timestamp();
        state
            .trust_ledger
            .record_defect(&format!("file::{}", primary_str), now - 120.0);
        state
            .trust_ledger
            .record_defect(&format!("file::{}", primary_str), now - 60.0);
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.0,
            4,
            now - 50.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.1,
            4,
            now - 40.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.2,
            4,
            now - 30.0,
        );

        let output = handle_apply_batch(
            &mut state,
            surgical::ApplyBatchInput {
                agent_id: "test".into(),
                edits: vec![surgical::BatchEditItem {
                    file_path: primary_str.clone(),
                    new_content: "pub fn core() -> i32 {\n    2\n}\n".into(),
                    description: Some("update return".into()),
                }],
                atomic: true,
                reingest: true,
                verify: true,
            },
        )
        .expect("apply batch");

        let verification = output.verification.expect("verification should be present");
        let impact = verification
            .high_impact_files
            .first()
            .expect("at least one impact entry should be present");
        let summary = impact
            .heuristic_summary
            .as_ref()
            .expect("heuristic summary should be present");
        assert!(summary.risk_score > 0.0);
        assert!(summary.heuristic_signals.tremor_observation_count >= 3);
        let surface_ref = impact
            .heuristics_surface_ref
            .as_ref()
            .expect("heuristics surface ref should be present");
        assert!(
            surface_ref.file_path.ends_with("src/core.rs"),
            "surface ref should point at the modified file"
        );
        assert_eq!(surface_ref.node_id, impact.node_id);
    }

    #[test]
    fn test_apply_surfaces_proactive_insights_for_risky_file() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.py");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "def core():\n    return 1\n").expect("write primary");
        std::fs::write(
            root.join("src/test_core.py"),
            "def test_core():\n    assert True\n",
        )
        .expect("write companion test");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let now = recent_test_timestamp();
        state
            .trust_ledger
            .record_defect(&format!("file::{}", primary_str), now - 120.0);
        state
            .trust_ledger
            .record_defect(&format!("file::{}", primary_str), now - 60.0);
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.0,
            4,
            now - 50.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.1,
            4,
            now - 40.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.2,
            4,
            now - 30.0,
        );

        let output = handle_apply(
            &mut state,
            surgical::ApplyInput {
                file_path: primary_str,
                agent_id: "test".into(),
                new_content: "def core():\n    return 2\n".into(),
                description: Some("update return".into()),
                reingest: true,
            },
        )
        .expect("apply");

        assert!(
            !output.proactive_insights.is_empty(),
            "apply should surface proactive insights for a risky file"
        );
        assert!(output.proactive_insights.iter().any(|insight| {
            insight.kind == "trust_drop"
                || insight.kind == "tremor_hotspot"
                || insight.kind == "untouched_test_companion"
        }));
    }

    #[test]
    fn test_apply_records_daemon_alerts_when_daemon_is_active() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.py");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "def core():\n    return 1\n").expect("write primary");
        std::fs::write(
            root.join("src/test_core.py"),
            "def test_core():\n    assert True\n",
        )
        .expect("write companion test");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        crate::daemon_handlers::handle_daemon_start(
            &mut state,
            layers::DaemonStartInput {
                agent_id: "test".into(),
                watch_paths: vec![root.to_string_lossy().to_string()],
                poll_interval_ms: 500,
            },
        )
        .expect("daemon start");

        let output = handle_apply(
            &mut state,
            surgical::ApplyInput {
                file_path: primary_str.clone(),
                agent_id: "test".into(),
                new_content: "def core():\n    return 2\n".into(),
                description: Some("update return".into()),
                reingest: true,
            },
        )
        .expect("apply");

        assert!(
            !output.proactive_insights.is_empty(),
            "apply should still surface proactive insights when daemon is active"
        );
        assert!(
            !state.daemon_alerts.is_empty(),
            "active daemon should persist alerts from proactive insights"
        );
        assert!(
            state.daemon_alerts.iter().any(|alert| {
                alert
                    .file_path
                    .as_deref()
                    .is_some_and(|path| path.contains("core.py"))
                    || alert
                        .node_id
                        .as_deref()
                        .is_some_and(|node| node.contains("core.py"))
                    || alert
                        .evidence
                        .iter()
                        .any(|evidence| evidence.contains("core.py"))
            }),
            "daemon alerts should retain file-level evidence for the modified surface"
        );
    }

    #[test]
    fn test_apply_batch_verdict_becomes_risky_for_high_heuristic_hotspot() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.py");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "def core():\n    return 1\n").expect("write primary");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let now = recent_test_timestamp();

        for offset in [300.0, 240.0, 180.0, 120.0, 60.0] {
            state
                .trust_ledger
                .record_defect(&format!("file::{}", primary_str), now - offset);
        }
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.5,
            6,
            now - 50.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.6,
            6,
            now - 40.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.7,
            6,
            now - 30.0,
        );

        let output = handle_apply_batch(
            &mut state,
            surgical::ApplyBatchInput {
                agent_id: "test".into(),
                edits: vec![surgical::BatchEditItem {
                    file_path: primary_str,
                    new_content: "def core():\n    return 99\n".into(),
                    description: Some("raise value".into()),
                }],
                atomic: true,
                reingest: true,
                verify: true,
            },
        )
        .expect("apply batch");

        let verification = output.verification.expect("verification should be present");
        assert_eq!(verification.verdict, "RISKY");
    }

    #[test]
    fn test_apply_batch_guides_next_step_from_verification_verdict() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.py");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "def core():\n    return 1\n").expect("write primary");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let now = recent_test_timestamp();
        state
            .trust_ledger
            .record_defect(&format!("file::{}", primary_str), now - 120.0);
        state
            .trust_ledger
            .record_defect(&format!("file::{}", primary_str), now - 60.0);
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.0,
            4,
            now - 50.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.1,
            4,
            now - 40.0,
        );
        state.tremor_registry.record_observation(
            &format!("file::{}", primary_str),
            1.2,
            4,
            now - 30.0,
        );

        let output = handle_apply_batch(
            &mut state,
            surgical::ApplyBatchInput {
                agent_id: "test".into(),
                edits: vec![surgical::BatchEditItem {
                    file_path: primary_str.clone(),
                    new_content: "def core():\n    return 2\n".into(),
                    description: Some("update return".into()),
                }],
                atomic: true,
                reingest: true,
                verify: true,
            },
        )
        .expect("apply batch");

        assert_eq!(output.proof_state, "proving");
        assert_eq!(
            output.next_suggested_tool.as_deref(),
            Some("heuristics_surface")
        );
        let done_event = output
            .progress_events
            .last()
            .expect("apply_batch should emit a final progress event");
        assert_eq!(done_event.event_type, "batch_completed");
        assert_eq!(done_event.proof_state.as_deref(), Some("proving"));
        assert_eq!(
            done_event.next_suggested_tool.as_deref(),
            Some("heuristics_surface")
        );
        assert!(
            !output.proactive_insights.is_empty(),
            "apply_batch should surface proactive insights"
        );
        assert!(
            !done_event.proactive_insights.is_empty(),
            "final batch progress event should carry proactive insights"
        );
        assert!(
            output
                .next_suggested_target
                .as_deref()
                .is_some_and(|target| target.ends_with("src/core.py")),
            "next suggested target should point at the modified file"
        );
        assert!(output
            .next_step_hint
            .as_deref()
            .is_some_and(|hint| hint.contains("highest-impact file")));
    }

    #[test]
    fn test_apply_batch_emits_progress_to_live_sink() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary_path = root.join("src/core.py");
        std::fs::create_dir_all(primary_path.parent().expect("primary parent"))
            .expect("mk primary parent");
        std::fs::write(&primary_path, "def core():\n    return 1\n").expect("write primary");

        let primary_str = primary_path.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let captured = Arc::new(Mutex::new(Vec::new()));
        let sink_events = captured.clone();
        state.apply_batch_progress_sink = Some(Arc::new(move |event| {
            sink_events
                .lock()
                .expect("capture lock")
                .push((event.phase.clone(), event.progress_pct));
        }));

        let _output = handle_apply_batch(
            &mut state,
            surgical::ApplyBatchInput {
                agent_id: "test".into(),
                edits: vec![surgical::BatchEditItem {
                    file_path: primary_str,
                    new_content: "def core():\n    return 2\n".into(),
                    description: Some("update return".into()),
                }],
                atomic: true,
                reingest: false,
                verify: false,
            },
        )
        .expect("apply batch");

        let events = captured.lock().expect("capture lock");
        assert!(
            events.iter().any(|(phase, _)| phase == "validate"),
            "live sink should receive validate phase"
        );
        assert!(
            events.iter().any(|(phase, _)| phase == "write"),
            "live sink should receive write phase"
        );
        assert!(
            events.iter().any(|(phase, _)| phase == "done"),
            "live sink should receive done phase"
        );
    }

    #[test]
    fn batch_view_reads_multiple_files_and_tracks_coverage() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let primary = root.join("src/core.rs");
        let secondary = root.join("src/extra.rs");
        std::fs::create_dir_all(primary.parent().expect("parent")).expect("mkdirs");
        std::fs::write(&primary, "fn core() {}\n").expect("write primary");
        std::fs::write(&secondary, "fn extra() {}\n").expect("write secondary");

        let primary_str = primary.to_string_lossy().to_string();
        let mut state = build_surgical_state(root, &primary_str);
        let output = handle_batch_view(
            &mut state,
            surgical::BatchViewInput {
                agent_id: "test".into(),
                files: vec![primary_str.clone(), secondary.to_string_lossy().to_string()],
                max_lines_per_file: 10,
                summary_mode: true,
                auto_ingest: false,
                max_output_chars: None,
            },
        )
        .expect("batch_view should succeed");

        assert_eq!(output.files_read, 2);
        assert_eq!(output.entries.len(), 2);
        assert!(output
            .entries
            .iter()
            .any(|entry| entry.file_path == primary_str));
        let coverage = state
            .coverage_sessions
            .get("test")
            .expect("coverage should be tracked");
        assert!(coverage
            .visited_files
            .contains(&primary.to_string_lossy().to_string()));
        assert!(coverage
            .visited_files
            .contains(&secondary.to_string_lossy().to_string()));
    }

    // -------------------------------------------------------------------------
    // M1ND_PROOF_GATE — live end-to-end probe through real dispatch.
    // -------------------------------------------------------------------------

    /// Process-global lock: M1ND_PROOF_GATE is read live from the environment, so
    /// these tests must not run concurrently with each other. Recovers from
    /// poison so one failing probe does not cascade-fail the others.
    fn proof_gate_env_lock() -> std::sync::MutexGuard<'static, ()> {
        static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
        let m = LOCK.get_or_init(|| Mutex::new(()));
        m.lock().unwrap_or_else(|p| p.into_inner())
    }

    /// Build a state whose graph contains ONE unrelated node, while the probe
    /// targets a separate on-disk file that is NOT in the graph. An ungraphed
    /// target resolves to an empty `node_id`, so `surgical_context_v2` produces
    /// no heuristic summary and no connected files and therefore reaches
    /// `proof_state == "ready_to_edit"` for that file.
    fn build_isolated_state(root: &std::path::Path) -> SessionState {
        let runtime_dir = root.join("runtime");
        std::fs::create_dir_all(&runtime_dir).expect("runtime dir");
        let config = McpConfig {
            graph_source: runtime_dir.join("graph.json"),
            plasticity_state: runtime_dir.join("plasticity.json"),
            runtime_dir: Some(runtime_dir),
            ..Default::default()
        };
        let mut graph = Graph::new();
        // A single unrelated node just so the graph finalizes; the probe target
        // is deliberately NOT this node.
        let other = root.join("src/other.rs");
        std::fs::create_dir_all(other.parent().unwrap()).expect("mk other parent");
        std::fs::write(&other, "pub fn other() {}\n").expect("write other");
        let other_str = other.to_string_lossy().to_string();
        let node = graph
            .add_node(
                &format!("file::{other_str}"),
                "other.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add node");
        graph.set_node_provenance(
            node,
            NodeProvenanceInput {
                source_path: Some(&other_str),
                line_start: Some(1),
                line_end: Some(1),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );
        graph.finalize().expect("finalize graph");
        let mut state =
            SessionState::initialize(graph, &config, DomainConfig::code()).expect("init session");
        state.ingest_roots = vec![root.to_string_lossy().to_string()];
        state.workspace_root = Some(root.to_string_lossy().to_string());
        state
    }

    #[test]
    fn proof_gate_blocks_then_allows_apply_through_real_dispatch() {
        let _guard = proof_gate_env_lock();
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let target = root.join("src/isolated.rs");
        std::fs::create_dir_all(target.parent().unwrap()).expect("mk parent");
        std::fs::write(&target, "pub fn a() {}\n").expect("write target");
        let target_str = target.to_string_lossy().to_string();

        let mut state = build_isolated_state(root);
        let agent = "probe-agent";

        std::env::set_var("M1ND_PROOF_GATE", "1");

        // (1) GATE ON, UNPROVEN apply -> refused with the proof-gate message.
        let apply_params = serde_json::json!({
            "agent_id": agent,
            "file_path": target_str,
            "new_content": "pub fn a() {}\npub fn b() {}\n",
        });
        let err = crate::server::dispatch_tool(&mut state, "apply", &apply_params)
            .expect_err("unproven apply must be refused");
        let msg = format!("{err}");
        println!("[probe] unproven apply refusal: {msg}");
        assert!(
            msg.contains("M1ND_PROOF_GATE is on")
                && msg.contains("not proven ready_to_edit")
                && msg.contains("surgical_context_v2"),
            "refusal message must be actionable, got: {msg}"
        );
        assert!(
            !state.is_proof_ready(agent, &target_str),
            "target must NOT be proved yet"
        );

        // (2) Drive surgical_context_v2 through dispatch -> reaches ready_to_edit
        //     and RECORDS the proof for this agent+target.
        let sctx_params = serde_json::json!({ "agent_id": agent, "file_path": target_str });
        let sctx_out =
            crate::server::dispatch_tool(&mut state, "surgical_context_v2", &sctx_params)
                .expect("surgical_context_v2 dispatch");
        let proof_state = sctx_out
            .get("result")
            .and_then(|r| r.get("proof_state"))
            .or_else(|| sctx_out.get("proof_state"))
            .and_then(|v| v.as_str())
            .unwrap_or("<none>");
        println!("[probe] surgical_context_v2 proof_state = {proof_state}");
        assert_eq!(
            proof_state, "ready_to_edit",
            "isolated low-risk file must reach ready_to_edit"
        );
        assert!(
            state.is_proof_ready(agent, &target_str),
            "proof must be recorded after ready_to_edit"
        );

        // (3) GATE ON, NOW-PROVEN apply -> passes the gate (write succeeds).
        let apply_out = crate::server::dispatch_tool(&mut state, "apply", &apply_params)
            .expect("proven apply must pass the gate");
        println!(
            "[probe] proven apply passed gate, ok={}",
            apply_out.get("result").is_some() || apply_out.is_object()
        );

        // (4) Cross-agent isolation: a DIFFERENT agent is still gated.
        let other_params = serde_json::json!({
            "agent_id": "other-agent",
            "file_path": target_str,
            "new_content": "pub fn a() {}\n",
        });
        let other_err = crate::server::dispatch_tool(&mut state, "apply", &other_params)
            .expect_err("different agent must still be gated");
        println!("[probe] cross-agent refusal: {other_err}");
        assert!(format!("{other_err}").contains("M1ND_PROOF_GATE is on"));

        std::env::remove_var("M1ND_PROOF_GATE");
    }

    #[test]
    fn proof_gate_off_allows_unproven_apply() {
        let _guard = proof_gate_env_lock();
        std::env::remove_var("M1ND_PROOF_GATE");
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let target = root.join("src/isolated.rs");
        std::fs::create_dir_all(target.parent().unwrap()).expect("mk parent");
        std::fs::write(&target, "pub fn a() {}\n").expect("write target");
        let target_str = target.to_string_lossy().to_string();
        let mut state = build_isolated_state(root);

        let apply_params = serde_json::json!({
            "agent_id": "no-proof-agent",
            "file_path": target_str,
            "new_content": "pub fn a() {}\npub fn c() {}\n",
        });
        // Gate OFF: unproven apply must NOT be refused by the proof gate.
        let out = crate::server::dispatch_tool(&mut state, "apply", &apply_params);
        println!(
            "[probe] gate OFF unproven apply result is_ok={}",
            out.is_ok()
        );
        assert!(
            out.is_ok(),
            "with gate OFF an unproven apply must pass: {out:?}"
        );
    }

    #[test]
    fn proof_gate_never_blocks_edit_preview() {
        let _guard = proof_gate_env_lock();
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let target = root.join("src/isolated.rs");
        std::fs::create_dir_all(target.parent().unwrap()).expect("mk parent");
        std::fs::write(&target, "pub fn a() {}\n").expect("write target");
        let target_str = target.to_string_lossy().to_string();
        let mut state = build_isolated_state(root);

        std::env::set_var("M1ND_PROOF_GATE", "1");
        let preview_params = serde_json::json!({
            "agent_id": "preview-agent",
            "file_path": target_str,
            "new_content": "pub fn a() {}\npub fn d() {}\n",
        });
        // edit_preview only stages; the gate must never block it even when ON.
        let out = crate::server::dispatch_tool(&mut state, "edit_preview", &preview_params);
        println!(
            "[probe] gate ON edit_preview result is_ok={} (must be Ok / not gate-refused)",
            out.is_ok()
        );
        if let Err(e) = &out {
            assert!(
                !format!("{e}").contains("M1ND_PROOF_GATE"),
                "edit_preview must never hit the proof gate, got: {e}"
            );
        }
        std::env::remove_var("M1ND_PROOF_GATE");
    }

    // -------------------------------------------------------------------------
    // Antibody auto-propose on fix (gamechanger #6).
    // -------------------------------------------------------------------------

    /// Build a state whose graph holds a Function node for `target` (with a
    /// discriminating label so a derived antibody is clean) plus an unrelated
    /// file node for `other`. Both files are written to disk so apply can touch
    /// them. Returns the function node's external id.
    fn build_findings_state(
        root: &std::path::Path,
        target: &std::path::Path,
        other: &std::path::Path,
    ) -> (SessionState, String) {
        let runtime_dir = root.join("runtime");
        std::fs::create_dir_all(&runtime_dir).expect("runtime dir");
        let config = McpConfig {
            graph_source: runtime_dir.join("graph.json"),
            plasticity_state: runtime_dir.join("plasticity.json"),
            runtime_dir: Some(runtime_dir),
            ..Default::default()
        };
        std::fs::create_dir_all(target.parent().unwrap()).expect("mk target parent");
        std::fs::write(target, "pub fn handle_login() {}\n").expect("write target");
        std::fs::create_dir_all(other.parent().unwrap()).expect("mk other parent");
        std::fs::write(other, "pub fn unrelated() {}\n").expect("write other");
        // Resolve the node paths through the SAME functions apply uses
        // (resolve_file_path + validate_path_safety, which canonicalizes:
        // resolves symlinks, 8.3 short→long names, and the \\?\ prefix on
        // Windows). This guarantees each node's source_path matches apply's
        // `validated_path` on every platform, so find_nodes_for_file links the
        // written file to its flagged node regardless of OS path form.
        let root_str = root.to_string_lossy().to_string();
        let ingest_roots = vec![root_str.clone()];
        let resolve_like_apply = |p: &std::path::Path| -> String {
            validate_path_safety(
                &resolve_file_path(&p.to_string_lossy(), &ingest_roots),
                &ingest_roots,
            )
            .map(|v| v.to_string_lossy().to_string())
            .unwrap_or_else(|_| p.to_string_lossy().to_string())
        };
        let target_str = resolve_like_apply(target);
        let other_str = resolve_like_apply(other);

        let mut graph = Graph::new();
        // A symbol (Function) node for the target: type + discriminating label
        // gives a clean antibody seed (specificity 0.667).
        let func_ext = format!("function::{target_str}::handle_login");
        let func = graph
            .add_node(&func_ext, "handle_login", NodeType::Function, &[], 0.0, 0.0)
            .expect("add func node");
        graph.set_node_provenance(
            func,
            NodeProvenanceInput {
                source_path: Some(&target_str),
                line_start: Some(1),
                line_end: Some(1),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );
        // The unrelated file node (never flagged).
        let other_node = graph
            .add_node(
                &format!("file::{other_str}"),
                "unrelated.rs",
                NodeType::File,
                &[],
                0.0,
                0.0,
            )
            .expect("add other node");
        graph.set_node_provenance(
            other_node,
            NodeProvenanceInput {
                source_path: Some(&other_str),
                line_start: Some(1),
                line_end: Some(1),
                excerpt: None,
                namespace: None,
                canonical: true,
            },
        );
        graph.finalize().expect("finalize graph");
        let mut state =
            SessionState::initialize(graph, &config, DomainConfig::code()).expect("init session");
        state.ingest_roots = ingest_roots;
        state.workspace_root = Some(root_str);
        (state, func_ext)
    }

    /// Pull the proposed_antibody insight out of an apply/edit_commit dispatch
    /// result, if present.
    fn extract_proposed_antibody(out: &serde_json::Value) -> Option<serde_json::Value> {
        let insights = out
            .get("result")
            .and_then(|r| r.get("proactive_insights"))
            .or_else(|| out.get("proactive_insights"))?;
        insights
            .as_array()?
            .iter()
            .find(|i| i.get("kind").and_then(|k| k.as_str()) == Some("proposed_antibody"))
            .cloned()
    }

    #[test]
    fn antibody_auto_propose_on_fix_then_roundtrip() {
        let _guard = proof_gate_env_lock();
        std::env::remove_var("M1ND_PROOF_GATE");
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        let target = root.join("src/auth.rs");
        let other = root.join("src/unrelated.rs");
        let (mut state, func_ext) = build_findings_state(root, &target, &other);
        let target_str = target.to_string_lossy().to_string();
        let other_str = other.to_string_lossy().to_string();
        let agent = "fix-agent";

        // (1) Simulate a scan/audit flagging the function node for this agent.
        //     (This is exactly what handle_scan now does after assembling a
        //     finding: note_finding(agent, finding.node_id, pattern, sev, path).)
        state.note_finding(agent, &func_ext, "auth_boundary", "critical", &target_str);
        assert!(
            state.get_finding(agent, &func_ext).is_some(),
            "finding must be recorded"
        );

        // (2) Apply (fix) the flagged file through real dispatch.
        let apply_params = serde_json::json!({
            "agent_id": agent,
            "file_path": target_str,
            "new_content": "pub fn handle_login() { /* validated */ }\n",
        });
        let apply_out = crate::server::dispatch_tool(&mut state, "apply", &apply_params)
            .expect("apply must succeed");
        let proposed = extract_proposed_antibody(&apply_out)
            .expect("apply of a flagged node must carry a proposed_antibody");
        println!(
            "[probe] proposed_antibody: {}",
            serde_json::to_string_pretty(&proposed).unwrap()
        );

        // The ready-to-run antibody_create arguments live in evidence[0].
        let args_str = proposed
            .get("evidence")
            .and_then(|e| e.as_array())
            .and_then(|a| a.first())
            .and_then(|v| v.as_str())
            .expect("evidence[0] must be the antibody_create arguments JSON");
        let args: serde_json::Value =
            serde_json::from_str(args_str).expect("antibody_create arguments must be valid JSON");
        println!(
            "[probe] antibody_create arguments: {}",
            serde_json::to_string_pretty(&args).unwrap()
        );
        // Schema sanity: required fields present and well-formed.
        assert_eq!(args.get("agent_id").and_then(|v| v.as_str()), Some(agent));
        assert!(
            args.get("pattern").is_some(),
            "pattern is required for create"
        );
        let nodes = args
            .pointer("/pattern/nodes")
            .and_then(|v| v.as_array())
            .expect("pattern.nodes must be an array");
        assert_eq!(nodes.len(), 1, "minimal viable seed = one node");
        assert_eq!(
            nodes[0].get("node_type").and_then(|v| v.as_str()),
            Some("function"),
            "seed must carry the edited node's structural type"
        );
        assert_eq!(
            nodes[0].get("label_contains").and_then(|v| v.as_str()),
            Some("handle_login"),
            "seed must carry a discriminating label fragment"
        );
        assert!(
            args.pointer("/pattern/edges")
                .and_then(|v| v.as_array())
                .map(|a| a.is_empty())
                .unwrap_or(false),
            "minimal viable seed has no fabricated edges"
        );

        // (3) Finding consumed: re-applying must NOT re-propose.
        assert!(
            state.get_finding(agent, &func_ext).is_none(),
            "finding must be consumed by the proposal"
        );
        let apply_again = crate::server::dispatch_tool(&mut state, "apply", &apply_params)
            .expect("second apply must succeed");
        assert!(
            extract_proposed_antibody(&apply_again).is_none(),
            "a consumed finding must not re-propose"
        );

        // (4) Round-trip: feed the proposed arguments to antibody_create through
        //     dispatch and prove they are ACCEPTED.
        let create_out = crate::server::dispatch_tool(&mut state, "antibody_create", &args)
            .expect("proposed antibody_create arguments must be accepted as-is");
        let antibody_id = create_out
            .pointer("/result/antibody_id")
            .or_else(|| create_out.get("antibody_id"))
            .and_then(|v| v.as_str())
            .expect("antibody_create must return an antibody_id")
            .to_string();
        println!(
            "[probe] antibody created id={antibody_id}, full result: {}",
            serde_json::to_string(&create_out).unwrap()
        );

        // (5) Scan with that antibody and prove it matches at least the seed node.
        let scan_params = serde_json::json!({
            "agent_id": agent,
            "antibody_ids": [antibody_id],
        });
        let scan_out = crate::server::dispatch_tool(&mut state, "antibody_scan", &scan_params)
            .expect("antibody_scan must succeed");
        let matches = scan_out
            .pointer("/result/matches")
            .or_else(|| scan_out.get("matches"))
            .and_then(|v| v.as_array())
            .expect("scan must return a matches array");
        println!("[probe] antibody_scan matches={}", matches.len());
        assert!(
            !matches.is_empty(),
            "the proposed antibody must match at least the seed node, got 0 matches: {}",
            serde_json::to_string(&scan_out).unwrap()
        );

        // (6) Editing a DIFFERENT, UNFLAGGED file must propose nothing.
        let other_apply = serde_json::json!({
            "agent_id": agent,
            "file_path": other_str,
            "new_content": "pub fn unrelated() { /* changed */ }\n",
        });
        let other_out = crate::server::dispatch_tool(&mut state, "apply", &other_apply)
            .expect("apply of unflagged file must succeed");
        assert!(
            extract_proposed_antibody(&other_out).is_none(),
            "an unflagged node must NOT trigger a proposed_antibody"
        );
        println!("[probe] unflagged file produced no proposed_antibody (correct)");
    }
}
