Skip to main content

relay_knowledge/domain/code/
views.rs

1use serde::{Deserialize, Serialize};
2
3use super::{
4    CodeCallRecord, CodeFeatureFlagRecord, CodeImportRecord, CodeRepositorySelector,
5    CodeRetrievalLayer, CodeRouteRecord, DomainError, FreshnessPolicy, RepositoryCodeRange,
6    error::required_text,
7};
8
9const MAX_CODEBASE_VIEW_CHANGED_PATHS: usize = 200;
10
11/// Deterministic repository understanding view kind.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum CodebaseViewKind {
15    ArchitectureLayers,
16    BusinessDomains,
17    DependencyTour,
18    ProcessFlow,
19    AffectedScope,
20}
21
22impl CodebaseViewKind {
23    /// Stable CLI, API, and MCP representation.
24    pub const fn as_str(self) -> &'static str {
25        match self {
26            Self::ArchitectureLayers => "architecture_layers",
27            Self::BusinessDomains => "business_domains",
28            Self::DependencyTour => "dependency_tour",
29            Self::ProcessFlow => "process_flow",
30            Self::AffectedScope => "affected_scope",
31        }
32    }
33}
34
35/// Request for a graph-derived repository understanding view.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct CodebaseViewRequest {
38    pub repository: CodeRepositorySelector,
39    pub view_kind: CodebaseViewKind,
40    pub freshness_policy: FreshnessPolicy,
41    pub limit: usize,
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub changed_paths: Vec<String>,
44}
45
46impl CodebaseViewRequest {
47    /// Validates view inputs and bounds result fan-out.
48    pub fn new(
49        repository: CodeRepositorySelector,
50        view_kind: CodebaseViewKind,
51        freshness_policy: FreshnessPolicy,
52        limit: usize,
53        changed_paths: Vec<String>,
54    ) -> Result<Self, DomainError> {
55        let limit = match limit {
56            1..=100 => limit,
57            0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
58            _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
59        };
60        let changed_paths = changed_paths
61            .into_iter()
62            .map(|path| required_text("changed_path", path))
63            .collect::<Result<Vec<_>, _>>()?;
64        if changed_paths.len() > MAX_CODEBASE_VIEW_CHANGED_PATHS {
65            return Err(DomainError::invalid(
66                "changed_paths",
67                "must contain 200 or fewer entries",
68            ));
69        }
70
71        Ok(Self {
72            repository,
73            view_kind,
74            freshness_policy,
75            limit,
76            changed_paths,
77        })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn view_request_allows_changed_paths_beyond_output_limit() {
87        let selector = CodeRepositorySelector::new("repo", "HEAD", Vec::new(), Vec::new()).unwrap();
88
89        let request = CodebaseViewRequest::new(
90            selector,
91            CodebaseViewKind::AffectedScope,
92            FreshnessPolicy::AllowStale,
93            1,
94            vec!["src/a.rs".to_owned(), "src/b.rs".to_owned()],
95        )
96        .unwrap();
97
98        assert_eq!(request.changed_paths.len(), 2);
99    }
100
101    #[test]
102    fn view_request_rejects_changed_paths_beyond_input_cap() {
103        let selector = CodeRepositorySelector::new("repo", "HEAD", Vec::new(), Vec::new()).unwrap();
104        let changed_paths = (0..=MAX_CODEBASE_VIEW_CHANGED_PATHS)
105            .map(|index| format!("src/{index}.rs"))
106            .collect();
107
108        let error = CodebaseViewRequest::new(
109            selector,
110            CodebaseViewKind::AffectedScope,
111            FreshnessPolicy::AllowStale,
112            100,
113            changed_paths,
114        )
115        .unwrap_err();
116
117        assert!(error.to_string().contains("changed_paths"));
118    }
119}
120
121/// Bounded raw graph rows used to derive codebase views.
122#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
123pub struct CodebaseViewSnapshot {
124    pub files: Vec<CodebaseViewFile>,
125    pub symbols: Vec<CodebaseViewSymbol>,
126    pub imports: Vec<CodeImportRecord>,
127    pub calls: Vec<CodebaseViewCall>,
128    pub routes: Vec<CodeRouteRecord>,
129    pub dependencies: Vec<CodebaseViewDependency>,
130    pub feature_flags: Vec<CodeFeatureFlagRecord>,
131    pub truncated: bool,
132}
133
134/// File evidence row in a codebase view snapshot.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct CodebaseViewFile {
137    pub path: String,
138    pub language_id: String,
139    pub parse_status: String,
140    pub line_count: usize,
141    pub is_generated: bool,
142}
143
144/// Symbol evidence row in a codebase view snapshot.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct CodebaseViewSymbol {
147    pub symbol_snapshot_id: String,
148    pub path: String,
149    pub language_id: String,
150    pub name: String,
151    pub qualified_name: String,
152    pub kind: String,
153    pub line_range: RepositoryCodeRange,
154}
155
156/// Call evidence row with optional resolved target path.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct CodebaseViewCall {
159    pub call: CodeCallRecord,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub callee_path: Option<String>,
162}
163
164/// Dependency evidence row from manifests and lockfiles.
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct CodebaseViewDependency {
167    pub dependency_id: String,
168    pub path: String,
169    pub language_id: String,
170    pub ecosystem: String,
171    pub package_name: String,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub requirement: Option<String>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub resolved_version: Option<String>,
176    pub dependency_group: String,
177    pub source_kind: String,
178    pub line_range: RepositoryCodeRange,
179}
180
181/// Graph-derived view node.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct CodebaseViewNode {
184    pub id: String,
185    pub label: String,
186    pub node_kind: String,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub path: Option<String>,
189    pub confidence: f64,
190    pub evidence_ids: Vec<String>,
191}
192
193/// Graph-derived view edge.
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195pub struct CodebaseViewEdge {
196    pub id: String,
197    pub source_id: String,
198    pub target_id: String,
199    pub edge_kind: String,
200    pub confidence: f64,
201    pub evidence_ids: Vec<String>,
202}
203
204/// Narrative section derived from graph facts and evidence.
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub struct CodebaseViewSection {
207    pub id: String,
208    pub title: String,
209    pub narrative: String,
210    pub confidence: f64,
211    pub node_ids: Vec<String>,
212    pub edge_ids: Vec<String>,
213    pub evidence_ids: Vec<String>,
214    #[serde(default, skip_serializing_if = "Vec::is_empty")]
215    pub diagnostics: Vec<String>,
216}
217
218/// Evidence reference backing a node, edge, or section.
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct CodebaseViewEvidence {
221    pub id: String,
222    pub evidence_kind: String,
223    pub path: String,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub symbol: Option<String>,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub line_range: Option<RepositoryCodeRange>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub edge_kind: Option<String>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub retrieval_layer: Option<CodeRetrievalLayer>,
232    pub detail: String,
233}
234
235/// View derivation budget and truncation metadata.
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237pub struct CodebaseViewBudget {
238    pub requested_limit: usize,
239    pub snapshot_row_limit: usize,
240    pub snapshot_truncated: bool,
241    pub nodes_truncated: bool,
242    pub edges_truncated: bool,
243    pub sections_truncated: bool,
244    pub evidence_truncated: bool,
245}
246
247impl CodebaseViewBudget {
248    /// Records the bounded work performed while deriving a view.
249    pub const fn new(
250        requested_limit: usize,
251        snapshot_row_limit: usize,
252        snapshot_truncated: bool,
253    ) -> Self {
254        Self {
255            requested_limit,
256            snapshot_row_limit,
257            snapshot_truncated,
258            nodes_truncated: false,
259            edges_truncated: false,
260            sections_truncated: false,
261            evidence_truncated: false,
262        }
263    }
264}