frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use serde::Serialize;

use crate::domain::{EvidenceChannel, FriggError, FriggResult};
use crate::searcher::policy;
use crate::searcher::{
    HybridRankedEvidence, HybridRankingIntent, SearchFilters, SearchHybridExecutionOutput,
    SearchHybridQuery, TextSearcher,
};
use crate::{settings::FriggConfig, settings::SemanticRuntimeCredentials};

use super::{
    HybridChannelWeights, PanicSemanticQueryEmbeddingExecutor, cleanup_workspace,
    prepare_workspace, temp_workspace_root,
};

#[derive(Debug, Serialize)]
struct HardPackTraceCase {
    #[serde(rename = "case_id")]
    case_id: &'static str,
    query_text: String,
    limit: usize,
    workspace_root: String,
    lexical_hits: Vec<HitSnapshot>,
    path_witness_hits: Vec<HitSnapshot>,
    ranked_hits: Vec<RankedHitSnapshot>,
    candidate_traces: Vec<CandidateTraceSnapshot>,
}

#[derive(Debug, Serialize)]
struct HitSnapshot {
    rank: usize,
    repository_id: String,
    path: String,
    line: usize,
    column: usize,
    score: f32,
    channel: String,
    excerpt: String,
}

#[derive(Debug, Serialize)]
struct RankedHitSnapshot {
    rank: usize,
    repository_id: String,
    path: String,
    line: usize,
    column: usize,
    blended_score: f32,
    lexical_score: f32,
    graph_score: f32,
    semantic_score: f32,
    excerpt: String,
}

#[derive(Debug, Serialize)]
struct CandidateTraceSnapshot {
    rank: usize,
    path: String,
    selection_rules: Vec<String>,
    path_witness_rules: Vec<String>,
    path_quality_rules: Vec<String>,
}

fn collect_channel_hits(
    output: &SearchHybridExecutionOutput,
    channel: EvidenceChannel,
    limit: usize,
) -> Vec<HitSnapshot> {
    output
        .channel_results
        .iter()
        .find(|result| result.channel == channel)
        .map(|result| {
            result
                .hits
                .iter()
                .take(limit)
                .enumerate()
                .map(|(index, hit)| HitSnapshot {
                    rank: index + 1,
                    repository_id: hit.document.repository_id.clone(),
                    path: hit.document.path.clone(),
                    line: hit.document.line,
                    column: hit.document.column,
                    score: hit.raw_score,
                    channel: hit.channel.as_str().to_owned(),
                    excerpt: hit.excerpt.clone(),
                })
                .collect()
        })
        .unwrap_or_default()
}

fn collect_ranked_hits(
    output: &SearchHybridExecutionOutput,
    limit: usize,
) -> Vec<RankedHitSnapshot> {
    output
        .matches
        .iter()
        .take(limit)
        .enumerate()
        .map(|(index, hit)| RankedHitSnapshot {
            rank: index + 1,
            repository_id: hit.document.repository_id.clone(),
            path: hit.document.path.clone(),
            line: hit.document.line,
            column: hit.document.column,
            blended_score: hit.blended_score,
            lexical_score: hit.lexical_score,
            graph_score: hit.graph_score,
            semantic_score: hit.semantic_score,
            excerpt: hit.excerpt.clone(),
        })
        .collect()
}

fn run_candidate_traces(
    matches: &[HybridRankedEvidence],
    intent: &HybridRankingIntent,
    query_text: &str,
    limit: usize,
) -> Vec<CandidateTraceSnapshot> {
    let mut selected: Vec<HybridRankedEvidence> = Vec::new();
    let mut traces = Vec::new();

    for (index, candidate) in matches.iter().take(limit).enumerate() {
        let path = &candidate.document.path;
        traces.push(CandidateTraceSnapshot {
            rank: index + 1,
            path: path.clone(),
            selection_rules: policy::selection_rule_trace(
                candidate.clone(),
                &selected,
                intent,
                query_text,
            ),
            path_witness_rules: policy::path_witness_rule_trace(path, intent, query_text),
            path_quality_rules: policy::path_quality_rule_trace(path, intent),
        });
        selected.push(candidate.clone());
    }

    traces
}

fn run_hard_pack_trace_case(
    case_id: &'static str,
    workspace_name: &str,
    files: &[(&str, &str)],
    query: &str,
    limit: usize,
) -> FriggResult<HardPackTraceCase> {
    let root = temp_workspace_root(workspace_name);
    prepare_workspace(&root, files)?;

    let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
    let intent = HybridRankingIntent::from_query(query);

    let output = searcher.search_hybrid_with_filters_using_executor(
        SearchHybridQuery {
            query: query.to_owned(),
            limit,
            weights: HybridChannelWeights::default(),
            semantic: Some(false),
        },
        SearchFilters::default(),
        &SemanticRuntimeCredentials::default(),
        &PanicSemanticQueryEmbeddingExecutor,
    )?;

    let lexical_hits = collect_channel_hits(&output, EvidenceChannel::LexicalManifest, 10);
    let path_witness_hits = collect_channel_hits(&output, EvidenceChannel::PathSurfaceWitness, 10);
    let ranked_hits = collect_ranked_hits(&output, 10);
    let candidate_traces = run_candidate_traces(&output.matches, &intent, query, 10);

    cleanup_workspace(&root);

    Ok(HardPackTraceCase {
        case_id,
        query_text: query.to_owned(),
        limit,
        workspace_root: root.to_string_lossy().into_owned(),
        lexical_hits,
        path_witness_hits,
        ranked_hits,
        candidate_traces,
    })
}

fn run_and_print_case(trace: HardPackTraceCase) -> FriggResult<()> {
    let payload = serde_json::to_string_pretty(&trace)
        .map_err(|error| FriggError::Internal(error.to_string()))?;
    println!("hard_pack_trace={payload}");
    Ok(())
}

#[test]
#[ignore = "manual hard-pack trace harness"]
fn hard_pack_trace_n8n_editor_queries_demote_ci_workflow_noise() -> FriggResult<()> {
    let trace = run_hard_pack_trace_case(
        "hard_pack_n8n_editor_queries_demote_ci_workflow_noise",
        "hard-pack-n8n-editor-vs-workflow-noise",
        &[
            (
                "packages/editor-ui/src/components/canvas/NodeDetails.vue",
                "export const canvasNodeDetails = 'editor ui vue canvas workflow node details playwright';\n",
            ),
            (
                "packages/editor-ui/cypress/e2e/canvas/node-details.cy.ts",
                "describe('editor ui vue canvas workflow node details playwright', () => {});\n",
            ),
            (
                "packages/core/src/workflow_runner.ts",
                "export const workflowRunner = 'workflow runtime';\n",
            ),
            (
                ".github/workflows/build-base-image.yml",
                "name: build image\njobs:\n  build:\n    steps:\n      - run: docker build .\n",
            ),
            (
                ".github/workflows/test-workflow-scripts-reusable.yml",
                "name: reusable workflow\njobs:\n  test:\n    steps:\n      - run: pnpm test\n",
            ),
        ],
        "editor ui vue canvas workflow node details playwright",
        5,
    )?;
    run_and_print_case(trace)
}

#[test]
#[ignore = "manual hard-pack trace harness"]
fn hard_pack_trace_supabase_runtime_queries_demote_repo_meta_and_template_noise() -> FriggResult<()>
{
    let trace = run_hard_pack_trace_case(
        "hard_pack_supabase_runtime_queries_demote_repo_meta_and_template_noise",
        "hard-pack-supabase-runtime-vs-meta-noise",
        &[
            (
                "supabase/functions/hello/index.ts",
                "export const hello = () => 'edge functions self hosted api runtime docker typescript';\n",
            ),
            (
                "apps/studio/pages/dashboard.tsx",
                "export default function Dashboard() { return <div>edge functions self hosted api runtime docker typescript</div>; }\n",
            ),
            (
                "apps/studio/tests/e2e/dashboard.spec.ts",
                "test('edge functions self hosted api runtime docker typescript', async () => {});\n",
            ),
            (
                "examples/auth/nextjs-full/lib/supabase/server.ts",
                "export const exampleServer = 'edge functions self hosted api runtime docker typescript';\n",
            ),
            (
                "apps/ui-library/registry/default/clients/react-router/lib/supabase/server.ts",
                "export const templateServer = 'edge functions self hosted api runtime docker typescript';\n",
            ),
            (
                "DEVELOPERS.md",
                "# Developers\nedge functions self hosted api runtime docker typescript\n",
            ),
            (
                "CONTRIBUTING.md",
                "# Contributing\nedge functions self hosted api runtime docker typescript\n",
            ),
            ("Makefile", "docker:\n\tdocker compose up\n"),
        ],
        "edge functions self hosted api runtime docker typescript",
        6,
    )?;
    run_and_print_case(trace)
}

#[test]
#[ignore = "manual hard-pack trace harness"]
fn hard_pack_trace_graphite_editor_subtree() -> FriggResult<()> {
    let trace = run_hard_pack_trace_case(
        "hard_pack_graphite_editor_subtree",
        "hard-pack-graphite-editor-subtree",
        &[
            (
                "editor/src/messages/panels/layers.rs",
                "pub fn canvas_panel_runtime() { let _ = \"editor panels canvas runtime\"; }\n",
            ),
            ("editor/tests/canvas_runtime.rs", "mod canvas_runtime {}\n"),
            (
                "node-graph/src/runtime.rs",
                "pub fn node_graph_runtime() { let _ = \"node graph runtime\"; }\n",
            ),
            (
                "desktop/wrapper/src/messages.rs",
                "pub fn desktop_wrapper_messages() { let _ = \"graphite editor panels canvas layout messages desktop wrapper svelte\"; }\n",
            ),
            (
                "Cargo.toml",
                "[workspace]\nmembers = [\"editor\", \"desktop\"]\n",
            ),
            ("Cargo.lock", "[[package]]\nname = \"graphite\"\n"),
            (
                "website/content/editor.md",
                "# Editor runtime\neditor panels canvas runtime\n",
            ),
        ],
        "graphite editor panels canvas layout messages desktop wrapper svelte",
        5,
    )?;
    run_and_print_case(trace)
}

#[test]
#[ignore = "manual hard-pack trace harness"]
fn hard_pack_trace_ruff_queries_keep_runtime_surfaces_above_docs_and_readme() -> FriggResult<()> {
    let trace = run_hard_pack_trace_case(
        "hard_pack_ruff_runtime_vs_docs",
        "hard-pack-ruff-runtime-vs-docs",
        &[
            (
                "crates/ruff_server/src/lib.rs",
                "pub fn formatter_server() { let _ = \"formatter server wasm flow\"; }\n",
            ),
            (
                "crates/ruff_wasm/src/lib.rs",
                "pub fn formatter_wasm() { let _ = \"formatter server wasm flow\"; }\n",
            ),
            ("README.md", "# Ruff\nformatter server wasm flow overview\n"),
            (
                "docs/formatter.md",
                "# Formatter\nformatter server wasm flow guide\n",
            ),
            (
                "CONTRIBUTING.md",
                "# Contributing\nformatter server wasm flow guide\n",
            ),
            ("Cargo.lock", "[[package]]\nname = \"ruff\"\n"),
        ],
        "formatter server wasm flow rust runtime",
        5,
    )?;
    run_and_print_case(trace)
}