90 lines
4.5 KiB
Markdown
90 lines
4.5 KiB
Markdown
# deporder — deterministic build-order planner for task DAGs
|
||
|
||
Lane: Codeforge (hard coding / agentic reasoning). Author: null.
|
||
Location of record: `/root/.hermes/wizard-outputs/null/deporder/`.
|
||
|
||
## What it is
|
||
|
||
A stdlib-only Python module + CLI that turns a task/dependency spec (JSON) into
|
||
a build plan:
|
||
|
||
* `topological_order` — serial order, lexicographically smallest ready task first
|
||
* `waves` — maximal parallelism batches (wave k = longest dependency chain has k edges)
|
||
* `critical_path` — weighted longest path (the makespan floor), with a unique,
|
||
lexicographically canonical path
|
||
* `schedule` — greedy list scheduling on N identical workers, with precedence and
|
||
capacity guarantees
|
||
* `find_cycle` — reports one cycle as a real path `x -> y -> ... -> x`
|
||
|
||
Every ordering decision is deterministic: ties break on lexicographically smallest
|
||
name, so two runs over the same spec — and runs over specs whose keys were inserted
|
||
in a different order — produce byte-identical output.
|
||
|
||
## CLI
|
||
|
||
```
|
||
python3 deporder.py plan examples/pipeline.json [--workers N]
|
||
```
|
||
|
||
Exit codes: `0` ok · `2` dependency cycle · `3` unknown dependency · `4` malformed spec.
|
||
Success prints the plan as JSON on stdout; failures print one diagnostic line on
|
||
stderr and nothing on stdout.
|
||
|
||
## Run the tests
|
||
|
||
```
|
||
python3 test_deporder.py # 42 tests, stdlib unittest, no pytest needed
|
||
python3 mutate_teeth.py # mutation harness: do the tests bite?
|
||
```
|
||
|
||
## Evidence (measured on this box, Python 3.14.7)
|
||
|
||
| Claim | How measured | Result |
|
||
|---|---|---|
|
||
| 42 tests pass | `python3 test_deporder.py` | `Ran 42 tests in 1.902s — OK` (exit 0) |
|
||
| Differential vs naive reference | 200 random DAGs × (order, waves, critical path) compared against O(V·E) fixpoint / O(V²) reference implementations inside the suite | all equal |
|
||
| Schedule invariants | 150 random DAGs × 1–4 workers: each task placed once, precedence `end[d] ≤ start[n]`, ≤ N tasks in flight (event sweep), per-worker non-overlap, `makespan ≥ critical path` | all hold |
|
||
| 1 worker == serial | `makespan == Σest` | exact |
|
||
| ≥N workers == critical path | `makespan == critical_path` | exact |
|
||
| Scale | 20 000 nodes / 39 987 edges: order + waves + critical path + 8-worker schedule | **0.92 s** (budget in-suite: 10 s) |
|
||
| Determinism | shuffled spec key insertion order → identical `json.dumps(sort_keys=True)` | identical |
|
||
| Tests have teeth | `python3 mutate_teeth.py`: 11 literal source mutations | **10/10 required mutants caught**, 1 documented equivalent, harness exit 0 |
|
||
| CLI contract | exit 0 on `examples/pipeline.json`; exit 2 + `dependency cycle: draft -> review -> publish -> draft`; exit 3 + `unknown dependencies: app -> missing_module` | as specified |
|
||
|
||
### Bugs this suite actually caught while building it
|
||
|
||
All three were found by running things, not by reading the code:
|
||
|
||
1. **Scheduler stall (infinite loop).** The clock-advance step took
|
||
`min(free_time, completion_time)` even when a worker was already free at `now`
|
||
with nothing ready — `now` stopped moving and `schedule()` never returned.
|
||
Caught by `PlanTests` hanging. Fixed by advancing only to strictly-future
|
||
events and raising `RuntimeError` on stall (unreachable for a DAG).
|
||
2. **O(V²) critical path.** `min(n for n in dist if dist[n] == max(dist.values()))`
|
||
recomputed the max inside the generator: a 20k-node plan took **15.98 s**.
|
||
Hoisting `max()` gave **0.85 s** (≈19× faster) with identical results.
|
||
3. **Dead branch found by the mutation harness.** The first version of the fix
|
||
above also consulted future worker-release times when choosing the next event.
|
||
The mutation that deleted that branch *survived* — it was unreachable, since
|
||
workers are only ever released by completions already drained at `now`. The
|
||
branch was removed rather than left as decoration.
|
||
|
||
## Mutation harness (`mutate_teeth.py`)
|
||
|
||
Each mutation is a literal source edit applied to a copy of `deporder.py` in a
|
||
temp dir; the mutation is *caught* if the suite exits non-zero on the mutant.
|
||
Mutations tagged `equivalent` are expected to survive — they document a
|
||
genuinely redundant guard rather than a hole in the tests.
|
||
|
||
See the harness output in `EVIDENCE.txt` (verbatim run log).
|
||
|
||
## Files
|
||
|
||
```
|
||
deporder.py implementation (module + CLI)
|
||
test_deporder.py 42-test stdlib suite (unit + differential + CLI + perf)
|
||
mutate_teeth.py mutation harness
|
||
examples/pipeline.json worked example (makespan 14.0 vs 18.0 serial, 3 workers)
|
||
EVIDENCE.txt verbatim transcripts of the runs quoted above
|
||
```
|