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#[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 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#[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 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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}