use std::collections::{BTreeMap, BTreeSet};
use crate::relation_graph::EntityKey;
use super::channels;
use super::graph::PriorityGraph;
pub(crate) fn surviving_seq_predecessors(
g: &PriorityGraph,
actionable: &BTreeSet<EntityKey>,
) -> BTreeMap<EntityKey, BTreeSet<EntityKey>> {
let mut preds: BTreeMap<EntityKey, BTreeSet<EntityKey>> = BTreeMap::new();
for &k in actionable {
let evicted: BTreeSet<(EntityKey, EntityKey)> = channels::evicted_seq_edges(g, k)
.into_iter()
.map(|(from, to, _reason)| (from, to))
.collect();
let mut set = BTreeSet::new();
if let Some(n) = g.projection.resolve(k) {
for (pred, _) in g.graph.in_edges(g.seq_overlay, n) {
if let Some(pk) = g.projection.key_of(pred)
&& actionable.contains(&pk)
&& !evicted.contains(&(pk, k))
{
set.insert(pk);
}
}
}
preds.insert(k, set);
}
preds
}
pub(crate) fn frontier_order(
actionable: &[EntityKey],
score: &dyn Fn(EntityKey) -> f64,
preds: &BTreeMap<EntityKey, BTreeSet<EntityKey>>,
) -> Vec<EntityKey> {
let mut emitted: BTreeSet<EntityKey> = BTreeSet::new();
let mut out: Vec<EntityKey> = Vec::with_capacity(actionable.len());
while out.len() < actionable.len() {
let ready: Vec<EntityKey> = actionable
.iter()
.copied()
.filter(|k| !emitted.contains(k))
.filter(|k| {
preds
.get(k)
.is_none_or(|ps| ps.iter().all(|p| emitted.contains(p)))
})
.collect();
let candidates: Vec<EntityKey> = if ready.is_empty() {
actionable
.iter()
.copied()
.filter(|k| !emitted.contains(k))
.collect()
} else {
ready
};
let Some(pick) = candidates.into_iter().max_by(|a, b| {
score(*a).total_cmp(&score(*b)).then_with(|| b.cmp(a))
}) else {
break;
};
emitted.insert(pick);
out.push(pick);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn key(id: u32) -> EntityKey {
EntityKey { prefix: "ISS", id }
}
#[test]
fn frontier_order_shared_upstream_leads_then_score_tiebreak() {
let nodes = [key(1), key(2), key(3)];
let mut preds: BTreeMap<EntityKey, BTreeSet<EntityKey>> = BTreeMap::new();
preds.insert(key(2), BTreeSet::from([key(1)]));
preds.insert(key(3), BTreeSet::from([key(1)]));
let score = |k: EntityKey| -> f64 {
match k.id {
1 => 0.0,
2 => 50.0,
3 => 100.0,
_ => 0.0,
}
};
let order = frontier_order(&nodes, &score, &preds);
let pos = |id: u32| order.iter().position(|k| k.id == id).unwrap();
assert!(
pos(1) < pos(2) && pos(1) < pos(3),
"shared upstream leads: {order:?}"
);
assert!(pos(3) < pos(2), "higher-score arm 3 before 2: {order:?}");
}
#[test]
fn frontier_order_chain_keeps_structure_over_score() {
let nodes = [key(1), key(2)];
let mut preds: BTreeMap<EntityKey, BTreeSet<EntityKey>> = BTreeMap::new();
preds.insert(key(2), BTreeSet::from([key(1)]));
let score = |k: EntityKey| -> f64 { if k.id == 1 { 2.0 } else { 100.0 } };
let order = frontier_order(&nodes, &score, &preds);
let pos = |id: u32| order.iter().position(|k| k.id == id).unwrap();
assert!(
pos(1) < pos(2),
"low-score predecessor leads on the chain: {order:?}"
);
}
#[test]
fn surviving_seq_predecessors_reads_after_chain() {
use crate::priority::graph;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join(".doctrine/backlog/issue/001")).unwrap();
std::fs::create_dir_all(root.join(".doctrine/backlog/issue/002")).unwrap();
std::fs::write(
root.join(".doctrine/backlog/issue/001/backlog-001.toml"),
"id = 1\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n",
)
.unwrap();
std::fs::write(
root.join(".doctrine/backlog/issue/001/backlog-001.md"),
"b\n",
)
.unwrap();
std::fs::write(
root.join(".doctrine/backlog/issue/002/backlog-002.toml"),
"id = 2\nslug = \"i\"\ntitle = \"I\"\nkind = \"issue\"\nstatus = \"open\"\n\
resolution = \"\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[relationships]\nafter = [{ to = \"ISS-001\", rank = 0 }]\n",
)
.unwrap();
std::fs::write(
root.join(".doctrine/backlog/issue/002/backlog-002.md"),
"b\n",
)
.unwrap();
let g = graph::build(root).unwrap();
let set: BTreeSet<EntityKey> = [key(1), key(2)].into_iter().collect();
let preds = surviving_seq_predecessors(&g, &set);
assert_eq!(
preds.get(&key(2)),
Some(&BTreeSet::from([key(1)])),
"ISS-002's surviving seq predecessor is ISS-001"
);
assert!(
preds.get(&key(1)).is_none_or(BTreeSet::is_empty),
"ISS-001 has no seq predecessor"
);
}
}