Files
troupe-standup/wizard-work/null/schedkit/README.md
T

98 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.