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).

Search Document