use std::collections::{BTreeMap, BTreeSet};
use anyhow::{Result, anyhow, bail};
use chrono::{DateTime, Utc};
use super::event::{Event, NodeStatus, PlanStatus, TriageDecision};
use super::ids::{IdeaId, NodeId, PlanId};
#[derive(Debug, Clone)]
pub struct Idea {
pub id: IdeaId,
pub source: String,
pub text: String,
pub refs: Vec<String>,
pub item_kind: super::event::ItemKind,
pub submitted_at: DateTime<Utc>,
pub triage: Option<(TriageDecision, Option<String>, DateTime<Utc>)>,
}
#[derive(Debug, Clone)]
pub struct Plan {
pub id: PlanId,
pub idea_id: IdeaId,
pub summary: String,
pub planner: String,
pub created_at: DateTime<Utc>,
pub status: PlanStatus,
pub nodes: BTreeMap<NodeId, PlanNode>,
pub edges: BTreeSet<(NodeId, NodeId)>,
}
#[derive(Debug, Clone)]
pub struct PlanNode {
pub id: NodeId,
pub kind: String,
pub params: serde_json::Map<String, serde_json::Value>,
pub targets: Vec<String>,
pub prompt_excerpt: Option<String>,
pub created_at: DateTime<Utc>,
pub status: NodeStatus,
pub last_status_change: DateTime<Utc>,
}
#[derive(Debug, Default, Clone)]
pub struct Funnel {
pub ideas: BTreeMap<IdeaId, Idea>,
pub plans: BTreeMap<PlanId, Plan>,
pub next_idea: u64,
pub next_plan: u64,
pub next_node: u64,
pub next_run: u64,
}
impl Funnel {
pub fn apply(&mut self, event: &Event) -> Result<()> {
match event {
Event::IdeaSubmitted { id, source, text, refs, item_kind, ts } => {
if self.ideas.contains_key(id) {
bail!("duplicate IdeaSubmitted for `{id}`");
}
self.ideas.insert(
id.clone(),
Idea {
id: id.clone(),
source: source.clone(),
text: text.clone(),
refs: refs.clone(),
item_kind: *item_kind,
submitted_at: *ts,
triage: None,
},
);
self.next_idea = self.next_idea.max(parse_seq(id.as_str()) + 1);
}
Event::IdeaTriaged { idea_id, decision, why, ts } => {
let idea = self.ideas.get_mut(idea_id)
.ok_or_else(|| anyhow!("IdeaTriaged for unknown idea `{idea_id}`"))?;
idea.triage = Some((*decision, why.clone(), *ts));
}
Event::PlanCreated { id, idea_id, summary, planner, ts } => {
if !self.ideas.contains_key(idea_id) {
bail!("PlanCreated for unknown idea `{idea_id}`");
}
if self.plans.contains_key(id) {
bail!("duplicate PlanCreated for `{id}`");
}
self.plans.insert(
id.clone(),
Plan {
id: id.clone(),
idea_id: idea_id.clone(),
summary: summary.clone(),
planner: planner.clone(),
created_at: *ts,
status: PlanStatus::Draft,
nodes: BTreeMap::new(),
edges: BTreeSet::new(),
},
);
self.next_plan = self.next_plan.max(parse_seq(id.as_str()) + 1);
}
Event::NodeAdded { plan_id, node_id, kind, params, targets, prompt_excerpt, ts } => {
let plan = self.plans.get_mut(plan_id)
.ok_or_else(|| anyhow!("NodeAdded for unknown plan `{plan_id}`"))?;
if plan.nodes.contains_key(node_id) {
bail!("duplicate NodeAdded `{node_id}` in plan `{plan_id}`");
}
plan.nodes.insert(
node_id.clone(),
PlanNode {
id: node_id.clone(),
kind: kind.clone(),
params: params.clone(),
targets: targets.clone(),
prompt_excerpt: prompt_excerpt.clone(),
created_at: *ts,
status: NodeStatus::Pending,
last_status_change: *ts,
},
);
self.next_node = self.next_node.max(parse_seq(node_id.as_str()) + 1);
}
Event::EdgeAdded { plan_id, from_node, to_node, ts: _ } => {
let plan = self.plans.get_mut(plan_id)
.ok_or_else(|| anyhow!("EdgeAdded for unknown plan `{plan_id}`"))?;
if !plan.nodes.contains_key(from_node) {
bail!("EdgeAdded references unknown from_node `{from_node}`");
}
if !plan.nodes.contains_key(to_node) {
bail!("EdgeAdded references unknown to_node `{to_node}`");
}
if from_node == to_node {
bail!("EdgeAdded would create self-loop on `{from_node}`");
}
let inserted = plan.edges.insert((from_node.clone(), to_node.clone()));
if !inserted {
bail!("duplicate edge {from_node} -> {to_node} in plan `{plan_id}`");
}
}
Event::NodeStatusChanged { plan_id, node_id, status, why: _, ts } => {
let plan = self.plans.get_mut(plan_id)
.ok_or_else(|| anyhow!("NodeStatusChanged for unknown plan `{plan_id}`"))?;
let node = plan.nodes.get_mut(node_id)
.ok_or_else(|| anyhow!("NodeStatusChanged for unknown node `{node_id}`"))?;
node.status = *status;
node.last_status_change = *ts;
}
Event::RunRecorded { plan_id, node_id, run_id, .. } => {
let plan = self.plans.get(plan_id)
.ok_or_else(|| anyhow!("RunRecorded for unknown plan `{plan_id}`"))?;
if !plan.nodes.contains_key(node_id) {
bail!("RunRecorded for unknown node `{node_id}`");
}
self.next_run = self.next_run.max(parse_seq(run_id.as_str()) + 1);
}
Event::PlanStatusChanged { plan_id, status, why: _, ts: _ } => {
let plan = self.plans.get_mut(plan_id)
.ok_or_else(|| anyhow!("PlanStatusChanged for unknown plan `{plan_id}`"))?;
plan.status = *status;
}
}
Ok(())
}
}
fn parse_seq(s: &str) -> u64 {
s.rsplit_once('-')
.and_then(|(_, tail)| tail.parse::<u64>().ok())
.unwrap_or(0)
}
impl Funnel {
pub fn promote_ready(&mut self) {
for plan in self.plans.values_mut() {
let status: BTreeMap<NodeId, NodeStatus> = plan
.nodes
.iter()
.map(|(id, n)| (id.clone(), n.status))
.collect();
for node in plan.nodes.values_mut() {
if node.status != NodeStatus::Pending {
continue;
}
let deps_ok = plan
.edges
.iter()
.filter(|(_, to)| to == &node.id)
.all(|(from, _)| matches!(status.get(from), Some(NodeStatus::Done)));
if deps_ok {
node.status = NodeStatus::Ready;
}
}
}
}
}