Two-lane packing, and the shelves ship with the export

Eric's wall answered the orientation question with a screenshot full
of red: the capacity model assumed one flat stack per opening, so his
spine-out cubes read 200% full. His reviewer's prescription, built to
its acceptance cases: each opening packs a FLAT lane (thinnest axes
against interior height, claiming the widest flat box's width) and a
STANDING lane beside it (boxes on edge against the remaining width;
standing boxes must fit height and depth upright). The largest-
footprint class lies flat, smaller boxes stand, any box's lane is
flippable per-assignment (▬/▮ toggle in the opening view, ✱ marks an
override), and ⚠ now means NO packing fits — not merely "tall stack".
Unmeasured boxes take no lane but keep their honesty tag; dual fill
bars show each lane's budget; a broken opening is never offered by
the suggester. On the real wall: the double-wides and half the cubes
went green (7 games = 13.12" flat + 2.91" standing), and the
remaining warnings mark cubes that genuinely hold 12-16 boxes.
Both reviewer acceptance tests pass verbatim.

And the export gains the humanity Eric asked for: game pages say
where each box lives, containers list their contents, and a shelves
page draws the wall as it physically stands — proportional cells,
zones, descriptions, every opening linking its residents. Publishing
a shelf layout is a choice: --no-shelves keeps the layer out.
"shelves" joins "art" as a reserved slug. 387 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-09 14:53:42 -04:00
co-authored by Claude Fable 5
parent d6ff5e73e9
commit e90a53fb6c
20 changed files with 1930 additions and 59 deletions
+45 -10
View File
@@ -59,23 +59,58 @@ def test_fits_opening_speaks_the_house_language():
assert fits_opening(big, TRAVEL) is None
def test_opening_report_counts_stack_and_warns():
def test_opening_report_packs_two_lanes():
thin = _entry("Thin", 11, 11, 2)
thick = _entry("Thick", 11, 11, 4)
unmeasured = _entry("Mystery", w=None)
report = opening_report(KALLAX, [("a", thin), ("b", thick), ("c", unmeasured)])
assert report["games"] == 3
assert report["stacked_in"] == 6
assert report["flat_in"] == 6 # both lie flat: 2 + 4 within 13.25
assert report["unmeasured"] == 1
assert report["overfull"] is False
# seven thick boxes overflow a 13.25" interior
stack = [(str(i), thick) for i in range(7)]
assert opening_report(KALLAX, stack)["overfull"] is True
# a misfit is named even when the stack has room
# a misfit is named even when the lanes have room
report = opening_report(KALLAX, [("a", _entry("Too Long", 11.5, 17, 2.25))])
assert report["misfits"] == ["Too Long"]
def test_reviewer_lane_cases():
"""The two acceptance cases: 3 flat + 4 standing in a 26.8" wide
passes; 12 standard boxes FORCED all-flat in a cube still warns."""
wide = {
"id": "w",
"label": "top",
"zone": "",
"width_in": 26.8,
"height_in": 13.25,
"depth_in": 15.4,
}
big = [_entry(f"Big{i}", 11.6, 11.6, 2.8) for i in range(3)]
small = [_entry(f"Small{i}", 11.5, 9, 2) for i in range(4)]
report = opening_report(
wide,
[(f"b{i}", g) for i, g in enumerate(big)]
+ [(f"s{i}", g) for i, g in enumerate(small)],
)
assert report["overfull"] is False and report["laneless"] == []
assert report["flat_in"] == 8.4 # 3 × 2.8 lying flat
assert report["standing_in"] == 8 # 4 × 2 on edge beside the stack
assert sorted(set(report["lanes"].values())) == ["flat", "standing"]
# 12 standard boxes forced flat: only ~4 fit the height; warn
twelve = [(f"g{i}", _entry(f"G{i}", 11.6, 11.6, 2.8)) for i in range(12)]
forced = opening_report(
KALLAX, twelve, overrides={f"g{i}": "flat" for i in range(12)}
)
assert forced["overfull"] is True
assert len(forced["laneless"]) == 8 # 4 fit the 13.25" height, 8 cannot
# unforced, the same twelve mostly still can't fit a single cube —
# flat takes 4, standing takes what remains of the width, rest warn
free = opening_report(KALLAX, twelve)
assert free["overfull"] is True
assert len(free["laneless"]) > 0
def test_effective_location_inherits_from_container():
games = {
"173634": _entry("Trove", bgg_id=173634),
@@ -401,7 +436,7 @@ def test_stored_in_chain_resolves_to_the_outermost_box(tmp_path):
assert {g["name"] for g in resident} == {"Trove", "Insert", "Minis"}
# only the physical box is stacked and counted
assert state["units"][0]["openings"][0]["games"] == 1
assert state["units"][0]["openings"][0]["stacked_in"] == 4
assert state["units"][0]["openings"][0]["flat_in"] == 4
assert state["unshelved"] == []
# deep cycles refuse: Trove into Minis would close the loop
@@ -787,9 +822,9 @@ def test_reunification_outranks_tightest_fit_and_full_shelves_stop_lying(tmp_pat
state = web.get("/api/shelves").json()
exp = next(g for g in state["unshelved"] if g["name"].endswith("Tower"))
# A1 holds the series-mate BUT its remaining height (13.25 - 3 - 11 < 2)
# can't take the expansion: honesty beats reunification, so the empty
# twin cube leads and A1 is not offered at all
# A1 holds the series-mate BUT its packing already fails (the base
# is laneless behind the hog): a broken opening is never offered, so
# honesty beats reunification and the empty twin cube leads
offered = [s["id"] for s in exp["suggestions"]]
assert ids["A1"] not in offered
assert offered[0] == ids["A2"]