Skip to content

Feature/hardware#43

Merged
Yona-Appletree merged 94 commits into
mainfrom
feature/hardware
Jun 16, 2026
Merged

Feature/hardware#43
Yona-Appletree merged 94 commits into
mainfrom
feature/hardware

Conversation

@Yona-Appletree

Copy link
Copy Markdown
Member

Summary

Validation

Contribution Terms

By opening this pull request, I confirm that I have the right to submit these
changes and agree to the contribution terms in CONTRIBUTING.md.

Yona-Appletree and others added 30 commits May 21, 2026 16:27
- Bootstrap lpc-node-registry with freshness-only artifact layer
- Acquire/release refcount, revision bumps, structured read failures
- Transient read_bytes via LpFs; delete lpc-slot-mockup
- Roadmap and M1 plan docs

Plan: docs/roadmaps/2026-05-21-artifact-routed-file-reload/m1-artifact-store/
Co-authored-by: Cursor <cursoragent@cursor.com>
…(M2)

- Implement registry types, def walker/shell, load_root bootstrap, and sync diff
- Report NodeDefUpdates with shell/body rules and kind-change semantics
- Add NodeDefView stub; 22 unit tests including gate scenarios T1–T5

Plan: docs/roadmaps/2026-05-21-artifact-routed-file-reload/m2-node-def-registry/
Co-authored-by: Cursor <cursoragent@cursor.com>
…load

- Per-node error state when SVG mapping resolve fails during project load
- Update fixture mapping tests to assert node-level failure
- Add early artifact-routed reload planning notes; format M5 roadmap tables

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add SourceFileSlot custom codec in lpc-model ($path, shorthand, inline)
- Add SourceFileRef, resolve_source_file, and materialize_source in parallel stack
- Use LpPath::extension for file extension hints; effective version max(slot, artifact)

Co-authored-by: Cursor <cursoragent@cursor.com>
- Registry owns ArtifactStore; load_root and sync return SyncResult
- Source revision bumps for GLSL/SVG-only file edits via per-def deps
- DefChangeDetail classification and S1–S6 integration scenario tests

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add standalone changeset-change-management roadmap with M1–M7
- Gate parent engine cutover on ChangeSet M6 diff + equivalence
- Document overlay-in-registry architecture and effective-read contract
- Add parent M10 ExplainSlot probe milestone; defer provenance from hot path

Co-authored-by: Cursor <cursoragent@cursor.com>
- Design overlay inside NodeDefRegistry with apply/discard lifecycle
- Four implementation phases: types, overlay store, apply, validation

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add change language types with serde and ChangeOverlay on NodeDefRegistry
- Implement apply/discard for SetBytes and Delete; slot ops deferred to M4
- Add overlay lifecycle integration tests for D1 and D3

Co-authored-by: Cursor <cursoragent@cursor.com>
- Design read_effective_bytes and NodeDefView effective get API
- Four phases: bytes read, def parse, view projection, validation

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add overlay-first effective reads and TOML parse path
- Wire NodeDefView::get/state to effective entry projection
- Keep registry.get committed-only; add effective_projection tests

Co-authored-by: Cursor <cursoragent@cursor.com>
- materialize_source checks ChangeOverlay before committed store
- Add NodeDefRegistry::materialize_source wrapper
- Add C4a–d asset overlay integration tests

Co-authored-by: Cursor <cursoragent@cursor.com>
- Apply SetSlot/Map*/OptionSet to overlay slot drafts forked from committed defs
- Serialize draft trees to TOML bytes for effective reads and commit prep
- Route inline child paths through invocation body mutation; add slot mutation helpers

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add CommitError and commit_overlay flush/re-derive path
- Expose NodeDefRegistry::commit returning SyncResult
- Add D2/D5/C2 integration tests in commit_promotion.rs
- Backfill M3/M4/M5 roadmap summaries and commit contract

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add ProjectSnapshot, diff(base, target), and assert_equivalent
- Slot-tree def diff with apply verification via serialized TOML
- A1/B1 gate tests against examples/basic and basic2
- Fix MapInsert for record maps and project nodes[*].def SetSlot routing

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep project snapshot diff and equivalence checks host/CI-only so
embedded consumers can omit the harness via default-features = false.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace overloaded Change* names with edit/ module types (EditOp,
ArtifactEdit, EditBatch, SlotOverlay) and update registry APIs. Add M8
session/sync plan docs, ChangeSet roadmap summary, and M7 doc cleanup.

Plan: docs/roadmaps/2026-05-21-changeset-change-management/m8-edit-session-sync/
Co-authored-by: Cursor <cursoragent@cursor.com>
- Route registry ingress through SyncOp batches and SyncOutcome
- Keep pending state in SlotOverlay only (Apply/Remove/ClearPending CRUD)
- Rename FsChange to FsEvent across lpfs, server, cli, and firmware
- Add pending_sync tests and update M8 plan docs

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace NodeDefRef/custom codec with a slotted Ref|Def invocation model,
generic VariantSet/SetSlot apply, and ref= TOML wire across examples and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Allow reserved child slots with unset = {} wire form; skip unset invocations
in the registry and reject them at project load time.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Tagged ArtifactEdit::{Slot, Asset} replaces flat mixed ops list
- Rename SetBytes to AssetEdit::ReplaceBody; slot ops use AssignValue/UseOption naming
- Align apply/diff and harness tests with overlay DefDraft vs Bytes split
- Add M10 plan docs and update change-language spec

Co-authored-by: Cursor <cursoragent@cursor.com>
- Key ArtifactStore by ArtifactId; drop raw u32 map keys and handle naming
- Use PascalCase ArtifactEdit kind tags on wire; transparent ArtifactId serde
- Add engine-registry-cutover roadmap with M0.1 stabilization design notes
- Point artifact-routed M6 at the promoted cutover roadmap

Co-authored-by: Cursor <cursoragent@cursor.com>
Move durable path↔ArtifactId registration into ArtifactStore; registry
consumes store lookups and unregisters only unreferenced artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
…entity

- Remove ArtifactId; catalog, registry, and edits key by ArtifactLocation
- Serialize locations as file:/… URI strings on the wire
- Simplify ArtifactStore to a single by_location map without id indirection

Co-authored-by: Cursor <cursoragent@cursor.com>
- Rename type and module; align helpers and errors to specifier terminology
- Update registry, engine, lpv-model call sites and active plan/roadmap docs

Co-authored-by: Cursor <cursoragent@cursor.com>
- Remove asset deps, path index, SourceRevisionBump, and sync_source_path
- Asset fs changes only bump ArtifactStore revision; nodes pull on prepare
- Register asset paths from defs for catalog reconcile; add artifact_revision_for_path

Co-authored-by: Cursor <cursoragent@cursor.com>
…verlay

- Add ArtifactOverlay keyed by ArtifactLoc with ordered slot upserts and PendingAsset
- Fold pending edits in projection and commit; remove DefDraft and SlotOverlay
- Add pending introspection API and implementation plan docs

Co-authored-by: Cursor <cursoragent@cursor.com>
…pserts

- Store slot pending as Vec<SlotEdit> with upsert via SlotEdit::pending_target
- Remove string slot key helpers; simplify has_pending_at_path to path equality

Co-authored-by: Cursor <cursoragent@cursor.com>
Yona-Appletree and others added 21 commits June 12, 2026 17:58
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Internally-tagged enums (#[serde(tag = ...)]) force serde's Content
buffering machinery, which cascades a second Deserialize instantiation
through every nested type. Flipping all 17 wire/snapshot enums to the
externally-tagged default eliminates the entire Content cluster from
the esp32c6 firmware (76 symbols -> 0).

Authoring formats are unaffected: TOML 'kind = ...' keys parse via the
Slotted codec, and 'sampling = ...' via FromLpValue, not serde.
Constraint stays untagged by design (peer-key authoring inference).

fw-esp32 esp32c6,server: .text -54,968 B, .rodata -16,696 B (-71.7 KB)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vendor lp-collection (lp-common @ c27c24b: ChunkedVec, ChunkedHashMap)
into lp-base/lp-collection and add VecMap/VecSet: sorted-Vec map/set
with the BTreeMap/BTreeSet API subset the workspace uses (incl. Entry,
range, Index, serde). Switch the workspace dep from git to path.

Swap all BTreeMap/BTreeSet uses in lpc-model, lpc-engine, lpc-wire,
lpc-registry, lpc-shared, lpc-hardware, fw-esp32, and lp-cli's project
scaffold. Every distinct BTreeMap (K, V) pair cost ~2-5 KB of B-tree
node machinery in flash; the sorted-vec forms compile to binary search
plus memmove and share Vec's existing codegen. Iteration order and key
uniqueness semantics are unchanged.

fw-esp32 esp32c6,server: .text -254,008 B, .rodata -25,416 B (-279 KB)
Cumulative with external serde tagging: image 3,447,616 -> ~3,096,500 B,
back under the 3 MB app partition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swap remaining BTreeMap/BTreeSet uses in lps-glsl, lps-shared, lpir,
lpvm, lp-shader, lpvm-native, lpvm-cranelift, and lps-frontend onto
lp_collection::{VecMap, VecSet}.

Note: lps-frontend's test target has a pre-existing compile error
(rfind on ChunkedVec iter at lib.rs:602) that predates this change.

fw-esp32 esp32c6,server: .text -72,232 B, .rodata -7,896 B (-80 KB)
Image: 3,096,464 -> ~3,016,300 B (~129 KB under the 3 MB partition)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Slotted derive inlined every embedded record's full shape at each use
site, so the shape-registry snapshot re-described the same records many
times over. NodeArtifact (EnumSlot<NodeDef>) alone serialized to 17.3 KB
by inlining all 11 node-def variants — larger than the device's 16 KB
project-read chunk budget, so the debug shape-sync frame carrying it
truncated and the client failed to parse the registry.

Make the record FieldSlot impl emit SlotShape::Ref { id: SHAPE_ID } when a
record is embedded as a field or enum-variant payload. The canonical
slot_shape() keeps the full record; embedding sites resolve through the
registry. Safe because the codegen registers every Slotted struct as its
own catalog entry, and the data codec already resolves Ref on read/write.
Use #[slot(record)] to force the old inline behavior.

Full catalog snapshot: 46,637 -> 22,114 B; largest single shape now 3.5 KB,
so the existing limit=4 shape paging keeps every frame under budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The RMT hardware driver layer was silent — only the provider logged a
channel handle, so a failed/misrouted RMT init or wrong GPIO gave no
signal. Add INFO logs at the driver open boundary: Esp32RmtWs281xDriver::open
(endpoint + resolved GPIO + byte_count), ensure_rmt_initialized (init /
reuse / conflict branches), and LedChannel::new (RMT channel config).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-existing lints that blocked `just ci`'s clippy gate once earlier crates
compiled: manual div_ceil and a missing is_empty in ChunkedVec, a redundant
field name in lpc-registry, and uninlined format args in lpc-engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These compile errors were masked in CI by an earlier-aborting crate; once that
cleared they blocked the test build:

- lps-filetests: texture_specs is now VecMap (was BTreeMap), LpirModule.functions
  is VecMap, and LpirBody replaces Vec<LpirOp> (add .into()); add the
  lp-collection dependency.
- lps-frontend: LpirBody.iter() is not double-ended; rfind -> filter().last().
- lp-cli: AssetSlot::path_value was removed; use artifact_value().to_string().

Cargo.lock carries the new lps-filetests -> lp-collection edge (plus
pre-existing dependency-resolution churn already present on the branch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AssetSlot derived plain struct serde, so it serialized as {value, revision}
instead of the compact authored form the semantic-slot test expects
(asset = "shader.glsl"). Give AssetSlot the ValueSlot pattern (serialize the
bare value, stamp the ambient revision on deserialize), and hand-write a
streaming Visitor for AssetSlotValue: an artifact path round-trips as a bare
string, inline bodies as tables.

Deliberately NOT #[serde(untagged)] — untagged (like internally-tagged) buffers
the whole input into serde's `Content` tree and re-parses, monomorphizing that
heavy machinery into the deserialize graph: the exact flash cost the
externally-tagged-enum work removed. A Visitor dispatches on input shape in one
streaming pass.

source_slot_sync: `source` is now an AssetSlot whose synced snapshot is the bare
artifact path value, not a record with a `path` field; navigate `source`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ambient revision is a process-wide atomic that parallel tests both set and
read while stamping deserialized slots, so libtest's thread pool made
round-trip equality tests (node_invocation_round_trips) flaky. Back it with a
per-thread cell under cfg(test) (production keeps the atomic; fw is never
cfg(test)).

Quarantine events_example_merges_bus_maps_into_visual_shader: it renders black
under heavy parallel CPU load (reproduces under `just ci`, passes standalone).
Ruled out the revision race and wall-clock; looks like a render/JIT data race.
Pre-existing; tracked separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Yona-Appletree and others added 7 commits June 13, 2026 13:18
Resolves a divergence where the branch's hardware refactor (lpc-hardware
extraction, Hw* renames, asset slots, sorted-vec collections) overlapped main's
work. Key resolutions:

- esp deps: adopt main's published crates (esp-radio 0.18.0) over the branch's
  local ../oss/esp-hal checkout (esp-radio 1.0.0-beta.0), which only built on
  one machine. Fixed the one resulting API delta: esp_hal::usb::usb_serial_jtag
  -> esp_hal::usb_serial_jtag (the local fork's reorg).
- Convergent fixes (both sides did them): shader nested-shape refs, LpirBody
  reverse-search for the Return op, clippy/fmt — took main's versions.
- Ported main's permissive_emu_hardware_manifest + HwResource
  clear_reservation/with_display_label into the extracted lpc-hardware crate
  with Hw* names.
- Adapted main's new fixture-diagnostics tests and fyeah-button example to the
  branch's APIs (add_child NodeInvocation arg, tick/render registry params,
  HwRegistry, ref-style node authoring).
- Kept the branch's espnow driver and fw-emu (active WIP) over main's variants.

just ci green (host tests, clippy, fmt, fw-esp32 build, filetests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…failures

The `events_example_merges_bus_maps_into_visual_shader` test was quarantined
as a "render/JIT data race" because it rendered black under heavy parallel
load. Root cause is not a race: `ShaderNode::ensure_compiled` zero-fills the
target and returns Ok when compilation fails (and a compile panic is caught and
funneled to the same fallback), so a shader that fails to compile renders a
silent black frame. The render path itself is deterministic.

- assert_bright_event_pixels now surfaces any node compile/runtime error (via a
  status-refresh tick + tree scan) instead of an opaque "not bright" assertion,
  so a future flake reports the real cranelift/frontend message.
- Remove #[ignore]; the test passes on both LpsGlsl and Naga frontends and under
  the full `just test` CI path.
- Add log::warn! breadcrumbs at the shader render/sample black-fallback sites so
  a swallowed compile failure leaves a trace at runtime too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The in-progress ESP-NOW ownership refactor was written against an esp-radio
API that does not exist in the pinned 0.18.0: `WifiController::new` and
`controller.esp_now()`. It did not compile.

Keep the intended design — split the interface into manager/sender/receiver and
drop the `Rc<RefCell>` check-in/out dance — but construct via the real 0.18.0
surface: `esp_radio::wifi::new(...) -> (WifiController, Interfaces)`, hold the
controller alive, and take + `split()` the single `interfaces.esp_now` the first
time the endpoint is opened (the registry lease enforces exclusive access; the
firmware opens the radio once and never closes it, so `split` consuming the
interface is fine). The device now owns the sender/receiver directly:
`receiver.receive()` for RX, `sender.send(&BROADCAST_ADDRESS, ..).wait()` for TX.

Validated on an esp32c6 via the `test_espnow` smoke test: board boots, "radio
ready ... channel=11", and broadcasts one message/sec with send returning Ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI was failing for three reasons:

- `unwinding` E0308: the crate is bound to the nightly `core::intrinsics::
  catch_unwind` ABI (0.2.8 returns an integer; 0.2.9 switched to the bool
  return added in a later nightly). With an unpinned `nightly`, CI drifted onto
  a newer toolchain than local dev, breaking the build-std compile. Pin
  rust-toolchain.toml (and the workflow) to nightly-2026-04-27 and add rust-src
  so the toolchain and `unwinding` 0.2.8 stay in lockstep. Bump the date and the
  unwinding version together when moving forward.
- unused `lp_collection::VecMap` import in lpvm-native rt_jit/compile_job.rs
  (the one use is fully qualified) tripped `-D warnings`.
- rustfmt: collapse a match arm added in 61b5910 that was committed unformatted.

Validated: `just check` (fmt-check + clippy-host + clippy-rv32, which compiles
fw-esp32/unwinding under the pinned toolchain) passes with no warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ckstep

Bumping the pinned nightly requires touching three things that must move
together: rust-toolchain.toml, the CI workflow, and the `unwinding` crate
version (its `catch_unwind` ABI is welded to the nightly). Automate it:

- scripts/bump-nightly.sh DATE (default today, UTC): rewrites both pins, runs
  `just check`, and only advances `unwinding` if the current version fails on the
  new nightly; re-checks; leaves all changes in the tree for review (never
  commits), reverting only a speculative unwinding bump on failure.
- `just bump-nightly [DATE]` wrapper.
- docs/toolchain-notes.md: explain the pin, the unwinding/nightly ABI coupling,
  and the bump procedure.

Also pins the commented-out validate-x64 job's toolchain for consistency (the
script keeps all `toolchain:` refs in sync so re-enabling a job can't
reintroduce nightly drift).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous commit pinned the workspace rust-toolchain.toml but missed
lp-fw/fw-esp32/rust-toolchain.toml, which independently said `channel =
"nightly"`. Recipes `cd` into lp-fw/fw-esp32, so that file wins there — the
firmware build-std step then ran on a rolling nightly that CI never installed
rust-src for ("library/Cargo.lock does not exist", clippy-fw-esp32). It passed
locally only because the local rolling nightly already had rust-src.

Pin the per-crate file to nightly-2026-04-27 with rust-src, and make
bump-nightly sync every rust-toolchain.toml in the repo (not just the root) so
the pins can't drift apart again. Validated: `cd lp-fw/fw-esp32` now resolves
nightly-2026-04-27 and `just clippy-fw-esp32` builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Yona-Appletree Yona-Appletree merged commit b18a75d into main Jun 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant