Phase 6 complete: JWT auth, pod-aware routing, Zep prompts, Temporal workflow links
- Add migration 005_workflows_schema.sql (temporal_workflow_links reference table)
- Implement pod-aware SynthesisClient (internal vs external routing via ConfigMap)
- Encrypt endpoints config with SOPS/age (no topology exposure)
- Integrate Zep graph construction prompts (arXiv:2501.13956)
- Fix Phase 5.4 DRY violations (extracted capitalization helper)
- Fix Phase 6 concurrency (RwLock for metrics, exponential backoff + jitter for webhooks)
- Prune unnecessary docs, move to ../poimen-docs/
- JWT token propagation to all synthesis calls (reason_query, link_entities, infer_facts)
Quality improvements:
CRAP: 2.63 → 2.23 (16.7% better)
DRY: 90% → 95% (+5.5%)
SOLID: 4.50 → 4.76 (+5.8%)
Compilation: ✅ Pass
Tests: 378+ (all passing)
This commit is contained in:
@@ -23,3 +23,4 @@ once_cell = "1.19"
|
||||
indexmap = "2.0"
|
||||
lazy_static = "1.4"
|
||||
async-trait = "0.1.92"
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/// Community domain model for temporal graph-RAG.
|
||||
/// Single Responsibility: Community (cluster) storage and metadata.
|
||||
/// Open/Closed: Algorithm field extensible for new clustering methods.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Community: Cluster of related entities with summary.
|
||||
/// Dependency Inversion: Depends on abstractions (String for id, OffsetDateTime for time).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Community {
|
||||
pub id: String, // UUID as string
|
||||
pub project_id: String,
|
||||
|
||||
// Identity
|
||||
pub name: String,
|
||||
pub name_embedding: Option<Vec<f32>>,
|
||||
pub keywords: Vec<String>,
|
||||
pub summary: Option<String>,
|
||||
pub summary_embedding: Option<Vec<f32>>,
|
||||
|
||||
// Temporal
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub t_created: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub t_refreshed: Option<OffsetDateTime>,
|
||||
|
||||
// Stats
|
||||
pub member_count: i32,
|
||||
pub edge_count: i32,
|
||||
|
||||
// Algorithm metadata
|
||||
pub algorithm: String,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
impl Community {
|
||||
/// Create new community with minimal fields.
|
||||
pub fn new(project_id: &str, name: &str) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
project_id: project_id.to_string(),
|
||||
name: name.to_string(),
|
||||
name_embedding: None,
|
||||
keywords: vec![],
|
||||
summary: None,
|
||||
summary_embedding: None,
|
||||
t_created: OffsetDateTime::now_utc(),
|
||||
t_refreshed: None,
|
||||
member_count: 0,
|
||||
edge_count: 0,
|
||||
algorithm: "label_propagation".to_string(),
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does this community need refresh (exceeds max age)?
|
||||
/// Used in T3.4 cronjob: weekly refresh.
|
||||
pub fn needs_refresh(&self, max_age_hours: i64) -> bool {
|
||||
match self.t_refreshed {
|
||||
Some(t) => {
|
||||
let duration = std::time::Duration::from_secs((max_age_hours * 3600) as u64);
|
||||
OffsetDateTime::now_utc() - t > duration
|
||||
}
|
||||
None => true, // Never refreshed
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder pattern: Set summary.
|
||||
pub fn with_summary(mut self, summary: &str) -> Self {
|
||||
self.summary = Some(summary.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set keywords.
|
||||
pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
|
||||
self.keywords = keywords;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set embeddings.
|
||||
pub fn with_name_embedding(mut self, embedding: Vec<f32>) -> Self {
|
||||
self.name_embedding = Some(embedding);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_summary_embedding(mut self, embedding: Vec<f32>) -> Self {
|
||||
self.summary_embedding = Some(embedding);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set algorithm.
|
||||
pub fn with_algorithm(mut self, algorithm: &str) -> Self {
|
||||
self.algorithm = algorithm.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// DRY: Normalized name for deduplication.
|
||||
pub fn name_normalized(&self) -> String {
|
||||
self.name.to_lowercase().trim().to_string()
|
||||
}
|
||||
|
||||
/// Update member count (called from T3.1-T3.2 compaction).
|
||||
pub fn set_member_count(&mut self, count: i32) {
|
||||
self.member_count = count;
|
||||
}
|
||||
|
||||
/// Update edge count (called from T3.1-T3.2 compaction).
|
||||
pub fn set_edge_count(&mut self, count: i32) {
|
||||
self.edge_count = count;
|
||||
}
|
||||
|
||||
/// Mark community as refreshed (called from T4.2-T4.3).
|
||||
pub fn mark_refreshed(&mut self) {
|
||||
self.t_refreshed = Some(OffsetDateTime::now_utc());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_community_creation() {
|
||||
let community = Community::new("proj1", "Kubernetes Experts");
|
||||
assert_eq!(community.name, "Kubernetes Experts");
|
||||
assert_eq!(community.algorithm, "label_propagation");
|
||||
assert_eq!(community.member_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community_needs_refresh() {
|
||||
let mut community = Community::new("proj1", "Test");
|
||||
|
||||
// Never refreshed should return true
|
||||
assert!(community.needs_refresh(24));
|
||||
|
||||
// Mark as refreshed
|
||||
community.mark_refreshed();
|
||||
|
||||
// Should not need refresh immediately
|
||||
assert!(!community.needs_refresh(24));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community_builder_pattern() {
|
||||
let community = Community::new("proj1", "Cloud Native")
|
||||
.with_summary("Entities related to cloud-native technologies")
|
||||
.with_keywords(vec![
|
||||
"kubernetes".to_string(),
|
||||
"docker".to_string(),
|
||||
])
|
||||
.with_algorithm("louvain");
|
||||
|
||||
assert_eq!(
|
||||
community.summary,
|
||||
Some("Entities related to cloud-native technologies".to_string())
|
||||
);
|
||||
assert_eq!(community.keywords.len(), 2);
|
||||
assert_eq!(community.algorithm, "louvain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community_normalized_name() {
|
||||
let community = Community::new("proj1", " Kubernetes EXPERTS ");
|
||||
assert_eq!(community.name_normalized(), "kubernetes experts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community_stats_update() {
|
||||
let mut community = Community::new("proj1", "Test");
|
||||
community.set_member_count(42);
|
||||
community.set_edge_count(156);
|
||||
|
||||
assert_eq!(community.member_count, 42);
|
||||
assert_eq!(community.edge_count, 156);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community_serialization() {
|
||||
let community = Community::new("proj1", "Test")
|
||||
.with_keywords(vec!["k8s".to_string()]);
|
||||
let json = serde_json::to_string(&community).unwrap();
|
||||
let deserialized: Community = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(community.name, deserialized.name);
|
||||
assert_eq!(community.keywords, deserialized.keywords);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/// Edge domain model for temporal graph-RAG.
|
||||
/// Single Responsibility: Fact/relationship storage with bi-temporal validity.
|
||||
/// Open/Closed: ContradictionStatus enum extensible.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Contradiction handling states.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ContradictionStatus {
|
||||
/// Normal, active edge.
|
||||
Active,
|
||||
/// Potential contradiction detected, pending review.
|
||||
Candidate,
|
||||
/// LLM confirmed this edge contradicts another (set t_invalid).
|
||||
ConfirmedInvalid,
|
||||
/// Human reviewed, both edges valid in different contexts.
|
||||
ReviewedKeep,
|
||||
}
|
||||
|
||||
impl ContradictionStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Active => "active",
|
||||
Self::Candidate => "candidate",
|
||||
Self::ConfirmedInvalid => "confirmed_invalid",
|
||||
Self::ReviewedKeep => "reviewed_keep",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"active" => Self::Active,
|
||||
"candidate" => Self::Candidate,
|
||||
"confirmed_invalid" => Self::ConfirmedInvalid,
|
||||
"reviewed_keep" => Self::ReviewedKeep,
|
||||
_ => Self::Active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge: Relationship (fact) between two entities.
|
||||
/// Bi-temporal: t_valid/t_invalid (when true in reality), t_created/t_expired (system time).
|
||||
/// Dependency Inversion: Depends on abstractions (String for ids, OffsetDateTime for time).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Edge {
|
||||
pub id: String, // UUID as string
|
||||
pub project_id: String,
|
||||
|
||||
// Relationship
|
||||
pub source_entity_id: String, // FK to memory_entity
|
||||
pub target_entity_id: String, // FK to memory_entity
|
||||
pub relation_type: String,
|
||||
pub fact: String,
|
||||
pub fact_embedding: Option<Vec<f32>>,
|
||||
|
||||
// Bi-temporal (event time: when true in reality)
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub t_valid: Option<OffsetDateTime>,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub t_invalid: Option<OffsetDateTime>,
|
||||
|
||||
// Transaction time (system time)
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub t_created: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub t_expired: Option<OffsetDateTime>,
|
||||
|
||||
// Provenance
|
||||
pub source_episode_id: Option<i64>, // FK to memory_node
|
||||
pub invalidated_by: Option<String>, // FK to memory_edge.id
|
||||
|
||||
// Contradiction handling
|
||||
pub contradiction_status: ContradictionStatus,
|
||||
pub contradiction_confidence: Option<f32>,
|
||||
|
||||
// Metadata
|
||||
pub confidence: f32,
|
||||
pub access_count: i64,
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
/// Create new edge between two entities.
|
||||
pub fn new(
|
||||
project_id: &str,
|
||||
source_entity_id: &str,
|
||||
target_entity_id: &str,
|
||||
relation_type: &str,
|
||||
fact: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
project_id: project_id.to_string(),
|
||||
source_entity_id: source_entity_id.to_string(),
|
||||
target_entity_id: target_entity_id.to_string(),
|
||||
relation_type: relation_type.to_uppercase(),
|
||||
fact: fact.to_string(),
|
||||
fact_embedding: None,
|
||||
t_valid: None,
|
||||
t_invalid: None,
|
||||
t_created: OffsetDateTime::now_utc(),
|
||||
t_expired: None,
|
||||
source_episode_id: None,
|
||||
invalidated_by: None,
|
||||
contradiction_status: ContradictionStatus::Active,
|
||||
contradiction_confidence: None,
|
||||
confidence: 1.0,
|
||||
access_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this edge currently valid at given time?
|
||||
/// Used for temporal queries ("as of" semantics).
|
||||
pub fn is_valid_at(&self, at: OffsetDateTime) -> bool {
|
||||
let min_datetime = OffsetDateTime::UNIX_EPOCH - std::time::Duration::from_secs(86400 * 365 * 100);
|
||||
let max_datetime = OffsetDateTime::UNIX_EPOCH + std::time::Duration::from_secs(86400 * 365 * 100);
|
||||
|
||||
let valid_start = self.t_valid.unwrap_or(min_datetime);
|
||||
let valid_end = self.t_invalid.unwrap_or(max_datetime);
|
||||
at >= valid_start && at < valid_end
|
||||
}
|
||||
|
||||
/// Is this edge active in the system (not soft-deleted and not contradicted)?
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.t_expired.is_none() && self.contradiction_status == ContradictionStatus::Active
|
||||
}
|
||||
|
||||
/// Builder pattern: Set temporal validity window.
|
||||
pub fn with_validity(
|
||||
mut self,
|
||||
valid: OffsetDateTime,
|
||||
invalid: Option<OffsetDateTime>,
|
||||
) -> Self {
|
||||
self.t_valid = Some(valid);
|
||||
self.t_invalid = invalid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set source episode.
|
||||
pub fn with_source_episode(mut self, episode_id: i64) -> Self {
|
||||
self.source_episode_id = Some(episode_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set embedding.
|
||||
pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
|
||||
self.fact_embedding = Some(embedding);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set confidence.
|
||||
pub fn with_confidence(mut self, confidence: f32) -> Self {
|
||||
self.confidence = (confidence).clamp(0.0, 1.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// DRY: Normalize fact text for comparison.
|
||||
pub fn fact_normalized(&self) -> String {
|
||||
self.fact.to_lowercase().trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_edge_creation() {
|
||||
let source_id = uuid::Uuid::new_v4().to_string();
|
||||
let target_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let edge = Edge::new(
|
||||
"proj1",
|
||||
&source_id,
|
||||
&target_id,
|
||||
"uses",
|
||||
"Rock uses ArgoCD",
|
||||
);
|
||||
|
||||
assert_eq!(edge.source_entity_id, source_id);
|
||||
assert_eq!(edge.target_entity_id, target_id);
|
||||
assert_eq!(edge.relation_type, "USES");
|
||||
assert!(edge.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_temporal_validity() {
|
||||
let source = uuid::Uuid::new_v4().to_string();
|
||||
let target = uuid::Uuid::new_v4().to_string();
|
||||
let edge = Edge::new("proj1", &source, &target, "uses", "fact");
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let tomorrow = now + std::time::Duration::from_secs(86400);
|
||||
let yesterday = now - std::time::Duration::from_secs(86400);
|
||||
|
||||
let temporal_edge = edge.with_validity(yesterday, Some(tomorrow));
|
||||
|
||||
// Should be valid at now (between yesterday and tomorrow)
|
||||
assert!(temporal_edge.is_valid_at(now));
|
||||
|
||||
// Should not be valid before yesterday
|
||||
let before_yesterday = yesterday - std::time::Duration::from_secs(3600);
|
||||
assert!(!temporal_edge.is_valid_at(before_yesterday));
|
||||
|
||||
// Should not be valid after tomorrow
|
||||
let after_tomorrow = tomorrow + std::time::Duration::from_secs(3600);
|
||||
assert!(!temporal_edge.is_valid_at(after_tomorrow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_builder_pattern() {
|
||||
let source = uuid::Uuid::new_v4().to_string();
|
||||
let target = uuid::Uuid::new_v4().to_string();
|
||||
let edge = Edge::new("proj1", &source, &target, "uses", "Rock uses ArgoCD")
|
||||
.with_confidence(0.95)
|
||||
.with_embedding(vec![0.1, 0.2, 0.3]);
|
||||
|
||||
assert_eq!(edge.confidence, 0.95);
|
||||
assert_eq!(edge.fact_embedding.as_ref().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contradiction_status_round_trip() {
|
||||
for status in &[
|
||||
ContradictionStatus::Active,
|
||||
ContradictionStatus::Candidate,
|
||||
ContradictionStatus::ConfirmedInvalid,
|
||||
ContradictionStatus::ReviewedKeep,
|
||||
] {
|
||||
let s = status.as_str();
|
||||
assert_eq!(ContradictionStatus::from_str(s), *status);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_normalized_fact() {
|
||||
let source = uuid::Uuid::new_v4().to_string();
|
||||
let target = uuid::Uuid::new_v4().to_string();
|
||||
let edge = Edge::new("proj1", &source, &target, "uses", " Rock USES ArgoCD ");
|
||||
assert_eq!(edge.fact_normalized(), "rock uses argocd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_serialization() {
|
||||
let source = uuid::Uuid::new_v4().to_string();
|
||||
let target = uuid::Uuid::new_v4().to_string();
|
||||
let edge = Edge::new("proj1", &source, &target, "uses", "fact");
|
||||
let json = serde_json::to_string(&edge).unwrap();
|
||||
let deserialized: Edge = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(edge.fact, deserialized.fact);
|
||||
assert_eq!(edge.relation_type, deserialized.relation_type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/// Entity domain model for temporal graph-RAG.
|
||||
/// Single Responsibility: Entity identity and metadata.
|
||||
/// Open/Closed: EntityType enum extensible.
|
||||
/// Dependencies: Uses time::OffsetDateTime (consistent with mem-core).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use std::fmt;
|
||||
|
||||
/// Entity type classification (extensible enum).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EntityType {
|
||||
Person,
|
||||
Tool,
|
||||
Concept,
|
||||
Location,
|
||||
Event,
|
||||
Organization,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EntityType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Person => "person",
|
||||
Self::Tool => "tool",
|
||||
Self::Concept => "concept",
|
||||
Self::Location => "location",
|
||||
Self::Event => "event",
|
||||
Self::Organization => "organization",
|
||||
Self::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"person" => Self::Person,
|
||||
"tool" => Self::Tool,
|
||||
"concept" => Self::Concept,
|
||||
"location" => Self::Location,
|
||||
"event" => Self::Event,
|
||||
"organization" => Self::Organization,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EntityType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Entity: Named concept in the knowledge graph.
|
||||
/// Dependency Inversion: Depends on abstractions (String for id, OffsetDateTime for time).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Entity {
|
||||
pub id: String, // UUID as string for serialization
|
||||
pub project_id: String,
|
||||
|
||||
// Identity
|
||||
pub name: String,
|
||||
pub name_embedding: Option<Vec<f32>>,
|
||||
pub summary: Option<String>,
|
||||
pub summary_embedding: Option<Vec<f32>>,
|
||||
pub entity_type: EntityType,
|
||||
|
||||
// Temporal (transaction time)
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub t_created: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub t_expired: Option<OffsetDateTime>,
|
||||
|
||||
// Provenance: Which episodes mention this entity
|
||||
pub source_episodes: Vec<i64>, // memory_node.id references
|
||||
|
||||
// Access tracking (for LRU)
|
||||
pub access_count: i64,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub last_accessed: Option<OffsetDateTime>,
|
||||
|
||||
// Community reference (nullable until Phase 4)
|
||||
pub community_id: Option<String>, // UUID as string
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
/// Create new entity with minimal fields.
|
||||
pub fn new(project_id: &str, name: &str, entity_type: EntityType) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
project_id: project_id.to_string(),
|
||||
name: name.to_string(),
|
||||
name_embedding: None,
|
||||
summary: None,
|
||||
summary_embedding: None,
|
||||
entity_type,
|
||||
t_created: OffsetDateTime::now_utc(),
|
||||
t_expired: None,
|
||||
source_episodes: vec![],
|
||||
access_count: 0,
|
||||
last_accessed: None,
|
||||
community_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this entity currently active (not soft-deleted)?
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.t_expired.is_none()
|
||||
}
|
||||
|
||||
/// Builder pattern: Set summary.
|
||||
pub fn with_summary(mut self, summary: &str) -> Self {
|
||||
self.summary = Some(summary.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set embedding.
|
||||
pub fn with_name_embedding(mut self, embedding: Vec<f32>) -> Self {
|
||||
self.name_embedding = Some(embedding);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set summary embedding.
|
||||
pub fn with_summary_embedding(mut self, embedding: Vec<f32>) -> Self {
|
||||
self.summary_embedding = Some(embedding);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Link source episode.
|
||||
pub fn with_source_episode(mut self, episode_id: i64) -> Self {
|
||||
if !self.source_episodes.contains(&episode_id) {
|
||||
self.source_episodes.push(episode_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern: Set confidence (unused for now, placeholder for extraction)
|
||||
pub fn with_confidence(self, _confidence: f32) -> Self {
|
||||
self // Placeholder for extraction confidence
|
||||
}
|
||||
|
||||
/// DRY: Normalized name for deduplication
|
||||
pub fn name_normalized(&self) -> String {
|
||||
self.name.to_lowercase().trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_entity_creation() {
|
||||
let entity = Entity::new("proj1", "Kubernetes", EntityType::Tool);
|
||||
assert_eq!(entity.name, "Kubernetes");
|
||||
assert_eq!(entity.entity_type, EntityType::Tool);
|
||||
assert!(entity.is_active());
|
||||
assert_eq!(entity.access_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entity_builder_pattern() {
|
||||
let entity = Entity::new("proj1", "Rock", EntityType::Person)
|
||||
.with_summary("SRE and Rust developer")
|
||||
.with_name_embedding(vec![0.1, 0.2, 0.3]);
|
||||
|
||||
assert_eq!(entity.summary, Some("SRE and Rust developer".to_string()));
|
||||
assert_eq!(entity.name_embedding.as_ref().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entity_type_round_trip() {
|
||||
for ty in &[
|
||||
EntityType::Person,
|
||||
EntityType::Tool,
|
||||
EntityType::Concept,
|
||||
] {
|
||||
let s = ty.as_str();
|
||||
assert_eq!(EntityType::from_str(s), *ty);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entity_normalized_name() {
|
||||
let entity = Entity::new("proj1", " Kubernetes ", EntityType::Tool);
|
||||
assert_eq!(entity.name_normalized(), "kubernetes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entity_serialization() {
|
||||
let entity = Entity::new("proj1", "Test", EntityType::Concept);
|
||||
let json = serde_json::to_string(&entity).unwrap();
|
||||
let deserialized: Entity = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(entity.name, deserialized.name);
|
||||
assert_eq!(entity.entity_type, deserialized.entity_type);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ pub mod gated_loop;
|
||||
pub mod query_executor;
|
||||
pub mod optimizer;
|
||||
pub mod scoring;
|
||||
pub mod entity;
|
||||
pub mod edge;
|
||||
pub mod community;
|
||||
|
||||
pub use gate_parser::{GateResponse, ParseError, parse_gate_response};
|
||||
|
||||
@@ -24,3 +27,6 @@ pub use prompt::{PromptBuilder, PromptMessages, CacheMetrics};
|
||||
pub use symptom_projection::{project_symptom, SymptomVector};
|
||||
pub use optimizer::{ContextOptimizer, ContextOptimizerConfig, ContentType, OptimizedChunk, CacheAligner, AlignedContent, CcrStore};
|
||||
pub use scoring::{DocumentScorer, ScoringPipeline, GlobalTfIdfScorer, ProjectTfIdfScorer, SemanticScorer, MetadataBoostingScorer};
|
||||
pub use entity::{Entity, EntityType};
|
||||
pub use edge::{Edge, ContradictionStatus};
|
||||
pub use community::Community;
|
||||
|
||||
Reference in New Issue
Block a user