feat: Implement the module model: rename, mitosis, scoped buses, panels, Studio face - #255
Draft
Yona-Appletree wants to merge 27 commits into
Draft
feat: Implement the module model: rename, mitosis, scoped buses, panels, Studio face#255Yona-Appletree wants to merge 27 commits into
Yona-Appletree wants to merge 27 commits into
Conversation
… wire bump)
Rust surface of the P1 rename: ProjectDef -> ModuleDef, NodeKind::Project
-> NodeKind::Module, NodeDef::Project -> NodeDef::Module, PROJECT_VARIANT
-> MODULE_VARIANT ("Project" -> "Module"), ModuleDef::KIND "project" ->
"module", format probe renamed to read_module_format_json. nodes/project/
moved to nodes/module/ (SHAPE_ID regen lands in a later commit).
WIRE_PROTO_VERSION 4 -> 5: NodeKind serializes its variant name in
inventory frames and TreePath segments change from .project to .module.
The word "project" survives for container concepts (project.json, Studio
open-a-project flows, PROJECT_FORMAT_VERSION) per docs/design/modules.md.
Plan: lp2025/2026-08-01-1003-modules-impl-roadmap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tree-path kind segment comes from kind_name(), so the P1 KIND change silently re-addresses every node path. This commit is ONLY the path string sweep (/demo.project/orbit.shader -> /demo.module/orbit.shader) so review can eyeball path strings in isolation. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rust test fixtures plus the artifact corpora (15 examples/, projects/test, fw-browser smoke-project) and docs/design/source-artifacts.md. The schemas/history/v1 snapshot stays frozen (it documents the v1 format) and the 2026-07-27 defect entry keeps its historical quote. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
just schema-gen: project.schema.json now pins kind Module, ModuleDef shape id moves (module_path! is part of the hash), NodeArtifact $ref and the shape dump filename follow. Stragglers: schema generator variant name, Studio node-kind icon lookup + NodeKindIcon::Module, debug-UI kind labels, package_manifest default kind, node_kind_slug/label, justfile format-bump path, schemas/README, modules.md section-2 done note. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y the sweep format!-embedded \"kind\": \"Project\" fixtures (engine test helper, server/browser/opfs/history tests), UiPackageCard/UiExampleCard story kind fields, embedded example kind, node-authoring error copy. Pane title chips keep "Project" (container concept). Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… relocation (P2) project.json becomes the non-node container manifest (format/uid/name), read by a streaming JsonSyntaxSource probe and written by a hand-rolled deterministic writer — deliberately NOT a Slotted type, so no second shape/codec surface links into firmware. The root module node moves to /module.json; ModuleDef drops format/uid/name. PROJECT_FORMAT_VERSION 2 -> 3 (bump-and-refuse; v2 schemas snapshotted to schemas/history/v2). The load gate now reads the container: missing or malformed project.json is a HARD refuse per settled D-A, and a pre-mitosis kind-tagged root fails with a diagnosable manifest error. Corpora (15 examples, projects/test, smoke-project) split into the two files; starter project emits 9 files; registry/engine/shared fixtures migrated; new gate tests incl. missing-manifest and pre-mitosis roots. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…split lpa-server, lpa-link fake device, lpa-fs-opfs, lpc-wire fixtures move to container manifest + module.json (delegated sweep); studio-core library, demo project, e2e harnesses, device-session and node-CRUD fixtures follow. Blank Created packages now also write a minimal module.json so they stay loadable — gated to blank creates so device pulls stay byte-faithful and adoption parity hashes cannot diverge. fw-browser uid patcher re-anchored on the manifest form (was silently broken by the split), smoke.html fetches module.json, package_manifest rewritten on ProjectManifest. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…P2 tests + ADR Settings popup renders format/uid/name read-only from the container manifest (UiProjectManifest on ProjectEditorView, sourced from the active library package); gallery rename now patches the manifest name as well as the slug. New tests: device-path wrong-format + missing-manifest refusal on the emulated firmware (rides the booted scene_render_emu), vendored module folder loads under the host project's gate (Q10 pinned), and a corpus byte-identity rewrite check for both split files. modules.md section-6/Q10 notes, schemas README, and the mitosis ADR. ADR: docs/adr/2026-08-01-project-module-mitosis-container-format-gate.md Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…provenance (P3) ModuleDef gains the authored surface the ratified model requires: bindings (R7's contention pick becomes authorable), exports (local slot -> inner-scope channel, bus: ref form; runtime interpretation lands with the module runtime), per-channel meta overrides (Q1 curation escape hatch, the spike's display vocabulary minus aliasing), and provenance. ProvenanceDef (author/version/license/created, settled Q7) is a capability of any node def; the container manifest carries the same four keys. Schemas regenerated; byte-stable round-trip pinned by test. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…egistry (P4)
Scope identity becomes a queryable engine fact (modules.md R1/R2):
ScopeRef {Module, Sink} stored on every runtime node entry, assigned by
ensure_runtime_spine on BOTH load and apply so an edited project can
never wear different scopes than a reloaded one; present on Pending/
Failed entries and untouched by payload reattach. Query API on the tree:
scope_of, scope_introduced_by, scopes, scope_channels (R3/R4 listing
grouping), scope_persist_path — sink scopes key by the authored playlist
entry (…/entries[k]) so panel state will follow the slot, not the
content, and stay stable under sibling reorder and reattach.
The dead runtime Bus/ChannelEntry value registry is deleted (its only
construction was an empty throwaway for DestroyCtx); Destroy/MemPressure
contexts slim to node+frame. No resolution behavior change — pinned by
the load-vs-apply differential and the untouched engine suite.
Plan: lp2025/2026-08-01-1003-modules-impl-roadmap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion rule deleted (P5)
QueryKey::Bus becomes {scope, channel}: bus demand carries the READING
node's scope into the cache and cycle-detection identity, so same-named
channels in different scopes can never collide or fake a cycle. The
resolver stays scope-dumb — the host answers 'which providers win for a
read from this scope' via NodeTree::providers_for_bus_read, a pure
outward walk (R5) that never descends into a scope, which is what makes
sink-scope no-demand hold by construction (R2) instead of via the
spike's probe-side filter. Provider re-reads key off the provider's own
scope; unscoped public reads resolve from the root scope.
The playlist ownership-suppression rule is deleted: entry children now
default-publish visual.out like every producer — into their entry's sink
scope, invisible to enclosing reads by construction (tests flipped to
pin the new shape, incl. probe-with-values never ticking a sink-scope
producer). Three correctness obligations pinned: adversarial two-scope
cache soundness, depth-2 writer-shadowing/inheritance (E5 minus the P6
mirror), and the load-vs-apply wiring differential.
Plan: lp2025/2026-08-01-1003-modules-impl-roadmap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed (P6) Every module node — root included — now wears a live ModuleNode runtime (harvested from the spike's ProjectNode mirror): produce resolves the module's own introduced scope's visual.out through the scoped resolver key and forwards render/sample dispatch to the producing node; a scope with no visual writer renders cleared (a module without a visual is a legitimate shape). Never feature-gated — the runtime links into every firmware image (C6 headroom after: 243,952 B). register_node_bindings' module early-return is gone: modules register authored bindings (the R7 contention pick), authored exports (inner channel republished under the export name — module-owned bus reads resolve from the INTRODUCED scope via the host reading-scope rule), and the automatic output→visual.out fallback publish for non-root modules (the drop-in rule). The apply-path root reattach installs the same runtime, so an applied project equals a reloaded one. Primary visual becomes an engine-reported role: WireBusChannel gains primary_visual, decided once in the probe (root-scope listing of the vocabulary channel); Studio's channel.name == "visual.out" string tests are gone. E5 depth-2 is pinned: a consumer beside an embedded module resolves the sibling's publish, never root's. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Supersedes the #218 branch ADRs (2026-07-28-scoped-buses, 2026-07-28-effects-are-projects) with pointers. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ommit branch switch The dx build rewrites lp-app/lpa-studio-web/assets/tailwind.css in the CI working tree; the dirty tracked file aborted 'git checkout -B' in the auto-commit step, so genuine story drift could never self-commit. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-modules-roadmap-4cdecb
…::Project refs Main's new feature-gate tables (lpc-model/src/feature.rs, lpc-engine/src/features.rs) referenced the pre-rename kind; renamed to NodeKind::Module (module stays ungated — None feature mapping). Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30722947317
Contributor
CI refreshed the story baselines on this branchThe Review every PNG in the PR's Files changed view (swipe / onion-skin). 14 more file(s)
|
WireScopeRef {Module, Sink} lands on the wire: WireBusChannel carries the
scope a channel lists in (channels enumerate PER SCOPE — same-named
channels in different scopes are distinct rows, each resolving its own
value) and WireBindingEndpoint::Bus carries the endpoint's scope (write
lands / read starts). Sink scopes never list — the modeled R2 property
honored by enumeration, no row filter. Display strings are gone from the
engine: Studio derives scope labels from its own node tree
(UiBusChannelView keys by (scope, name); the bus pane prefixes embedded
scopes minimally until the module face lands). WIRE_PROTO 5 -> 6.
NodeTree grows the listing counterparts of the shadowing walk
(scope_channels split by endpoint direction, providers/consumers-in-
scope, bus_read_scope shared with the resolve host). Probe tests verified
running (no duplicated-attr hazard); new test pins two same-named scoped
rows arriving structured with per-scope values.
Plan: lp2025/2026-08-01-1003-modules-impl-roadmap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PanelWriterStore on the Engine, keyed (scope, channel) with VecMap (firmware-safe): writers materialize on first touch, latch until an explicit clear, and live OUTSIDE the binding index — apply_project_changes rebuilds bindings from defs and must never destroy an engaged control (pinned by test). The resolve-host walk overlays the store: an engaged scope counts as having a writer for shadowing, and the engaged entry REPLACES the scope's provider set at BindingPriority::panel() — which is what makes max-priority-wins hold on ByKey merge channels too (the settled decision), and what turns two ambiguous authored writers into a resolvable channel while engaged. All 8 EngineResolveHost sites route the store, including the probe's: engaged values resolve in probe reads and surface as WireBindingOrigin::Panel provider rows for the UI. Engage/shadow/inherit/clear, replace-not-merge, probe visibility, and apply-survival are all pinned by tests. panel.md P4 implemented note. Plan: lp2025/2026-08-01-1003-modules-impl-roadmap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ontrols Panel controls stop editing authored defaults and start writing the channel they present. A control whose backing slot consumes a bus channel now dispatches PanelWrite at that (scope, channel) down the runtime command channel — no overlay entry, no dirty flag, no Save panel row — on the playlist-activate precedent. A control with no channel behind it keeps the slot-edit path, so nothing that worked before stops working. - wire: WireProjectCommand::PanelWrite/PanelClear as PROJECT-level arms (they address a scope, not a node); three-way clear (channel / scope / all); WIRE_PROTO 6. - server: Project::panel_write/panel_clear; a gesture naming a scope whose owner is gone is a normal Rejected, never a transport error. - studio: PanelWriteOp/PanelClearOp with verdict-chase tightening, coalesced per (scope, channel) in the actor's batch planner beside the slot-edit flood rule — a clear is a barrier, so write/clear/write reaches the server in order. - UiPanelTarget rides the probe's consuming Bus endpoint onto the control; knob, fader, and toggle dispatch through it, and an engaged control gets a clear affordance. - momentary (P14): the TTL is an engine-time deadline and the writer despawns in the tick, so a dropped client's gesture releases itself; renewal is just the next write. panel.md: P8/P14 implemented notes; P-Q4 settled as leaned (clear-all reaches sink scopes); P-Q1 settled minimally (no slew, seam recorded as writer-side shaping). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scarf test: dimmed from a phone at 4 a.m., unplugged, replugged — back dim, with not one bright frame. That requirement is what fixes the restore seam at Engine construction (Project::new, and Project::reload because it rebuilds the Engine), before the first tick and therefore before the first render, rather than at some later ready event. On device that is the boot path. - /.lp/panel.json in the framework tier, version 1, bump-and-refuse on anything else; lenient load (missing, corrupt, wrong version, unknown scope -> clean boot) and best-effort write. - Keys are scope-path/channel from ScopeRef::persist_path, so state survives reload, reattach and sibling reorder; a sink scope keys by the ENTRY, so state follows the slot, not the content. - Writes throttled ~10s and gated on a new PanelWriterStore mutation COUNTER: a clear plus a re-write inside one window leaves an identically-shaped map, which a (len, revision) comparison would miss. An idle project writes nothing. Unload flushes past the throttle. - Momentary writers never persist (P14). Auto-save records its own choice, so "off" does not quietly re-enable overnight. The prerequisite, done first: /.lp/** is filtered out of refresh_artifacts before anything reads the batch. Without it every save would rebuild the binding graph and the rebuild would schedule the next save. applied_refresh_count makes that observable rather than assumed. Also makes the fixture brightness fader derive a panel target like the shader knobs do. It is inert today — see the defect below — but the scarf's own control must not be the one that silently lacks the feature. Findings recorded rather than absorbed: - docs/debt/panel-state-serde-flash-cost.md — this costs 50,512 B of C6 flash. ELF symbol diff says it is a SECOND LpValue JSON codec: the image already carries read_lp_value<JsonSyntaxSource> (13.6 KB) for artifacts, and the serde derive instantiated a whole new one against serde_json. Exit criteria names the cheap fix. - docs/defects/2026-08-02-authored-source-bindings-silently-dropped.md — the loader registers authored `source` bindings only for hand-listed slot names per kind, and drops any others with no error. Fixture brightness therefore cannot be bound to a channel at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main moved substantially since 2026-08-01 (firmware manifest, capability
reporting on hello, device backup, fixture power limits, the slot-role
taxonomy). Beyond textual conflicts, four decisions:
- WIRE PROTO renumbered. Main took 5 for the ServerHello reshape; this
branch had claimed 5 (module rename) and 6 (scoped buses). Mine move
up: rename = 6, scope/panel = 7, WIRE_PROTO_VERSION = 7.
- Slot taxonomy adopted: `policy = "read_only_persisted"` is now
`role = "fixed"`, SlotPolicy -> SlotRole. ModuleDef.nodes carries
Fixed; ModuleMirrorState drops its container-level default (every
field already declares `produced`). Regenerated the one schema this
changed.
- project_def.rs was modified on main and deleted here by the mitosis.
Main's edits were exactly the policy->role migration, so the deletion
stands and the migration moved to module_def.rs.
- Engine gains BOTH sides: panel_writers beside safe_output_clamp_q16.
Three format-migration gaps, all the same shape — a migration only
reaches the fixtures that exist when it runs, and main added these on
the other side of the fork:
- five projects/test/quad-*-v3 projects still carried a pre-mitosis
single project.json; split into manifest + module.json.
- the device-backup e2e test seeded {"kind":"Project"} for its fake
device, which the D-A manifest gate now refuses — the device went
unidentified and the backup named itself "device". Migrated.
- three SlotPolicy references inside #[cfg(test)] modules, which
`just check` never compiles (clippy-host has no --all-targets).
`just check` and `just test` green; the one failure in the last run was
the known load-sensitive lps-probe perf assert (12.0s under load, 8.4s
alone against a 10s bound), logged to its debt entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main moved again mid-merge (PR #259). Four resolutions: - SlotRole::State is now REQUIRED on produced fields, so ModuleMirrorState regains a container default_role = "state" — the same concept whose older spelling (default_policy = "read_only_transient") this branch had just removed as stale. The vocabulary changed twice; the declaration was always needed. - Main strengthened the overlay-changed-at probe to load a real root and validate the edit. Post-mitosis `nodes` is Fixed and identity lives in the manifest, so the probe now stages `exports[glow]` — a genuinely writable authored map on ModuleDef — and seeds BOTH halves of the project (the container manifest gates the load; D-A refuses without). - Story baselines are CI-canonical: took main's regenerated PNGs, including one it deleted. - Main's new debug-clear tests address `/demo.project/...`; P1 renamed path segments to `.module`. Swept all five occurrences. Every failure in this merge shares one shape: this branch renamed a vocabulary, main kept authoring against the old one, and no CI could see the combination. Four so far — NodeKind::Project, the project.json container split, SlotPolicy, and `.project` path segments. The gate is green except the known load-sensitive lps-probe perf assert, whose verdict is deferred to CI: a sibling agent session had this machine at load average 143, so flake and regression are locally indistinguishable. Logged that limitation in its debt entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The firmware manifest is supposed to describe the image it is embedded in, and every chip's checked-in expectation still claimed wireProto 5. Renumbering the protocol to 7 (main took 5 for the ServerHello reshape, so the module rename became 6 and scoped buses 7) made all four stale, which is exactly what this check exists to catch: a protocol bump must be declared, not slipped in. CI caught it on all three firmware jobs. Verified locally per chip: - esp32c6: image 3,003,104 B / 3,145,728 B, headroom 142,624 B; manifest check passes. - esp32s3: image 1,926,336 B / 6,291,456 B, headroom 4,365,120 B; manifest check passes. Nothing structural: the Xtensa firmware builds the panel work fine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30766114996
…sion Both local failures (12.02s, 10.88s/11.56s) were machine load; CI's dedicated runner passed the suite. Recorded so the next person reading this entry knows the ambiguity was resolved, and what it cost to resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
















Plan: lp2025/2026-08-01-1003-modules-impl-roadmap
Path: ~/.photomancer/planning/lp2025/2026-08-01-1003-modules-impl-roadmap/plan.md
Implements the ratified modules/buses/panels design (docs/design/modules.md, panel.md, PR #230). This PR carries the P1–P6 stretch, ending at gate G3 (behavior review).
Complete:
ADRs: 2026-08-01-project-module-mitosis-container-format-gate, 2026-08-01-scoped-bus-engine-architecture (supersede the #218 branch ADRs).
At gate G3 (behavior) — remaining phases P7–P13 continue after the gate. Do not merge PR #248 or the #218 branch — harvest sources only.
Resume notes: read plan.md + phase files in the planning dir; discovery in ../2026-07-31-1002-modules-buses-panels/notes.md.
🤖 Generated with Claude Code