Scoped buses + embeddable projects (effects slice) - #218
Conversation
Channels are now keyed (scope, name): every project node introduces a named bus scope, playlist entries wrap children in anonymous scopes (retiring the suppress-entry-visual-defaults rule), consumes resolve to the nearest enclosing scope with a writer (else root), produces always write their own scope. Every project node's runtime mirrors its scope's visual.out as produced output (playlist-pattern render forwarding), so an embedded project is playlist-playable with zero playlist changes. ADR: docs/adr/2026-07-28-scoped-buses.md (+ effects-are-projects ADR covering the M2 model slice landing next). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rd lift
ProjectDef grows optional controls{name: PromotedControlDef} (alias to a
direct child's slot, no value of its own) plus author/version/license;
all skip-if-default so existing artifacts are byte-identical, schemas
additive with no format bump. The 'kind Project cannot be created as a
child node' authoring guard is lifted (effects-are-projects ADR); child
project.json format fields are tolerated and ignored (root-only probe);
effect_starter_files composes the New Effect folder starter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ffect source An embedded project renders as an effect card: output-mirror hero, promoted-control knobs whose addresses are the inner child's slots (the standard address-routed write path carries edits, dirty, bound-violet), broken aliases render disabled with Invalid, provenance line below. The add picker grows an Effect source that expands the effect folder starter at effects/<name>/ for both attach sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…list import Two CC0 effect examples ship as workbench projects (preview rig wrapping a vendorable effect folder); an engine test renders both through the LpsGlsl compiler via their output mirrors. The gallery's kind chips are real (Projects | Effects), and the add picker's Effects section vendors a shipped effect into effects/<name>/ and attaches it in one gesture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cts-planning-f4f51b # Conflicts: # lp-core/lpc-model/src/lib.rs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt index drawMeteor indexed meteors[slot] with a runtime parameter; a uniform array of structs has no element address resolvable at lower time, so members must be read with constant indices at the call site and passed in as scalars (the shape examples/events/shader.glsl already carries). Measured: the runtime-indexed form compile-fails on 4 of 5 targets. Every automated gate stayed green on the broken shader. The engine render test runs the host backend, which accepts the construct — adding a node-status assertion did not catch it either, so its comment now says plainly what it cannot gate. Adds a filetest pinning the supported idiom on all five targets, plus defect + debt entries for the missing cross-target example gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A non-root project node's output mirror now carries a produced default onto visual.out at fallback priority. Per rule 4 that lands in the project NODE's own nearest scope (the parent's) — the node sits there; only its children are inside the scope it introduces — so an effect is drop-in while its interior stays isolated. Root is excluded: its containing scope is the one its mirror reads. Accepted consequence: embedding an effect where visual.out is already driven leaves two equal-priority writers and resolves ambiguous until the author picks one. The nested-project test now pins the writer set and that ambiguity rather than a winner. The workbench fixtures drop their explicit node: refs and ride the default, which is also what makes the effect gallery thumbnails preview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bus-graph probe resolves a value for every listed channel. Retiring the playlist suppression rule gave each entry child its own anonymous Entry-scope visual.out, so those channels started appearing — and the probe then DEMANDED every entry child's shader on each bus refresh, rendering inactive entries every frame. Found live on examples/fyeah-sign, where the blast entry ran constantly instead of only while active. Entry scopes are sinks by construction (the playlist reads each child's produced output slot directly, bus-independent), so they are filtered out of the graph entirely — restoring pre-change bus-pane parity for playlists while named project scopes stay visible. Regression test pinned and verified to fail without the filter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Closing per ADR This was never going to merge, and closing it now says so out loud instead of leaving it to look like a stalled review. The converged model in The branch is NOT deleted and must not be. What survives, and where it goes
Dropped outright: The "effects are projects" insight survives unchanged as "effects are modules" — a category, not a structure. Two things the implementation must not inherit
Deliberately closing slightly ahead of the ADR's "once harvesting is complete" — M11 has not run yet. A closed PR harvests exactly as well as an open one, and this had been sitting open as a bookmark with no CI and no merge path since 2026-07-31. Plan: |
Scoped buses + embeddable projects — the effects slice. Plan:
~/.photomancer/planning/lp2025/2026-07-27-composite-effects/.From Studio you can now author an effect as its own small project, see
two shipped effects in an Effects gallery category, open one to
study it, and drop one into your own project — with controls promoted
from the effect's inner nodes appearing as knobs on its card.
The model (2 ADRs)
Scoped buses with writer-shadowing (
docs/adr/2026-07-28-scoped-buses.md)Channels are keyed
(scope, name). Every project node introduces a namedscope around its children; playlist entries wrap each owned child in an
anonymous scope. Consumes resolve to the nearest enclosing scope with a
writer (else root), so an effect inherits host
time; produces alwayswrite their own scope, so an effect cannot clobber a host channel. This
retires the playlist-entry suppression special case — it was a
degenerate one-channel version of the same idea.
Every project node mirrors its scope's
visual.outas a producedoutput, which is what makes an effect playlist-playable with zeroplaylist changes. A non-root mirror also default-publishes to its
containing scope, so effects are drop-in.
Effects are projects (
docs/adr/2026-07-28-effects-are-projects.md)No new node kind:
ProjectDefgains optionalcontrols(aliases to adirect child's slot, carrying no value of their own) plus
author/version/license. All skip-if-default, so existing artifactsstay byte-identical and there is no format bump. The "Project cannot be a
child node" authoring guard is lifted.
Studio
Embedded projects render as an effect card: mirror preview, promoted
knobs, provenance line. Promotion is DTO-level aliasing — each control's
address is the inner child's slot, so edits, dirty state, and
bound-violet ride the standard write path with no forwarding layer. The
add picker grows a New Effect creator and an Effects section that
vendors a shipped effect into
effects/<name>/in one gesture.Content
examples/effects/plasma(pure visual) andexamples/effects/meteor(compute sim + visual renderer, controls promoted from both children).
Both CC0;
examples/effects/README.mdnotes all sample content is CC0unless stated otherwise.
Verified live
Gallery chips filter; plasma opens with the bus showing root channels
bare next to scope-qualified
plasma›visual.out; dragging a promotedknob moves the child's slot (one underlying slot); importing meteor
yields 4 channels carrying
plasma›visual.outandmeteor›visual.out— two effects coexisting, neither clobbering theother or root.
Findings folded in
Fixed; it compile-fails on 4 of 5 targets. Notably the engine render
test cannot gate this (host backend accepts it) — verified by
restoring the break. Filetest added pinning the supported idiom on all
five targets, plus a defect entry and a debt entry for the missing
cross-target example gate.
suppression gave entry children their own scope channels, and the
bus-graph probe resolves a value per listed channel — so it demanded
every entry's shader on each refresh. Entry scopes are sinks by
construction and are now filtered out. Regression test verified to fail
without the fix.
dtclamp in meteor's sim throttled motion on slow frames only,which is why it looked fine on GPU and stuttered on the CPU tier.
Validation
just check,just test,just schema-checkgreen; firmware packs at91.49% of the app partition. Story baselines deliberately not captured
locally —
validate-storiesowns those.Draft: awaiting the final review gate.
🤖 Generated with Claude Code