use std::path::Path;
use crate::relation_graph::{self, EntityKey};
use super::channels;
use super::graph::{self, NodeAttr, PriorityGraph};
use super::partition::{StatusClass, status_class};
use super::view::{
Actionability, ActionabilityBlock, BlockersView, Explanation, NextRow, ReasonKind, SurveyRow,
};
fn attr(g: &PriorityGraph, key: EntityKey) -> Option<&NodeAttr> {
g.attrs.get(&key)
}
fn kind_of(g: &PriorityGraph, key: EntityKey) -> String {
attr(g, key).map_or_else(|| key.prefix.to_string(), |a| a.kind.prefix.to_string())
}
fn title_of(g: &PriorityGraph, key: EntityKey) -> String {
attr(g, key).map_or_else(|| key.canonical(), |a| a.title.clone())
}
fn status_of(g: &PriorityGraph, key: EntityKey) -> String {
attr(g, key)
.and_then(|a| a.status.clone())
.unwrap_or_else(|| "—".to_string())
}
fn class_of(g: &PriorityGraph, key: EntityKey) -> StatusClass {
match attr(g, key) {
Some(a) => status_class(a.kind, a.status.as_deref()),
None => StatusClass::Unrecognised,
}
}
fn eligibility_reason(g: &PriorityGraph, key: EntityKey) -> ReasonKind {
ReasonKind::Eligibility {
status: attr(g, key).and_then(|a| a.status.clone()),
class: class_of(g, key),
}
}
fn refs(keys: &[EntityKey]) -> Vec<String> {
keys.iter().map(|k| k.canonical()).collect()
}
fn actionability(g: &PriorityGraph, key: EntityKey) -> Actionability {
if channels::blocked(g, key) {
Actionability::Blocked
} else {
Actionability::Actionable
}
}
struct SurveyDecorated {
key: EntityKey,
act: Actionability,
consequence: u32,
blockers: Vec<String>,
}
pub(crate) fn survey(root: &Path, all: bool) -> anyhow::Result<Vec<SurveyRow>> {
let g = graph::build(root)?;
let mut rows: Vec<SurveyDecorated> = g
.attrs
.keys()
.copied()
.filter(|&k| {
if all {
return true;
}
channels::eligible(&g, k) && !channels::promoted(&g, k)
})
.map(|k| SurveyDecorated {
key: k,
act: actionability(&g, k),
consequence: channels::consequence(&g, k),
blockers: refs(&channels::blocked_by(&g, k)),
})
.collect();
rows.sort_by(|a, b| {
let act = act_rank(a.act).cmp(&act_rank(b.act));
let cons = b.consequence.cmp(&a.consequence); act.then(cons).then_with(|| a.key.cmp(&b.key))
});
let rows = rows
.into_iter()
.map(|d| {
let mut reasons = vec![eligibility_reason(&g, d.key)];
if !d.blockers.is_empty() {
reasons.push(ReasonKind::BlockedBy {
items: d.blockers.clone(),
});
}
reasons.push(ReasonKind::Consequence {
inbound: d.consequence,
});
SurveyRow {
id: d.key.canonical(),
title: title_of(&g, d.key),
kind: kind_of(&g, d.key),
status: status_of(&g, d.key),
act: d.act,
consequence: d.consequence,
blockers: d.blockers,
reasons,
}
})
.collect();
Ok(rows)
}
fn act_rank(a: Actionability) -> u8 {
match a {
Actionability::Actionable => 0,
Actionability::Blocked => 1,
}
}
pub(crate) fn next(root: &Path) -> anyhow::Result<Vec<NextRow>> {
let g = graph::build(root)?;
let order = channels::order_key(&g);
let rows = order
.into_iter()
.filter(|&k| channels::actionable(&g, k) && !channels::promoted(&g, k))
.map(|k| {
let blocking = refs(&channels::blocking(&g, k));
let mut reasons = vec![eligibility_reason(&g, k)];
if !blocking.is_empty() {
reasons.push(ReasonKind::Blocking {
items: blocking.clone(),
});
}
NextRow {
id: k.canonical(),
title: title_of(&g, k),
kind: kind_of(&g, k),
status: status_of(&g, k),
act: Actionability::Actionable,
reasons,
blockers: Vec::new(),
blocking,
}
})
.collect();
Ok(rows)
}
fn parse_key(id: &str) -> anyhow::Result<EntityKey> {
let (kref, qid) = crate::integrity::parse_canonical_ref(id)?;
Ok(EntityKey {
prefix: kref.kind.prefix,
id: qid,
})
}
pub(crate) fn blockers(root: &Path, id: &str, transitive: bool) -> anyhow::Result<BlockersView> {
let key = parse_key(id)?;
let g = graph::build(root)?;
relation_graph::require_minted(&g.projection, key)?;
let (blocked_by, blocking) = if transitive {
(
channels::blocked_by_transitive(&g, key),
channels::blocking_transitive(&g, key),
)
} else {
(channels::blocked_by(&g, key), channels::blocking(&g, key))
};
Ok(BlockersView {
id: key.canonical(),
transitive,
blocked_by: refs(&blocked_by),
blocking: refs(&blocking),
})
}
pub(crate) fn explain(root: &Path, id: &str) -> anyhow::Result<Explanation> {
let key = parse_key(id)?;
let g = graph::build(root)?;
relation_graph::require_minted(&g.projection, key)?;
let eligibility = eligibility_reason(&g, key);
let chain = channels::blocked_by_transitive(&g, key);
let blocker_chain = if chain.is_empty() {
Vec::new()
} else {
vec![ReasonKind::BlockedBy {
items: refs(&chain),
}]
};
let evictions = channels::evicted_seq_edges(&g, key)
.into_iter()
.map(|(from, to, reason)| ReasonKind::EvictedEdge {
from: from.canonical(),
to: to.canonical(),
reason,
})
.collect();
let cycle = channels::dep_cycles(&g)
.into_iter()
.find(|c| c.contains(&key));
let consequence = ReasonKind::Consequence {
inbound: channels::consequence(&g, key),
};
let mut blocker_chain = blocker_chain;
if let Some(component) = cycle {
let nodes = component.into_iter().map(EntityKey::canonical).collect();
blocker_chain.push(ReasonKind::CycleDegraded { nodes });
}
Ok(Explanation {
id: key.canonical(),
eligibility,
blocker_chain,
evictions,
consequence,
})
}
pub(crate) fn actionability_block_from(
scanned: &[relation_graph::ScannedEntity],
root: &Path,
id: &str,
) -> anyhow::Result<ActionabilityBlock> {
let key = parse_key(id)?;
let g = graph::build_from(scanned, root)?;
relation_graph::require_minted(&g.projection, key)?;
Ok(ActionabilityBlock {
eligible: channels::eligible(&g, key),
actionable: channels::actionable(&g, key),
blockers: refs(&channels::blocked_by(&g, key)),
blocking: refs(&channels::blocking(&g, key)),
consequence: channels::consequence(&g, key),
})
}