1use std::collections::{HashMap, HashSet};
7
8pub use brainwires_core::graph::EntityType;
10
11#[derive(Debug, Clone)]
13pub struct Entity {
14 pub name: String,
16 pub entity_type: EntityType,
18 pub message_ids: Vec<String>,
20 pub first_seen: i64,
22 pub last_seen: i64,
24 pub mention_count: u32,
26}
27
28impl Entity {
29 pub fn new(name: String, entity_type: EntityType, message_id: String, timestamp: i64) -> Self {
31 Self {
32 name,
33 entity_type,
34 message_ids: vec![message_id],
35 first_seen: timestamp,
36 last_seen: timestamp,
37 mention_count: 1,
38 }
39 }
40
41 pub fn add_mention(&mut self, message_id: String, timestamp: i64) {
43 if !self.message_ids.contains(&message_id) {
44 self.message_ids.push(message_id);
45 }
46 self.last_seen = timestamp.max(self.last_seen);
47 self.mention_count += 1;
48 }
49}
50
51#[derive(Debug, Clone)]
53pub enum Relationship {
54 Defines {
56 definer: String,
58 defined: String,
60 context: String,
62 },
63 References {
65 from: String,
67 to: String,
69 },
70 Modifies {
72 modifier: String,
74 modified: String,
76 change_type: String,
78 },
79 DependsOn {
81 dependent: String,
83 dependency: String,
85 },
86 Contains {
88 container: String,
90 contained: String,
92 },
93 CoOccurs {
95 entity_a: String,
97 entity_b: String,
99 message_id: String,
101 },
102}
103
104#[derive(Debug, Clone)]
106pub struct ExtractionResult {
107 pub entities: Vec<(String, EntityType)>,
109 pub relationships: Vec<Relationship>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum ContradictionKind {
118 ConflictingDefinition,
120 ConflictingModification,
122}
123
124#[derive(Debug, Clone)]
130pub struct ContradictionEvent {
131 pub kind: ContradictionKind,
133 pub subject: String,
135 pub existing_context: String,
137 pub new_context: String,
139}
140
141#[derive(Debug, Default)]
145pub struct EntityStore {
146 entities: HashMap<String, Entity>,
147 relationships: Vec<Relationship>,
148 contradictions: Vec<ContradictionEvent>,
150}
151
152impl EntityStore {
153 pub fn new() -> Self {
155 Self::default()
156 }
157
158 pub fn add_extraction(&mut self, result: ExtractionResult, message_id: &str, timestamp: i64) {
160 for (name, entity_type) in result.entities {
161 let key = format!("{}:{}", entity_type.as_str(), name);
162 if let Some(entity) = self.entities.get_mut(&key) {
163 entity.add_mention(message_id.to_string(), timestamp);
164 } else {
165 self.entities.insert(
166 key,
167 Entity::new(name, entity_type, message_id.to_string(), timestamp),
168 );
169 }
170 }
171
172 for new_rel in result.relationships {
174 self.check_and_record_contradiction(&new_rel);
175 self.relationships.push(new_rel);
176 }
177 }
178
179 fn check_and_record_contradiction(&mut self, new_rel: &Relationship) {
183 match new_rel {
184 Relationship::Defines {
185 definer,
186 defined,
187 context: new_ctx,
188 } => {
189 for existing in &self.relationships {
190 if let Relationship::Defines {
191 definer: ex_definer,
192 defined: ex_defined,
193 context: ex_ctx,
194 } = existing
195 && ex_definer == definer
196 && ex_defined == defined
197 && ex_ctx != new_ctx
198 {
199 self.contradictions.push(ContradictionEvent {
200 kind: ContradictionKind::ConflictingDefinition,
201 subject: format!("{}::{}", definer, defined),
202 existing_context: ex_ctx.clone(),
203 new_context: new_ctx.clone(),
204 });
205 break;
206 }
207 }
208 }
209 Relationship::Modifies {
210 modifier,
211 modified,
212 change_type: new_change,
213 } => {
214 for existing in &self.relationships {
215 if let Relationship::Modifies {
216 modifier: ex_modifier,
217 modified: ex_modified,
218 change_type: ex_change,
219 } = existing
220 && ex_modifier == modifier
221 && ex_modified == modified
222 && ex_change != new_change
223 {
224 self.contradictions.push(ContradictionEvent {
225 kind: ContradictionKind::ConflictingModification,
226 subject: format!("{}::{}", modifier, modified),
227 existing_context: ex_change.clone(),
228 new_context: new_change.clone(),
229 });
230 break;
231 }
232 }
233 }
234 _ => {}
235 }
236 }
237
238 pub fn pending_contradictions(&self) -> &[ContradictionEvent] {
241 &self.contradictions
242 }
243
244 pub fn drain_contradictions(&mut self) -> Vec<ContradictionEvent> {
247 std::mem::take(&mut self.contradictions)
248 }
249
250 pub fn get(&self, name: &str, entity_type: &EntityType) -> Option<&Entity> {
252 let key = format!("{}:{}", entity_type.as_str(), name);
253 self.entities.get(&key)
254 }
255
256 pub fn get_by_type(&self, entity_type: &EntityType) -> Vec<&Entity> {
258 self.entities
259 .values()
260 .filter(|e| &e.entity_type == entity_type)
261 .collect()
262 }
263
264 pub fn get_top_entities(&self, limit: usize) -> Vec<&Entity> {
266 let mut entities: Vec<_> = self.entities.values().collect();
267 entities.sort_by(|a, b| b.mention_count.cmp(&a.mention_count));
268 entities.into_iter().take(limit).collect()
269 }
270
271 pub fn get_related(&self, entity_name: &str) -> Vec<String> {
273 let mut related = HashSet::new();
274 for rel in &self.relationships {
275 match rel {
276 Relationship::CoOccurs {
277 entity_a, entity_b, ..
278 } => {
279 if entity_a == entity_name {
280 related.insert(entity_b.clone());
281 } else if entity_b == entity_name {
282 related.insert(entity_a.clone());
283 }
284 }
285 Relationship::Contains {
286 container,
287 contained,
288 } => {
289 if container == entity_name {
290 related.insert(contained.clone());
291 } else if contained == entity_name {
292 related.insert(container.clone());
293 }
294 }
295 Relationship::References { from, to } => {
296 if from == entity_name {
297 related.insert(to.clone());
298 } else if to == entity_name {
299 related.insert(from.clone());
300 }
301 }
302 Relationship::DependsOn {
303 dependent,
304 dependency,
305 } => {
306 if dependent == entity_name {
307 related.insert(dependency.clone());
308 } else if dependency == entity_name {
309 related.insert(dependent.clone());
310 }
311 }
312 Relationship::Modifies {
313 modifier, modified, ..
314 } => {
315 if modifier == entity_name {
316 related.insert(modified.clone());
317 } else if modified == entity_name {
318 related.insert(modifier.clone());
319 }
320 }
321 Relationship::Defines {
322 definer, defined, ..
323 } => {
324 if definer == entity_name {
325 related.insert(defined.clone());
326 } else if defined == entity_name {
327 related.insert(definer.clone());
328 }
329 }
330 }
331 }
332 related.into_iter().collect()
333 }
334
335 pub fn get_message_ids(&self, entity_name: &str) -> Vec<String> {
337 self.entities
338 .values()
339 .filter(|e| e.name == entity_name)
340 .flat_map(|e| e.message_ids.clone())
341 .collect()
342 }
343
344 pub fn all_entities(&self) -> impl Iterator<Item = &Entity> {
346 self.entities.values()
347 }
348
349 pub fn all_relationships(&self) -> &[Relationship] {
351 &self.relationships
352 }
353
354 pub fn stats(&self) -> EntityStoreStats {
356 let mut by_type = HashMap::new();
357 for entity in self.entities.values() {
358 *by_type.entry(entity.entity_type.as_str()).or_insert(0) += 1;
359 }
360 EntityStoreStats {
361 total_entities: self.entities.len(),
362 total_relationships: self.relationships.len(),
363 entities_by_type: by_type,
364 }
365 }
366}
367
368impl brainwires_core::graph::EntityStoreT for EntityStore {
369 fn entity_names_by_type(&self, entity_type: &EntityType) -> Vec<String> {
370 self.get_by_type(entity_type)
371 .iter()
372 .map(|e| e.name.clone())
373 .collect()
374 }
375
376 fn top_entity_info(&self, limit: usize) -> Vec<(String, EntityType)> {
377 self.get_top_entities(limit)
378 .iter()
379 .map(|e| (e.name.clone(), e.entity_type.clone()))
380 .collect()
381 }
382}
383
384#[derive(Debug)]
386pub struct EntityStoreStats {
387 pub total_entities: usize,
389 pub total_relationships: usize,
391 pub entities_by_type: HashMap<&'static str, usize>,
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_entity_type_as_str() {
401 assert_eq!(EntityType::File.as_str(), "file");
402 assert_eq!(EntityType::Function.as_str(), "function");
403 }
404
405 #[test]
406 fn test_entity_lifecycle() {
407 let mut entity = Entity::new("main.rs".into(), EntityType::File, "msg-1".into(), 100);
408 assert_eq!(entity.mention_count, 1);
409 entity.add_mention("msg-2".into(), 200);
410 assert_eq!(entity.mention_count, 2);
411 assert_eq!(entity.last_seen, 200);
412 }
413
414 #[test]
415 fn test_entity_store() {
416 let mut store = EntityStore::new();
417 let result = ExtractionResult {
418 entities: vec![
419 ("main.rs".into(), EntityType::File),
420 ("process".into(), EntityType::Function),
421 ],
422 relationships: vec![],
423 };
424 store.add_extraction(result, "msg-1", 100);
425 assert_eq!(store.stats().total_entities, 2);
426 }
427
428 #[test]
431 fn test_no_contradiction_on_fresh_store() {
432 let mut store = EntityStore::new();
433 let result = ExtractionResult {
434 entities: vec![],
435 relationships: vec![Relationship::Defines {
436 definer: "main".into(),
437 defined: "return_type".into(),
438 context: "returns i32".into(),
439 }],
440 };
441 store.add_extraction(result, "msg-1", 100);
442 assert!(store.pending_contradictions().is_empty());
443 }
444
445 #[test]
446 fn test_contradicting_definitions_flagged() {
447 let mut store = EntityStore::new();
448
449 store.add_extraction(
450 ExtractionResult {
451 entities: vec![],
452 relationships: vec![Relationship::Defines {
453 definer: "main".into(),
454 defined: "return_type".into(),
455 context: "returns i32".into(),
456 }],
457 },
458 "msg-1",
459 100,
460 );
461
462 store.add_extraction(
464 ExtractionResult {
465 entities: vec![],
466 relationships: vec![Relationship::Defines {
467 definer: "main".into(),
468 defined: "return_type".into(),
469 context: "returns String".into(),
470 }],
471 },
472 "msg-2",
473 200,
474 );
475
476 let contradictions = store.pending_contradictions();
477 assert_eq!(contradictions.len(), 1);
478 assert_eq!(
479 contradictions[0].kind,
480 ContradictionKind::ConflictingDefinition
481 );
482 assert_eq!(contradictions[0].subject, "main::return_type");
483 assert_eq!(contradictions[0].existing_context, "returns i32");
484 assert_eq!(contradictions[0].new_context, "returns String");
485 }
486
487 #[test]
488 fn test_identical_definitions_not_flagged() {
489 let mut store = EntityStore::new();
490
491 for msg_id in ["msg-1", "msg-2"] {
492 store.add_extraction(
493 ExtractionResult {
494 entities: vec![],
495 relationships: vec![Relationship::Defines {
496 definer: "Config".into(),
497 defined: "timeout".into(),
498 context: "30 seconds".into(),
499 }],
500 },
501 msg_id,
502 100,
503 );
504 }
505
506 assert!(store.pending_contradictions().is_empty());
507 }
508
509 #[test]
510 fn test_contradicting_modifications_flagged() {
511 let mut store = EntityStore::new();
512
513 store.add_extraction(
514 ExtractionResult {
515 entities: vec![],
516 relationships: vec![Relationship::Modifies {
517 modifier: "patch_v2".into(),
518 modified: "timeout".into(),
519 change_type: "increase".into(),
520 }],
521 },
522 "msg-1",
523 100,
524 );
525
526 store.add_extraction(
527 ExtractionResult {
528 entities: vec![],
529 relationships: vec![Relationship::Modifies {
530 modifier: "patch_v2".into(),
531 modified: "timeout".into(),
532 change_type: "decrease".into(),
533 }],
534 },
535 "msg-2",
536 200,
537 );
538
539 let contradictions = store.pending_contradictions();
540 assert_eq!(contradictions.len(), 1);
541 assert_eq!(
542 contradictions[0].kind,
543 ContradictionKind::ConflictingModification
544 );
545 }
546
547 #[test]
548 fn test_drain_contradictions_clears_buffer() {
549 let mut store = EntityStore::new();
550
551 for ctx in ["returns i32", "returns String"] {
552 store.add_extraction(
553 ExtractionResult {
554 entities: vec![],
555 relationships: vec![Relationship::Defines {
556 definer: "main".into(),
557 defined: "return_type".into(),
558 context: ctx.into(),
559 }],
560 },
561 "msg-1",
562 100,
563 );
564 }
565
566 assert!(!store.pending_contradictions().is_empty());
567 let drained = store.drain_contradictions();
568 assert_eq!(drained.len(), 1);
569 assert!(store.pending_contradictions().is_empty());
570 }
571
572 #[test]
573 fn test_both_relationships_retained_after_contradiction() {
574 let mut store = EntityStore::new();
575
576 store.add_extraction(
577 ExtractionResult {
578 entities: vec![],
579 relationships: vec![Relationship::Defines {
580 definer: "fn".into(),
581 defined: "x".into(),
582 context: "old".into(),
583 }],
584 },
585 "msg-1",
586 100,
587 );
588
589 store.add_extraction(
590 ExtractionResult {
591 entities: vec![],
592 relationships: vec![Relationship::Defines {
593 definer: "fn".into(),
594 defined: "x".into(),
595 context: "new".into(),
596 }],
597 },
598 "msg-2",
599 200,
600 );
601
602 assert_eq!(store.all_relationships().len(), 2);
604 let event = &store.pending_contradictions()[0];
605 assert_eq!(event.existing_context, "old");
606 assert_eq!(event.new_context, "new");
607 }
608}