forge-pilot 0.1.0

Closed-loop orchestrator over runtime advisories, kernel oracles, and canonical Forge export/import lanes
Documentation
use super::retrieval::{RetrievalHit, RetrievalOutcome};
use super::types::{RepoChatAnswer, RepoChatGroundingMode, RepoQuestionRoute};
use crate::bootstrap::{BootstrapCurrentState, BootstrapManifestSnapshot, BootstrapRichness};
use crate::observe::Observation;
use std::collections::BTreeSet;

pub(crate) fn general_non_repo_answer(question: &str) -> RepoChatAnswer {
    RepoChatAnswer {
        answer: format!(
            "That question does not look repo-specific, so repo chat is not grounding it against imported workspace source: {question}"
        ),
        grounded: false,
        abstained: true,
        citations: Vec::new(),
        caveat: Some("Only clear non-repo questions are eligible for provider fallback.".into()),
        manifest_id: None,
        route: RepoQuestionRoute::GeneralNonRepo,
        grounding_mode: RepoChatGroundingMode::ProviderFallbackEligible,
        evidence: Vec::new(),
    }
}

pub(crate) fn missing_import_answer(route: RepoQuestionRoute) -> RepoChatAnswer {
    RepoChatAnswer {
        answer: "I do not have imported source memory for this namespace yet, so I cannot answer grounded repo questions.".into(),
        grounded: false,
        abstained: true,
        citations: Vec::new(),
        caveat: Some("Run bootstrap-source first, then ask again.".into()),
        manifest_id: None,
        route,
        grounding_mode: RepoChatGroundingMode::Ungrounded,
        evidence: Vec::new(),
    }
}

pub(crate) fn missing_manifest_answer(
    route: RepoQuestionRoute,
    current_state: &BootstrapCurrentState,
) -> RepoChatAnswer {
    RepoChatAnswer {
        answer: "The imported current-state object does not resolve to a current manifest snapshot, so I cannot answer grounded repo questions honestly.".into(),
        grounded: false,
        abstained: true,
        citations: Vec::new(),
        caveat: Some(format!(
            "Current-state manifest is unavailable or inconsistent. import_disposition={}",
            current_state.import_disposition
        )),
        manifest_id: current_state.manifest_id.clone(),
        route,
        grounding_mode: RepoChatGroundingMode::Ungrounded,
        evidence: Vec::new(),
    }
}

pub(crate) fn assemble_answer(
    observation: &Observation,
    manifest: &BootstrapManifestSnapshot,
    question: &str,
    retrieval: RetrievalOutcome,
) -> RepoChatAnswer {
    match retrieval.route {
        RepoQuestionRoute::Change => assemble_change_answer(manifest, retrieval),
        RepoQuestionRoute::Ownership => assemble_ownership_answer(manifest, retrieval),
        RepoQuestionRoute::Navigation => assemble_navigation_answer(manifest, retrieval),
        RepoQuestionRoute::Structure => assemble_structure_answer(manifest, retrieval),
        RepoQuestionRoute::DeepSemantic => {
            assemble_deep_semantic_answer(observation, manifest, question, retrieval)
        }
        RepoQuestionRoute::GeneralNonRepo => general_non_repo_answer(question),
    }
}

fn assemble_navigation_answer(
    manifest: &BootstrapManifestSnapshot,
    retrieval: RetrievalOutcome,
) -> RepoChatAnswer {
    if retrieval.evidence.is_empty() {
        return abstained_manifest_answer(
            manifest,
            retrieval.route,
            retrieval.grounding_mode,
            "I cannot ground that navigation question in the current imported source state.",
            retrieval.caveat.or(Some(
                "No current file, symbol, or chunk matched the question.".into(),
            )),
        );
    }

    let lines = retrieval
        .evidence
        .iter()
        .take(3)
        .map(describe_hit)
        .collect::<Vec<_>>()
        .join(" ");
    answer_from_hits(
        manifest,
        retrieval,
        lines,
        Some("Answer is grounded in the current imported workspace-source state.".into()),
    )
}

fn assemble_structure_answer(
    manifest: &BootstrapManifestSnapshot,
    retrieval: RetrievalOutcome,
) -> RepoChatAnswer {
    if retrieval.evidence.is_empty() {
        return abstained_manifest_answer(
            manifest,
            retrieval.route,
            retrieval.grounding_mode,
            "I cannot ground that structure question in the current imported source state.",
            retrieval.caveat.or(Some(
                "No current multi-file structure evidence matched the question.".into(),
            )),
        );
    }

    let paths = retrieval
        .evidence
        .iter()
        .map(|hit| hit.evidence.path.clone())
        .collect::<BTreeSet<_>>()
        .into_iter()
        .collect::<Vec<_>>();
    let mut answer = format!(
        "In current manifest {}, the relevant structure is spread across {}.",
        manifest.manifest_id,
        paths.join(", ")
    );
    if let Some(first) = retrieval.evidence.first() {
        answer.push(' ');
        answer.push_str(&describe_hit(first));
    }
    answer_from_hits(manifest, retrieval, answer, None)
}

fn assemble_deep_semantic_answer(
    observation: &Observation,
    manifest: &BootstrapManifestSnapshot,
    _question: &str,
    retrieval: RetrievalOutcome,
) -> RepoChatAnswer {
    if retrieval.evidence.is_empty() {
        return abstained_manifest_answer(
            manifest,
            retrieval.route,
            retrieval.grounding_mode,
            "I cannot ground that deeper semantic question in the current imported source state.",
            retrieval.caveat.or(Some(
                "No current symbol, chunk, or file evidence matched the question.".into(),
            )),
        );
    }
    if observation.source_inventory.imported_current_state.richness != BootstrapRichness::Symbolized
        && retrieval.evidence.len() < 2
    {
        return abstained_manifest_answer(
            manifest,
            retrieval.route,
            retrieval.grounding_mode,
            "I do not have enough semantic coverage in the current imported state to answer that deeply and honestly.",
            retrieval.caveat.or(Some("Current state is not fully symbolized for a deeper semantic answer.".into())),
        );
    }

    let answer = retrieval
        .evidence
        .iter()
        .take(3)
        .map(describe_hit)
        .collect::<Vec<_>>()
        .join(" ");
    answer_from_hits(manifest, retrieval, answer, None)
}

fn assemble_ownership_answer(
    manifest: &BootstrapManifestSnapshot,
    retrieval: RetrievalOutcome,
) -> RepoChatAnswer {
    if retrieval.evidence.is_empty() {
        return abstained_manifest_answer(
            manifest,
            retrieval.route,
            retrieval.grounding_mode,
            "I cannot identify ownership from the current imported source state.",
            Some("Imported repo state carries paths and symbols, not human or team ownership metadata.".into()),
        );
    }

    abstained_manifest_answer(
        manifest,
        retrieval.route,
        retrieval.grounding_mode,
        "I can point to the relevant files, but I cannot honestly claim a human or team owner from imported source memory alone.",
        Some("Imported repo state exposes structural location, not explicit ownership truth.".into()),
    )
}

fn assemble_change_answer(
    manifest: &BootstrapManifestSnapshot,
    retrieval: RetrievalOutcome,
) -> RepoChatAnswer {
    let Some(delta) = retrieval.delta.as_ref() else {
        return abstained_manifest_answer(
            manifest,
            retrieval.route,
            retrieval.grounding_mode,
            "I cannot answer that change question honestly because the latest manifest delta is unavailable.",
            Some("Change questions require the latest manifest delta from imported current-state.".into()),
        );
    };

    if delta.new_files.is_empty()
        && delta.changed_files.is_empty()
        && delta.deleted_files.is_empty()
        && delta.source_unchanged_derived_changed_files.is_empty()
    {
        return RepoChatAnswer {
            answer: format!(
                "Latest manifest delta for {} reports no source or derived changes.",
                manifest.manifest_id
            ),
            grounded: true,
            abstained: false,
            citations: vec![super::types::RepoChatCitation {
                path: ".".into(),
                record_kind: "manifest".into(),
                chunk_id: None,
                symbol_id: None,
            }],
            caveat: retrieval.caveat,
            manifest_id: Some(manifest.manifest_id.clone()),
            route: retrieval.route,
            grounding_mode: retrieval.grounding_mode,
            evidence: vec![super::types::RepoChatEvidence {
                manifest_id: manifest.manifest_id.clone(),
                path: ".".into(),
                evidence_type: super::types::RepoChatEvidenceType::Manifest,
                symbol_name: None,
                chunk_id: None,
                line_start: None,
                line_end: None,
                score: 1.0,
                rationale: "Latest manifest delta reports no changes.".into(),
            }],
        };
    }

    let mut parts = vec![format!(
        "Latest manifest delta for {} shows {} new, {} changed, {} deleted, and {} derived-only file changes.",
        manifest.manifest_id,
        delta.new_files.len(),
        delta.changed_files.len(),
        delta.deleted_files.len(),
        delta.source_unchanged_derived_changed_files.len()
    )];
    let selected = retrieval
        .evidence
        .iter()
        .take(3)
        .map(describe_hit)
        .collect::<Vec<_>>();
    if !selected.is_empty() {
        parts.push(selected.join(" "));
    }
    answer_from_hits(manifest, retrieval, parts.join(" "), None)
}

fn answer_from_hits(
    manifest: &BootstrapManifestSnapshot,
    retrieval: RetrievalOutcome,
    answer: String,
    fallback_caveat: Option<String>,
) -> RepoChatAnswer {
    RepoChatAnswer {
        answer,
        grounded: true,
        abstained: false,
        citations: retrieval
            .evidence
            .iter()
            .map(|hit| hit.citation.clone())
            .collect(),
        caveat: retrieval.caveat.or(fallback_caveat),
        manifest_id: Some(manifest.manifest_id.clone()),
        route: retrieval.route,
        grounding_mode: retrieval.grounding_mode,
        evidence: retrieval
            .evidence
            .iter()
            .map(|hit| hit.evidence.clone())
            .collect(),
    }
}

fn abstained_manifest_answer(
    manifest: &BootstrapManifestSnapshot,
    route: RepoQuestionRoute,
    grounding_mode: RepoChatGroundingMode,
    answer: &str,
    caveat: Option<String>,
) -> RepoChatAnswer {
    RepoChatAnswer {
        answer: answer.into(),
        grounded: false,
        abstained: true,
        citations: Vec::new(),
        caveat,
        manifest_id: Some(manifest.manifest_id.clone()),
        route,
        grounding_mode,
        evidence: Vec::new(),
    }
}

fn describe_hit(hit: &RetrievalHit) -> String {
    match hit.record_kind.as_str() {
        "symbol" => {
            let name = hit.evidence.symbol_name.as_deref().unwrap_or("unknown");
            match (hit.evidence.line_start, hit.evidence.line_end) {
                (Some(start), Some(end)) if start == end => {
                    format!(
                        "`{name}` is defined in {} at line {}.",
                        hit.evidence.path, start
                    )
                }
                (Some(start), Some(end)) => {
                    format!(
                        "`{name}` is defined in {} at lines {}-{}.",
                        hit.evidence.path, start, end
                    )
                }
                _ => format!("`{name}` is defined in {}.", hit.evidence.path),
            }
        }
        "chunk" => format!(
            "{} contains relevant chunk evidence: {}.",
            hit.evidence.path, hit.snippet
        ),
        "file" => format!(
            "{} contains relevant file-level evidence: {}.",
            hit.evidence.path, hit.snippet
        ),
        "changed" => format!(
            "{} is marked changed in the latest manifest delta.",
            hit.evidence.path
        ),
        "new" => format!(
            "{} is marked new in the latest manifest delta.",
            hit.evidence.path
        ),
        "deleted" => format!(
            "{} is marked deleted in the latest manifest delta.",
            hit.evidence.path
        ),
        "derived_changed" => {
            format!(
                "{} has derived-only changes in the latest manifest delta.",
                hit.evidence.path
            )
        }
        _ => format!("{} contributes grounded evidence.", hit.evidence.path),
    }
}