use std::collections::{BTreeMap, HashSet};
use std::path::Path;

use crate::db;
use crate::paths::state::StateLayout;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;
use crate::timestamps;
use anyhow::Result;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

const SCHEMA_VERSION: u32 = 6;
const DEFAULT_MAX_ACTIVE_MEMORY_ENTRIES_PER_SCOPE: usize = 12;
const DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION: usize = 8;
pub(crate) const SESSION_RENDER_SURFACE_ORDER: &[&str] = &[
    "effective_memory",
    "handoff",
    "execution_gates",
    "escalation",
    "recovery",
    "git_state",
    "session_state",
];
const TOOL_SURFACE_FINGERPRINT_VARS: &[&str] = &[
    "CCD_TOOL_SURFACE_FINGERPRINT",
    "CCD_MCP_TOOL_SURFACE_FINGERPRINT",
];

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectionDigests {
    pub effective_memory: String,
    pub handoff: String,
    #[serde(default)]
    pub execution_gates: String,
    #[serde(default)]
    pub escalation: String,
    #[serde(default)]
    pub recovery: String,
    #[serde(default)]
    pub git_state: String,
    #[serde(default)]
    pub session_state: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledStateStore {
    pub schema_version: u32,
    #[serde(default = "default_projection_target")]
    pub target: ProjectionTarget,
    pub source_fingerprint: String,
    #[serde(default)]
    pub pod_identity_active: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub projection_digests: Option<ProjectionDigests>,
    pub effective_memory: CompiledMemoryView,
    pub handoff: CompiledHandoffView,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledMemoryView {
    pub profile: Vec<String>,
    #[serde(alias = "repo")]
    pub locality: Vec<String>,
    #[serde(default)]
    pub branch: Vec<String>,
    #[serde(default)]
    pub clone: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledHandoffView {
    pub title: String,
    pub immediate_actions: Vec<String>,
    pub completed_state: Vec<String>,
    pub operational_guardrails: Vec<String>,
    pub key_files: Vec<String>,
    pub definition_of_done: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct CompiledStateSurfaceView {
    pub path: String,
    pub status: &'static str,
    pub schema_version: u32,
    pub target: &'static str,
    pub source_fingerprint: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub refresh_reason: Option<&'static str>,
}

pub struct PreparedCompiledState {
    pub surface: CompiledStateSurfaceView,
    pub store: CompiledStateStore,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectionDeltaStats {
    pub skipped: u32,
    pub refreshed: u32,
    pub total: u32,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectionTokenTelemetry {
    pub surface_tokens: BTreeMap<String, u64>,
    pub format_tokens: BTreeMap<String, u64>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ProjectionResolution<T> {
    pub value: T,
    pub cadence: ProjectionFormatCadenceCurrent,
}

#[derive(Debug, Clone, Serialize)]
pub struct ProjectionFormatCadenceCurrent {
    pub target: &'static str,
    pub format: &'static str,
    pub cache_status: &'static str,
    pub observed_at_epoch_s: u64,
    pub source_fingerprint: String,
    #[serde(skip_serializing_if = "String::is_empty")]
    pub tool_surface_fingerprint: String,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct ProjectionFormatCadenceSummary {
    pub event_count: u32,
    pub hit_count: u32,
    pub miss_count: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hit_ratio: Option<f64>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectionFormat {
    Narrative,
    Symbolic,
    Bundle,
}

impl ProjectionFormat {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Narrative => "narrative",
            Self::Symbolic => "symbolic",
            Self::Bundle => "bundle",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectionTarget {
    Default,
    Planning,
    Session,
}

impl ProjectionTarget {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Default => "default",
            Self::Planning => "planning",
            Self::Session => "session",
        }
    }
}

fn persisted_target() -> ProjectionTarget {
    ProjectionTarget::Session
}

fn default_projection_target() -> ProjectionTarget {
    ProjectionTarget::Default
}

#[derive(Debug, Clone, Copy)]
struct ProjectionBounds {
    max_active_memory_entries_per_scope: usize,
    max_active_handoff_items_per_section: usize,
}

#[cfg_attr(not(test), allow(dead_code))]
pub fn ensure_for_runtime(
    layout: &StateLayout,
    loaded: &runtime_state::LoadedRuntimeState,
) -> Result<PreparedCompiledState> {
    let compiled = preview_for_target_with_cache(layout, loaded, persisted_target())?.value;
    Ok(PreparedCompiledState {
        surface: computed_surface_view(layout, &compiled),
        store: compiled,
    })
}

pub fn refresh_after_write(
    repo_root: &Path,
    layout: &StateLayout,
    locality_id: &str,
) -> Result<CompiledStateStore> {
    compile_from_runtime_for_target(repo_root, layout, locality_id, persisted_target())
}

/// Recompile from an already-loaded runtime state, avoiding a redundant disk reload.
pub fn refresh_after_write_with_loaded(
    layout: &StateLayout,
    loaded: &runtime_state::LoadedRuntimeState,
) -> Result<CompiledStateStore> {
    preview_for_target_with_cache(layout, loaded, persisted_target())
        .map(|resolution| resolution.value)
}

pub struct StartCompiledResult {
    pub compiled: PreparedCompiledState,
    pub sources: runtime_state::RuntimeSourceSurfaces,
    pub recovery: runtime_state::LoadedRuntimeRecoveryState,
}

fn computed_surface_view(
    layout: &StateLayout,
    store: &CompiledStateStore,
) -> CompiledStateSurfaceView {
    CompiledStateSurfaceView {
        path: layout.state_db_path().display().to_string(),
        status: "computed",
        schema_version: store.schema_version,
        target: store.target.as_str(),
        source_fingerprint: store.source_fingerprint.clone(),
        refresh_reason: None,
    }
}

/// On-demand runtime projection for the start command hot path.
///
/// The default contract computes the derived session view directly from
/// canonical sources instead of requiring a persisted sidecar artifact.
pub fn ensure_for_start(
    layout: &StateLayout,
    raw: runtime_state::RawRuntimeSources,
) -> Result<StartCompiledResult> {
    let loaded = runtime_state::normalize_raw_into_loaded(raw)?;
    let compiled = preview_for_target_with_cache(layout, &loaded, persisted_target())?.value;
    Ok(StartCompiledResult {
        compiled: PreparedCompiledState {
            surface: computed_surface_view(layout, &compiled),
            store: compiled,
        },
        sources: loaded.sources,
        recovery: loaded.recovery,
    })
}

pub fn compute_projection_digests(store: &CompiledStateStore) -> ProjectionDigests {
    ProjectionDigests {
        effective_memory: sha256_string(&render_memory_content(
            &store.effective_memory,
            store.pod_identity_active,
        )),
        handoff: sha256_string(&render_handoff_content(&store.handoff)),
        execution_gates: String::new(),
        escalation: String::new(),
        recovery: String::new(),
        git_state: String::new(),
        session_state: String::new(),
    }
}

/// Compute digests for all 8 tracked surfaces from their canonical JSON
/// representations. Both the session-start baseline and the radar/checkpoint
/// current-digest codepaths call this function to guarantee hash parity.
pub fn compute_extended_digests(
    base: &ProjectionDigests,
    execution_gates_json: &str,
    escalation_json: &str,
    recovery_json: &str,
    git_state_json: &str,
    session_state_json: &str,
) -> ProjectionDigests {
    ProjectionDigests {
        effective_memory: base.effective_memory.clone(),
        handoff: base.handoff.clone(),
        execution_gates: sha256_string(execution_gates_json),
        escalation: sha256_string(escalation_json),
        recovery: sha256_string(recovery_json),
        git_state: sha256_string(git_state_json),
        session_state: sha256_string(session_state_json),
    }
}

pub(crate) fn current_tool_surface_fingerprint() -> String {
    db::projection_cache::normalize_tool_surface_fingerprint(
        TOOL_SURFACE_FINGERPRINT_VARS
            .iter()
            .find_map(|name| {
                std::env::var(name)
                    .ok()
                    .map(|value| value.trim().to_owned())
                    .filter(|value| !value.is_empty())
            })
            .as_deref(),
    )
}

pub(crate) fn estimate_tokens(chars: usize) -> u64 {
    u64::try_from(chars.div_ceil(4)).unwrap_or(u64::MAX)
}

pub fn render_narrative_projection_content(store: &CompiledStateStore) -> String {
    let mut sections = Vec::new();

    let effective_memory =
        render_memory_content(&store.effective_memory, store.pod_identity_active);
    if !effective_memory.is_empty() {
        sections.push(("Effective Memory", effective_memory));
    }

    let handoff = render_handoff_content(&store.handoff);
    if !handoff.is_empty() {
        sections.push(("Handoff", handoff));
    }

    sections
        .into_iter()
        .map(|(title, body)| format!("{title}\n{body}"))
        .collect::<Vec<_>>()
        .join("\n\n")
}

pub fn compute_projection_delta_stats(
    previous: &ProjectionDigests,
    current: &ProjectionDigests,
) -> ProjectionDeltaStats {
    let pairs: &[(&str, &str)] = &[
        (&previous.effective_memory, &current.effective_memory),
        (&previous.handoff, &current.handoff),
        (&previous.execution_gates, &current.execution_gates),
        (&previous.escalation, &current.escalation),
        (&previous.recovery, &current.recovery),
        (&previous.git_state, &current.git_state),
        (&previous.session_state, &current.session_state),
    ];

    let mut stats = ProjectionDeltaStats {
        skipped: 0,
        refreshed: 0,
        total: pairs.len() as u32,
    };

    for (previous_digest, current_digest) in pairs {
        if !previous_digest.is_empty() && previous_digest == current_digest {
            stats.skipped = stats.skipped.saturating_add(1);
        } else {
            stats.refreshed = stats.refreshed.saturating_add(1);
        }
    }

    stats
}

pub fn compute_surface_token_counts(
    store: &CompiledStateStore,
    execution_gates_json: &str,
    escalation_json: &str,
    recovery_json: &str,
    git_state_json: &str,
    session_state_json: &str,
) -> BTreeMap<String, u64> {
    let mut counts = BTreeMap::new();
    counts.insert(
        "effective_memory".to_owned(),
        estimate_tokens(
            render_memory_content(&store.effective_memory, store.pod_identity_active)
                .chars()
                .count(),
        ),
    );
    counts.insert(
        "handoff".to_owned(),
        estimate_tokens(render_handoff_content(&store.handoff).chars().count()),
    );
    counts.insert(
        "execution_gates".to_owned(),
        estimate_tokens(execution_gates_json.chars().count()),
    );
    counts.insert(
        "escalation".to_owned(),
        estimate_tokens(escalation_json.chars().count()),
    );
    counts.insert(
        "recovery".to_owned(),
        estimate_tokens(recovery_json.chars().count()),
    );
    counts.insert(
        "git_state".to_owned(),
        estimate_tokens(git_state_json.chars().count()),
    );
    counts.insert(
        "session_state".to_owned(),
        estimate_tokens(session_state_json.chars().count()),
    );
    counts
}

pub fn compute_format_token_counts(
    narrative: &CompiledStateStore,
    symbolic: &SymbolicProjectionView,
    bundle: &BundleProjectionView,
) -> BTreeMap<String, u64> {
    let mut counts = BTreeMap::new();
    counts.insert(
        ProjectionFormat::Narrative.as_str().to_owned(),
        estimate_tokens(
            render_narrative_projection_content(narrative)
                .chars()
                .count(),
        ),
    );
    counts.insert(
        ProjectionFormat::Symbolic.as_str().to_owned(),
        estimate_tokens(render_symbolic_content(symbolic).chars().count()),
    );
    counts.insert(
        ProjectionFormat::Bundle.as_str().to_owned(),
        estimate_tokens(bundle.markdown.chars().count()),
    );
    counts
}

pub fn cadence_summary_for_target(
    layout: &StateLayout,
    target: ProjectionTarget,
    limit: usize,
) -> Result<BTreeMap<String, ProjectionFormatCadenceSummary>> {
    if !layout.state_db_path().exists() {
        return Ok(default_cadence_summary_map());
    }
    let pods_root = layout.pods_root();
    let db = db::StateDb::open(&layout.state_db_path(), Some(&pods_root))?;
    let events =
        db::projection_cache::list_recent_events_for_target(db.conn(), target.as_str(), limit)?;
    let mut summaries = default_cadence_summary_map();
    for event in events {
        let entry = summaries.entry(event.format).or_default();
        entry.event_count = entry.event_count.saturating_add(1);
        if event.cache_status == "hit" {
            entry.hit_count = entry.hit_count.saturating_add(1);
        } else {
            entry.miss_count = entry.miss_count.saturating_add(1);
        }
    }
    for summary in summaries.values_mut() {
        if summary.event_count > 0 {
            summary.hit_ratio = Some(round_ratio(
                (summary.hit_count as f64) / (summary.event_count as f64),
            ));
        }
    }
    Ok(summaries)
}

fn default_cadence_summary_map() -> BTreeMap<String, ProjectionFormatCadenceSummary> {
    let mut summaries = BTreeMap::new();
    for format in [
        ProjectionFormat::Narrative,
        ProjectionFormat::Symbolic,
        ProjectionFormat::Bundle,
    ] {
        summaries.insert(
            format.as_str().to_owned(),
            ProjectionFormatCadenceSummary::default(),
        );
    }
    summaries
}

fn round_ratio(value: f64) -> f64 {
    (value * 1000.0).round() / 1000.0
}

pub(crate) fn sha256_string(content: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(content.as_bytes());
    format!("{:x}", hasher.finalize())
}

pub fn render_memory_content(view: &CompiledMemoryView, _pod_identity_active: bool) -> String {
    let mut body = String::new();
    append_section(&mut body, "Profile Memory", &view.profile);
    append_section(&mut body, "Project Memory", &view.locality);
    append_section(&mut body, "Work Stream Memory", &view.branch);
    append_section(&mut body, "Workspace Memory", &view.clone);
    body
}

pub fn render_handoff_content(view: &CompiledHandoffView) -> String {
    let mut body = String::new();
    if !view.title.is_empty() {
        append_section(&mut body, "Title", std::slice::from_ref(&view.title));
    }
    append_section(&mut body, "Immediate Actions", &view.immediate_actions);
    append_section(&mut body, "Completed State", &view.completed_state);
    append_section(
        &mut body,
        "Operational Guardrails",
        &view.operational_guardrails,
    );
    append_section(&mut body, "Key Files", &view.key_files);
    append_section(&mut body, "Definition of Done", &view.definition_of_done);
    body
}

#[derive(Debug, Clone, Serialize)]
pub struct ContextDelta {
    pub previous_fingerprint: String,
    pub current_fingerprint: String,
    pub changed_surfaces: Vec<ChangedSurface>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ChangedSurface {
    pub name: &'static str,
    pub previous_digest: String,
    pub current_digest: String,
}

fn surface_name_to_static(name: &str) -> &'static str {
    match name {
        "effective_memory" => "effective_memory",
        "handoff" => "handoff",
        "execution_gates" => "execution_gates",
        "escalation" => "escalation",
        "recovery" => "recovery",
        "git_state" => "git_state",
        "session_state" => "session_state",
        _ => "unknown",
    }
}

#[cfg_attr(not(test), allow(dead_code))]
pub fn compute_context_delta(
    previous: &ProjectionDigests,
    current: &ProjectionDigests,
    previous_fp: &str,
    current_fp: &str,
) -> Option<ContextDelta> {
    let pairs: &[(&str, &str, &str)] = &[
        (
            "effective_memory",
            &previous.effective_memory,
            &current.effective_memory,
        ),
        ("handoff", &previous.handoff, &current.handoff),
        (
            "execution_gates",
            &previous.execution_gates,
            &current.execution_gates,
        ),
        ("escalation", &previous.escalation, &current.escalation),
        ("recovery", &previous.recovery, &current.recovery),
        ("git_state", &previous.git_state, &current.git_state),
        (
            "session_state",
            &previous.session_state,
            &current.session_state,
        ),
    ];

    let mut changed = Vec::new();
    for &(name, prev, curr) in pairs {
        if prev.is_empty() || curr.is_empty() {
            continue;
        }
        if prev != curr {
            changed.push(ChangedSurface {
                name: surface_name_to_static(name),
                previous_digest: prev.to_owned(),
                current_digest: curr.to_owned(),
            });
        }
    }

    if changed.is_empty() {
        return None;
    }

    Some(ContextDelta {
        previous_fingerprint: previous_fp.to_owned(),
        current_fingerprint: current_fp.to_owned(),
        changed_surfaces: changed,
    })
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolicProjectionView {
    pub target: ProjectionTarget,
    pub source_fingerprint: String,
    pub facts: Vec<SymbolicFact>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleProjectionView {
    pub target: ProjectionTarget,
    pub source_fingerprint: String,
    pub task: String,
    pub sections: Vec<BundleSection>,
    pub markdown: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BundleSection {
    pub kind: BundleSectionKind,
    pub title: String,
    pub items: Vec<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BundleSectionKind {
    Do,
    Remember,
    Focus,
    Rules,
    Files,
    CloseOut,
    State,
}

impl BundleSectionKind {
    fn title(self) -> &'static str {
        match self {
            Self::Do => "Do",
            Self::Remember => "Remember",
            Self::Focus => "Focus",
            Self::Rules => "Rules",
            Self::Files => "Files",
            Self::CloseOut => "Close-out",
            Self::State => "State",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SymbolicFact {
    pub surface: String,
    pub kind: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ordinal: Option<usize>,
    pub text: String,
}

#[cfg_attr(not(test), allow(dead_code))]
pub fn render_symbolic_content(view: &SymbolicProjectionView) -> String {
    view.facts
        .iter()
        .map(render_symbolic_fact)
        .collect::<Vec<_>>()
        .join("\n")
}

// Kept for consistency checks and other direct preview call sites that do not
// want to record projection-cache telemetry.
pub fn preview_for_target(
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
) -> Result<CompiledStateStore> {
    compile_loaded_runtime_for_target(loaded, target)
}

pub fn preview_for_target_with_cache(
    layout: &StateLayout,
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
) -> Result<ProjectionResolution<CompiledStateStore>> {
    let source_fingerprint = fingerprint_for_inputs(&loaded.sources, loaded.pod_identity_active)?;
    preview_with_precomputed_fingerprint(layout, None, loaded, target, &source_fingerprint, true)
}

pub(crate) fn preview_with_precomputed_fingerprint(
    layout: &StateLayout,
    shared_db: Option<&db::StateDb>,
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
    source_fingerprint: &str,
    record_event: bool,
) -> Result<ProjectionResolution<CompiledStateStore>> {
    resolve_cached_projection(
        layout,
        shared_db,
        target,
        ProjectionFormat::Narrative,
        source_fingerprint,
        record_event,
        || compile_loaded_runtime_for_target(loaded, target),
    )
}

pub fn preview_symbolic_for_target_with_cache(
    layout: &StateLayout,
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
) -> Result<ProjectionResolution<SymbolicProjectionView>> {
    let source_fingerprint = fingerprint_for_inputs(&loaded.sources, loaded.pod_identity_active)?;
    preview_symbolic_with_precomputed_fingerprint(layout, None, loaded, target, &source_fingerprint)
}

pub(crate) fn preview_symbolic_with_precomputed_fingerprint(
    layout: &StateLayout,
    shared_db: Option<&db::StateDb>,
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
    source_fingerprint: &str,
) -> Result<ProjectionResolution<SymbolicProjectionView>> {
    resolve_cached_projection(
        layout,
        shared_db,
        target,
        ProjectionFormat::Symbolic,
        source_fingerprint,
        true,
        || compile_loaded_symbolic_projection(loaded, target),
    )
}

pub fn preview_bundle_for_target_with_cache(
    layout: &StateLayout,
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
) -> Result<ProjectionResolution<BundleProjectionView>> {
    let source_fingerprint = fingerprint_for_inputs(&loaded.sources, loaded.pod_identity_active)?;
    preview_bundle_with_precomputed_fingerprint(layout, None, loaded, target, &source_fingerprint)
}

pub(crate) fn preview_bundle_with_precomputed_fingerprint(
    layout: &StateLayout,
    shared_db: Option<&db::StateDb>,
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
    source_fingerprint: &str,
) -> Result<ProjectionResolution<BundleProjectionView>> {
    resolve_cached_projection(
        layout,
        shared_db,
        target,
        ProjectionFormat::Bundle,
        source_fingerprint,
        true,
        || {
            let narrative = preview_with_precomputed_fingerprint(
                layout,
                shared_db,
                loaded,
                target,
                source_fingerprint,
                false,
            )?;
            Ok(compile_bundle_projection(&narrative.value))
        },
    )
}

pub(crate) fn bundle_view_from_store(store: &CompiledStateStore) -> BundleProjectionView {
    compile_bundle_projection(store)
}

fn resolve_cached_projection<T, F>(
    layout: &StateLayout,
    shared_db: Option<&db::StateDb>,
    target: ProjectionTarget,
    format: ProjectionFormat,
    source_fingerprint: &str,
    record_event: bool,
    compute: F,
) -> Result<ProjectionResolution<T>>
where
    T: Serialize + DeserializeOwned,
    F: FnOnce() -> Result<T>,
{
    let observed_at_epoch_s = session_state::now_epoch_s()?;
    let tool_surface_fingerprint = current_tool_surface_fingerprint();
    // Reuse a caller-provided handle when present; otherwise open locally
    // so this function remains callable from per-module paths that do not
    // own an upstream shared handle yet.
    let owned_db = match shared_db {
        Some(_) => None,
        None => Some({
            let pods_root = layout.pods_root();
            db::StateDb::open(&layout.state_db_path(), Some(&pods_root))?
        }),
    };
    let db: &db::StateDb = shared_db.unwrap_or_else(|| {
        owned_db
            .as_ref()
            .expect("owned_db populated when shared_db is None")
    });

    let cached = db::projection_cache::read_entry(
        db.conn(),
        target.as_str(),
        format.as_str(),
        source_fingerprint,
        &tool_surface_fingerprint,
    )?;

    let (value, cache_status) = if let Some(entry) = cached {
        match serde_json::from_str(&entry.payload_json) {
            Ok(value) => (value, "hit"),
            Err(_) => {
                let value = compute()?;
                persist_cached_projection(
                    db.conn(),
                    observed_at_epoch_s,
                    target,
                    format,
                    source_fingerprint,
                    &tool_surface_fingerprint,
                    &value,
                )?;
                (value, "miss")
            }
        }
    } else {
        let value = compute()?;
        persist_cached_projection(
            db.conn(),
            observed_at_epoch_s,
            target,
            format,
            source_fingerprint,
            &tool_surface_fingerprint,
            &value,
        )?;
        (value, "miss")
    };

    if record_event {
        // When the caller supplied a shared handle, reuse it for the
        // session-id lookup via the shared-db-with-migration helper so
        // trees where `state.db` exists but `session_state.json` has not
        // yet been migrated still resolve the active session correctly.
        // Otherwise fall back to the per-module session probe, which runs
        // migration through its own opener.
        let session_id = match shared_db {
            Some(db) => session_state::load_session_id_from_shared_db(db, layout)?,
            None => session_state::load_session_id(layout)?,
        };
        db::projection_cache::record_event(
            db.conn(),
            &db::projection_cache::ProjectionCacheEventRecord {
                id: None,
                observed_at_epoch_s,
                session_id,
                target: target.as_str().to_owned(),
                format: format.as_str().to_owned(),
                source_fingerprint: source_fingerprint.to_owned(),
                tool_surface_fingerprint: tool_surface_fingerprint.clone(),
                cache_status: cache_status.to_owned(),
            },
        )?;
    }

    Ok(ProjectionResolution {
        value,
        cadence: ProjectionFormatCadenceCurrent {
            target: target.as_str(),
            format: format.as_str(),
            cache_status,
            observed_at_epoch_s,
            source_fingerprint: source_fingerprint.to_owned(),
            tool_surface_fingerprint,
        },
    })
}

fn persist_cached_projection<T: Serialize>(
    conn: &rusqlite::Connection,
    observed_at_epoch_s: u64,
    target: ProjectionTarget,
    format: ProjectionFormat,
    source_fingerprint: &str,
    tool_surface_fingerprint: &str,
    value: &T,
) -> Result<()> {
    db::projection_cache::upsert_entry(
        conn,
        &db::projection_cache::ProjectionCacheEntryRecord {
            id: None,
            observed_at_epoch_s,
            target: target.as_str().to_owned(),
            format: format.as_str().to_owned(),
            source_fingerprint: source_fingerprint.to_owned(),
            tool_surface_fingerprint: tool_surface_fingerprint.to_owned(),
            payload_json: serde_json::to_string(value)?,
        },
    )
}

fn compile_from_runtime_for_target(
    repo_root: &Path,
    layout: &StateLayout,
    locality_id: &str,
    target: ProjectionTarget,
) -> Result<CompiledStateStore> {
    let loaded = runtime_state::load_runtime_state(repo_root, layout, locality_id)?;
    preview_for_target_with_cache(layout, &loaded, target).map(|resolution| resolution.value)
}

fn compile_loaded_runtime_for_target(
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
) -> Result<CompiledStateStore> {
    let mut compiled = compile_runtime_projection(
        &loaded.state,
        &loaded.sources,
        target,
        loaded.pod_identity_active,
    )?;
    dedupe_compiled_projection(&mut compiled);
    compiled.projection_digests = Some(compute_projection_digests(&compiled));
    Ok(compiled)
}

fn compile_loaded_symbolic_projection(
    loaded: &runtime_state::LoadedRuntimeState,
    target: ProjectionTarget,
) -> Result<SymbolicProjectionView> {
    let mut projection = compile_symbolic_projection(
        &loaded.state,
        &loaded.sources,
        target,
        loaded.pod_identity_active,
    )?;
    if let Some(anchor) = loaded.execution_gates.view.attention_anchor.as_ref() {
        projection.facts.push(SymbolicFact {
            surface: "execution_gates".to_owned(),
            kind: "attention_anchor".to_owned(),
            ordinal: Some(anchor.index),
            text: anchor.text.clone(),
        });
    }
    Ok(projection)
}

fn compile_runtime_projection(
    state: &runtime_state::RuntimeState,
    sources: &runtime_state::RuntimeSourceSurfaces,
    target: ProjectionTarget,
    pod_identity_active: bool,
) -> Result<CompiledStateStore> {
    let bounds = default_projection_bounds();
    let handoff_target = handoff_target(target);

    let mut compiled = CompiledStateStore {
        schema_version: SCHEMA_VERSION,
        target,
        source_fingerprint: fingerprint_for_inputs(sources, pod_identity_active)?,
        pod_identity_active,
        projection_digests: None,
        effective_memory: CompiledMemoryView {
            profile: project_memory_entries(
                &state.memory.profile,
                bounds.max_active_memory_entries_per_scope,
            ),
            locality: project_memory_entries(
                &state.memory.locality,
                bounds.max_active_memory_entries_per_scope,
            ),
            branch: project_memory_entries(
                &state.memory.branch,
                bounds.max_active_memory_entries_per_scope,
            ),
            clone: project_memory_entries(
                &state.memory.clone,
                bounds.max_active_memory_entries_per_scope,
            ),
        },
        handoff: project_handoff(&state.handoff, bounds, handoff_target),
    };
    dedupe_compiled_projection(&mut compiled);
    compiled.projection_digests = Some(compute_projection_digests(&compiled));
    Ok(compiled)
}

fn compile_symbolic_projection(
    state: &runtime_state::RuntimeState,
    sources: &runtime_state::RuntimeSourceSurfaces,
    target: ProjectionTarget,
    pod_identity_active: bool,
) -> Result<SymbolicProjectionView> {
    let bounds = default_projection_bounds();
    let handoff_target = handoff_target(target);
    let mut facts = Vec::new();

    let _ = pod_identity_active;
    append_memory_facts(
        &mut facts,
        "profile_memory",
        project_active_memory_entries(
            &state.memory.profile,
            bounds.max_active_memory_entries_per_scope,
        ),
    );
    append_memory_facts(
        &mut facts,
        "locality_memory",
        project_active_memory_entries(
            &state.memory.locality,
            bounds.max_active_memory_entries_per_scope,
        ),
    );
    append_memory_facts(
        &mut facts,
        "branch_memory",
        project_active_memory_entries(
            &state.memory.branch,
            bounds.max_active_memory_entries_per_scope,
        ),
    );
    append_memory_facts(
        &mut facts,
        "clone_memory",
        project_active_memory_entries(
            &state.memory.clone,
            bounds.max_active_memory_entries_per_scope,
        ),
    );
    if !state.handoff.title.trim().is_empty() {
        facts.push(SymbolicFact {
            surface: "handoff_title".to_owned(),
            kind: "title".to_owned(),
            ordinal: None,
            text: state.handoff.title.clone(),
        });
    }
    append_handoff_facts(
        &mut facts,
        "handoff_immediate_actions",
        project_active_handoff_items(
            &state.handoff.immediate_actions,
            bounds.max_active_handoff_items_per_section,
        ),
    );
    if handoff_target.include_completed_state {
        append_handoff_facts(
            &mut facts,
            "handoff_completed_state",
            project_active_handoff_items(
                &state.handoff.completed_state,
                bounds.max_active_handoff_items_per_section,
            ),
        );
    }
    append_handoff_facts(
        &mut facts,
        "handoff_operational_guardrails",
        project_active_handoff_items(
            &state.handoff.operational_guardrails,
            bounds.max_active_handoff_items_per_section,
        ),
    );
    if handoff_target.include_key_files {
        append_handoff_facts(
            &mut facts,
            "handoff_key_files",
            project_active_handoff_items(
                &state.handoff.key_files,
                bounds.max_active_handoff_items_per_section,
            ),
        );
    }
    if handoff_target.include_definition_of_done {
        append_handoff_facts(
            &mut facts,
            "handoff_definition_of_done",
            project_active_handoff_items(
                &state.handoff.definition_of_done,
                bounds.max_active_handoff_items_per_section,
            ),
        );
    }

    Ok(SymbolicProjectionView {
        target,
        source_fingerprint: fingerprint_for_inputs(sources, pod_identity_active)?,
        facts,
    })
}

fn compile_bundle_projection(store: &CompiledStateStore) -> BundleProjectionView {
    let mut sections = Vec::new();

    push_bundle_section(
        &mut sections,
        BundleSectionKind::Do,
        store.handoff.immediate_actions.clone(),
    );
    push_bundle_section(
        &mut sections,
        BundleSectionKind::Remember,
        bundle_memory_items(&store.effective_memory, store.pod_identity_active),
    );
    push_bundle_section(
        &mut sections,
        BundleSectionKind::Rules,
        store.handoff.operational_guardrails.clone(),
    );
    push_bundle_section(
        &mut sections,
        BundleSectionKind::Files,
        store.handoff.key_files.clone(),
    );
    push_bundle_section(
        &mut sections,
        BundleSectionKind::CloseOut,
        store.handoff.definition_of_done.clone(),
    );
    let task = bundle_task(&store.handoff.title, store.target);
    let markdown = render_bundle_markdown(&task, &sections);

    BundleProjectionView {
        target: store.target,
        source_fingerprint: store.source_fingerprint.clone(),
        task,
        sections,
        markdown,
    }
}

fn handoff_target(target: ProjectionTarget) -> HandoffProjectionTarget {
    match target {
        ProjectionTarget::Default => HandoffProjectionTarget {
            include_completed_state: true,
            include_key_files: true,
            include_definition_of_done: true,
        },
        ProjectionTarget::Planning => HandoffProjectionTarget {
            include_completed_state: false,
            include_key_files: false,
            include_definition_of_done: true,
        },
        ProjectionTarget::Session => HandoffProjectionTarget {
            include_completed_state: false,
            include_key_files: false,
            include_definition_of_done: false,
        },
    }
}

#[derive(Debug, Clone, Copy)]
struct HandoffProjectionTarget {
    include_completed_state: bool,
    include_key_files: bool,
    include_definition_of_done: bool,
}

fn default_projection_bounds() -> ProjectionBounds {
    ProjectionBounds {
        max_active_memory_entries_per_scope: DEFAULT_MAX_ACTIVE_MEMORY_ENTRIES_PER_SCOPE,
        max_active_handoff_items_per_section: DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION,
    }
}

pub(crate) fn fingerprint_for_inputs(
    sources: &runtime_state::RuntimeSourceSurfaces,
    pod_identity_active: bool,
) -> Result<String> {
    let payload = serde_json::to_vec(&FingerprintInputs {
        profile_memory: FingerprintSurface::from(&sources.profile_memory),
        locality_memory: FingerprintSurface::from(&sources.locality_memory),
        branch_memory: FingerprintSurface::from(&sources.branch_memory),
        clone_memory: FingerprintSurface::from(&sources.clone_memory),
        execution_gates: FingerprintSurface::from(&sources.execution_gates),
        handoff: FingerprintSurface::from(&sources.handoff),
        pod_identity_active,
    })?;
    let mut hasher = Sha256::new();
    hasher.update(payload);
    Ok(format!("{:x}", hasher.finalize()))
}

fn project_memory_entries(
    entries: &[runtime_state::RuntimeMemoryEntry],
    max_active_entries: usize,
) -> Vec<String> {
    project_active_memory_entries(entries, max_active_entries)
        .into_iter()
        .map(runtime_state::RuntimeMemoryEntry::projection_text)
        .collect()
}

fn project_handoff(
    state: &runtime_state::RuntimeHandoffState,
    bounds: ProjectionBounds,
    target: HandoffProjectionTarget,
) -> CompiledHandoffView {
    CompiledHandoffView {
        title: state.title.clone(),
        immediate_actions: project_handoff_items(
            &state.immediate_actions,
            bounds.max_active_handoff_items_per_section,
        ),
        completed_state: if target.include_completed_state {
            project_handoff_items(
                &state.completed_state,
                bounds.max_active_handoff_items_per_section,
            )
        } else {
            Vec::new()
        },
        operational_guardrails: project_handoff_items(
            &state.operational_guardrails,
            bounds.max_active_handoff_items_per_section,
        ),
        key_files: if target.include_key_files {
            project_handoff_items(
                &state.key_files,
                bounds.max_active_handoff_items_per_section,
            )
        } else {
            Vec::new()
        },
        definition_of_done: if target.include_definition_of_done {
            project_handoff_items(
                &state.definition_of_done,
                bounds.max_active_handoff_items_per_section,
            )
        } else {
            Vec::new()
        },
    }
}

fn dedupe_compiled_projection(store: &mut CompiledStateStore) {
    let mut seen = HashSet::new();
    dedupe_strings(&mut store.effective_memory.profile, &mut seen);
    dedupe_strings(&mut store.effective_memory.locality, &mut seen);
    dedupe_strings(&mut store.effective_memory.branch, &mut seen);
    dedupe_strings(&mut store.effective_memory.clone, &mut seen);
    dedupe_strings(&mut store.handoff.immediate_actions, &mut seen);
    dedupe_strings(&mut store.handoff.completed_state, &mut seen);
    dedupe_strings(&mut store.handoff.operational_guardrails, &mut seen);
    dedupe_strings(&mut store.handoff.key_files, &mut seen);
    dedupe_strings(&mut store.handoff.definition_of_done, &mut seen);
}

fn dedupe_strings(items: &mut Vec<String>, seen: &mut HashSet<String>) {
    items.retain(|item| seen.insert(item.clone()));
}

fn project_handoff_items(
    items: &[runtime_state::RuntimeHandoffItem],
    max_active_items: usize,
) -> Vec<String> {
    project_active_handoff_items(items, max_active_items)
        .into_iter()
        .map(|item| item.text.clone())
        .collect()
}

fn project_active_memory_entries(
    entries: &[runtime_state::RuntimeMemoryEntry],
    max_active_entries: usize,
) -> Vec<&runtime_state::RuntimeMemoryEntry> {
    collect_active_items(
        entries
            .iter()
            .filter(|entry| runtime_memory_entry_is_active(entry)),
        max_active_entries,
        runtime_state::RuntimeMemoryEntry::projection_text,
    )
}

fn runtime_memory_entry_is_active(entry: &runtime_state::RuntimeMemoryEntry) -> bool {
    if !entry.lifecycle.is_active() {
        return false;
    }

    let Some(expires_at) = entry.expires_at.as_deref() else {
        return true;
    };

    // Structured-memory validation does not yet enforce timestamp grammar, so
    // projection only applies expiry filtering when the authored hint parses.
    match timestamps::parse_rfc3339(expires_at) {
        Some(expires_at) => expires_at > timestamps::now_utc(),
        None => true,
    }
}

fn project_active_handoff_items(
    items: &[runtime_state::RuntimeHandoffItem],
    max_active_items: usize,
) -> Vec<&runtime_state::RuntimeHandoffItem> {
    collect_active_items(
        items.iter().filter(|item| item.lifecycle.is_active()),
        max_active_items,
        |item| item.text.clone(),
    )
}

fn collect_active_items<'a, T, I, F>(items: I, max_active_items: usize, key: F) -> Vec<&'a T>
where
    I: IntoIterator<Item = &'a T>,
    F: Fn(&T) -> String,
{
    let mut seen = HashSet::new();
    let mut selected = Vec::new();
    for item in items {
        let marker = key(item);
        if marker.trim().is_empty() {
            continue;
        }
        if seen.insert(marker) {
            selected.push(item);
            if selected.len() == max_active_items {
                break;
            }
        }
    }
    selected
}

fn append_memory_facts(
    facts: &mut Vec<SymbolicFact>,
    surface: &str,
    entries: Vec<&runtime_state::RuntimeMemoryEntry>,
) {
    for (index, entry) in entries.into_iter().enumerate() {
        facts.push(SymbolicFact {
            surface: surface.to_owned(),
            kind: symbolic_memory_kind(entry),
            ordinal: Some(index + 1),
            text: entry.text.clone(),
        });
    }
}

fn append_handoff_facts(
    facts: &mut Vec<SymbolicFact>,
    surface: &str,
    items: Vec<&runtime_state::RuntimeHandoffItem>,
) {
    for (index, item) in items.into_iter().enumerate() {
        facts.push(SymbolicFact {
            surface: surface.to_owned(),
            kind: "item".to_owned(),
            ordinal: Some(index + 1),
            text: item.text.clone(),
        });
    }
}

fn symbolic_memory_kind(entry: &runtime_state::RuntimeMemoryEntry) -> String {
    match &entry.origin {
        runtime_state::RuntimeMemoryOrigin::Narrative => "narrative".to_owned(),
        runtime_state::RuntimeMemoryOrigin::Structured { entry_type, .. } => entry_type.clone(),
    }
}

fn bundle_task(title: &str, target: ProjectionTarget) -> String {
    let trimmed = title.trim();
    if trimmed.is_empty() {
        return format!("{} session bundle", target.as_str());
    }

    trimmed
        .strip_prefix("Next Session:")
        .unwrap_or(trimmed)
        .trim()
        .to_owned()
}

fn bundle_memory_items(view: &CompiledMemoryView, _pod_identity_active: bool) -> Vec<String> {
    let mut items = Vec::new();
    items.extend(view.profile.iter().map(|item| format!("Profile: {item}")));
    items.extend(view.locality.iter().map(|item| format!("Project: {item}")));
    items.extend(
        view.branch
            .iter()
            .map(|item| format!("Work stream: {item}")),
    );
    items.extend(view.clone.iter().map(|item| format!("Workspace: {item}")));
    items
}

fn push_bundle_section(
    sections: &mut Vec<BundleSection>,
    kind: BundleSectionKind,
    items: Vec<String>,
) {
    if items.is_empty() {
        return;
    }

    sections.push(BundleSection {
        kind,
        title: kind.title().to_owned(),
        items,
    });
}

#[cfg_attr(not(test), allow(dead_code))]
fn render_symbolic_fact(fact: &SymbolicFact) -> String {
    match fact.ordinal {
        Some(ordinal) => format!("{}|{}|{}|{}", fact.surface, fact.kind, ordinal, fact.text),
        None => format!("{}|{}|{}", fact.surface, fact.kind, fact.text),
    }
}

pub fn render_bundle_markdown(task: &str, sections: &[BundleSection]) -> String {
    let mut body = String::from("# CCD Bundle\n\n");
    body.push_str("Task: ");
    body.push_str(task);
    body.push('\n');

    for section in sections {
        body.push('\n');
        body.push_str("## ");
        body.push_str(&section.title);
        body.push('\n');
        for item in &section.items {
            body.push_str("- ");
            body.push_str(item);
            body.push('\n');
        }
    }

    body
}

fn append_section(body: &mut String, title: &str, items: &[String]) {
    if items.is_empty() {
        return;
    }

    if !body.is_empty() {
        body.push('\n');
    }
    body.push_str("### ");
    body.push_str(title);
    body.push_str("\n\n");
    for item in items {
        body.push_str("- ");
        body.push_str(item);
        body.push('\n');
    }
}

#[derive(Serialize)]
struct FingerprintInputs<'a> {
    profile_memory: FingerprintSurface<'a>,
    locality_memory: FingerprintSurface<'a>,
    branch_memory: FingerprintSurface<'a>,
    clone_memory: FingerprintSurface<'a>,
    execution_gates: FingerprintSurface<'a>,
    handoff: FingerprintSurface<'a>,
    pod_identity_active: bool,
}

#[derive(Serialize)]
struct FingerprintSurface<'a> {
    status: &'static str,
    content: &'a str,
}

impl<'a> From<&'a runtime_state::RuntimeTextSurface> for FingerprintSurface<'a> {
    fn from(value: &'a runtime_state::RuntimeTextSurface) -> Self {
        Self {
            status: match value.status {
                runtime_state::RuntimeTextSurfaceStatus::LoadedNative => "loaded",
                other => other.as_str(),
            },
            content: &value.content,
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use tempfile::tempdir;

    use super::*;
    use crate::memory::entries::StructuredMemoryEntry;
    use crate::profile::ProfileName;
    use crate::state::runtime::{
        LoadedRuntimeExecutionGates, LoadedRuntimeRecoveryState, LoadedRuntimeState,
        RuntimeHandoffItem, RuntimeHandoffState, RuntimeLifecycle, RuntimeMemoryEntry,
        RuntimeMemoryState, RuntimeRecoveryState, RuntimeRecoverySurface, RuntimeRecoverySurfaces,
        RuntimeSourceSurfaces, RuntimeState, RuntimeTextSurface, RuntimeTextSurfaceStatus,
    };
    use crate::state::session_gates;
    fn missing_clone_memory_surface() -> RuntimeTextSurface {
        RuntimeTextSurface {
            kind: "clone_memory",
            path: PathBuf::from("/tmp/clone-memory.md"),
            status: RuntimeTextSurfaceStatus::Missing,
            content: String::new(),
            migrated_from: None,
        }
    }

    fn missing_branch_memory_surface() -> RuntimeTextSurface {
        RuntimeTextSurface {
            kind: "branch_memory",
            path: PathBuf::from("/tmp/branch-memory.md"),
            status: RuntimeTextSurfaceStatus::Missing,
            content: String::new(),
            migrated_from: None,
        }
    }

    fn missing_execution_gates_surface() -> RuntimeTextSurface {
        RuntimeTextSurface {
            kind: "execution_gates",
            path: PathBuf::from("/tmp/state.db"),
            status: RuntimeTextSurfaceStatus::Missing,
            content: String::new(),
            migrated_from: None,
        }
    }

    fn missing_execution_gates() -> LoadedRuntimeExecutionGates {
        LoadedRuntimeExecutionGates {
            source: missing_execution_gates_surface(),
            view: session_gates::ExecutionGatesView {
                path: "/tmp/state.db".to_owned(),
                status: "missing",
                seeded_from: None,
                revision: None,
                total_count: 0,
                unfinished_count: 0,
                attention_anchor: None,
                gates: Vec::new(),
            },
        }
    }

    #[test]
    fn projects_runtime_state_into_compiled_views() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![
                    RuntimeMemoryEntry::narrative("Keep projections bounded."),
                    RuntimeMemoryEntry {
                        lifecycle: RuntimeLifecycle::Inactive,
                        ..RuntimeMemoryEntry::narrative("Skip stale notes.")
                    },
                    RuntimeMemoryEntry::narrative("Keep projections bounded."),
                ],
                locality: vec![RuntimeMemoryEntry::from_structured_entry(
                    StructuredMemoryEntry {
                        id: "mem_rule".to_owned(),
                        entry_type: "rule".to_owned(),
                        state: "active".to_owned(),
                        created_at: "2026-03-10T10:00:00Z".to_owned(),
                        last_touched_session: 1,
                        origin: "manual".to_owned(),
                        superseded_at: None,
                        decay_class: None,
                        expires_at: None,
                        tags: Vec::new(),
                        source_ref: None,
                        supersedes: Vec::new(),
                        content: "Prefer deterministic writes.".to_owned(),
                    },
                )],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Runtime".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Add runtime-state scaffolding.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: vec![RuntimeHandoffItem {
                    text: "Archived work.".to_owned(),
                    lifecycle: RuntimeLifecycle::Inactive,
                }],
                operational_guardrails: Vec::new(),
                key_files: Vec::new(),
                definition_of_done: Vec::new(),
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let compiled =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Default, false)
                .expect("compiled projection");

        assert_eq!(compiled.target, ProjectionTarget::Default);
        assert_eq!(
            compiled.effective_memory.profile,
            vec!["Keep projections bounded.".to_owned()]
        );
        assert_eq!(
            compiled.effective_memory.locality,
            vec!["rule: Prefer deterministic writes.".to_owned()]
        );
        assert_eq!(compiled.handoff.title, "Next Session: Runtime");
        assert_eq!(
            compiled.handoff.immediate_actions,
            vec!["Add runtime-state scaffolding.".to_owned()]
        );
        assert!(compiled.handoff.completed_state.is_empty());
        assert!(!compiled.source_fingerprint.is_empty());
    }

    #[test]
    fn projects_runtime_state_without_expired_active_memory_entries() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![
                    RuntimeMemoryEntry::from_structured_entry(StructuredMemoryEntry {
                        id: "mem_expired".to_owned(),
                        entry_type: "rule".to_owned(),
                        state: "active".to_owned(),
                        created_at: "2026-03-10T10:00:00Z".to_owned(),
                        last_touched_session: 1,
                        origin: "manual".to_owned(),
                        superseded_at: None,
                        decay_class: Some("active".to_owned()),
                        expires_at: Some("2000-01-01T00:00:00Z".to_owned()),
                        tags: Vec::new(),
                        source_ref: None,
                        supersedes: Vec::new(),
                        content: "Expired rule should stay out of preload.".to_owned(),
                    }),
                    RuntimeMemoryEntry::from_structured_entry(StructuredMemoryEntry {
                        id: "mem_current".to_owned(),
                        entry_type: "rule".to_owned(),
                        state: "active".to_owned(),
                        created_at: "2026-03-10T11:00:00Z".to_owned(),
                        last_touched_session: 2,
                        origin: "manual".to_owned(),
                        superseded_at: None,
                        decay_class: Some("stable".to_owned()),
                        expires_at: Some("2999-01-01T00:00:00Z".to_owned()),
                        tags: Vec::new(),
                        source_ref: None,
                        supersedes: Vec::new(),
                        content: "Future rule should remain in preload.".to_owned(),
                    }),
                ],
                locality: Vec::new(),
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState::default(),
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Missing,
                content: String::new(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Missing,
                content: String::new(),
                migrated_from: None,
            },
        };

        let compiled =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Default, false)
                .expect("compiled projection");

        assert_eq!(
            compiled.effective_memory.profile,
            vec!["rule: Future rule should remain in preload.".to_owned()]
        );
    }

    #[test]
    fn unparseable_expires_at_keeps_entry_active() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::from_structured_entry(
                    StructuredMemoryEntry {
                        id: "mem_bad_ts".to_owned(),
                        entry_type: "rule".to_owned(),
                        state: "active".to_owned(),
                        created_at: "2026-03-10T10:00:00Z".to_owned(),
                        last_touched_session: 1,
                        origin: "manual".to_owned(),
                        superseded_at: None,
                        decay_class: Some("active".to_owned()),
                        expires_at: Some("not-a-timestamp".to_owned()),
                        tags: Vec::new(),
                        source_ref: None,
                        supersedes: Vec::new(),
                        content: "Entry with unparseable expiry stays active.".to_owned(),
                    },
                )],
                locality: Vec::new(),
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState::default(),
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Missing,
                content: String::new(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Missing,
                content: String::new(),
                migrated_from: None,
            },
        };

        let compiled =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Default, false)
                .expect("compiled projection");

        assert_eq!(
            compiled.effective_memory.profile,
            vec!["rule: Entry with unparseable expiry stays active.".to_owned()]
        );
    }

    #[test]
    fn ensure_for_runtime_projects_loaded_state_without_reloading_authored_files() {
        let temp = tempdir().expect("tempdir");
        let layout = StateLayout::new(
            temp.path().join(".ccd"),
            temp.path().join(".git/ccd"),
            ProfileName::new("main").expect("profile"),
        );
        let loaded = LoadedRuntimeState {
            state: RuntimeState {
                memory: RuntimeMemoryState {
                    profile: vec![RuntimeMemoryEntry::narrative(
                        "Use the loaded runtime view.",
                    )],
                    locality: Vec::new(),
                    branch: Vec::new(),
                    clone: Vec::new(),
                },
                handoff: RuntimeHandoffState {
                    title: "Next Session: Runtime-first projection".to_owned(),
                    immediate_actions: vec![RuntimeHandoffItem {
                        text: "Avoid adapter rereads.".to_owned(),
                        lifecycle: RuntimeLifecycle::Active,
                    }],
                    completed_state: Vec::new(),
                    operational_guardrails: Vec::new(),
                    key_files: Vec::new(),
                    definition_of_done: Vec::new(),
                },
            },
            sources: RuntimeSourceSurfaces {
                profile_memory: RuntimeTextSurface {
                    kind: "profile_memory",
                    path: PathBuf::from("/virtual/profile-memory.md"),
                    status: RuntimeTextSurfaceStatus::Loaded,
                    content: "# Profile Memory\n\n- Use the loaded runtime view.\n".to_owned(),
                    migrated_from: None,
                },
                locality_memory: RuntimeTextSurface {
                    kind: "locality_memory",
                    path: PathBuf::from("/virtual/repo-memory.md"),
                    status: RuntimeTextSurfaceStatus::Missing,
                    content: String::new(),
                    migrated_from: None,
                },
                branch_memory: missing_branch_memory_surface(),
                clone_memory: missing_clone_memory_surface(),
                execution_gates: missing_execution_gates_surface(),
                handoff: RuntimeTextSurface {
                    kind: "handoff",
                    path: PathBuf::from("/virtual/handoff.md"),
                    status: RuntimeTextSurfaceStatus::Loaded,
                    content: "# Next Session: Runtime-first projection\n".to_owned(),
                    migrated_from: None,
                },
            },
            execution_gates: missing_execution_gates(),
            recovery: LoadedRuntimeRecoveryState {
                state: RuntimeRecoveryState::default(),
                sources: RuntimeRecoverySurfaces {
                    checkpoint: RuntimeRecoverySurface {
                        kind: "checkpoint",
                        path: PathBuf::from("/virtual/checkpoint.json"),
                        status: "missing",
                    },
                    working_buffer: RuntimeRecoverySurface {
                        kind: "working_buffer",
                        path: PathBuf::from("/virtual/working_buffer.md"),
                        status: "missing",
                    },
                },
            },
            pod_identity_active: false,
        };

        let prepared = ensure_for_runtime(&layout, &loaded).expect("prepare compiled state");
        assert_eq!(prepared.surface.status, "computed");
        assert_eq!(prepared.surface.target, "session");
        assert_eq!(prepared.store.target, ProjectionTarget::Session);
        assert_eq!(
            prepared.store.effective_memory.profile,
            vec!["Use the loaded runtime view.".to_owned()]
        );
        assert_eq!(
            prepared.store.handoff.immediate_actions,
            vec!["Avoid adapter rereads.".to_owned()]
        );
    }

    #[test]
    fn projection_applies_default_active_state_bounds_after_filtering_and_dedupe() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: (0..(DEFAULT_MAX_ACTIVE_MEMORY_ENTRIES_PER_SCOPE + 3))
                    .map(|index| RuntimeMemoryEntry::narrative(format!("Profile rule {index:02}")))
                    .chain(std::iter::once(RuntimeMemoryEntry::narrative(
                        "Profile rule 00".to_owned(),
                    )))
                    .collect(),
                locality: (0..(DEFAULT_MAX_ACTIVE_MEMORY_ENTRIES_PER_SCOPE + 2))
                    .map(|index| RuntimeMemoryEntry::narrative(format!("Repo note {index:02}")))
                    .collect(),
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Bounded projection".to_owned(),
                immediate_actions: (0..(DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION + 3))
                    .map(|index| RuntimeHandoffItem {
                        text: format!("Immediate action {index:02}"),
                        lifecycle: RuntimeLifecycle::Active,
                    })
                    .collect(),
                completed_state: Vec::new(),
                operational_guardrails: Vec::new(),
                key_files: (0..(DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION + 2))
                    .map(|index| RuntimeHandoffItem {
                        text: format!("src/file_{index:02}.rs"),
                        lifecycle: RuntimeLifecycle::Active,
                    })
                    .collect(),
                definition_of_done: Vec::new(),
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let compiled =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Default, false)
                .expect("compiled projection");

        assert_eq!(
            compiled.effective_memory.profile.len(),
            DEFAULT_MAX_ACTIVE_MEMORY_ENTRIES_PER_SCOPE
        );
        assert_eq!(
            compiled.effective_memory.profile[0],
            "Profile rule 00".to_owned()
        );
        assert_eq!(
            compiled.effective_memory.profile.last().unwrap(),
            &format!(
                "Profile rule {:02}",
                DEFAULT_MAX_ACTIVE_MEMORY_ENTRIES_PER_SCOPE - 1
            )
        );
        assert_eq!(
            compiled.effective_memory.locality.len(),
            DEFAULT_MAX_ACTIVE_MEMORY_ENTRIES_PER_SCOPE
        );
        assert_eq!(
            compiled.handoff.immediate_actions.len(),
            DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION
        );
        assert_eq!(
            compiled.handoff.immediate_actions.last().unwrap(),
            &format!(
                "Immediate action {:02}",
                DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION - 1
            )
        );
        assert_eq!(
            compiled.handoff.key_files.len(),
            DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION
        );
        assert_eq!(
            compiled.handoff.key_files.last().unwrap(),
            &format!(
                "src/file_{:02}.rs",
                DEFAULT_MAX_ACTIVE_HANDOFF_ITEMS_PER_SECTION - 1
            )
        );
    }

    #[test]
    fn planning_target_omits_completed_state_and_key_files() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::narrative("Retain operator rules.")],
                locality: vec![RuntimeMemoryEntry::narrative("Retain repo lessons.")],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Plan the slice".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Review the backlog.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: vec![RuntimeHandoffItem {
                    text: "Completed work should stay out of planning.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                operational_guardrails: vec![RuntimeHandoffItem {
                    text: "Keep projections fail-closed.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                key_files: vec![RuntimeHandoffItem {
                    text: "`src/state/runtime_export.rs`".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                definition_of_done: vec![RuntimeHandoffItem {
                    text: "Scoped target is explicit.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let compiled =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Planning, false)
                .expect("compiled planning projection");

        assert_eq!(compiled.target, ProjectionTarget::Planning);
        assert_eq!(
            compiled.effective_memory.profile,
            vec!["Retain operator rules.".to_owned()]
        );
        assert_eq!(
            compiled.handoff.immediate_actions,
            vec!["Review the backlog.".to_owned()]
        );
        assert_eq!(
            compiled.handoff.operational_guardrails,
            vec!["Keep projections fail-closed.".to_owned()]
        );
        assert_eq!(
            compiled.handoff.definition_of_done,
            vec!["Scoped target is explicit.".to_owned()]
        );
        assert!(compiled.handoff.completed_state.is_empty());
        assert!(compiled.handoff.key_files.is_empty());
    }

    #[test]
    fn session_target_omits_completed_state_key_files_and_definition_of_done() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::narrative("Retain operator rules.")],
                locality: vec![RuntimeMemoryEntry::narrative("Retain repo lessons.")],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Start the slice".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Review the backlog.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: vec![RuntimeHandoffItem {
                    text: "Completed work should stay out of the start bundle.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                operational_guardrails: vec![RuntimeHandoffItem {
                    text: "Keep projections fail-closed.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                key_files: vec![RuntimeHandoffItem {
                    text: "`src/state/runtime_export.rs`".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                definition_of_done: vec![RuntimeHandoffItem {
                    text: "Scoped target is explicit.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let compiled =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Session, false)
                .expect("compiled session projection");

        assert_eq!(compiled.target, ProjectionTarget::Session);
        assert_eq!(
            compiled.handoff.immediate_actions,
            vec!["Review the backlog.".to_owned()]
        );
        assert_eq!(
            compiled.handoff.operational_guardrails,
            vec!["Keep projections fail-closed.".to_owned()]
        );
        assert!(compiled.handoff.completed_state.is_empty());
        assert!(compiled.handoff.key_files.is_empty());
        assert!(compiled.handoff.definition_of_done.is_empty());
    }

    #[test]
    fn narrative_projection_dedupes_repeated_lines_across_surfaces() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::narrative("Keep projections bounded.")],
                locality: vec![RuntimeMemoryEntry::narrative("Keep projections bounded.")],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Dedupe".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Keep projections bounded.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: Vec::new(),
                operational_guardrails: vec![RuntimeHandoffItem {
                    text: "Keep projections bounded.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                key_files: Vec::new(),
                definition_of_done: vec![RuntimeHandoffItem {
                    text: "Keep projections bounded.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let compiled =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Planning, false)
                .expect("compiled planning projection");

        assert_eq!(
            compiled.effective_memory.profile,
            vec!["Keep projections bounded.".to_owned()]
        );
        assert!(compiled.effective_memory.locality.is_empty());
        assert!(compiled.handoff.immediate_actions.is_empty());
        assert!(compiled.handoff.operational_guardrails.is_empty());
        assert!(compiled.handoff.definition_of_done.is_empty());
    }

    #[test]
    fn symbolic_projection_uses_fact_kinds_and_default_target_sections() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![
                    RuntimeMemoryEntry::narrative("Retain operator rules."),
                    RuntimeMemoryEntry::from_structured_entry(StructuredMemoryEntry {
                        id: "mem_rule".to_owned(),
                        entry_type: "rule".to_owned(),
                        state: "active".to_owned(),
                        created_at: "2026-03-10T12:00:00Z".to_owned(),
                        last_touched_session: 7,
                        origin: "manual".to_owned(),
                        superseded_at: None,
                        decay_class: None,
                        expires_at: None,
                        tags: vec!["projection".to_owned()],
                        source_ref: None,
                        supersedes: Vec::new(),
                        content: "Prefer deterministic writes.".to_owned(),
                    }),
                ],
                locality: vec![RuntimeMemoryEntry::narrative("Keep repo state bounded.")],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Evaluate symbolic facts".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Measure prompt entropy.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: vec![RuntimeHandoffItem {
                    text: "Narrative preview already works.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                operational_guardrails: vec![RuntimeHandoffItem {
                    text: "Keep the cache derived.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                key_files: vec![RuntimeHandoffItem {
                    text: "`src/state/compiled.rs`".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                definition_of_done: vec![RuntimeHandoffItem {
                    text: "Decision is documented.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let symbolic =
            compile_symbolic_projection(&state, &sources, ProjectionTarget::Default, false)
                .unwrap();

        assert_eq!(symbolic.target, ProjectionTarget::Default);
        assert!(symbolic.facts.contains(&SymbolicFact {
            surface: "profile_memory".to_owned(),
            kind: "narrative".to_owned(),
            ordinal: Some(1),
            text: "Retain operator rules.".to_owned(),
        }));
        assert!(symbolic.facts.contains(&SymbolicFact {
            surface: "profile_memory".to_owned(),
            kind: "rule".to_owned(),
            ordinal: Some(2),
            text: "Prefer deterministic writes.".to_owned(),
        }));
        assert!(symbolic.facts.contains(&SymbolicFact {
            surface: "handoff_completed_state".to_owned(),
            kind: "item".to_owned(),
            ordinal: Some(1),
            text: "Narrative preview already works.".to_owned(),
        }));
        assert!(symbolic.facts.contains(&SymbolicFact {
            surface: "handoff_key_files".to_owned(),
            kind: "item".to_owned(),
            ordinal: Some(1),
            text: "`src/state/compiled.rs`".to_owned(),
        }));
    }

    #[test]
    fn symbolic_planning_target_omits_completed_state_and_key_files() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::narrative("Retain operator rules.")],
                locality: vec![RuntimeMemoryEntry::narrative("Retain repo lessons.")],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Plan the slice".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Review the backlog.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: vec![RuntimeHandoffItem {
                    text: "Completed work should stay out of planning.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                operational_guardrails: vec![RuntimeHandoffItem {
                    text: "Keep projections fail-closed.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                key_files: vec![RuntimeHandoffItem {
                    text: "`src/state/runtime_export.rs`".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                definition_of_done: vec![RuntimeHandoffItem {
                    text: "Scoped target is explicit.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let symbolic =
            compile_symbolic_projection(&state, &sources, ProjectionTarget::Planning, false)
                .unwrap();

        assert_eq!(symbolic.target, ProjectionTarget::Planning);
        assert!(!symbolic
            .facts
            .iter()
            .any(|fact| fact.surface == "handoff_completed_state"));
        assert!(!symbolic
            .facts
            .iter()
            .any(|fact| fact.surface == "handoff_key_files"));
        assert_eq!(
            render_symbolic_content(&symbolic),
            "\
profile_memory|narrative|1|Retain operator rules.\n\
locality_memory|narrative|1|Retain repo lessons.\n\
handoff_title|title|Next Session: Plan the slice\n\
handoff_immediate_actions|item|1|Review the backlog.\n\
handoff_operational_guardrails|item|1|Keep projections fail-closed.\n\
handoff_definition_of_done|item|1|Scoped target is explicit."
        );
    }

    #[test]
    fn symbolic_session_target_omits_definition_of_done_and_key_files() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::narrative("Retain operator rules.")],
                locality: vec![RuntimeMemoryEntry::narrative("Retain repo lessons.")],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Start the slice".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Review the backlog.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: vec![RuntimeHandoffItem {
                    text: "Completed work should stay out of the start bundle.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                operational_guardrails: vec![RuntimeHandoffItem {
                    text: "Keep projections fail-closed.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                key_files: vec![RuntimeHandoffItem {
                    text: "`src/state/runtime_export.rs`".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                definition_of_done: vec![RuntimeHandoffItem {
                    text: "Scoped target is explicit.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
            },
        };
        let sources = RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "profile".to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "repo".to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: "handoff".to_owned(),
                migrated_from: None,
            },
        };

        let symbolic =
            compile_symbolic_projection(&state, &sources, ProjectionTarget::Session, false)
                .unwrap();

        assert_eq!(symbolic.target, ProjectionTarget::Session);
        assert!(!symbolic
            .facts
            .iter()
            .any(|fact| fact.surface == "handoff_completed_state"));
        assert!(!symbolic
            .facts
            .iter()
            .any(|fact| fact.surface == "handoff_key_files"));
        assert!(!symbolic
            .facts
            .iter()
            .any(|fact| fact.surface == "handoff_definition_of_done"));
        assert_eq!(
            render_symbolic_content(&symbolic),
            "\
profile_memory|narrative|1|Retain operator rules.\n\
locality_memory|narrative|1|Retain repo lessons.\n\
handoff_title|title|Next Session: Start the slice\n\
handoff_immediate_actions|item|1|Review the backlog.\n\
handoff_operational_guardrails|item|1|Keep projections fail-closed."
        );
    }

    fn make_test_sources(
        profile: &str,
        repo: &str,
        _focus: &str,
        handoff: &str,
    ) -> RuntimeSourceSurfaces {
        RuntimeSourceSurfaces {
            profile_memory: RuntimeTextSurface {
                kind: "profile_memory",
                path: PathBuf::from("/tmp/profile-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: profile.to_owned(),
                migrated_from: None,
            },
            locality_memory: RuntimeTextSurface {
                kind: "locality_memory",
                path: PathBuf::from("/tmp/repo-memory.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: repo.to_owned(),
                migrated_from: None,
            },
            branch_memory: missing_branch_memory_surface(),
            clone_memory: missing_clone_memory_surface(),
            execution_gates: missing_execution_gates_surface(),
            handoff: RuntimeTextSurface {
                kind: "handoff",
                path: PathBuf::from("/tmp/handoff.md"),
                status: RuntimeTextSurfaceStatus::Loaded,
                content: handoff.to_owned(),
                migrated_from: None,
            },
        }
    }

    #[test]
    fn projection_digests_are_deterministic() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::narrative("Rule one.")],
                locality: vec![RuntimeMemoryEntry::narrative("Repo note.")],
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Determinism".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Check hashes.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: Vec::new(),
                operational_guardrails: Vec::new(),
                key_files: Vec::new(),
                definition_of_done: Vec::new(),
            },
        };
        let sources = make_test_sources("profile", "repo", "focus", "handoff");

        let first =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Session, false).unwrap();
        let second =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Session, false).unwrap();

        let d1 = first.projection_digests.unwrap();
        let d2 = second.projection_digests.unwrap();
        assert_eq!(d1, d2);
    }

    #[test]
    fn projection_digests_stable_when_filtered_input_changes() {
        let state = RuntimeState {
            memory: RuntimeMemoryState {
                profile: vec![RuntimeMemoryEntry::narrative("Rule one.")],
                locality: Vec::new(),
                branch: Vec::new(),
                clone: Vec::new(),
            },
            handoff: RuntimeHandoffState {
                title: "Next Session: Filtered".to_owned(),
                immediate_actions: vec![RuntimeHandoffItem {
                    text: "Act now.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                completed_state: vec![RuntimeHandoffItem {
                    text: "Completed work.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                operational_guardrails: Vec::new(),
                key_files: vec![RuntimeHandoffItem {
                    text: "`src/file.rs`".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
                definition_of_done: vec![RuntimeHandoffItem {
                    text: "Tests pass.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                }],
            },
        };

        let state_modified = RuntimeState {
            handoff: RuntimeHandoffState {
                completed_state: vec![
                    RuntimeHandoffItem {
                        text: "Completed work.".to_owned(),
                        lifecycle: RuntimeLifecycle::Active,
                    },
                    RuntimeHandoffItem {
                        text: "Extra completed item.".to_owned(),
                        lifecycle: RuntimeLifecycle::Active,
                    },
                ],
                key_files: vec![
                    RuntimeHandoffItem {
                        text: "`src/file.rs`".to_owned(),
                        lifecycle: RuntimeLifecycle::Active,
                    },
                    RuntimeHandoffItem {
                        text: "`src/new_file.rs`".to_owned(),
                        lifecycle: RuntimeLifecycle::Active,
                    },
                ],
                definition_of_done: vec![
                    RuntimeHandoffItem {
                        text: "Tests pass.".to_owned(),
                        lifecycle: RuntimeLifecycle::Active,
                    },
                    RuntimeHandoffItem {
                        text: "Docs updated.".to_owned(),
                        lifecycle: RuntimeLifecycle::Active,
                    },
                ],
                ..state.handoff.clone()
            },
            ..state.clone()
        };

        let sources = make_test_sources("profile", "repo", "focus", "handoff");

        let original =
            compile_runtime_projection(&state, &sources, ProjectionTarget::Session, false).unwrap();
        let modified =
            compile_runtime_projection(&state_modified, &sources, ProjectionTarget::Session, false)
                .unwrap();

        let d_original = original.projection_digests.unwrap();
        let d_modified = modified.projection_digests.unwrap();
        assert_eq!(
            d_original, d_modified,
            "session target filters completed_state, key_files, and definition_of_done — changing only those should not change projection digests"
        );
    }

    #[test]
    fn context_delta_returns_only_changed_surfaces() {
        let previous = ProjectionDigests {
            effective_memory: "aaa".to_owned(),
            handoff: "ccc".to_owned(),
            execution_gates: "ddd".to_owned(),
            escalation: "eee".to_owned(),
            recovery: "fff".to_owned(),
            git_state: "ggg".to_owned(),
            session_state: "hhh".to_owned(),
        };
        let current = ProjectionDigests {
            effective_memory: "aaa".to_owned(),
            handoff: "ccc_changed".to_owned(),
            execution_gates: "ddd".to_owned(),
            escalation: "eee".to_owned(),
            recovery: "fff".to_owned(),
            git_state: "ggg".to_owned(),
            session_state: "hhh".to_owned(),
        };

        let delta =
            compute_context_delta(&previous, &current, "fp_old", "fp_new").expect("delta present");
        assert_eq!(delta.changed_surfaces.len(), 1);
        assert_eq!(delta.changed_surfaces[0].name, "handoff");
        assert_eq!(delta.previous_fingerprint, "fp_old");
        assert_eq!(delta.current_fingerprint, "fp_new");

        let no_delta = compute_context_delta(&previous, &previous, "fp", "fp");
        assert!(no_delta.is_none());
    }

    #[test]
    fn projection_digests_ignores_old_repo_focus_field() {
        let old_json = r#"{"effective_memory":"aaa","repo_focus":"bbb","handoff":"ccc"}"#;
        let digests: ProjectionDigests = serde_json::from_str(old_json)
            .expect("old repo_focus field should deserialize as ignored legacy data");
        assert_eq!(digests.effective_memory, "aaa");
        assert_eq!(digests.handoff, "ccc");
        let reserialized = serde_json::to_string(&digests).expect("serialize");
        assert!(!reserialized.contains("repo_focus"));
        assert!(!reserialized.contains("workflow_signal"));
    }

    #[test]
    fn projection_digests_deserializes_legacy_minimal_shape() {
        let old_json = r#"{"effective_memory":"aaa","handoff":"ccc"}"#;
        let digests: ProjectionDigests = serde_json::from_str(old_json)
            .expect("legacy digests should deserialize without workflow_signal");
        assert_eq!(digests.effective_memory, "aaa");
        assert_eq!(digests.handoff, "ccc");
    }

    #[test]
    fn projection_digests_ignore_legacy_workflow_signal_field() {
        let json = r#"{"effective_memory":"aaa","workflow_signal":"bbb","handoff":"ccc"}"#;
        let digests: ProjectionDigests = serde_json::from_str(json).unwrap();
        assert_eq!(digests.effective_memory, "aaa");
        assert_eq!(digests.handoff, "ccc");
        assert_eq!(digests.execution_gates, "");
        assert_eq!(digests.escalation, "");
        assert_eq!(digests.recovery, "");
        assert_eq!(digests.git_state, "");
        assert_eq!(digests.session_state, "");
    }

    #[test]
    fn compute_extended_digests_hashes_all_surfaces() {
        let base = ProjectionDigests {
            effective_memory: "mem_hash".to_owned(),
            handoff: "ho_hash".to_owned(),
            execution_gates: String::new(),
            escalation: String::new(),
            recovery: String::new(),
            git_state: String::new(),
            session_state: String::new(),
        };
        let extended = compute_extended_digests(&base, "gates", "esc", "rec", "git", "sess");
        assert_eq!(extended.effective_memory, "mem_hash");
        assert_eq!(extended.handoff, "ho_hash");
        assert_ne!(extended.execution_gates, "");
        assert_ne!(extended.escalation, "");
        assert_ne!(extended.recovery, "");
        assert_ne!(extended.git_state, "");
        assert_ne!(extended.session_state, "");
        // Same input should produce same hash
        let extended2 = compute_extended_digests(&base, "gates", "esc", "rec", "git", "sess");
        assert_eq!(extended.execution_gates, extended2.execution_gates);
    }

    #[test]
    fn context_delta_skips_empty_extended_fields() {
        let old = ProjectionDigests {
            effective_memory: "aaa".to_owned(),
            handoff: "ccc".to_owned(),
            execution_gates: String::new(), // old baseline didn't have these
            escalation: String::new(),
            recovery: String::new(),
            git_state: String::new(),
            session_state: String::new(),
        };
        let new = ProjectionDigests {
            effective_memory: "aaa".to_owned(),
            handoff: "ddd".to_owned(), // changed
            execution_gates: "xxx".to_owned(),
            escalation: "yyy".to_owned(),
            recovery: "zzz".to_owned(),
            git_state: "ggg".to_owned(),
            session_state: "sss".to_owned(),
        };
        let delta = compute_context_delta(&old, &new, "fp1", "fp2");
        assert!(delta.is_some());
        let delta = delta.unwrap();
        // Only handoff should show as changed — extended fields skipped because old is empty
        assert_eq!(delta.changed_surfaces.len(), 1);
        assert_eq!(delta.changed_surfaces[0].name, "handoff");
    }
}
