Skip to content

Backend CDI leaf-value reads are not draft-aware — establish LayoutState::effective_config_tree() seam #26

Description

@JohnSL

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions