use serde::{Deserialize, Serialize};
use super::metadata::GraphMetadata;
use super::nodes::{Edge, Node, NodeId, NodeType};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct IntentGraph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub metadata: GraphMetadata,
}
impl IntentGraph {
pub fn new(metadata: GraphMetadata) -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
metadata,
}
}
pub fn add_node(&mut self, node: Node) {
self.nodes.push(node);
}
pub fn add_edge(&mut self, edge: Edge) {
self.edges.push(edge);
}
pub fn node_by_id(&self, id: &NodeId) -> Option<&Node> {
self.nodes.iter().find(|n| n.id == *id)
}
pub fn edges_from(&self, id: &NodeId) -> Vec<&Edge> {
self.edges.iter().filter(|e| e.source == *id).collect()
}
pub fn edges_to(&self, id: &NodeId) -> Vec<&Edge> {
self.edges.iter().filter(|e| e.target == *id).collect()
}
pub fn update_counts(&mut self) {
self.metadata.node_count = Some(self.nodes.len());
self.metadata.edge_count = Some(self.edges.len());
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DagFlowStage {
pub stage: String,
pub node_types: Vec<NodeType>,
pub produces: Vec<NodeType>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DagFlow {
pub stages: Vec<DagFlowStage>,
pub validation_rules: DagValidationRules,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DagValidationRules {
pub no_cycles: bool,
pub root_nodes_must_be: Vec<NodeType>,
pub leaf_nodes_prefer: Vec<NodeType>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct EdgeConstraints {
pub implements: EdgeConstraint,
pub tests: EdgeConstraint,
pub covers: EdgeConstraint,
#[serde(rename = "traces-to")]
pub traces_to: EdgeConstraint,
#[serde(rename = "derives-from")]
pub derives_from: EdgeConstraint,
pub resolves: EdgeConstraint,
pub blocks: EdgeConstraint,
#[serde(rename = "depends-on")]
pub depends_on: EdgeConstraint,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EdgeConstraint {
pub description: String,
pub allowed: Vec<EdgeConstraintEntry>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EdgeConstraintEntry {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Ontology {
pub version: String,
pub schema_uri: String,
pub dag_flow: DagFlow,
pub edge_constraints: EdgeConstraints,
}
impl Default for Ontology {
fn default() -> Self {
Self {
version: "1.0.0".to_string(),
schema_uri: "https://phenotype.dev/schemas/agileplus-intent-ontology/v1.json".to_string(),
dag_flow: DagFlow {
stages: vec![
DagFlowStage { stage: "intake".to_string(), node_types: vec![NodeType::Intent], produces: vec![NodeType::Plan] },
DagFlowStage { stage: "planning".to_string(), node_types: vec![NodeType::Plan], produces: vec![NodeType::Feature, NodeType::Story] },
DagFlowStage { stage: "breakdown".to_string(), node_types: vec![NodeType::Feature, NodeType::Story], produces: vec![NodeType::Task] },
DagFlowStage { stage: "specification".to_string(), node_types: vec![NodeType::Task], produces: vec![NodeType::Spec] },
DagFlowStage { stage: "implementation".to_string(), node_types: vec![NodeType::Task], produces: vec![NodeType::Commit] },
DagFlowStage { stage: "verification".to_string(), node_types: vec![NodeType::Commit, NodeType::PR], produces: vec![NodeType::Test, NodeType::Artifact] },
DagFlowStage { stage: "integration".to_string(), node_types: vec![NodeType::PR], produces: vec![NodeType::Artifact] },
],
validation_rules: DagValidationRules {
no_cycles: true,
root_nodes_must_be: vec![NodeType::Intent],
leaf_nodes_prefer: vec![NodeType::Test, NodeType::Artifact],
},
},
edge_constraints: EdgeConstraints {
implements: EdgeConstraint {
description: "Target node implements the source node's requirement.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "Intent".to_string(), to: "Feature".to_string() },
EdgeConstraintEntry { from: "Intent".to_string(), to: "Story".to_string() },
EdgeConstraintEntry { from: "Feature".to_string(), to: "Task".to_string() },
EdgeConstraintEntry { from: "Story".to_string(), to: "Task".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Commit".to_string() },
EdgeConstraintEntry { from: "Spec".to_string(), to: "Feature".to_string() },
EdgeConstraintEntry { from: "Spec".to_string(), to: "Task".to_string() },
],
},
tests: EdgeConstraint {
description: "Target node tests the source node's behavior.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "Feature".to_string(), to: "Test".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Test".to_string() },
EdgeConstraintEntry { from: "Commit".to_string(), to: "Test".to_string() },
EdgeConstraintEntry { from: "PR".to_string(), to: "Test".to_string() },
EdgeConstraintEntry { from: "Bug".to_string(), to: "Test".to_string() },
],
},
covers: EdgeConstraint {
description: "Target node provides coverage or evidence for the source node.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "Feature".to_string(), to: "Test".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Test".to_string() },
EdgeConstraintEntry { from: "Feature".to_string(), to: "Artifact".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Artifact".to_string() },
EdgeConstraintEntry { from: "Spec".to_string(), to: "Feature".to_string() },
],
},
traces_to: EdgeConstraint {
description: "Generic traceability link for cross-cutting concerns.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "*".to_string(), to: "*".to_string() },
],
},
derives_from: EdgeConstraint {
description: "Target is the parent or origin of the source.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "Feature".to_string(), to: "Intent".to_string() },
EdgeConstraintEntry { from: "Story".to_string(), to: "Intent".to_string() },
EdgeConstraintEntry { from: "Story".to_string(), to: "Feature".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Feature".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Story".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Bug".to_string() },
],
},
resolves: EdgeConstraint {
description: "Target resolves or fixes the source issue.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "Bug".to_string(), to: "Commit".to_string() },
EdgeConstraintEntry { from: "Bug".to_string(), to: "PR".to_string() },
EdgeConstraintEntry { from: "Bug".to_string(), to: "Task".to_string() },
],
},
blocks: EdgeConstraint {
description: "Target is blocked by the source.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "Task".to_string(), to: "Task".to_string() },
EdgeConstraintEntry { from: "Bug".to_string(), to: "Task".to_string() },
EdgeConstraintEntry { from: "Bug".to_string(), to: "PR".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "PR".to_string() },
EdgeConstraintEntry { from: "PR".to_string(), to: "Feature".to_string() },
],
},
depends_on: EdgeConstraint {
description: "Source depends on target for completion.".to_string(),
allowed: vec![
EdgeConstraintEntry { from: "Task".to_string(), to: "Task".to_string() },
EdgeConstraintEntry { from: "Feature".to_string(), to: "Feature".to_string() },
EdgeConstraintEntry { from: "Story".to_string(), to: "Story".to_string() },
EdgeConstraintEntry { from: "PR".to_string(), to: "PR".to_string() },
EdgeConstraintEntry { from: "Task".to_string(), to: "Artifact".to_string() },
],
},
},
}
}
}
impl IntentGraph {
pub fn validate_id_format(&self) -> Vec<String> {
let mut errors = Vec::new();
let re = regex::Regex::new(r"^[A-Z][a-z]+#[a-z0-9\-]+$").unwrap();
for node in &self.nodes {
if !re.is_match(&node.id.0) {
errors.push(format!("Invalid node ID format: {}", node.id));
}
}
errors
}
}