Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4dc470f
feat: Debug edits leave dirty/save accounting; Clear verb at three sc…
Yona-Appletree Aug 1, 2026
67ae3cb
feat: Debug section on node cards with unmissable hazard treatment
Yona-Appletree Aug 1, 2026
e1219d2
chore: regenerate tailwind.css — live-tint utilities no longer refere…
Yona-Appletree Aug 1, 2026
e609b93
style: soften the debug hazard treatment ~3-5x (G1 feedback)
Yona-Appletree Aug 1, 2026
99e07da
feat: Debug section collapses by default; rows never reflow on edit
Yona-Appletree Aug 1, 2026
2d72512
style: debug chevron shows actual state only, no hover rotation (G1 f…
Yona-Appletree Aug 1, 2026
18ac35b
feat: Debug values are session-only — nothing authored in files (D2)
Yona-Appletree Aug 2, 2026
e684597
feat: OutputDef.test_pattern — the Debug-slot proof case (harvested b…
Yona-Appletree Aug 2, 2026
8ac837a
docs: ADR ratifying the slot taxonomy; S4/S5/W9 paydown; debt register
Yona-Appletree Aug 2, 2026
5fe6157
Merge branch 'claude/ephemeral-debug-slots-impl-210c61' into claude/d…
Yona-Appletree Aug 2, 2026
8b1bbc2
docs: apply nested-modules review to the taxonomy ADR + glossary
Yona-Appletree Aug 2, 2026
672e851
fix: scrub authored controls stanzas from 5 test projects added on main
Yona-Appletree Aug 2, 2026
02ce18b
Merge remote-tracking branch 'origin/claude/ephemeral-debug-slots-imp…
Yona-Appletree Aug 2, 2026
a628712
fix: overlay-changed_at test stages a valid edit under W9 validation
Yona-Appletree Aug 2, 2026
520591c
Merge remote-tracking branch 'origin/main' into claude/debug-slots-p2-p6
Yona-Appletree Aug 2, 2026
a73a39a
chore(studio): auto-refresh story baselines [validate-stories]
github-actions[bot] Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions docs/adr/2026-07-05-studio-pane-grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,33 @@ hierarchy type is deliberately separate because the two levels announce
different things, but both follow the same grammar: one priority-merged
affordance on the detail trigger, text in the popup.

### The debug family (added 2026-08-01)

The colour language above assigns one meaning per family: **amber/yellow =
unsaved**, **blue = live**, **violet = bound** (never green — green is valid
only), **red = failed**, and **flat orange = device/roster health**. Debug
(`2026-08-01-debug-slots-taxonomy.md`) needed a treatment that reuses none of
them and reads as "you have put the system in a debug state", so it takes the
attention (orange) family in a **striped** form: diagonal hazard stripes.
Flat orange stays device health; the stripes are what distinguish the two, in
the repo's form-not-just-colour tradition (the stepped knob's gaps *are* the
scale).

The split the grammar cares about is where the mapping lives: core carries a
distinct **semantic** variant — `UiAffordance::Debug`, in the same
priority-merged vocabulary as `Unsaved`/`Error` — and the web layer holds the
single orange-plus-stripes mapping (`lpa-studio-web/src/app/affordance.rs`
plus one `style.css` block of `.lp-debug-*` tokens). Re-skinning debug later
is one mapping edit, never a call-site hunt. Debug is also unlike the other
families in that it announces **territory, not an event**: the node card's
Debug section is striped whenever it exists, idle or not, because knowing a
value will never be saved *before* touching it is the whole point.

Two consequences for the chrome described above: the `DirtySummary` that
feeds it no longer has a transient bucket (Debug carries no dirty weight —
the struct is `{ persisted, failed }`), and the affordance vocabulary's
`Live` variant was re-homed to `Debug` rather than gaining a sixth family.

### Consumers

Adopted by the node pane (P3: selection toggle in `primary`, tabs in
Expand Down
288 changes: 288 additions & 0 deletions docs/adr/2026-08-01-debug-slots-taxonomy.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ holds the full context.
| Host-process `lpa-server` stdout capture into the Studio console (terminal-only today) | `2026-07-05-studio-logging-model` | Host-process workflow needs in-console server logs |
| Console filter persistence and text search (session-only, no search today) | `2026-07-05-studio-logging-model` | Console usage patterns make refiltering per session annoying |
| Per-item overlay gating (fetch-full-on-change assumes small overlays) | `2026-07-04-studio-editing-model` (a) | Measured overlay fetch cost matters |
| Singular `ProjectRegistry::mutate` bypasses policy/type validation (only `mutate_batch` enforces) | `2026-07-04-studio-editing-model` (d) | Any new caller of `mutate` |
| ~~Singular `ProjectRegistry::mutate` bypasses policy/type validation (only `mutate_batch` enforces)~~ — **closed 2026-08-01**: `mutate` now runs the same `validate_mutation`; the bypass survives only as the crate-private `stage_dedicated_op` the node-authoring ops use to write `Fixed` containers | `2026-07-04-studio-editing-model` (d) | Closed |
| Alternative dirty modes (touched-mode / deliberate value pinning) — minimal-diff normalization fixed dirty to "differs from saved" | `2026-07-04-studio-editing-model` (f) | A concrete pinning/touched-mode use case appears |
| Device-pane adoption of the pane grammar (`StudioPane`/`DetailPopover`/`UiPaneAction`) | `2026-07-05-studio-pane-grammar` (a) | Next device-pane UX work |
| Save visibility while scrolling (project header scrolls with the sidebar; the strip was always visible) | `2026-07-05-studio-pane-grammar` (b) | The M2a UX gate or later use flags losing always-visible Save |
Expand Down Expand Up @@ -106,7 +106,7 @@ holds the full context.
| Playlist strip evolutions: timeline view, cue-trigger UI, autoplay-to-cue controls | `2026-07-26-node-card-faces` | Playlist interaction work resumes |
| Fixture mapping editor drawer (face's planned custom drawer; fixture has only advanced today) | `2026-07-26-node-card-faces` | Fixture mapping UX work begins |
| Hardware placard-follow walk under a live trigger (the rest of the refinement round — live knob values, friendly titles, entry-thumb warming, knob keyboard a11y, activate-by-click, drawer-state re-home — landed 2026-07-27, PR #158) | `2026-07-26-node-card-faces` | The next hardware walk |
| Sim button press / debug pokes adopt the runtime command channel as new `WireNodeCommand` variants | `2026-07-27-runtime-node-command-channel` | Sim-button UX or runtime debug tooling work begins |
| ~~Sim button press / debug pokes adopt the runtime command channel as new `WireNodeCommand` variants~~ — **obsolete 2026-08-01** (`2026-08-01-debug-slots-taxonomy`): button input is punted to the input initiative (record/replay needs source-level injection), and debug pokes are Debug slots, not commands. The channel stays the right home for genuine events | `2026-07-27-runtime-node-command-channel` | Obsolete |
| Cache-friendly prompt shape: the per-turn system prompt embeds the current shader source ahead of the cache prefix, so staged edits invalidate the cache (measured; live sessions read ~0 cached tokens) | `2026-07-25-studio-shader-agent-architecture` (2026-07-27 addendum) | Next agent round — P0; redesign + eval re-measure, not a blind fix |
| Pre-4.6 Anthropic thinking shape (`enabled` + `budget_tokens`; current `adaptive` shape 400s on Sonnet/Haiku 4.5) | `2026-07-25-studio-shader-agent-architecture` (2026-07-27 addendum) | An older Anthropic model is configured deliberately |
| OpenRouter reasoning opt-in (`reasoning: {}` request field, provider-gated) | `2026-07-25-studio-shader-agent-architecture` (2026-07-27 addendum) | OpenRouter thinking visibility is asked for |
Expand Down Expand Up @@ -134,6 +134,9 @@ holds the full context.
| Float→int through the soft-float ABI (`__fixsfsi`/`__fixunssfsi` are skipped because the ABI leaves out-of-range and NaN undefined) | `2026-07-31-soft-float-via-compiler-builtins` | The C6 harness's probe data shows the ROM matching `compiler_builtins` at those edges |
| Xtensa `F32Lowering` arm (`Unsupported` today — the S3 has an FPU, so soft float would be the wrong default) | `2026-07-31-soft-float-via-compiler-builtins` | The Xtensa FPU emulator and emitter land (roadmap M6/M7) |
| Soft-float performance measurement on the C6 (nothing here says how slow Float mode is) | `2026-07-31-soft-float-via-compiler-builtins` | A perf surface exists to show it (roadmap D3) |
| `Debug` naming re-check (ratified as provisional — the corpus is all diagnostics, but the clock's `rate`/`scrub_offset_seconds` read as transport) | `2026-08-01-debug-slots-taxonomy` (a) | The clock's transport controls move to a transport surface |
| Debug indication on preview/play surfaces (D8 covered the workspace only; a running installation in test-pattern mode shows nothing outside the editor) | `2026-08-01-debug-slots-taxonomy` (b) | The panels/play-mode work defines its own chrome |
| `TEST_PATTERN_RGB` is full white — the max-current case on long strips, deliberate for pin discovery | `2026-08-01-debug-slots-taxonomy` (c) | Someone runs it on a long strip and wants it dimmer |

## Relationship To Shared Planning

Expand Down
4 changes: 4 additions & 0 deletions docs/debt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,9 @@ stay in place when retired; the log is the history).
| [s3-frame-cost-scales-per-fixture](s3-frame-cost-scales-per-fixture.md) | carried | 2026-07-31 | lpc-engine resolver + lpc-hardware registry | Frame cost is flat ~8.4 ms/fixture: per-frame dataflow re-resolution + per-frame endpoint-status recomputation; the shader JIT is ~1%, sends 11% |
| [c6-on-legacy-ws281x-driver](c6-on-legacy-ws281x-driver.md) | retired | 2026-07-31 | lp-fw/fw-esp32c6/src/output | C6 ran its own single-channel WS281x driver, not `lp-ws281x`; retired 2026-08-01 when the C6 moved onto the shared core and gained its second channel |
| [example-shaders-not-compile-gated](example-shaders-not-compile-gated.md) | carried | 2026-07-29 | examples GLSL + CI + lps-filetests | an example shader can compile on the host yet fail on 4 of 5 targets; the break surfaces only when a human opens it in Studio |
| [clock-transport-has-no-transport-ui](clock-transport-has-no-transport-ui.md) | carried | 2026-05-12 | clock node + studio faces | scrubbing a show means typing seconds into a generic slider; the misfit also keeps the `Debug` name provisional |
| [project-reload-drops-debug-silently](project-reload-drops-debug-silently.md) | carried | 2026-07-04 | lpa-server project lifecycle | the documented recovery path discards every pending edit and Debug override with no return value, event, or notice |
| [registry-apis-without-production-callers](registry-apis-without-production-callers.md) | carried | 2026-07-04 | lpc-registry project APIs | `discard_overlay` is public, test-only API that duplicates the `MutationOp::Clear` path and silently drifts from it |
| [save-notice-assumes-header-dispatch](save-notice-assumes-header-dispatch.md) | carried | 2026-07-04 | lpa-studio-core save flow | "no persisted edits to write" is phrased for the gated header path but the asset editors dispatch the same op ungated |
| [safe-mode-dim-boot-unproven](safe-mode-dim-boot-unproven.md) | retired | 2026-08-01 | fw-esp32c6/bootctl + lpc-engine safe clamp | RETIRED 2026-08-01: dim boot seen on silicon (serial: record found/consumed/clamped 26/255; heartbeat outputClamp; eyes: dim) |
| [studio-no-reconnect-after-replug](studio-no-reconnect-after-replug.md) | carried | 2026-07-31 | lpa-link/browser-serial + studio device cards | every bootloader-mode op ends on a replug Studio cannot see; the op card waits forever and the user reloads the tab |
54 changes: 54 additions & 0 deletions docs/debt/clock-transport-has-no-transport-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
status: carried
since: 2026-05-12
logged: 2026-08-01
area: clock node / studio node faces
related:
- "../adr/2026-08-01-debug-slots-taxonomy.md"
- "../adr/2026-07-26-node-card-faces.md"
- "plan notes: ~/.photomancer/planning/lp2025/2026-07-31-1736-ephemeral-slots/notes.md (S9, D6)"
---
# The clock's transport controls have no transport UI

**Shape** — `ClockControls`
(`lp-core/lpc-model/src/nodes/clock/clock_controls.rs`) exposes
`running`, `rate`, and `scrub_offset_seconds` as Debug-role slots, and
the engine consumes all three every frame. The UI for them is the
generic slot renderer: a toggle, a number, and — for the scrub offset —
a plain slider whose unit is "seconds added to the clock". There is no
transport surface: no timeline, no scrub bar, no jog, no
return-to-zero, no readout of where the show currently is. Scrubbing a
show by typing an offset into a numeric field is the tell.

It is structural rather than a missing widget because of *where the
controls live*. Debug is defined as diagnostics/authoring overrides with
no durable value underneath, and `running`/`rate`/`scrub_offset_seconds`
only half fit: driving time by hand to inspect a show is diagnostics,
but transport is a first-class performance concept that wants its own
home (project-level, not buried in one node's card). The taxonomy ADR
records this as the known tension in the `Debug` name, and expects it to
resolve by those controls moving to a transport surface — at which point
Debug holds exactly the diagnostics it describes. Building a scrub UI
inside the clock's Debug section now would cement the wrong home.

**Carrying cost** — Inspecting a show at a chosen time is clumsy enough
that it mostly is not done; the scrub slot reads as unfinished; and the
`Debug` category carries an example that undercuts its own definition,
which costs explanation every time the taxonomy is taught.

**Workarounds** — Set `controls.scrub_offset_seconds` in the clock
card's Debug section and read the resulting time from the clock's
produced state; Clear (per value or per node) returns to live time.

**Incident log**
- 2026-08-01 — catalogued as S9 in the Debug-slots discovery sweep and
cited in D6 as the reason `Debug` is ratified as provisional. The
Debug section (P3) at least makes the controls findable and marks the
system as debug-driven while an offset is held; the transport gap is
untouched.

**Exit criteria** — A transport surface exists (scrub/rate/run at the
project level, with a position readout), the clock's three controls move
onto it, and the `Debug` naming re-check in
`../adr/2026-08-01-debug-slots-taxonomy.md` (follow-up (a)) can be
answered against a Debug category that holds only diagnostics.
58 changes: 58 additions & 0 deletions docs/debt/project-reload-drops-debug-silently.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
status: carried
since: 2026-07-04
logged: 2026-08-01
area: lpa-server project lifecycle
related:
- "../adr/2026-08-01-debug-slots-taxonomy.md"
- "../adr/2026-07-04-studio-editing-model.md"
- "registry-apis-without-production-callers.md"
- "plan notes: ~/.photomancer/planning/lp2025/2026-07-31-1736-ephemeral-slots/notes.md (S8)"
---
# `Project::reload()` discards the whole overlay with no signal

**Shape** — `Project::reload()`
(`lp-app/lpa-server/src/project.rs:397`) is documented as a recovery
path: "discard live runtime state and rebuild from the committed
filesystem". It drops the runtime and rebuilds registry and engine from
`ProjectLoader::load_from_root`, which means the device-side overlay
goes with it — every pending edit, and every **Debug** override
(`clock.controls.*`, `output.test_pattern`). Persisted edits are
recoverable in principle (the client mirror can re-stage them); Debug
overrides are not authored anywhere, so they are simply gone.

Dying on reload is the *correct* lifetime for a Debug value — the
taxonomy ADR makes it a deliberate property ("a rebooted installation
must not come up in test-pattern mode"), and overlay persistence across
restarts was explicitly rejected ("a device-crashing edit must not
crash-loop"). The debt is that the drop is **silent and unaccounted**:
no return value, no event, no notice mentions that pending state
vanished, and the client would discover it only through the next
overlay read. Today that costs nothing, because `reload()` has no
production caller (it is the documented future recovery path). It
becomes a real defect the moment recovery is wired up — the flow that
calls it is exactly the flow where a user has a reason to be confused
about what they just lost.

**Carrying cost** — Zero today, deferred and concentrated: the eventual
recovery flow has to (re)discover the semantics and design the
messaging, and the current signature gives it nothing to report.
Catalogued as S8 during the Debug-slots discovery sweep.

**Workarounds** — Do not call `reload()` from a user-facing flow without
first deciding what the user is told. The information needed for a good
notice is available before the drop: `registry.overlay()` can be
counted (and classified — `SlotRoleResolution::persistence`) at the top
of the function.

**Incident log**
- 2026-08-01 — re-verified during the Debug-slots plan (P6): still no
production caller, and the D7 change makes the loss *more* invisible
than before (Debug edits no longer appear in the Save panel, so
nothing on screen hints they existed). Logged, not fixed.

**Exit criteria** — The first production caller lands together with a
decision about the drop: either `reload()` reports what it discarded
(counts, split persisted vs Debug) so the flow can say so, or the
recovery flow re-stages the client mirror's persisted edits and tells
the user Debug overrides were cleared.
56 changes: 56 additions & 0 deletions docs/debt/registry-apis-without-production-callers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
status: carried
since: 2026-07-04
logged: 2026-08-01
area: lpc-registry / lpa-server project APIs
related:
- "../adr/2026-07-04-studio-editing-model.md"
- "../adr/2026-08-01-debug-slots-taxonomy.md"
- "plan notes: ~/.photomancer/planning/lp2025/2026-07-31-1736-ephemeral-slots/notes.md (S7)"
---
# `ProjectRegistry::discard_overlay` is public API with no production caller

**Shape** — `ProjectRegistry::discard_overlay`
(`lp-core/lpc-registry/src/registry/project_registry.rs:444`) clears the
whole overlay and re-derives the inventory. Nothing in production calls
it: the studio's "Revert to saved" goes through `MutationOp::Clear` on
the ordinary mutation path, and the only caller is
`lp-core/lpc-registry/tests/apply.rs:188`. So the method is exercised
exclusively by the test that exists to exercise it — a shape that reads
as coverage but proves nothing about a path anyone takes.

This is a condition rather than a one-off deletion because the registry
has more than one of these: a public surface accretes recovery-shaped
entry points ("discard everything", "reload from disk") that seem
obviously useful, are cheap to keep compiling, and quietly diverge from
the paths that actually run. Each one is a second implementation of a
behaviour the real path already has, and the divergence surfaces only
when someone finally wires it up. `Project::reload()` is the sibling
case (see `project-reload-drops-debug-silently.md`).

The Debug-slots work is a concrete example of the divergence risk: the
mutation path grew role-aware retention and validation, and a bypassing
"clear it all" entry point is exactly the kind of thing that would not
have been updated with it.

**Carrying cost** — Low but real: dead API widens the crate's contract,
its test consumes gate time while asserting nothing about production,
and every refactor of overlay semantics has to reason about a caller
that does not exist. It cost one triage pass during the Debug-slots plan
(catalogued as S7).

**Workarounds** — When changing overlay semantics, treat `discard_overlay`
as a mirror of the `MutationOp::Clear` path and update both, or the next
person to wire it up inherits a stale behaviour.

**Incident log**
- 2026-08-01 — re-confirmed during the Debug-slots plan (P6 paydown
sweep): still no production caller. Logged rather than deleted; the
paydown list for that plan was closed at three fixes, and deleting a
public API deserves its own look at whether the recovery story wants
it.

**Exit criteria** — Either `discard_overlay` gains a real caller (a
recovery flow that needs a non-mutation clear) with a test that goes
through that caller, or it is deleted along with its test and
"revert everything" stays the single `MutationOp::Clear` path.
Loading