knowledge_runtime/entity/
code_ids.rs1use crate::ids::{EntityId, ScopeKey};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum CodeEntityKind {
8 Repo,
9 Crate,
10 Module,
11 File,
12 Function,
13 Type,
14 Test,
15 Trait,
16 Impl,
17 Const,
18 Macro,
19}
20
21impl CodeEntityKind {
22 pub fn as_str(&self) -> &'static str {
23 match self {
24 Self::Repo => "repo",
25 Self::Crate => "crate",
26 Self::Module => "module",
27 Self::File => "file",
28 Self::Function => "function",
29 Self::Type => "type",
30 Self::Test => "test",
31 Self::Trait => "trait",
32 Self::Impl => "impl",
33 Self::Const => "const",
34 Self::Macro => "macro",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct CodeEntity {
45 pub id: EntityId,
47 pub kind: CodeEntityKind,
49 pub qualified_path: String,
51 pub short_name: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub file_path: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub line: Option<u32>,
59}
60
61pub fn code_entity_id(kind: &CodeEntityKind, qualified_path: &str, scope: &ScopeKey) -> EntityId {
71 let scope_prefix = scope
72 .repo_id
73 .as_deref()
74 .or(scope.workspace_id.as_deref())
75 .unwrap_or(&scope.namespace);
76
77 EntityId::new(format!(
78 "code:{}:{}:{}",
79 scope_prefix,
80 kind.as_str(),
81 qualified_path
82 ))
83}
84
85pub fn code_entity_display_path(id: &EntityId) -> Option<String> {
90 let s = id.as_str();
91 let rest = s.strip_prefix("code:")?;
92 let after_scope = rest.find(':').map(|i| &rest[i + 1..])?;
94 Some(after_scope.to_string())
95}
96
97pub fn parse_code_entity_id(id: &EntityId) -> Option<(String, CodeEntityKind, String)> {
102 let s = id.as_str();
103 let rest = s.strip_prefix("code:")?;
104
105 let first_colon = rest.find(':')?;
107 let scope_prefix = &rest[..first_colon];
108 let after_scope = &rest[first_colon + 1..];
109
110 let second_colon = after_scope.find(':')?;
111 let kind_str = &after_scope[..second_colon];
112 let path = &after_scope[second_colon + 1..];
113
114 let kind = match kind_str {
115 "repo" => CodeEntityKind::Repo,
116 "crate" => CodeEntityKind::Crate,
117 "module" => CodeEntityKind::Module,
118 "file" => CodeEntityKind::File,
119 "function" => CodeEntityKind::Function,
120 "type" => CodeEntityKind::Type,
121 "test" => CodeEntityKind::Test,
122 "trait" => CodeEntityKind::Trait,
123 "impl" => CodeEntityKind::Impl,
124 "const" => CodeEntityKind::Const,
125 "macro" => CodeEntityKind::Macro,
126 _ => return None,
127 };
128
129 Some((scope_prefix.to_string(), kind, path.to_string()))
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn code_id_includes_repo_scope() {
138 let scope = ScopeKey {
139 namespace: "ns".into(),
140 domain: None,
141 workspace_id: None,
142 repo_id: Some("myrepo".into()),
143 };
144 let id = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope);
145 assert_eq!(id.as_str(), "code:myrepo:function:lib::search");
146 }
147
148 #[test]
149 fn code_id_falls_back_to_workspace() {
150 let scope = ScopeKey {
151 namespace: "ns".into(),
152 domain: None,
153 workspace_id: Some("ws1".into()),
154 repo_id: None,
155 };
156 let id = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope);
157 assert_eq!(id.as_str(), "code:ws1:function:lib::search");
158 }
159
160 #[test]
161 fn code_id_falls_back_to_namespace() {
162 let scope = ScopeKey::namespace_only("prod");
163 let id = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope);
164 assert_eq!(id.as_str(), "code:prod:function:lib::search");
165 }
166
167 #[test]
168 fn same_path_different_repos_yields_different_ids() {
169 let scope_a = ScopeKey {
170 namespace: "ns".into(),
171 domain: None,
172 workspace_id: None,
173 repo_id: Some("repo-a".into()),
174 };
175 let scope_b = ScopeKey {
176 namespace: "ns".into(),
177 domain: None,
178 workspace_id: None,
179 repo_id: Some("repo-b".into()),
180 };
181 let id_a = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope_a);
182 let id_b = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope_b);
183 assert_ne!(id_a, id_b);
184 }
185
186 #[test]
187 fn same_scoped_path_remains_stable() {
188 let scope = ScopeKey {
189 namespace: "ns".into(),
190 domain: None,
191 workspace_id: None,
192 repo_id: Some("repo-a".into()),
193 };
194 let id1 = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope);
195 let id2 = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope);
196 assert_eq!(id1, id2);
197 }
198
199 #[test]
200 fn roundtrip_parse() {
201 let scope = ScopeKey {
202 namespace: "ns".into(),
203 domain: None,
204 workspace_id: None,
205 repo_id: Some("myrepo".into()),
206 };
207 let kind = CodeEntityKind::Function;
208 let path = "semantic_memory::lib::MemoryStore::search";
209 let id = code_entity_id(&kind, path, &scope);
210
211 let (parsed_scope, parsed_kind, parsed_path) = parse_code_entity_id(&id).unwrap();
212 assert_eq!(parsed_scope, "myrepo");
213 assert_eq!(parsed_kind, kind);
214 assert_eq!(parsed_path, path);
215 }
216
217 #[test]
218 fn display_path_strips_scope() {
219 let scope = ScopeKey {
220 namespace: "ns".into(),
221 domain: None,
222 workspace_id: None,
223 repo_id: Some("myrepo".into()),
224 };
225 let id = code_entity_id(&CodeEntityKind::Function, "lib::search", &scope);
226 assert_eq!(
227 code_entity_display_path(&id).unwrap(),
228 "function:lib::search"
229 );
230 }
231
232 #[test]
233 fn parse_invalid_id_returns_none() {
234 let bad = EntityId::new("not-a-code-id");
235 assert!(parse_code_entity_id(&bad).is_none());
236 }
237
238 #[test]
239 fn all_kinds_roundtrip() {
240 let scope = ScopeKey::namespace_only("ns");
241 let kinds = [
242 CodeEntityKind::Repo,
243 CodeEntityKind::Crate,
244 CodeEntityKind::Module,
245 CodeEntityKind::File,
246 CodeEntityKind::Function,
247 CodeEntityKind::Type,
248 CodeEntityKind::Test,
249 CodeEntityKind::Trait,
250 CodeEntityKind::Impl,
251 CodeEntityKind::Const,
252 CodeEntityKind::Macro,
253 ];
254 for kind in kinds {
255 let id = code_entity_id(&kind, "test::path", &scope);
256 let (_, parsed, _) = parse_code_entity_id(&id).unwrap();
257 assert_eq!(parsed, kind);
258 }
259 }
260}