Exact, explainable dishwasher rack load planning from geometry you declare.
DishLoadout is a zero-runtime-dependency CLI that turns a reusable JSON rack profile and a dish inventory into an optimal, collision-free load. It produces a machine-verifiable receipt, a top-view SVG, and a standalone script-free HTML report—all locally, without accounts, uploads, cameras, or appliance control.
中文快速开始 · live generated demo · input format · failure repair
Important
A plan is exact only for the declared discrete profile. It is not a cleaning, hygiene, drainage, breakage, or manufacturer-safety certification. Your appliance and item manuals always override an example profile.
Dishwasher advice tools usually provide generic tips or troubleshooting. Robot research projects solve camera, grasping, or simulation problems. DishLoadout occupies a different, deliberately narrow gap: reproducible household rack assignment over user-declared anchors, keep-outs, tags, loaded footprints, and height limits.
The exact-name GitHub search for DishLoadout returned no repositories before
v0.1.0, and the closest search results were decision-tree advice or robotics/ML
projects rather than an offline geometry optimizer. The dated search method and
links are recorded in docs/research.md. That is evidence for
a useful gap, not a promise of popularity or proof that no adjacent work exists.
Requirements: uv and CPython 3.11 or newer.
git clone https://github.com/KanadeK/dishloadout.git
cd dishloadout
uv sync --locked
uv run dishloadout validate examples/family-dinner.json
uv run dishloadout plan examples/family-dinner.json --output-dir build/family
uv run dishloadout verify examples/family-dinner.json build/family/plan.jsonExpected output:
VALID: Family dinner · 2 racks · 15 positions · 15 expanded items
FULL: placed 15/15 · priority 115 · nodes 4156 · artifacts build\family
VERIFIED: 15 placements · status full · optimal true
The plan command creates a new directory and refuses to overwrite an existing one:
build/family/
├── plan.json # stable Plan v1 receipt
├── plan.svg # top-view rack drawing
└── index.html # standalone report; no JavaScript or remote assets
Each rack is an independent two-dimensional plane. A profile declares:
- rack width, depth, and loaded-height limit;
- candidate anchor points and their accepted dish kinds;
- allowed 0°/90° orientations, tags, and preference costs;
- rectangular keep-outs such as a declared spray-tower clearance;
- item quantity, loaded footprint, height, priority, tag constraints, and an explicit dishwasher-safety acknowledgement.
DishLoadout expands stable item instance IDs, rejects infeasible placements, then runs bounded branch-and-bound search. The objective is lexicographic:
- maximize total loaded priority;
- maximize loaded item count;
- minimize total position preference cost;
- choose the stable lexicographically smallest assignment.
Only a completed search sets optimal: true. If the declared node budget is
exhausted, the CLI exits 4 and records status: "search-limit" instead of
presenting the incumbent as proven optimal.
| Scenario | Expected result | Exit |
|---|---|---|
examples/family-dinner.json |
15/15, exact full plan | 0 |
examples/overloaded-party.json |
4/6, exact partial plan with reasons | 1 |
examples/search-limit.json |
incomplete search, optimal: false |
4 |
examples/invalid-duplicate-position.json |
input rejected before search | 3 |
Try each path without treating expected non-zero statuses as shell failures:
uv run dishloadout plan examples/overloaded-party.json --output-dir build/party
uv run dishloadout plan examples/search-limit.json --output-dir build/limit
uv run dishloadout validate examples/invalid-duplicate-position.jsonStable process codes:
| Code | Meaning |
|---|---|
| 0 | valid input, full plan, or verified plan |
| 1 | exact partial plan, or semantic verification failed |
| 2 | command-line usage error from argparse |
| 3 | malformed or invalid Scenario/Plan input |
| 4 | search budget exhausted; optimality is not claimed |
| 5 | input/output failure, including an existing output directory |
Unassigned records include stable reason codes such as
NOT_DISHWASHER_SAFE, TAG_CONSTRAINT, HEIGHT_EXCEEDED,
NO_GEOMETRIC_FIT, and NO_CONFLICT_FREE_POSITION. See the full contract in
docs/input-format.md.
One command runs the complete local release gate:
uv run python scripts/check.pyIt checks the lockfile, formatting, lint, strict typing, tests and coverage, all success/failure examples, generated-demo reproducibility, wheel/sdist builds, a clean wheel install, and the installed console entry point. The same gate runs on Windows and Ubuntu with Python 3.11 and 3.14.
If it fails, do not delete the lockfile or weaken a check. Follow the exact repair route in docs/repair.md, rerun the focused command, and then rerun the full gate.
DishLoadout does not:
- infer geometry from photos or ship an AI model;
- model water flow, detergent, drying, hygiene, breakage, or drainage;
- provide manufacturer-branded profiles or claim certification;
- control an appliance, fetch URLs, upload data, or collect telemetry;
- silently switch to a heuristic when exact search exceeds its budget.
See SPEC.md, docs/architecture.md, and ADR-0001 for the frozen v0.1 boundary.
uv sync --locked
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy src tests scriptsContributions are welcome when they preserve deterministic output and the claim boundary. Read CONTRIBUTING.md and SECURITY.md before opening a pull request or reporting a vulnerability.
MIT © KanadeK.