use serde::Serialize;
use sqlx::PgPool;
use uuid::Uuid;
use crate::db::repositories::{
actions as actions_repo, assumptions as assumptions_repo, decisions as decisions_repo,
drift as drift_repo, evidence as evidence_repo, memos as memos_repo,
relations as relations_repo,
};
use crate::domain::actions::Action;
use crate::domain::assumptions::Assumption;
use crate::domain::decisions::Decision;
use crate::domain::drift::DriftSignal;
use crate::domain::evidence::Evidence;
use crate::domain::relations::EntityRelation;
use crate::domain::EntityType;
use crate::error::{AppError, AppResult};
use super::documents::normalize_limit;
const NODE_LABEL_MAX: usize = 80;
#[derive(Debug, Serialize)]
pub struct GraphNode {
pub id: Uuid,
#[serde(rename = "type")]
pub node_type: String,
pub label: String,
pub status: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct GraphEdge {
pub id: Uuid,
pub source: Uuid,
pub target: Uuid,
#[serde(rename = "type")]
pub edge_type: String,
}
#[derive(Debug, Serialize)]
pub struct Graph {
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
}
#[derive(Debug, Serialize)]
pub struct DecisionContext {
pub decision: Decision,
pub assumptions: Vec<Assumption>,
pub actions: Vec<Action>,
pub evidence: Vec<Evidence>,
pub relations: Vec<EntityRelation>,
pub drift_signals: Vec<DriftSignal>,
}
pub async fn list_decisions(
pool: &PgPool,
status: Option<&str>,
limit: Option<i64>,
) -> AppResult<Vec<Decision>> {
Ok(decisions_repo::list(pool, status, normalize_limit(limit)).await?)
}
pub async fn list_assumptions(pool: &PgPool, limit: Option<i64>) -> AppResult<Vec<Assumption>> {
Ok(assumptions_repo::list(pool, normalize_limit(limit)).await?)
}
pub async fn list_actions(pool: &PgPool, limit: Option<i64>) -> AppResult<Vec<Action>> {
Ok(actions_repo::list(pool, normalize_limit(limit)).await?)
}
pub async fn list_evidence(pool: &PgPool, limit: Option<i64>) -> AppResult<Vec<Evidence>> {
Ok(evidence_repo::list(pool, normalize_limit(limit)).await?)
}
pub async fn get_decision(pool: &PgPool, id: Uuid) -> AppResult<Decision> {
decisions_repo::get(pool, id)
.await?
.ok_or_else(|| AppError::NotFound(format!("decision {id} not found")))
}
pub async fn get_decision_context(pool: &PgPool, id: Uuid) -> AppResult<DecisionContext> {
let decision = get_decision(pool, id).await?;
let relations = relations_repo::list_for_entity(pool, id).await?;
let mut assumptions = Vec::new();
let mut actions = Vec::new();
let mut evidence = Vec::new();
for rel in &relations {
let (other_id, other_type) = if rel.from_entity_id == id {
(rel.to_entity_id, rel.to_entity_type.as_str())
} else {
(rel.from_entity_id, rel.from_entity_type.as_str())
};
match other_type {
"assumption" => {
if let Some(a) = assumptions_repo::get(pool, other_id).await? {
assumptions.push(a);
}
}
"action" => {
if let Some(a) = actions_repo::get(pool, other_id).await? {
actions.push(a);
}
}
"evidence" => {
if let Some(e) = evidence_repo::get(pool, other_id).await? {
evidence.push(e);
}
}
_ => {}
}
}
let drift_signals = drift_repo::list_for_entity(pool, id).await?;
Ok(DecisionContext {
decision,
assumptions,
actions,
evidence,
relations,
drift_signals,
})
}
pub async fn build_graph(pool: &PgPool) -> AppResult<Graph> {
let mut nodes: Vec<GraphNode> = Vec::new();
for d in decisions_repo::list(pool, None, 1000).await? {
nodes.push(GraphNode {
id: d.id,
node_type: EntityType::Decision.as_str().to_string(),
label: truncate(&d.title),
status: Some(d.status),
});
}
for a in assumptions_repo::list(pool, 1000).await? {
nodes.push(GraphNode {
id: a.id,
node_type: EntityType::Assumption.as_str().to_string(),
label: truncate(&a.statement),
status: Some(a.status),
});
}
for a in actions_repo::list(pool, 1000).await? {
nodes.push(GraphNode {
id: a.id,
node_type: EntityType::Action.as_str().to_string(),
label: truncate(&a.title),
status: Some(a.status),
});
}
for e in evidence_repo::list(pool, 1000).await? {
nodes.push(GraphNode {
id: e.id,
node_type: EntityType::Evidence.as_str().to_string(),
label: truncate(&e.text),
status: None,
});
}
for s in drift_repo::list(pool, None, 1000).await? {
nodes.push(GraphNode {
id: s.id,
node_type: EntityType::DriftSignal.as_str().to_string(),
label: truncate(&s.summary),
status: Some(s.status),
});
}
for m in memos_repo::list(pool, 1000).await? {
nodes.push(GraphNode {
id: m.id,
node_type: EntityType::Memo.as_str().to_string(),
label: truncate(&m.title),
status: None,
});
}
let edges = relations_repo::list_all(pool)
.await?
.into_iter()
.map(|r| GraphEdge {
id: r.id,
source: r.from_entity_id,
target: r.to_entity_id,
edge_type: r.relation_type,
})
.collect();
Ok(Graph { nodes, edges })
}
fn truncate(s: &str) -> String {
let s = s.trim();
if s.chars().count() <= NODE_LABEL_MAX {
s.to_string()
} else {
let truncated: String = s.chars().take(NODE_LABEL_MAX).collect();
format!("{truncated}…")
}
}