Skip to main content

dk_runner/steps/semantic/
context.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::Result;
5use uuid::Uuid;
6
7use dk_core::types::{CallEdge, Symbol};
8use dk_engine::repo::Engine;
9
10use super::checks::{ChangedFile, CheckContext};
11
12/// Build a `CheckContext` by querying the Engine's graph stores for the
13/// "before" snapshot and parsing the materialized changeset files for the
14/// "after" snapshot.
15///
16/// # Arguments
17///
18/// * `engine` — the dk-engine orchestrator (symbol store, call graph, etc.)
19/// * `repo_id` — the repository UUID
20/// * `changeset_files` — relative paths of changed files
21/// * `work_dir` — the directory where changeset files have been materialized
22pub async fn build_check_context(
23    engine: &Arc<Engine>,
24    repo_id: Uuid,
25    changeset_files: &[String],
26    work_dir: &Path,
27) -> Result<CheckContext> {
28    let mut before_symbols: Vec<Symbol> = Vec::new();
29    let mut after_symbols: Vec<Symbol> = Vec::new();
30    let mut before_call_graph: Vec<CallEdge> = Vec::new();
31    let mut changed_files: Vec<ChangedFile> = Vec::new();
32
33    for file_path in changeset_files {
34        // ── Before state: query symbols from DB ──
35        let file_symbols = engine
36            .symbol_store()
37            .find_by_file(repo_id, file_path)
38            .await
39            .unwrap_or_default();
40
41        // Gather call graph edges for every before-symbol.
42        for sym in &file_symbols {
43            let callee_edges = engine
44                .call_graph_store()
45                .find_callees(sym.id)
46                .await
47                .unwrap_or_default();
48            before_call_graph.extend(callee_edges);
49
50            let caller_edges = engine
51                .call_graph_store()
52                .find_callers(sym.id)
53                .await
54                .unwrap_or_default();
55            before_call_graph.extend(caller_edges);
56        }
57
58        before_symbols.extend(file_symbols);
59
60        // ── After state: parse the materialized file ──
61        let abs_path = work_dir.join(file_path);
62        let rel_path = Path::new(file_path);
63
64        if abs_path.exists() {
65            let bytes = tokio::fs::read(&abs_path).await?;
66            let content = String::from_utf8_lossy(&bytes).to_string();
67
68            // Only parse files the parser supports.
69            if engine.parser().supports_file(rel_path) {
70                match engine.parser().parse_file(rel_path, &bytes) {
71                    Ok(analysis) => {
72                        after_symbols.extend(analysis.symbols);
73                    }
74                    Err(e) => {
75                        tracing::warn!("Failed to parse {}: {e}", file_path);
76                    }
77                }
78            }
79
80            changed_files.push(ChangedFile {
81                path: file_path.clone(),
82                content: Some(content),
83            });
84        } else {
85            // File was deleted.
86            changed_files.push(ChangedFile {
87                path: file_path.clone(),
88                content: None,
89            });
90        }
91    }
92
93    // ── Dependencies (repo-level, before state) ──
94    let before_deps = engine
95        .dep_store()
96        .find_by_repo(repo_id)
97        .await
98        .unwrap_or_default();
99
100    // For now the after-deps mirror the before-deps since we don't parse
101    // manifest files from the changeset yet.
102    let after_deps = before_deps.clone();
103
104    // After call graph: we don't yet persist parsed call edges back, so use
105    // an empty vec. The quality checks that need it will work on
106    // before_call_graph (the full DB state).
107    let after_call_graph = Vec::new();
108
109    Ok(CheckContext {
110        before_symbols,
111        after_symbols,
112        before_call_graph,
113        after_call_graph,
114        before_deps,
115        after_deps,
116        changed_files,
117    })
118}