Problem
Bowties has coherent draft-layer read seams on the frontend (effectiveLayoutStore.effectiveValue) and for facilities/channels documents on the backend (LayoutState::effective_facilities() / effective_channels(), ADR-0015 2026-07-03 extension), but no equivalent seam for backend CDI leaf-value reads.
LayoutState::config_tree(key) returns captured-over-saved NodeConfigTree. Draft edits reach the backend as LeafNode.modified_value (populated by the ADR-0012 mirror). Only one non-test caller — collect_event_leaves_recursive in bowties-core/src/node_tree.rs — reads through LeafNode::effective_value(). The other 13 callers of config_tree() read leaf.value and silently ignore drafts.
The gap has been latent since Spec 016 (2026-06-26, commit c7ba296 "feat: live channel state with eager resolution on connect"). At that date the backend did not yet carry drafts at all; reading leaf.value was the correct choice for the world that existed then. The 2026-07-03 mirror extension (ADR-0012) started populating modified_value on the backend, but scoped its output as save-flow input (write_modified_values), not as a general layered-read seam. ADR-0015's same-day extension established layered reads for facilities/channels documents but explicitly deferred a CDI-leaf-value equivalent.
The D6 adoption invariant ("composed bowtie ADOPTS the producer channel's existing event IDs, never regenerates them") papers over the gap for every prior workflow: composed bowties always adopt event IDs from producer leaves that already carry those IDs in the saved layer, so backend readers return the correct id from the producer side while ignoring the drafted consumer id.
Spec 020 (ABS) exposed the gap in a user-visible way because compiled templates adopt LED-pin event IDs onto target-node CDI leaves that have no upstream producer — the signal-head is the producer of its own aspect events, and the fresh drafts are the only place the correct IDs exist pre-save. The Signal-output channel row and the FacilityCard slot state dot both show "Unknown" pre-save because resolve_channel_event_ids reads baseline leaf.value and misses the composer's drafts. Fixed narrowly on the ABS branch by pointing channel_events::collect_event_leaves_under_prefix at effective_value(leaf) (single-file, single-behavior fix — tracked as a fresh slice in specs/020-abs-signaling/slices-event-wiring.md).
Proposed architectural fix
- Add
LayoutState::effective_config_tree(key: &NodeKey) -> Option<Cow<NodeConfigTree>> (or an equivalent shape — probably read-only accessor that layers LeafNode.modified_value over the tree in-place for value reads).
- Migrate the remaining backend readers of
config_tree() to the effective view where they consume leaf values. Structural walks (children iteration, address lookups) stay on config_tree().
- Add an ADR-0015 dated extension: "Backend CDI-leaf-value reads use
effective_config_tree; config_tree becomes internal / structural-only."
- Add an entry to
aiwiki/seams.md: "Backend CDI Layered Read" (Owner: LayoutState::effective_config_tree; Contributors: LeafNode::effective_value, configDraftMirrorOrchestrator; Consumers: resolve_channel_event_ids, compile_logic_for_facility, compose_facility_bowties, catalog rebuilds, …).
- Note this ADR extension in
aiwiki/architecture-health.md.
Callers to migrate
Non-test call sites of LayoutState::config_tree() (from 2026-07-26 audit — bump list before starting work):
app/src-tauri/src/commands/channel_events.rs resolve_channel_event_ids — already fixed narrowly on the ABS branch. Confirm it moves to the new seam.
app/src-tauri/src/commands/facility_bowties.rs — per-node CDI seeding for composer + in-process producer-event-id resolution.
app/src-tauri/src/commands/logic_adapter.rs — compile input path, compile output path (compile target path can stay on config_tree() — writes structural offsets, not user-drafted values).
app/src-tauri/src/commands/layout_capture.rs — inline compose seeding.
app/src-tauri/src/commands/bowties.rs catalog build — already routes through collect_event_leaves_recursive which uses effective_value; confirm.
app/src-tauri/src/commands/cdi.rs — get_node_tree and its fallback loops return whole trees; frontend layers via effectiveLayoutStore. Leave on config_tree(); the frontend seam is the layered surface here.
Regression class prevented
Any future backend CDI-reading command silently ignoring drafts. Every downstream workflow — compiled-template composers for future templates (Head Repeater, Approach Lighting, block-signal interlock, …), catalog rebuilds, sync classifiers, load-time reconcilers — inherits correct behavior automatically. Structurally prevents the "next feature exposes the same latent gap" fingerprint.
Deferral rationale
Not blocking the ABS release. The narrow fix on channel_events.rs handles the only user-visible surface today. The rest of the callers are guarded by D6 (adoption invariant paper-overs the gap) or don't touch user-drafted leaves (structural reads). The architectural refactor is sequenced after Track 2 (spec 020) finishes so the compile/reset/compose triad is stable before consolidation.
Related
- ADR-0012 (2026-07-03 extension — draft-to-backend mirror; write-flow scope)
- ADR-0015 (2026-07-03 extension — layered reads for facilities/channels documents; CDI leaf values deferred)
- Spec 020 (ABS) — first user-visible exposure of the gap; narrow fix landed on the branch
Problem
Bowties has coherent draft-layer read seams on the frontend (
effectiveLayoutStore.effectiveValue) and for facilities/channels documents on the backend (LayoutState::effective_facilities()/effective_channels(), ADR-0015 2026-07-03 extension), but no equivalent seam for backend CDI leaf-value reads.LayoutState::config_tree(key)returns captured-over-savedNodeConfigTree. Draft edits reach the backend asLeafNode.modified_value(populated by the ADR-0012 mirror). Only one non-test caller —collect_event_leaves_recursiveinbowties-core/src/node_tree.rs— reads throughLeafNode::effective_value(). The other 13 callers ofconfig_tree()readleaf.valueand silently ignore drafts.The gap has been latent since Spec 016 (2026-06-26, commit
c7ba296"feat: live channel state with eager resolution on connect"). At that date the backend did not yet carry drafts at all; readingleaf.valuewas the correct choice for the world that existed then. The 2026-07-03 mirror extension (ADR-0012) started populatingmodified_valueon the backend, but scoped its output as save-flow input (write_modified_values), not as a general layered-read seam. ADR-0015's same-day extension established layered reads for facilities/channels documents but explicitly deferred a CDI-leaf-value equivalent.The D6 adoption invariant ("composed bowtie ADOPTS the producer channel's existing event IDs, never regenerates them") papers over the gap for every prior workflow: composed bowties always adopt event IDs from producer leaves that already carry those IDs in the saved layer, so backend readers return the correct id from the producer side while ignoring the drafted consumer id.
Spec 020 (ABS) exposed the gap in a user-visible way because compiled templates adopt LED-pin event IDs onto target-node CDI leaves that have no upstream producer — the signal-head is the producer of its own aspect events, and the fresh drafts are the only place the correct IDs exist pre-save. The Signal-output channel row and the FacilityCard slot state dot both show "Unknown" pre-save because
resolve_channel_event_idsreads baselineleaf.valueand misses the composer's drafts. Fixed narrowly on the ABS branch by pointingchannel_events::collect_event_leaves_under_prefixateffective_value(leaf)(single-file, single-behavior fix — tracked as a fresh slice inspecs/020-abs-signaling/slices-event-wiring.md).Proposed architectural fix
LayoutState::effective_config_tree(key: &NodeKey) -> Option<Cow<NodeConfigTree>>(or an equivalent shape — probably read-only accessor that layersLeafNode.modified_valueover the tree in-place for value reads).config_tree()to the effective view where they consume leaf values. Structural walks (children iteration, address lookups) stay onconfig_tree().effective_config_tree;config_treebecomes internal / structural-only."aiwiki/seams.md: "Backend CDI Layered Read" (Owner:LayoutState::effective_config_tree; Contributors:LeafNode::effective_value,configDraftMirrorOrchestrator; Consumers:resolve_channel_event_ids,compile_logic_for_facility,compose_facility_bowties, catalog rebuilds, …).aiwiki/architecture-health.md.Callers to migrate
Non-test call sites of
LayoutState::config_tree()(from 2026-07-26 audit — bump list before starting work):app/src-tauri/src/commands/channel_events.rsresolve_channel_event_ids— already fixed narrowly on the ABS branch. Confirm it moves to the new seam.app/src-tauri/src/commands/facility_bowties.rs— per-node CDI seeding for composer + in-process producer-event-id resolution.app/src-tauri/src/commands/logic_adapter.rs— compile input path, compile output path (compile target path can stay onconfig_tree()— writes structural offsets, not user-drafted values).app/src-tauri/src/commands/layout_capture.rs— inline compose seeding.app/src-tauri/src/commands/bowties.rscatalog build — already routes throughcollect_event_leaves_recursivewhich useseffective_value; confirm.app/src-tauri/src/commands/cdi.rs—get_node_treeand its fallback loops return whole trees; frontend layers viaeffectiveLayoutStore. Leave onconfig_tree(); the frontend seam is the layered surface here.Regression class prevented
Any future backend CDI-reading command silently ignoring drafts. Every downstream workflow — compiled-template composers for future templates (Head Repeater, Approach Lighting, block-signal interlock, …), catalog rebuilds, sync classifiers, load-time reconcilers — inherits correct behavior automatically. Structurally prevents the "next feature exposes the same latent gap" fingerprint.
Deferral rationale
Not blocking the ABS release. The narrow fix on
channel_events.rshandles the only user-visible surface today. The rest of the callers are guarded by D6 (adoption invariant paper-overs the gap) or don't touch user-drafted leaves (structural reads). The architectural refactor is sequenced after Track 2 (spec 020) finishes so the compile/reset/compose triad is stable before consolidation.Related