1use docx_store::models::{DocBlock, DocSource, RelationRecord, Symbol};
2use docx_store::schema::{
3 REL_CONTAINS,
4 REL_INHERITS,
5 REL_MEMBER_OF,
6 REL_PARAM_TYPE,
7 REL_REFERENCES,
8 REL_RETURNS,
9 REL_SEE_ALSO,
10};
11use surrealdb::Connection;
12
13use super::{ControlError, DocxControlPlane};
14
15impl<C: Connection> DocxControlPlane<C> {
16 pub async fn get_symbol(
21 &self,
22 project_id: &str,
23 symbol_key: &str,
24 ) -> Result<Option<Symbol>, ControlError> {
25 let record = self
26 .store
27 .get_symbol_by_project(project_id, symbol_key)
28 .await?;
29 if record.is_some() {
30 return Ok(record);
31 }
32 Ok(self.store.get_symbol(symbol_key).await?)
33 }
34
35 pub async fn list_doc_blocks(
40 &self,
41 project_id: &str,
42 symbol_key: &str,
43 ingest_id: Option<&str>,
44 ) -> Result<Vec<DocBlock>, ControlError> {
45 Ok(self
46 .store
47 .list_doc_blocks(project_id, symbol_key, ingest_id)
48 .await?)
49 }
50
51 pub async fn search_symbols(
56 &self,
57 project_id: &str,
58 name: &str,
59 limit: usize,
60 ) -> Result<Vec<Symbol>, ControlError> {
61 Ok(self
62 .store
63 .list_symbols_by_name(project_id, name, limit)
64 .await?)
65 }
66
67 pub async fn search_doc_blocks(
72 &self,
73 project_id: &str,
74 text: &str,
75 limit: usize,
76 ) -> Result<Vec<DocBlock>, ControlError> {
77 Ok(self.store.search_doc_blocks(project_id, text, limit).await?)
78 }
79
80 pub async fn list_symbol_kinds(
85 &self,
86 project_id: &str,
87 ) -> Result<Vec<String>, ControlError> {
88 Ok(self.store.list_symbol_kinds(project_id).await?)
89 }
90
91 pub async fn list_members_by_scope(
96 &self,
97 project_id: &str,
98 scope: &str,
99 limit: usize,
100 ) -> Result<Vec<Symbol>, ControlError> {
101 Ok(self
102 .store
103 .list_members_by_scope(project_id, scope, limit)
104 .await?)
105 }
106
107 pub async fn get_symbol_adjacency(
112 &self,
113 project_id: &str,
114 symbol_key: &str,
115 limit: usize,
116 ) -> Result<SymbolAdjacency, ControlError> {
117 let limit = limit.max(1);
118 let symbol = self.get_symbol(project_id, symbol_key).await?;
119 let Some(symbol) = symbol else {
120 return Ok(SymbolAdjacency::default());
121 };
122 let doc_blocks = self
123 .list_doc_blocks(project_id, symbol_key, None)
124 .await?;
125 let mut ingest_ids = doc_blocks
126 .iter()
127 .filter_map(|block| block.ingest_id.clone())
128 .collect::<Vec<_>>();
129 ingest_ids.sort();
130 ingest_ids.dedup();
131 let doc_sources = self.store.list_doc_sources(project_id, &ingest_ids).await?;
132 let symbol_id = symbol.id.clone().unwrap_or_else(|| symbol.symbol_key.clone());
133
134 let member_of = self
135 .list_relations(REL_MEMBER_OF, project_id, &symbol_id, limit)
136 .await?;
137 let contains = self
138 .list_relations(REL_CONTAINS, project_id, &symbol_id, limit)
139 .await?;
140 let returns = self
141 .list_relations(REL_RETURNS, project_id, &symbol_id, limit)
142 .await?;
143 let param_types = self
144 .list_relations(REL_PARAM_TYPE, project_id, &symbol_id, limit)
145 .await?;
146 let see_also = self
147 .list_relations(REL_SEE_ALSO, project_id, &symbol_id, limit)
148 .await?;
149 let inherits = self
150 .list_relations(REL_INHERITS, project_id, &symbol_id, limit)
151 .await?;
152 let references = self
153 .list_relations(REL_REFERENCES, project_id, &symbol_id, limit)
154 .await?;
155
156 let mut related_symbols = Vec::new();
157 for relation in member_of
158 .iter()
159 .chain(contains.iter())
160 .chain(returns.iter())
161 .chain(param_types.iter())
162 .chain(see_also.iter())
163 .chain(inherits.iter())
164 .chain(references.iter())
165 {
166 if let Some(symbol_key) = record_id_to_symbol_key(&relation.in_id)
167 && let Some(found) = self.get_symbol(project_id, symbol_key).await?
168 {
169 related_symbols.push(found);
170 }
171 if let Some(symbol_key) = record_id_to_symbol_key(&relation.out_id)
172 && let Some(found) = self.get_symbol(project_id, symbol_key).await?
173 {
174 related_symbols.push(found);
175 }
176 }
177
178 related_symbols.sort_by(|left, right| left.symbol_key.cmp(&right.symbol_key));
179 related_symbols.dedup_by(|left, right| left.symbol_key == right.symbol_key);
180
181 Ok(SymbolAdjacency {
182 symbol: Some(symbol),
183 doc_blocks,
184 doc_sources,
185 member_of,
186 contains,
187 returns,
188 param_types,
189 see_also,
190 inherits,
191 references,
192 related_symbols,
193 })
194 }
195
196 async fn list_relations(
197 &self,
198 table: &str,
199 project_id: &str,
200 symbol_id: &str,
201 limit: usize,
202 ) -> Result<Vec<RelationRecord>, ControlError> {
203 let outgoing = self
204 .store
205 .list_relations_from_symbol(table, project_id, symbol_id, limit)
206 .await?;
207 let incoming = self
208 .store
209 .list_relations_to_symbol(table, project_id, symbol_id, limit)
210 .await?;
211 Ok(merge_relations(outgoing, incoming))
212 }
213}
214
215#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
217pub struct SymbolAdjacency {
218 pub symbol: Option<Symbol>,
219 pub doc_blocks: Vec<DocBlock>,
220 pub doc_sources: Vec<DocSource>,
221 pub member_of: Vec<RelationRecord>,
222 pub contains: Vec<RelationRecord>,
223 pub returns: Vec<RelationRecord>,
224 pub param_types: Vec<RelationRecord>,
225 pub see_also: Vec<RelationRecord>,
226 pub inherits: Vec<RelationRecord>,
227 pub references: Vec<RelationRecord>,
228 pub related_symbols: Vec<Symbol>,
229}
230
231fn record_id_to_symbol_key(record_id: &str) -> Option<&str> {
233 record_id.strip_prefix("symbol:")
234}
235
236fn merge_relations(
238 mut left: Vec<RelationRecord>,
239 right: Vec<RelationRecord>,
240) -> Vec<RelationRecord> {
241 let mut seen = std::collections::HashSet::new();
242 for relation in &left {
243 seen.insert(relation_key(relation));
244 }
245 for relation in right {
246 let key = relation_key(&relation);
247 if seen.insert(key) {
248 left.push(relation);
249 }
250 }
251 left
252}
253
254fn relation_key(relation: &RelationRecord) -> (String, String, Option<String>) {
256 (
257 relation.in_id.clone(),
258 relation.out_id.clone(),
259 relation.kind.clone(),
260 )
261}