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:
2026-09-05 00:31:28 -07:00
parent b07b6fc046
commit 41c203ffed
110 changed files with 22681 additions and 11914 deletions
+1
View File
@@ -23,3 +23,4 @@ once_cell = "1.19"
indexmap = "2.0"
lazy_static = "1.4"
async-trait = "0.1.92"
uuid = { workspace = true }
+188
View File
@@ -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);
}
}
+254
View File
@@ -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);
}
}
+198
View File
@@ -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);
}
}
+6
View File
@@ -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;