From an independent review of the branch:
- Regression: existing non-raster-but-showable visuals (.svg diagrams) were
dropped by the renderable filter — they were served fine before via <img>
(/thumb passes SVG through). Add .svg to MEDIA_IMAGE_EXT.
- Doc accuracy: the board dedupes decisions by (category, subject), not category
alone (category-only would wrongly merge distinct decisions that share a
category, e.g. TTS vs image provider_selection). Correct AGENT_GUIDE to say
the pair is the key and to reuse the same subject when re-logging.
- Coverage: the new visual-selection logic was untested (which let the earlier
missing-file regression through). Add TestStoryboardVisualSelection covering
the .tsx-animation exclusion, snapshot fallback (exact + <id>_* match), SVG
renderability, the preserved missing-file indicator, and takes = renderable.
Not changed (reviewed, deliberate): .narr clamp at --fs-scale 1.16 degrades
gracefully via the fade + click-to-expand modal; decision dedupe stays keyed on
(category, subject) as the more-correct behavior.
The renderable-only filter from the previous commit also dropped assets whose
file is missing, so the storyboard lost its "asset in manifest, file missing"
slot (broke tests/backlot/test_state.py::test_full_project).
Distinguish the two cases: a missing image/video/diagram still surfaces as the
missing indicator; only an asset that EXISTS but can't be shown (a .tsx atelier
composition) falls back to a per-scene snapshot/placeholder.
Fixes surfaced by the "How Salt Changed History" run:
- Decisions rail showed a stale choice. Contract now requires a superseding
decision_log entry when a logged choice changes mid-run; the board collapses
by category+subject and renders the latest as current, tagged "revised"
(AGENT_GUIDE Decision Communication Contract; board.js renderDecisions).
- Assets gate jumped by a draft render. The review surface is the filmstrip:
render per-scene stills to snapshots/<scene_id>.png (scripts/atelier_snapshots.py)
and STOP; the full/draft render is the compose stage, post-approval
(checkpoint-protocol, bespoke-composition).
- Broken thumbnails / missing scenes. A .tsx "animation" asset is no longer a
renderable visual — resolve snapshots/<scene_id>.* or a shot-spec placeholder;
add <img onerror> fallback and a BESPOKE placeholder (state.py, board.js).
- Clicking a card video did nothing. .thumb video had no sizing, so the frame
and click box drifted; add object-fit:cover. Click-to-play on the render-hero
player; narration is clickable -> modal with the full text (board.css/js).
- Type too small everywhere. Add one --fs-scale (1.16) knob; every font-size is
calc(px * var(--fs-scale)) so text scales proportionally and reversibly,
verified for zero overflow at desktop + tablet widths.
- F-01: cost bar crit (red) state past 90% of budget
- F-02: normalize() hardens fetched board state against sparse payloads
- F-03: /thumb 404s for videos with no extractable poster frame instead
of serving raw video bytes
- F-04: checkpoint artifact path refs only resolve inside the project dir
- F-05 (board half): stall detection — in_progress stage with no disk
activity >10min renders red 'stalled?' + header badge flips to STALLED?;
verified against the real wedged why-cities-glow project
- eval harness from dogfood session committed (visual regression +
interaction smoke, capture watcher, server/gate test suites) +
regression tests for each finding; 46 backlot tests green, visual eval
green (restage-before-capture note logged)
- README 'Watch It Happen' section with four staged screenshots (fictional
demo productions, generated placeholder art — no private project content)
+ governance section notes enforced approval gates
- scripts/backlot_screenshot_stage.py: stages 4 demo projects in 4 pipeline
states against OPENMONTAGE_PROJECTS_DIR and captures the shots with
Playwright — reproducible README imagery
- lib/paths.py: OPENMONTAGE_PROJECTS_DIR env override (checkpoints, events,
and the board all follow one root); backlot/state.py now imports it
(was still defining its own copy — the exact drift the review warned of)
- undeclared-stage checkpoints (e.g. a legacy 'idea' run under a cinematic
manifest) now slot into their canonical rail position instead of dangling
after publish, drawn dashed + italic with an 'unlisted' note
- ChangeHub subscriptions filtered per project: unrelated-project bursts
can no longer flood a board's queue and starve its own change signal
- thumbnail temp files unique per request (concurrent-miss race on the
shared .tmp path corrupted the cache)
- watcher change-mapping is pure string work (no per-path resolve() in
thousand-file change batches); 'C:'-style project ids rejected
- replay: tz-naive timestamps treated as UTC; final render/script no
longer leak at t0 on storyboard-less projects; single tick chain on
rapid pause/play; drag-safe scrubber (label tracks input, board renders
on release); render-video playback survives SSE re-renders
- state: scene id 0 joins correctly, nested depth>0 events don't corrupt
generating state, out-of-project asset paths honestly unserveable,
negative durations clamped, tolerant manifest stage parse
- UI: decisions alt filter precedence fixed, activity counts parallel
same-tool runs, el() html sink removed, NaN-safe formatters
- replay mode: scrub a completed run from checkpoint history + event
timestamps — stage rail rewinds, script/storyboard/renders appear at
their real moments, generating shimmer replays; ~20s full-run playback
- stage history_entries exposed in BoardState (powers replay + versions)
- entrance choreography plays on first paint only (not on SSE refreshes)
- live-run fixes found by driving a simulated production against the
board: null rendered by native append, activity ticker showing closed
starts as running, scene-id label handling for 'scene-N' ids
- scripts/backlot_simulate_run.py: drives a fake production through the
REAL contract (init_project, in_progress heartbeats, gated
awaiting_human -> approved, per-scene events, growing manifest) —
live-board verification + demo driver
- backlot/README.md
- backlot/state.py: BoardState from disk — stage rail with gate audit
(gate_skipped detection from history/), scene_plan x script x
asset_manifest storyboard join, takes, generating-state from events,
media discovery incl. atelier root-render heuristic, degradation ladder,
library summaries
- backlot/server.py: FastAPI on 4750 — /api/projects, /api/project/{id}/state,
SSE change feeds (project + library) fed by a watchfiles watcher,
/media with range support and traversal protection, UI mounts
- backlot/__main__.py: 'python -m backlot open [project]' idempotent
launcher (spawns detached server, opens browser); 'serve' foreground
- verified against real projects: 73 listed, full state for
signal-from-tomorrow, 206 range responses, SSE change push on
filesystem write