1use std::collections::{BTreeMap, BTreeSet, HashSet};
2
3use docx_store::models::{DocBlock, DocSource, RelationRecord, Symbol};
4use docx_store::schema::{
5 REL_CONTAINS, REL_INHERITS, REL_MEMBER_OF, REL_OBSERVED_IN, REL_PARAM_TYPE, REL_REFERENCES,
6 REL_RETURNS, REL_SEE_ALSO, TABLE_DOC_BLOCK, TABLE_DOC_SOURCE, TABLE_SYMBOL,
7};
8use surrealdb::Connection;
9
10use crate::store::StoreError;
11
12use super::{ControlError, DocxControlPlane};
13
14const ADVANCED_SEARCH_MIN_FILTERS: usize = 1;
15
16impl<C: Connection> DocxControlPlane<C> {
17 pub async fn get_symbol(
22 &self,
23 project_id: &str,
24 symbol_key: &str,
25 ) -> Result<Option<Symbol>, ControlError> {
26 Ok(self
27 .store
28 .get_symbol_by_project(project_id, symbol_key)
29 .await?)
30 }
31
32 pub async fn list_doc_blocks(
37 &self,
38 project_id: &str,
39 symbol_key: &str,
40 ingest_id: Option<&str>,
41 ) -> Result<Vec<DocBlock>, ControlError> {
42 Ok(self
43 .store
44 .list_doc_blocks(project_id, symbol_key, ingest_id)
45 .await?)
46 }
47
48 pub async fn search_symbols(
53 &self,
54 project_id: &str,
55 name: &str,
56 limit: usize,
57 ) -> Result<Vec<Symbol>, ControlError> {
58 Ok(self
59 .store
60 .list_symbols_by_name(project_id, name, limit)
61 .await?)
62 }
63
64 pub async fn search_symbols_advanced(
69 &self,
70 project_id: &str,
71 request: SearchSymbolsAdvancedRequest,
72 limit: usize,
73 ) -> Result<SearchSymbolsAdvancedResult, ControlError> {
74 let normalized = request.normalized();
75 if normalized.active_filter_count() < ADVANCED_SEARCH_MIN_FILTERS {
76 return Err(ControlError::Store(StoreError::InvalidInput(
77 "at least one search filter is required".to_string(),
78 )));
79 }
80
81 let symbols = self
82 .store
83 .search_symbols_advanced(
84 project_id,
85 normalized.name.as_deref(),
86 normalized.qualified_name.as_deref(),
87 normalized.symbol_key.as_deref(),
88 normalized.signature.as_deref(),
89 limit,
90 )
91 .await?;
92 let total_returned = symbols.len();
93
94 Ok(SearchSymbolsAdvancedResult {
95 symbols,
96 total_returned,
97 applied_filters: normalized,
98 })
99 }
100
101 pub async fn search_doc_blocks(
106 &self,
107 project_id: &str,
108 text: &str,
109 limit: usize,
110 ) -> Result<Vec<DocBlock>, ControlError> {
111 Ok(self
112 .store
113 .search_doc_blocks(project_id, text, limit)
114 .await?)
115 }
116
117 pub async fn list_symbol_kinds(&self, project_id: &str) -> Result<Vec<String>, ControlError> {
122 Ok(self.store.list_symbol_kinds(project_id).await?)
123 }
124
125 pub async fn audit_project_completeness(
130 &self,
131 project_id: &str,
132 ) -> Result<ProjectCompletenessAudit, ControlError> {
133 let symbol_count = self
134 .store
135 .count_rows_for_project(TABLE_SYMBOL, project_id)
136 .await?;
137 let doc_block_count = self
138 .store
139 .count_rows_for_project(TABLE_DOC_BLOCK, project_id)
140 .await?;
141 let doc_source_count = self
142 .store
143 .count_rows_for_project(TABLE_DOC_SOURCE, project_id)
144 .await?;
145
146 let symbols_missing_source_path_count = self
147 .store
148 .count_symbols_missing_field(project_id, "source_path")
149 .await?;
150 let symbols_missing_line_count = self
151 .store
152 .count_symbols_missing_field(project_id, "line")
153 .await?;
154 let symbols_missing_col_count = self
155 .store
156 .count_symbols_missing_field(project_id, "col")
157 .await?;
158
159 let doc_block_symbol_keys = self.store.list_doc_block_symbol_keys(project_id).await?;
160 let symbols_with_doc_blocks_count = doc_block_symbol_keys
161 .into_iter()
162 .collect::<HashSet<_>>()
163 .len();
164
165 let observed_in_symbols = self.store.list_observed_in_symbol_refs(project_id).await?;
166 let symbols_with_observed_in_count = observed_in_symbols
167 .into_iter()
168 .collect::<HashSet<_>>()
169 .len();
170
171 let relation_edge_counts = relation_names()
172 .into_iter()
173 .map(|relation| async move {
174 let count = self
175 .store
176 .count_rows_for_project(relation, project_id)
177 .await?;
178 Ok::<RelationEdgeCount, ControlError>(RelationEdgeCount {
179 relation: relation.to_string(),
180 count,
181 })
182 })
183 .collect::<Vec<_>>();
184
185 let mut relation_edge_counts = futures::future::try_join_all(relation_edge_counts).await?;
186 relation_edge_counts.sort_by(|left, right| left.relation.cmp(&right.relation));
187
188 let relation_counts = relation_edge_counts
189 .iter()
190 .map(|entry| (entry.relation.clone(), entry.count))
191 .collect::<BTreeMap<_, _>>();
192
193 Ok(ProjectCompletenessAudit {
194 project_id: project_id.to_string(),
195 symbol_count,
196 doc_block_count,
197 doc_source_count,
198 symbols_missing_source_path_count,
199 symbols_missing_line_count,
200 symbols_missing_col_count,
201 symbols_with_doc_blocks_count,
202 symbols_with_observed_in_count,
203 relation_counts,
204 relation_edge_counts,
205 })
206 }
207
208 pub async fn list_members_by_scope(
213 &self,
214 project_id: &str,
215 scope: &str,
216 limit: usize,
217 ) -> Result<Vec<Symbol>, ControlError> {
218 Ok(self
219 .store
220 .list_members_by_scope(project_id, scope, limit)
221 .await?)
222 }
223
224 pub async fn get_symbol_adjacency(
231 &self,
232 project_id: &str,
233 symbol_key: &str,
234 limit: usize,
235 ) -> Result<SymbolAdjacency, ControlError> {
236 let limit = limit.max(1);
237 let symbol = self.get_symbol(project_id, symbol_key).await?;
238 let Some(symbol) = symbol else {
239 return Ok(SymbolAdjacency::default());
240 };
241 let doc_blocks = self.list_doc_blocks(project_id, symbol_key, None).await?;
242 let mut ingest_ids = doc_blocks
243 .iter()
244 .filter_map(|block| block.ingest_id.clone())
245 .collect::<Vec<_>>();
246 ingest_ids.sort();
247 ingest_ids.dedup();
248 let symbol_id = symbol
249 .id
250 .clone()
251 .unwrap_or_else(|| symbol.symbol_key.clone());
252
253 let adj = self
254 .store
255 .fetch_symbol_adjacency(&symbol_id, project_id, limit)
256 .await?;
257
258 let doc_sources_from_doc_blocks =
259 self.store.list_doc_sources(project_id, &ingest_ids).await?;
260 let observed_doc_source_ids = adj
261 .observed_in
262 .iter()
263 .filter_map(|edge| record_id_to_doc_source_id(&edge.out_id))
264 .map(str::to_string)
265 .collect::<BTreeSet<_>>()
266 .into_iter()
267 .collect::<Vec<_>>();
268 let doc_sources_from_observed_in = self
269 .store
270 .list_doc_sources_by_ids(project_id, &observed_doc_source_ids)
271 .await?;
272 let hydration_summary = DocSourceHydrationSummary {
273 from_doc_blocks: doc_sources_from_doc_blocks.len(),
274 from_observed_in: doc_sources_from_observed_in.len(),
275 deduped_total: 0,
276 };
277 let (doc_sources, hydration_summary) = merge_doc_sources(
278 doc_sources_from_doc_blocks,
279 doc_sources_from_observed_in,
280 hydration_summary,
281 );
282
283 let mut related_keys = std::collections::HashSet::new();
284 for relation in adj
285 .member_of
286 .iter()
287 .chain(adj.contains.iter())
288 .chain(adj.returns.iter())
289 .chain(adj.param_types.iter())
290 .chain(adj.see_also.iter())
291 .chain(adj.inherits.iter())
292 .chain(adj.references.iter())
293 .chain(adj.observed_in.iter())
294 {
295 if let Some(key) = record_id_to_symbol_key(&relation.in_id) {
296 related_keys.insert(key.to_string());
297 }
298 if let Some(key) = record_id_to_symbol_key(&relation.out_id) {
299 related_keys.insert(key.to_string());
300 }
301 }
302
303 let related_keys: Vec<String> = related_keys.into_iter().collect();
304 let related_futs: Vec<_> = related_keys
305 .iter()
306 .map(|key| self.get_symbol(project_id, key))
307 .collect();
308 let related_results = futures::future::join_all(related_futs).await;
309 let mut related_symbols: Vec<Symbol> = related_results
310 .into_iter()
311 .filter_map(|r| r.ok().flatten())
312 .collect();
313 related_symbols.sort_by(|left, right| left.symbol_key.cmp(&right.symbol_key));
314 related_symbols.dedup_by(|left, right| left.symbol_key == right.symbol_key);
315
316 Ok(SymbolAdjacency {
317 symbol: Some(symbol),
318 doc_blocks,
319 doc_sources,
320 hydration_summary,
321 member_of: adj.member_of,
322 contains: adj.contains,
323 returns: adj.returns,
324 param_types: adj.param_types,
325 see_also: adj.see_also,
326 inherits: adj.inherits,
327 references: adj.references,
328 observed_in: adj.observed_in,
329 related_symbols,
330 })
331 }
332}
333
334#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
336pub struct SymbolAdjacency {
337 pub symbol: Option<Symbol>,
338 pub doc_blocks: Vec<DocBlock>,
339 pub doc_sources: Vec<DocSource>,
340 pub hydration_summary: DocSourceHydrationSummary,
341 pub member_of: Vec<RelationRecord>,
342 pub contains: Vec<RelationRecord>,
343 pub returns: Vec<RelationRecord>,
344 pub param_types: Vec<RelationRecord>,
345 pub see_also: Vec<RelationRecord>,
346 pub inherits: Vec<RelationRecord>,
347 pub references: Vec<RelationRecord>,
348 pub observed_in: Vec<RelationRecord>,
349 pub related_symbols: Vec<Symbol>,
350}
351
352#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
354pub struct DocSourceHydrationSummary {
355 pub from_doc_blocks: usize,
356 pub from_observed_in: usize,
357 pub deduped_total: usize,
358}
359
360#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
362pub struct SearchSymbolsAdvancedRequest {
363 pub name: Option<String>,
364 pub qualified_name: Option<String>,
365 pub symbol_key: Option<String>,
366 pub signature: Option<String>,
367}
368
369impl SearchSymbolsAdvancedRequest {
370 #[must_use]
371 pub fn normalized(self) -> Self {
372 Self {
373 name: normalize_optional(self.name),
374 qualified_name: normalize_optional(self.qualified_name),
375 symbol_key: normalize_optional(self.symbol_key),
376 signature: normalize_optional(self.signature),
377 }
378 }
379
380 #[must_use]
381 pub fn active_filter_count(&self) -> usize {
382 [
383 self.name.as_ref(),
384 self.qualified_name.as_ref(),
385 self.symbol_key.as_ref(),
386 self.signature.as_ref(),
387 ]
388 .iter()
389 .filter(|value| value.is_some())
390 .count()
391 }
392}
393
394#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
396pub struct SearchSymbolsAdvancedResult {
397 pub symbols: Vec<Symbol>,
398 pub total_returned: usize,
399 pub applied_filters: SearchSymbolsAdvancedRequest,
400}
401
402#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
404pub struct RelationEdgeCount {
405 pub relation: String,
406 pub count: usize,
407}
408
409#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
411pub struct ProjectCompletenessAudit {
412 pub project_id: String,
413 pub symbol_count: usize,
414 pub doc_block_count: usize,
415 pub doc_source_count: usize,
416 pub symbols_missing_source_path_count: usize,
417 pub symbols_missing_line_count: usize,
418 pub symbols_missing_col_count: usize,
419 pub symbols_with_doc_blocks_count: usize,
420 pub symbols_with_observed_in_count: usize,
421 pub relation_counts: BTreeMap<String, usize>,
422 pub relation_edge_counts: Vec<RelationEdgeCount>,
423}
424
425fn record_id_to_symbol_key(record_id: &str) -> Option<&str> {
427 record_id.strip_prefix("symbol:")
428}
429
430fn record_id_to_doc_source_id(record_id: &str) -> Option<&str> {
432 record_id.strip_prefix("doc_source:")
433}
434
435fn merge_doc_sources(
436 from_doc_blocks: Vec<DocSource>,
437 from_observed_in: Vec<DocSource>,
438 mut summary: DocSourceHydrationSummary,
439) -> (Vec<DocSource>, DocSourceHydrationSummary) {
440 let mut all = from_doc_blocks;
441 all.extend(from_observed_in);
442
443 let mut seen = HashSet::new();
444 all.retain(|source| {
445 let key = source
446 .id
447 .clone()
448 .unwrap_or_else(|| format!("missing:{}", source.project_id));
449 seen.insert(key)
450 });
451 all.sort_by(|left, right| {
452 let left_key = left.id.as_deref().unwrap_or_default();
453 let right_key = right.id.as_deref().unwrap_or_default();
454 left_key.cmp(right_key)
455 });
456 summary.deduped_total = all.len();
457 (all, summary)
458}
459
460fn normalize_optional(value: Option<String>) -> Option<String> {
461 value.and_then(|inner| {
462 let trimmed = inner.trim();
463 if trimmed.is_empty() {
464 None
465 } else {
466 Some(trimmed.to_string())
467 }
468 })
469}
470
471fn relation_names() -> Vec<&'static str> {
472 vec![
473 REL_MEMBER_OF,
474 REL_CONTAINS,
475 REL_RETURNS,
476 REL_PARAM_TYPE,
477 REL_SEE_ALSO,
478 REL_INHERITS,
479 REL_REFERENCES,
480 REL_OBSERVED_IN,
481 ]
482}