bankai/graph
The task dependency graph — pure functions, no AST evaluation.
SEMANTICS (documented once, used everywhere):
A blocking relationship Relationship(target, relation) on task T means
“T cannot proceed until target is satisfied” — ordinary work is satisfied
only by Completed; a Gate is satisfied by a local manual, due-timer, or
already-verified signed fact. ParentChild establishes hierarchy but does NOT
block readiness. All other relation types (RelatesTo, Duplicates, Supersedes,
RepliesTo, DiscoveredFrom, Tracks, CausedBy, Validates) are informational.
READINESS POLICY: Blocks, WaitsFor, ConditionalBlocks → target must be satisfied as above. Gates are satisfaction facts and never appear as executable ready work. Wisps are scratch state and also never appear as executable ready work. ParentChild and all other relation types → informational only.
CYCLE DETECTION: Blocks/WaitsFor/ConditionalBlocks edges are cycle-checked in the dep graph. ParentChild is cycle-checked through the parent_id chain (hierarchy cycles). Informational relations are not cycle-checked.
Per ADR-0001 amendment these are bankai’s own small pure functions over the task DAG (cycle_detect / topological_sort are trivial here; aarondb’s graph module is coupled to its internal index and drags a web-framework dep tree).
Values
pub fn all_edges(
tasks: List(types.Task),
) -> List(#(String, String))
All dependency edges across a set of tasks.
pub fn cycle_edges(
tasks: List(types.Task),
) -> List(#(String, String))
Edges that participate in a dependency cycle (cycles). An edge
(a -> b) means a depends on b; it is cyclic when b can reach a.
Self-loops count. Every blocking relation participates because all_edges
filters through is_blocking_relation.
pub fn dependency_edges(
task: types.Task,
) -> List(#(String, String))
Dependency edges (dependent -> dependency) for one task’s blocking relations. Blocks, WaitsFor, and ConditionalBlocks all constitute blocking edges.
pub fn gate_is_open(task: types.Task, now: Int) -> Bool
Gates do not derive readiness from task status. A manual gate opens only
after explicit satisfaction; a timer gate opens once its due timestamp is
reached. This pure policy is credential-free and leaves PR/CI/remote gate
adapters as future producers of the same gate_satisfied fact.
pub fn is_active(status: types.TaskStatus) -> Bool
Is this status “active” (work that still bears on readiness — not done and
not abandoned)? Pub so the CLI’s stale drift filter reuses the single
definition of “active” instead of re-deriving it.
pub fn is_blocking_relation(rel: types.RelationType) -> Bool
Returns True when a relationship type requires its target to be satisfied.
pub fn is_deferred(task: types.Task, now: Int) -> Bool
pub fn is_ready(task: types.Task, done: set.Set(String)) -> Bool
A task is ready iff it is active (not Completed/Closed) AND every blocking
relationship target is present in the supplied satisfied-id set. Context-free
callers normally use ready_tasks; is_ready remains the low-level predicate.
ParentChild and informational relations do not block readiness.
pub fn is_ready_at(
task: types.Task,
done: set.Set(String),
now: Int,
) -> Bool
pub fn readiness_explanation(
task: types.Task,
tasks: List(types.Task),
now: Int,
) -> json.Json
Explain readiness from the same predicates used by ready_tasks_at.
The output is stable data: clients never need to reconstruct policy.
pub fn ready_tasks(tasks: List(types.Task)) -> List(types.Task)
All currently-ready executable work at now. An open gate is a satisfied
dependency fact, not a work item, so gates never appear in this list.
pub fn ready_tasks_at(
tasks: List(types.Task),
now: Int,
) -> List(types.Task)
Ready executable work at now, excluding deferred tasks, gate facts, and wisps.
pub fn task_is_ready(
task: types.Task,
tasks: List(types.Task),
now: Int,
) -> Bool
Context-aware readiness treats an open gate as a satisfied blocker. This is how timer/manual/signed gate resolution deterministically wakes waiters without rewriting each waiter’s relationship or status.
pub fn topological_sort(tasks: List(types.Task)) -> List(String)
Topological order of task ids: dependencies before dependents. Best-effort if a cycle is present (remaining ids appended, sorted).
pub fn would_cycle(
edges: List(#(String, String)),
proposed: #(String, String),
) -> Bool
Would adding edge proposed = #(dependent, dependency) to edges create a
cycle? Yes iff the dependency can already reach the dependent (closing the
loop), or it’s a self-loop. Used to gate AddRelation.
pub fn would_cycle_parent_chain(
tasks: List(types.Task),
child_id: String,
proposed_parent_id: String,
) -> Bool
Would adding a child with parent_id to tasks create a parent hierarchy
cycle? A cycle occurs if the proposed parent is already a descendant of the
proposed child (walking parent_id chains upward).