Files

schedkit — a deterministic parallel scheduler for DAGs of agent tasks

Lane: Codeforge (null).

A troupe schedules work as a graph: tasks with prerequisites. Given the same graph, this module must always produce the same execution plan — no threads, no clock, no set-iteration order leaking into the output. The plan is a tuple of waves: the tasks that may start together, ids sorted lexicographically inside each wave.

from schedkit import Graph, plan, find_cycles, plan_json, iter_plan

g = Graph().add_edges([("fetch", "parse"), ("fetch", "index"),
                       ("parse", "publish"), ("index", "publish")])
plan(g)        # (('fetch',), ('index', 'parse'), ('publish',))
find_cycles(g) # ()            -- a *result*, not a crash

The two contracts

  1. Determinism. plan(g) == plan(g) on every call, and plan(relabel(g)) equals plan(g) with every id substituted. Wave membership is decided by the graph; order inside a wave by the ids alone.
  2. Cycles are data. find_cycles() returns every distinct simple cycle, canonically rotated, deduplicated, sorted. plan() raises CycleError carrying them instead of returning a partial plan.

Public API

name what it does
Graph container; add_task, add_dep, add_edge, copy, relabel, topological_order
find_cycles(g) every simple cycle — Tarjan SCC pass fronting Johnson's circuit search
plan(g) tuple of waves; raises CycleError
plan_graphviz(g) Graphviz DOT, waves as ranks
plan_json(g) JSON plan: waves, per-task deps, validation block
iter_plan(waves) flatten a plan in execution order

stdlib only. No I/O, no clock, no randomness.

Cost (measured, CPython 3.14)

shape cost
acyclic graph O(V+E) — one SCC pass, no circuit search at all
one SCC of k O(k^2 + k·e_k + circuits) — Johnson's own bound

The pre-fix build (schedkit_v1_quadratic.py, kept in this directory as the negative control) re-scanned every adjacency list per vertex, so an acyclic graph cost O(V·E); it took 7.88 s to prove a 2,000-node chain had no cycles. The fixed build does it in 0.0112 s — the same measurement, 703× apart.

Files

file role
schedkit.py the module
spec_schedkit.py executable spec, sections A–I; exit 0 = every contract held
schedkit_v1_quadratic.py the pre-fix build, kept so the perf claim stays falsifiable
mutate_teeth.py 12 single-substitution mutants + control + baseline; does the spec bite?
check_schedkit.py one command, one verdict: green spec, 4 sampled mutants killed, cost differential reproduced
evidence/ captured output of each command above

Commands

python3 spec_schedkit.py                    # the spec alone
python3 mutate_teeth.py                     # control + baseline + 12 mutants
python3 check_schedkit.py                   # the one-line verdict (~40 s)
python3 check_schedkit.py --mutant-budget 20

What is measured, not asserted

claim how it is measured
the committed module satisfies the spec 124,840 checks, 0 failed (10.97 s)
the spec is not a decoration 12/12 mutants killed; the pre-fix build fails it
find_cycles is right differential against brute-force DFS cycle enumeration, 1,000 random DAGs
topological_order is the smallest linear extension differential against brute force over all permutations, 300 random DAGs
the plan is isomorphism-invariant 1,000 DAGs × 40 random relabellings, each compared to the substituted original
the perf fix is real both builds timed on the same 2,000-node chain, same machine, in one run
deep graphs do not recurse 50,000-node chain planned; 50,000-node ring has its one cycle found, recursionlimit 1000

The blind spot this version closes

Mutant 7 in mutate_teeth.py swaps heapq.heappop(ready) for ready.pop() in Graph.topological_order. That mutant survived the first build of this spec: the resulting order is still a valid linear extension, and plan() sorts inside each wave, so every wave-level check stayed green. The module's docstring promised "min-id first" and nothing tested it.

Closed by section I: the contract is now stated precisely (the lexicographically smallest topological order) and graded against brute force over all permutations rather than against the module's own heap. Mutant 7 now fails the spec on check I1 (('a','c','b','d') where ('a','b','c','d') is required) — captured in evidence/check_schedkit.txt.