Summary
emit_streams_for in crates/patchlang/src/builder/canvas_emit.rs:742 silently drops AES67 streams whose interface_id references a card-slot interface (i.e., a port contributed by an installed expansion card rather than the chassis template).
Repro
Setup: any device with cardSlotGroups and an installed AES67 card. Concrete example from a real project — Riedel Artist 64 with an AES67-108 G2 card installed in Client_Slot[1]. User adds an AES67 stream on the card's AES67_Out port.
The frontend serializes the stream as:
{
"label": "Artist_64_to_QSYS",
"protocol": "AES67",
"channel_count": 8,
"interface_id": "slot::Client::0__1__pl::AES67_108_G2::AES67_Out"
}
Expected: a stream declaration appears in the emitted .patch referencing the card-slot port.
Actual: no stream declaration is emitted. The AES67 stream silently disappears, and reload from the resulting .patch returns no streams for that instance.
Root cause
// canvas_emit.rs:742
for stream in streams {
let Some(iface) = inst.interfaces.iter().find(|i| i.id == stream.interface_id) else {
continue; // ← drops the stream
};
...
}
inst.interfaces contains only chassis interfaces. The compound interface_id produced by computeEffectiveInterfaces (frontend) for card ports doesn't match anything in that list, so the else continue fires and the AES67 stream is silently lost. There's no diagnostic, error, or warning.
This contradicts the design-guide spec (docs/patchlang-design-guide/compiler.md:464) which says card ports flat-merge into the instance's effective namespace, and the corresponding S03/S04/S05 rules that route/connect/bus emission already use effective ports.
Verification
Diagnosis cross-checked against:
patchlang-architecture skill — confirmed the sidecar-as-fallback frontend workaround is an explicit anti-pattern and that "fix the root cause" belongs in PatchLang
signalcanvas-patchlang skill (v0.2.8) — confirmed the spec says card ports flat-merge with no slot-qualified syntax, consistent with this bug being an implementation gap rather than a spec ambiguity
Live diagnostics from a running project (browser DevTools, in-memory canvasScene store) confirmed:
- Card-slot AES67 streams have compound
interfaceId containing __ (e.g., slot::Client::0__1__pl::AES67_108_G2::AES67_Out)
- Chassis-port streams have
interfaceId of the form pl::TemplateName::PortName (no __) and round-trip cleanly — only card-slot AES67 streams are affected
Likely broader scope
Other stream-capable card protocols (Dante, QLAN, Ravenna) on card-slot ports almost certainly hit the same path. Beyond streams, the same pattern probably needs review in:
emit_connections_for — connections to/from card-slot ports
emit_channel_labels — labels on card-slot ports
emit_routes_for — internal routes on card-slot ports
We haven't confirmed those break — only flagged them. May warrant a sweep through canvas_emit.rs.
Suggested fix
Resolve compound interface_id values against installed_cards before falling back to "drop." Either:
- Build an effective interface lookup at the top of
emit_streams_for (chassis + installed-card ports), keyed by the same compound ID format the frontend uses, OR
- Parse the compound form
{slotId}__{cardIfaceId} and look up the card port directly via the slot/card lookup tables.
Should also probably emit a diagnostic if a stream's port can't be resolved at all, instead of silent continue.
Frontend workaround in place
To unblock users, the SignalCanvas frontend now stores card-slot AES67 streams in the .layout.json sidecar and restores them on load (restoreSidecarCardSlotStreams in loadFromPatchLangHelpers.ts). This violates the patchlang-architecture principle that the sidecar holds positions only — it's marked TEMP and pointed at this issue. Delete the workaround once the Rust fix ships.
Affected version
WASM rebuilt from SignalCanvasLang master at SignalCanvas commit cfff868 (May 5, 2026). Master HEAD: 7bd94ed.
Summary
emit_streams_forincrates/patchlang/src/builder/canvas_emit.rs:742silently drops AES67 streams whoseinterface_idreferences a card-slot interface (i.e., a port contributed by an installed expansion card rather than the chassis template).Repro
Setup: any device with
cardSlotGroupsand an installed AES67 card. Concrete example from a real project — Riedel Artist 64 with an AES67-108 G2 card installed inClient_Slot[1]. User adds an AES67 stream on the card'sAES67_Outport.The frontend serializes the stream as:
{ "label": "Artist_64_to_QSYS", "protocol": "AES67", "channel_count": 8, "interface_id": "slot::Client::0__1__pl::AES67_108_G2::AES67_Out" }Expected: a
streamdeclaration appears in the emitted.patchreferencing the card-slot port.Actual: no
streamdeclaration is emitted. The AES67 stream silently disappears, and reload from the resulting.patchreturns no streams for that instance.Root cause
inst.interfacescontains only chassis interfaces. The compoundinterface_idproduced bycomputeEffectiveInterfaces(frontend) for card ports doesn't match anything in that list, so theelse continuefires and the AES67 stream is silently lost. There's no diagnostic, error, or warning.This contradicts the design-guide spec (
docs/patchlang-design-guide/compiler.md:464) which says card ports flat-merge into the instance's effective namespace, and the corresponding S03/S04/S05 rules that route/connect/bus emission already use effective ports.Verification
Diagnosis cross-checked against:
patchlang-architectureskill — confirmed the sidecar-as-fallback frontend workaround is an explicit anti-pattern and that "fix the root cause" belongs in PatchLangsignalcanvas-patchlangskill (v0.2.8) — confirmed the spec says card ports flat-merge with no slot-qualified syntax, consistent with this bug being an implementation gap rather than a spec ambiguityLive diagnostics from a running project (browser DevTools, in-memory canvasScene store) confirmed:
interfaceIdcontaining__(e.g.,slot::Client::0__1__pl::AES67_108_G2::AES67_Out)interfaceIdof the formpl::TemplateName::PortName(no__) and round-trip cleanly — only card-slot AES67 streams are affectedLikely broader scope
Other stream-capable card protocols (Dante, QLAN, Ravenna) on card-slot ports almost certainly hit the same path. Beyond streams, the same pattern probably needs review in:
emit_connections_for— connections to/from card-slot portsemit_channel_labels— labels on card-slot portsemit_routes_for— internal routes on card-slot portsWe haven't confirmed those break — only flagged them. May warrant a sweep through
canvas_emit.rs.Suggested fix
Resolve compound
interface_idvalues againstinstalled_cardsbefore falling back to "drop." Either:emit_streams_for(chassis + installed-card ports), keyed by the same compound ID format the frontend uses, OR{slotId}__{cardIfaceId}and look up the card port directly via the slot/card lookup tables.Should also probably emit a diagnostic if a stream's port can't be resolved at all, instead of silent
continue.Frontend workaround in place
To unblock users, the SignalCanvas frontend now stores card-slot AES67 streams in the
.layout.jsonsidecar and restores them on load (restoreSidecarCardSlotStreamsinloadFromPatchLangHelpers.ts). This violates the patchlang-architecture principle that the sidecar holds positions only — it's markedTEMPand pointed at this issue. Delete the workaround once the Rust fix ships.Affected version
WASM rebuilt from
SignalCanvasLangmaster at SignalCanvas commitcfff868(May 5, 2026). Master HEAD:7bd94ed.