-
2026-08-16 —
process_batchno longer runs the output-template post-pass when the retry leaves failures. The newstill_failedearly return is correct as a failure signal, but it is also consumer-visible: on a PARTIALLY failed batch the workflow post-pass now produces no packed/converted siblings at all, where it previously ran over whatever succeeded. Recorded here because the behaviour change was not covered by the entry that introduced the new error reporting. -
2026-08-16 — The compositor could pick a CONTENT colour as the background and silently destroy the maps it reported as fixed.
_solid_backgroundranked corner colours by how many corners they touch and broke ties withmax(), which returns the first maximum — i.e. scan order. A padding-off export routinely runs an island into the corners, so a large island can hold two corners and tie the true background; whichever the scan reached first won. Losing that coin-flip inverts every mask: the composite fills the islands instead of the background, and because the wrong colour is then RECORDED, the second pass "succeeds" through the known-bg shortcut andprocess_batchreturns RETRIED — success — over destroyed maps. Candidates are now ranked by the area they actually cover, which cannot tie that way; every existing gate (>=2 corners pooled per layer,_BG_MIN_SHAREof every layer) is unchanged. Reproduced end to end before fixing: two 16x16 flat Roughness layers with islands run to the UV edge (A cols 0-5, B cols 10-15) tie 4 corners each; the island colour won,retry_failedfilled the ISLANDS, andAB_Roughness.pngcame out entirely background withprocess_batchreturning RETRIED. It also contaminated the batch —_seed_masksORs masks across types, so the inverted mask drove the union to full coverage and degraded a normalBase_Colorpair to last-layer-wins, erasing one object's island. Note this was a regression in FAILURE MODE too: the pre-change code leftself.masksempty and returned MASK_FAILURE, a loud failure the user could act on. Pinned by three tests — a unit-level tie (the shipped corner-tolerance case mirrored with the content strip on the LEFT so it is scanned first) plus the two end-to-end cases above; all three fail on the old tie-break and pass on the new one. The remaining half of this area (the known-bg retry has no self-check, so a wrong recorded colour still cannot be caught) is logged in.claude/BACKLOG.md. Suite: 3208 tests, 3193 passed, 0 failed. -
2026-08-16 — Scoped cleanup primitives:
TaskFactory.stage_deferred_context(key, cm)andCoreUtils.teardown_guard(logger, what)(core_utils/task_factory.py,core_utils/_core_utils.py).stage_deferred_contextenters a context manager now and stages its__exit__as the keyed, LIFO, exception-isolated deferred restore — the bridge that lets asnapshot -> mutate -> restorescope be written ONCE (a plainwithfor scripts) and handed to the exporter when the write must still see the mutation; acontextlib.ExitStackcomposes several under one key.teardown_guardis the one teardown policy: a restore that raises is logged, never re-raised (never masks the body's error, never silently swallowed). Verified: test_task_factory 24, test_core_utils 8 green. -
2026-08-16 —
MapCompositor: masks are the union over every map type, a solid background survives islands in the corners, 16-bit sources no longer load as solid white, and edge-to-edge sources get one diagnosis (core_utils/engines/textures/map_compositor.py). Live-reproduced on a two-set Painter export (padding off): every combined map came out ~85% default background. Three stacked defects. (1) Painter writes Height asI;16and the compositor's implicit.convert("RGBA")/get_backgroundhit Pillow'sI;16 -> L/RGBAclip — a mid-grey (~32767) height map read as all-255. Layers inI/I;16*/Fare now reduced once at batch entry throughImgUtils.convert_i_to_l(÷257 rescale), retiring the mode-I->.convert("RGB")special case (the same clip). (2) Masks were seeded from ONE type — whichever composited first with four identical corners — and keyed off that colour. That was Height, whose mid-grey is both its bg AND its undisplaced surface, so the masks covered 5%/12% of islands that are really 51%/39%; meanwhile Base_Color's genuine black bg was never detected because islands touch two corners._seed_masksnow takes every type:alpha > 0for transparent bgs,pixel != bgfor opaque solid ones (_solid_background: majority of corners across all layers + >=10% dominance), OR-combined per layer — content that is flat in one map is textured in another. Masks are seeded lazily in the retry phase (a clean batch never pays), and layers whose masks overlap >5% of their union get a warning naming the cause (object baked into both sets / shared UV space / bg that is content) since the composite there is last-wins. (3) The second composite pass re-probed corners on the filled layers, so a correctly-masked island in a corner made the retried type fail silently (its return value was dropped);retry_failednow records the fill colour per type and_composite_typehonours it, and leftovers are reported and fail the batch. When no type has a detectable bg at all — fully dilated exports — the run reports that once, naming the types, instead of "Composite failed" per layer. 11 failing-first / rewritten tests;test_map_compositor67/67; end-to-end fidelity on the real set: output == source inside each layer's mask (98.5-100% outside the shared-object overlap). -
2026-08-16 —
StepTogglegains astalepolicy and its ramp starts at the ideal (core_utils/step_toggle.py).stale="home"(default, on/off toggles) keeps the old behavior — a press after the timeout returns home;stale="restart"(an action whose home is only an undo, e.g. framing) begins a fresh cycle at step 1 instead, so the key always acts after a pause and home stays reachable only by completing the cycle inside the timeout. Set per instance or per press (advance(stale=...)); unknown policies raise.began_cycleis now true on any stale press that lands on step 1 (the point to re-snapshot the payload).StepToggle.scalesdefaultsspreadto0.0: the first step is exactly1.0however long the cycle (scales(2) -> [1.0, 1.45]) — a softened start read as the key undershooting in both DCC frame macros;spread > 0still opts in. 5 tests added, 1 rewritten;test_step_toggle30/30.