From 4ccc6222abadf934bc8b016462727c4de6530c27 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 16:20:36 -0700 Subject: [PATCH 1/9] docs: add design for surfacing forced cells in the begin UI Spec covers disabling Inspector fields for forced cells, highlighting forced cells and their producing edge in the D3 graph, and extending the demo source with a conditional relationship that forces a cell. Co-Authored-By: Claude Sonnet 5 --- ...2026-07-09-begin-forced-cells-ui-design.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md diff --git a/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md new file mode 100644 index 0000000..16292c1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md @@ -0,0 +1,110 @@ +# `begin`: Surface Forced Cells in the UI + +**Date:** 2026-07-09 +**Author:** Sean Parent (with Claude) +**Status:** Approved + +## Problem + +`property_model::Sheet` already exposes `is_forced(id)` and `forced_cells()` (added in +the "Planner: Forced-Output Cells" work), reporting which cells some active +relationship's method structure guarantees will always be overwritten by `propagate()`, +regardless of write-recency strength. Writing to a forced cell is either silently +overwritten on the next propagate or edited by the user in a field that can never +actually take effect. + +The `begin` demo app does not use this API at all: the Inspector sidebar renders every +cell as an editable text field, forced or not, and the D3 graph gives no visual +indication that a cell (or the edge feeding it) is forced. The demo source also has no +example of a conditional relationship that forces a cell, so there is nothing to +exercise this behavior against. + +## Design + +### Inspector: disable forced fields + +`SpTextfield` (`begin/src/spectrum.rs`) gains a `disabled: bool` prop, mapped to the +`disabled` boolean attribute on `` the same way `invalid` already maps to +the `invalid` attribute (present when `true`, omitted when `false`). + +`CellRow` (`begin/src/inspector.rs`) computes: + +```rust +let forced = use_memo(move || sheet.read().is_forced(id)); +``` + +and passes `disabled: *forced.read()` to `SpTextfield`. The custom element's native +disabled behavior blocks focus and input at the DOM level, so no additional guard is +needed in the `oninput` handler. + +### Graph: highlight forced cells and their producing edge + +`bridge::GraphData` (`begin/src/bridge.rs`) gains a `forced: Vec` field — +stable cell-node IDs, populated the same way as the existing `changed` field: + +```rust +let forced = sheet.forced_cells().map(cell_node_id).collect(); +``` + +`graph.js`'s `update()` builds a `Set` from `data.forced` and: +- toggles a `forced` CSS class on cell `` elements whose ID is in the set +- toggles a `forced-edge` CSS class on constraint `` elements whose *target* is a + forced cell (the edge from the relationship that produces it; direction is already + established by the existing directed-link logic when a plan is cached) + +`graph.css` adds: + +```css +.node-cell.forced { stroke: #8e44ad; stroke-width: 3; } +.link.forced-edge { stroke: #8e44ad; stroke-width: 3; } +``` + +A distinct purple, chosen not to collide with the existing pulse color (`#f90`) or +branch colors (`#4a90d9` / `#e67e22`). Forced cells are always part of a currently +*active* relationship, so this never conflicts with the existing inactive-relationship +dimming (which only applies to relationships an active control link has switched off). + +### Demo source: a conditional relationship that forces a cell + +`DEMO_SOURCE` (`begin/src/app.rs`) adds one cell and one method to the existing `p` +conditional's `1i32` branch, rather than introducing an unrelated second conditional: + +``` +cell g: f64; +... +conditional p { + 0i32 => { ... } + 1i32 => { + method [f] -> [c] { f * 2.0 } + method [c] -> [f] { c / 2.0 } + method [c] -> [g] { c * 10.0 } + } +} +``` + +`[c] -> [g]` is a single-method relationship, so `g` is forced whenever branch `1i32` is +active and not forced otherwise — directly exercising "forced only while its owning +conditional branch is active." Setting `p` to `1` in the running demo disables `g`'s +Inspector field and highlights `g` and its incoming edge in the graph; setting `p` back +to `0` (or anything else) re-enables it. + +## Testing + +- `begin/src/bridge.rs`: unit test that `to_graph_data` includes a forced cell's node ID + in `GraphData::forced` after `propagate()` activates the forcing branch, and omits it + when the branch is inactive. +- `begin/src/spectrum.rs` / `inspector.rs`: no new Rust-testable behavior beyond the + `disabled` prop threading through `SpTextfield`, which is a thin wrapper already + covered by existing component patterns; no dedicated unit test needed (rendering isn't + exercised by `cargo test` for Dioxus components in this crate today, consistent with + the rest of `inspector.rs`). +- Manual verification: run the app, toggle `p` between `0` and `1`, confirm `g`'s field + disables/enables and the graph highlights `g` + its incoming edge accordingly. + +## Out of Scope + +- No changes to `property-model`; `is_forced`/`forced_cells` already exist. +- No tooltip or label annotation on forced fields — disabling the input is sufficient + per current design; a label annotation can be added later if it proves necessary. +- No highlighting of the relationship node that produces a forced cell, only the cell + and its incoming edge, per the approved design. From 953e441aa1a0237ed0b3e374be8d4506b7cbe880 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 16:43:40 -0700 Subject: [PATCH 2/9] feat(begin): report forced cells in GraphData GraphData::forced mirrors the existing changed field, populated from Sheet::forced_cells(), so graph_view/graph.js can highlight cells (and their producing edge) that an active relationship guarantees will always be overwritten by propagate(). Co-Authored-By: Claude Sonnet 5 --- begin/src/bridge.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/begin/src/bridge.rs b/begin/src/bridge.rs index 0cf8100..b6d7aa6 100644 --- a/begin/src/bridge.rs +++ b/begin/src/bridge.rs @@ -191,6 +191,10 @@ pub struct GraphData { pub links: Vec, /// Stable IDs of cells that changed during the last `propagate()` call. pub changed: Vec, + /// Stable IDs of cells forced by an active relationship (see + /// [`property_model::Sheet::is_forced`]); consumers should disable input for these + /// cells and may render them distinctly. + pub forced: Vec, /// `true` when at least one relationship has a cached plan and constraint links are directed /// where plans exist; `false` when no plan has been computed. pub arrows: bool, @@ -342,11 +346,13 @@ pub fn to_graph_data(sheet: &Sheet, labels: &Labels) -> GraphData { } let changed = sheet.changed().map(cell_node_id).collect(); + let forced = sheet.forced_cells().map(cell_node_id).collect(); GraphData { nodes, links, changed, + forced, arrows, } } @@ -486,6 +492,28 @@ mod tests { (sheet, labels) } + fn sheet_with_forced_conditional() -> (Sheet, Labels) { + let mut sheet = Sheet::new(); + let mut labels = Labels::new(); + + let a = sheet.add_cell(2.0_f64); + labels.add_cell::(a, "a"); + let b = sheet.add_cell(0.0_f64); + labels.add_cell::(b, "b"); + let p = sheet.add_cell(0_i32); + labels.add_cell::(p, "p"); + + let rel = sheet + .add_relationship(vec![Method::from_fn_1_1(a, b, |v: &f64| Ok(*v))]) + .unwrap(); + + sheet + .add_conditional(p, vec![(vec![0_i32], vec![rel])], vec![]) + .unwrap(); + + (sheet, labels) + } + #[test] fn to_graph_data_produces_correct_node_counts() { let (sheet, labels) = demo_sheet(); @@ -705,4 +733,37 @@ mod tests { "GraphData must not contain groups" ); } + + #[test] + fn to_graph_data_forced_field_contains_forced_cell() { + let (mut sheet, labels) = sheet_with_forced_conditional(); + sheet.propagate().unwrap(); + + let b_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("b")) + .unwrap(); + + let data = to_graph_data(&sheet, &labels); + assert!(data.forced.contains(&cell_node_id(b_id))); + } + + #[test] + fn to_graph_data_forced_field_excludes_cell_when_branch_inactive() { + let (mut sheet, labels) = sheet_with_forced_conditional(); + let p_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p")) + .unwrap(); + sheet.write(p_id, 1_i32).unwrap(); + sheet.propagate().unwrap(); + + let b_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("b")) + .unwrap(); + + let data = to_graph_data(&sheet, &labels); + assert!(!data.forced.contains(&cell_node_id(b_id))); + } } From 02e359b281b01733386f618a0b02a4dee5f32a6b Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 16:47:03 -0700 Subject: [PATCH 3/9] feat(begin): disable Inspector fields for forced cells A forced cell's value is always overwritten by an active relationship on the next propagate(), regardless of what the user types, so the field is disabled rather than silently discarding edits. Co-Authored-By: Claude Sonnet 5 --- begin/src/inspector.rs | 3 +++ begin/src/spectrum.rs | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/begin/src/inspector.rs b/begin/src/inspector.rs index 9667b92..9180496 100644 --- a/begin/src/inspector.rs +++ b/begin/src/inspector.rs @@ -67,6 +67,8 @@ fn CellRow( .unwrap_or_default() }); + let forced = use_memo(move || sheet.read().is_forced(id)); + let mut input = use_signal(|| value.peek().clone()); let mut is_focused = use_signal(|| false); let mut has_error = use_signal(|| false); @@ -90,6 +92,7 @@ fn CellRow( id: field_id, value: input.read().clone(), invalid: *has_error.read(), + disabled: *forced.read(), // Dioxus's event serializer only reads event.target.value for // HTMLInputElement — custom elements (sp-textfield) always give "". // Use dioxus.send() in JS and eval.recv() to read the live value. diff --git a/begin/src/spectrum.rs b/begin/src/spectrum.rs index 965df27..6f443b2 100644 --- a/begin/src/spectrum.rs +++ b/begin/src/spectrum.rs @@ -33,12 +33,14 @@ pub fn SpTheme(color: String, scale: String, children: Element) -> Element { /// /// Maps to ``. Fires standard DOM `input`, `focus`, and `blur` /// events. Setting `invalid` to `true` renders the SWC error state (red ring -/// and `aria-invalid`). +/// and `aria-invalid`). Setting `disabled` to `true` renders the SWC disabled +/// state and blocks focus/input at the DOM level. #[component] pub fn SpTextfield( id: String, value: String, invalid: bool, + disabled: bool, oninput: EventHandler, onfocus: EventHandler, onblur: EventHandler, @@ -49,6 +51,7 @@ pub fn SpTextfield( "value": "{value}", // Boolean attribute: omit entirely when false; presence = invalid. "invalid": if invalid { "true" }, + "disabled": if disabled { "true" }, oninput: move |e| oninput.call(e), onfocus: move |e| onfocus.call(e), onblur: move |e| onblur.call(e), From 21c2e2cab810db67a71342c8642b795f90de0ecb Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 16:49:56 -0700 Subject: [PATCH 4/9] feat(begin): highlight forced cells and their producing edge in the graph Cells reported by GraphData::forced (see Task 1) get a distinct purple outline, along with the constraint edge that feeds them, so it's visible at a glance which cells can never be edited. Co-Authored-By: Claude Sonnet 5 --- begin/assets/graph.css | 10 ++++++++++ begin/assets/graph.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/begin/assets/graph.css b/begin/assets/graph.css index 1642512..eaef6e9 100644 --- a/begin/assets/graph.css +++ b/begin/assets/graph.css @@ -61,3 +61,13 @@ html, body { stroke-width: 1.5; fill: none; } + +.node-cell.forced { + stroke: #8e44ad; + stroke-width: 3; +} + +.link.forced-edge { + stroke: #8e44ad; + stroke-width: 3; +} diff --git a/begin/assets/graph.js b/begin/assets/graph.js index 59f069d..f21c904 100644 --- a/begin/assets/graph.js +++ b/begin/assets/graph.js @@ -250,6 +250,21 @@ }); }()); + // Highlight forced cells (see property_model::Sheet::is_forced) and the + // constraint edge that produces each one. Forced cells always belong to a + // currently active relationship, so this never overlaps with the inactive- + // relationship dimming above. + (function () { + var forcedSet = new Set(data.forced || []); + cellLayer.selectAll('rect') + .classed('forced', function (d) { return forcedSet.has(d.id); }); + linkLayer.selectAll('line') + .classed('forced-edge', function (d) { + var tgtId = typeof d.target === 'object' ? d.target.id : d.target; + return forcedSet.has(tgtId); + }); + }()); + // NEW: Conditional diamond nodes (rotated rect) condLayer.selectAll('rect') .data(condNodes, function (d) { return d.id; }) From faa96fa54a36669868134306550476ce3f825189 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 16:58:58 -0700 Subject: [PATCH 5/9] feat(begin): add a forced cell to the demo source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEMO_SOURCE's p == 1 branch now also activates a single-method relationship g = c * 10, so g is forced (per Sheet::is_forced) only while that branch is active — exercising the Inspector-disable and graph-highlight behavior added in prior tasks. Co-Authored-By: Claude Sonnet 5 --- begin/src/app.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/begin/src/app.rs b/begin/src/app.rs index 153f110..4dfb540 100644 --- a/begin/src/app.rs +++ b/begin/src/app.rs @@ -12,8 +12,11 @@ use crate::spectrum::SpTheme; /// (`a × b = c` and `d × e = f`) linked by a conditional on `p`. /// /// - `p = 0`: the relationship `c = f` (bidirectional) becomes active. -/// - `p = 1`: the relationship `c = f × 2` (bidirectional) becomes active. -/// - Any other `p`: the two systems are independent. +/// - `p = 1`: the relationship `c = f × 2` (bidirectional) becomes active, and a +/// single-method relationship `g = c × 10` also becomes active — `g` is *forced* +/// while this branch is active (see [`property_model::Sheet::is_forced`]), so its +/// Inspector field is disabled and it is highlighted in the graph. +/// - Any other `p`: the two systems are independent and `g` is not forced. pub const DEMO_SOURCE: &str = r#"sheet demo { cell a: f64 = 2.0; cell b: f64 = 3.0; @@ -21,6 +24,7 @@ pub const DEMO_SOURCE: &str = r#"sheet demo { cell d: f64 = 4.0; cell e: f64 = 5.0; cell f: f64; + cell g: f64; cell p: i32 = 0; relationship { @@ -45,6 +49,12 @@ pub const DEMO_SOURCE: &str = r#"sheet demo { method [c] -> [f] { c / 2.0 } } } + + conditional p { + 1i32 => { + method [c] -> [g] { c * 10.0 } + } + } } "#; @@ -99,3 +109,62 @@ pub fn App() -> Element { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn demo_source_g_not_forced_when_p_is_zero() { + let outcome = build_sheet(DEMO_SOURCE); + let (sheet, labels) = outcome.sheet_labels.expect("DEMO_SOURCE must build"); + let g_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("g")) + .unwrap(); + assert!(!sheet.is_forced(g_id), "g should not be forced when p == 0"); + } + + #[test] + fn demo_source_g_forced_when_p_is_one() { + let outcome = build_sheet(DEMO_SOURCE); + let (mut sheet, labels) = outcome.sheet_labels.expect("DEMO_SOURCE must build"); + let p_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p")) + .unwrap(); + let g_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("g")) + .unwrap(); + + sheet.write(p_id, 1_i32).unwrap(); + sheet.propagate().unwrap(); + + assert!(sheet.is_forced(g_id), "g should be forced when p == 1"); + } + + #[test] + fn demo_source_g_unforced_again_after_p_returns_to_zero() { + let outcome = build_sheet(DEMO_SOURCE); + let (mut sheet, labels) = outcome.sheet_labels.expect("DEMO_SOURCE must build"); + let p_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p")) + .unwrap(); + let g_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("g")) + .unwrap(); + + sheet.write(p_id, 1_i32).unwrap(); + sheet.propagate().unwrap(); + sheet.write(p_id, 0_i32).unwrap(); + sheet.propagate().unwrap(); + + assert!( + !sheet.is_forced(g_id), + "g should not be forced once p == 0 again" + ); + } +} From 60146da5034c2f8f09f3d72de1b198993bf905c9 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 21:06:01 -0700 Subject: [PATCH 6/9] docs: add implementation plan for begin forced-cells UI Co-Authored-By: Claude Sonnet 5 --- .../plans/2026-07-09-begin-forced-cells-ui.md | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md diff --git a/docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md b/docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md new file mode 100644 index 0000000..c90b45a --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md @@ -0,0 +1,522 @@ +# `begin`: Surface Forced Cells in the UI Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Query `property_model::Sheet::is_forced`/`forced_cells` after every `propagate()` in the `begin` demo app, disable Inspector fields for forced cells, highlight forced cells and their producing edge in the D3 graph, and extend the demo source with a conditional relationship that forces a cell. + +**Architecture:** The Inspector reads `Sheet::is_forced` directly per cell (no new plumbing); the D3 graph gets a new `GraphData::forced: Vec` field populated from `Sheet::forced_cells()`, consumed by `graph.js`/`graph.css` the same way the existing `changed` field drives the pulse animation. The demo source (`DEMO_SOURCE` in `begin/src/app.rs`) gains one cell and one single-method relationship inside an existing conditional branch, so toggling `p` also toggles a forced cell. + +**Tech Stack:** Rust, Dioxus 0.7 (`begin` crate), `property-model` crate (already exposes `is_forced`/`forced_cells`), D3.js v7 (`begin/assets/graph.js`), plain CSS (`begin/assets/graph.css`). + +## Global Constraints + +- `cargo fmt --all` must be run before every commit (enforced by pre-commit hook). +- `cargo build --workspace` and `cargo test --workspace` must produce zero compiler warnings. +- `cargo clippy --workspace --exclude begin -- -D warnings` and `cargo clippy -p begin --no-default-features -- -D warnings` must both be clean before the branch is considered done. +- Every function needs a `///` contract-style doc comment (summary, preconditions/postconditions only where non-obvious, `Complexity` bullet whenever not O(1)). +- Unit tests are derived from the contract/public interface only, never from implementation details. +- Never commit directly to `main`; this work happens on the `worktree-forced-to-disabled` branch. + +--- + +### Task 1: `GraphData` reports forced cells + +**Files:** +- Modify: `begin/src/bridge.rs:186-197` (`GraphData` struct), `begin/src/bridge.rs:344-352` (`to_graph_data` tail) +- Test: `begin/src/bridge.rs` (`#[cfg(test)] mod tests`, same file) + +**Interfaces:** +- Consumes: `property_model::Sheet::forced_cells(&self) -> impl Iterator + '_` (already implemented in `property-model/src/sheet.rs:692`); the existing private `cell_node_id(id: CellId) -> String` helper in `bridge.rs:199-201`. +- Produces: `GraphData::forced: Vec` — stable cell-node IDs (`"c{ffi}"`) of cells forced as of the last `propagate()`. Later tasks (Task 3) consume this field by name. + +- [ ] **Step 1: Write the failing tests** + +Add to the `#[cfg(test)] mod tests` block at the bottom of `begin/src/bridge.rs` (after the existing `sheet_with_conditional` helper, before its first use): + +```rust + fn sheet_with_forced_conditional() -> (Sheet, Labels) { + let mut sheet = Sheet::new(); + let mut labels = Labels::new(); + + let a = sheet.add_cell(2.0_f64); + labels.add_cell::(a, "a"); + let b = sheet.add_cell(0.0_f64); + labels.add_cell::(b, "b"); + let p = sheet.add_cell(0_i32); + labels.add_cell::(p, "p"); + + let rel = sheet + .add_relationship(vec![Method::from_fn_1_1(a, b, |v: &f64| Ok(*v))]) + .unwrap(); + + sheet + .add_conditional(p, vec![(vec![0_i32], vec![rel])], vec![]) + .unwrap(); + + (sheet, labels) + } + + #[test] + fn to_graph_data_forced_field_contains_forced_cell() { + let (mut sheet, labels) = sheet_with_forced_conditional(); + sheet.propagate().unwrap(); + + let b_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("b")) + .unwrap(); + + let data = to_graph_data(&sheet, &labels); + assert!(data.forced.contains(&cell_node_id(b_id))); + } + + #[test] + fn to_graph_data_forced_field_excludes_cell_when_branch_inactive() { + let (mut sheet, labels) = sheet_with_forced_conditional(); + let p_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p")) + .unwrap(); + sheet.write(p_id, 1_i32).unwrap(); + sheet.propagate().unwrap(); + + let b_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("b")) + .unwrap(); + + let data = to_graph_data(&sheet, &labels); + assert!(!data.forced.contains(&cell_node_id(b_id))); + } +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `cargo test -p begin --no-default-features to_graph_data_forced_field` +Expected: compile error — `no field \`forced\` on type \`GraphData\`` (the struct literal at the end of `to_graph_data` doesn't build one yet, and the test reads `data.forced`). + +- [ ] **Step 3: Add the `forced` field and populate it** + +In `begin/src/bridge.rs`, add a field to `GraphData` (after the existing `changed` field, `bridge.rs:192-193`): + +```rust + /// Stable IDs of cells that changed during the last `propagate()` call. + pub changed: Vec, + /// Stable IDs of cells forced by an active relationship (see + /// [`property_model::Sheet::is_forced`]); consumers should disable input for these + /// cells and may render them distinctly. + pub forced: Vec, +``` + +At the tail of `to_graph_data` (`bridge.rs:344-352`), populate it alongside `changed`: + +```rust + let changed = sheet.changed().map(cell_node_id).collect(); + let forced = sheet.forced_cells().map(cell_node_id).collect(); + + GraphData { + nodes, + links, + changed, + forced, + arrows, + } +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `cargo test -p begin --no-default-features to_graph_data_forced_field` +Expected: PASS (2 passed) + +Run: `cargo test -p begin --no-default-features` +Expected: all existing `bridge.rs` tests still pass (the new field doesn't change any existing assertion, since none of them check `GraphData`'s field set directly except `to_graph_data_no_groups_field`, which only asserts the JSON doesn't contain `"groups"` — unaffected by adding `forced`). + +- [ ] **Step 5: Commit** + +```bash +git add begin/src/bridge.rs +git commit -m "$(cat <<'EOF' +feat(begin): report forced cells in GraphData + +GraphData::forced mirrors the existing `changed` field, populated from +Sheet::forced_cells(), so graph_view/graph.js can highlight cells (and +their producing edge) that an active relationship guarantees will +always be overwritten by propagate(). + +Co-Authored-By: Claude Sonnet 5 +EOF +)" +``` + +--- + +### Task 2: Inspector disables forced fields + +**Files:** +- Modify: `begin/src/spectrum.rs:37-57` (`SpTextfield`) +- Modify: `begin/src/inspector.rs:43-95` (`CellRow`) + +**Interfaces:** +- Consumes: `property_model::Sheet::is_forced(&self, id: CellId) -> bool` (already implemented, `property-model/src/sheet.rs:682`). +- Produces: `SpTextfield { disabled: bool, .. }` — a new required prop, mapped to the `disabled` boolean attribute. `CellRow` now disables its field whenever the cell is forced. No other task depends on new names from this task. + +This task has no dedicated Rust unit test: `spectrum.rs` wraps a single custom element with no test infrastructure today (consistent with the rest of the file), and `CellRow`'s wiring is a one-line prop pass-through. Verify by building and, in Task 3's manual check, observing the field actually disable. + +- [ ] **Step 1: Add the `disabled` prop to `SpTextfield`** + +In `begin/src/spectrum.rs`, replace the `SpTextfield` component (lines 32-57): + +```rust +/// Single-line text input. +/// +/// Maps to ``. Fires standard DOM `input`, `focus`, and `blur` +/// events. Setting `invalid` to `true` renders the SWC error state (red ring +/// and `aria-invalid`). Setting `disabled` to `true` renders the SWC disabled +/// state and blocks focus/input at the DOM level. +#[component] +pub fn SpTextfield( + id: String, + value: String, + invalid: bool, + disabled: bool, + oninput: EventHandler, + onfocus: EventHandler, + onblur: EventHandler, +) -> Element { + rsx! { + sp-textfield { + "id": "{id}", + "value": "{value}", + // Boolean attribute: omit entirely when false; presence = invalid. + "invalid": if invalid { "true" }, + "disabled": if disabled { "true" }, + oninput: move |e| oninput.call(e), + onfocus: move |e| onfocus.call(e), + onblur: move |e| onblur.call(e), + } + } +} +``` + +- [ ] **Step 2: Wire `CellRow` to disable forced cells** + +In `begin/src/inspector.rs`, add a memo next to the existing `value` memo (after `inspector.rs:61-68`, before `let mut input = ...` at `inspector.rs:70`): + +```rust + let forced = use_memo(move || sheet.read().is_forced(id)); +``` + +Then update the `SpTextfield` call (`inspector.rs:89-95`) to pass it through: + +```rust + SpTextfield { + id: field_id, + value: input.read().clone(), + invalid: *has_error.read(), + disabled: *forced.read(), + // Dioxus's event serializer only reads event.target.value for + // HTMLInputElement — custom elements (sp-textfield) always give "". + // Use dioxus.send() in JS and eval.recv() to read the live value. + oninput: move |_: FormEvent| { +``` + +(leave the rest of the `oninput`/`onfocus`/`onblur` block unchanged). + +- [ ] **Step 3: Build to verify it compiles** + +Run: `cargo build -p begin --no-default-features` +Expected: builds cleanly, no warnings. + +- [ ] **Step 4: Commit** + +```bash +git add begin/src/spectrum.rs begin/src/inspector.rs +git commit -m "$(cat <<'EOF' +feat(begin): disable Inspector fields for forced cells + +A forced cell's value is always overwritten by an active relationship +on the next propagate(), regardless of what the user types, so the +field is disabled rather than silently discarding edits. + +Co-Authored-By: Claude Sonnet 5 +EOF +)" +``` + +--- + +### Task 3: Graph highlights forced cells and their producing edge + +**Files:** +- Modify: `begin/assets/graph.js:220-251` (near the existing inactive-relationship dimming block, inside `update()`) +- Modify: `begin/assets/graph.css:60-63` (end of file) + +**Interfaces:** +- Consumes: `GraphData.forced: Vec` (Task 1) as `data.forced` in JS. +- Produces: CSS classes `forced` (on cell ``s) and `forced-edge` (on constraint ``s) — purely visual, no other task depends on these names. + +- [ ] **Step 1: Build the forced set and toggle CSS classes** + +In `begin/assets/graph.js`, inside `update()`, immediately after the existing inactive-relationship IIFE (the block ending at line 251, right before the `// NEW: Conditional diamond nodes` comment at line 253), add: + +```javascript + // Highlight forced cells (see property_model::Sheet::is_forced) and the + // constraint edge that produces each one. Forced cells always belong to a + // currently active relationship, so this never overlaps with the inactive- + // relationship dimming above. + (function () { + var forcedSet = new Set(data.forced || []); + cellLayer.selectAll('rect') + .classed('forced', function (d) { return forcedSet.has(d.id); }); + linkLayer.selectAll('line') + .classed('forced-edge', function (d) { + var tgtId = typeof d.target === 'object' ? d.target.id : d.target; + return forcedSet.has(tgtId); + }); + }()); +``` + +- [ ] **Step 2: Add the CSS rules** + +In `begin/assets/graph.css`, after the existing `.link-control` rule (end of file, lines 60-63), add: + +```css +.node-cell.forced { + stroke: #8e44ad; + stroke-width: 3; +} + +.link.forced-edge { + stroke: #8e44ad; + stroke-width: 3; +} +``` + +- [ ] **Step 3: Manually verify in the running app** + +Run: `dx serve --platform desktop` (from the `begin/` directory) +Steps: +1. Wait for the desktop window to open with the default demo graph. +2. In the Inspector, set `p` to `1` and confirm (from Task 4, once applied) `g`'s field becomes disabled and the `g` cell rect plus its incoming edge from the `[c] -> [g]` relationship turn purple with a thicker outline. +3. Set `p` back to `0` and confirm `g`'s field re-enables and the highlight disappears. + +(This step is a manual check, not a `- [ ] Commit` gate on its own — it's re-run at the end of Task 4 once the demo source actually has `g`. Proceed to commit this task's JS/CSS changes now; the end-to-end visual behavior is confirmed once Task 4 lands.) + +- [ ] **Step 4: Commit** + +```bash +git add begin/assets/graph.js begin/assets/graph.css +git commit -m "$(cat <<'EOF' +feat(begin): highlight forced cells and their producing edge in the graph + +Cells reported by GraphData::forced (see Task 1) get a distinct purple +outline, along with the constraint edge that feeds them, so it's +visible at a glance which cells can never be edited. + +Co-Authored-By: Claude Sonnet 5 +EOF +)" +``` + +--- + +### Task 4: Demo source gains a conditional relationship that forces a cell + +**Files:** +- Modify: `begin/src/app.rs:11-49` (`DEMO_SOURCE` and its doc comment) +- Test: `begin/src/app.rs` (new `#[cfg(test)] mod tests` block) + +**Interfaces:** +- Consumes: `property_model::Sheet::is_forced`, `Sheet::write`, `Sheet::propagate`, `Sheet::cells` (all existing); `crate::source_panel::build_sheet` (already imported in `app.rs`). +- Produces: `DEMO_SOURCE` now declares a cell named `"g"`; no other task depends on this by name, but Task 3's manual verification (Step 3 above) exercises it end-to-end. + +- [ ] **Step 1: Write the failing tests** + +Add a new `#[cfg(test)] mod tests` block at the end of `begin/src/app.rs`: + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn demo_source_g_not_forced_when_p_is_zero() { + let outcome = build_sheet(DEMO_SOURCE); + let (sheet, labels) = outcome.sheet_labels.expect("DEMO_SOURCE must build"); + let g_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("g")) + .unwrap(); + assert!(!sheet.is_forced(g_id), "g should not be forced when p == 0"); + } + + #[test] + fn demo_source_g_forced_when_p_is_one() { + let outcome = build_sheet(DEMO_SOURCE); + let (mut sheet, labels) = outcome.sheet_labels.expect("DEMO_SOURCE must build"); + let p_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p")) + .unwrap(); + let g_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("g")) + .unwrap(); + + sheet.write(p_id, 1_i32).unwrap(); + sheet.propagate().unwrap(); + + assert!(sheet.is_forced(g_id), "g should be forced when p == 1"); + } + + #[test] + fn demo_source_g_unforced_again_after_p_returns_to_zero() { + let outcome = build_sheet(DEMO_SOURCE); + let (mut sheet, labels) = outcome.sheet_labels.expect("DEMO_SOURCE must build"); + let p_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("p")) + .unwrap(); + let g_id = sheet + .cells() + .find(|&id| labels.cells.get(&id).map(|m| m.label.as_str()) == Some("g")) + .unwrap(); + + sheet.write(p_id, 1_i32).unwrap(); + sheet.propagate().unwrap(); + sheet.write(p_id, 0_i32).unwrap(); + sheet.propagate().unwrap(); + + assert!(!sheet.is_forced(g_id), "g should not be forced once p == 0 again"); + } +} +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `cargo test -p begin --no-default-features demo_source_g` +Expected: FAIL — `called \`Option::unwrap()\` on a \`None\` value` (no cell named `"g"` exists in `DEMO_SOURCE` yet). + +- [ ] **Step 3: Update `DEMO_SOURCE`** + +Replace `begin/src/app.rs:11-49` with: + +```rust +/// Default pm-lang source: two independent bidirectional constraint systems +/// (`a × b = c` and `d × e = f`) linked by a conditional on `p`. +/// +/// - `p = 0`: the relationship `c = f` (bidirectional) becomes active. +/// - `p = 1`: the relationship `c = f × 2` (bidirectional) becomes active, and a +/// single-method relationship `g = c × 10` also becomes active — `g` is *forced* +/// while this branch is active (see [`property_model::Sheet::is_forced`]), so its +/// Inspector field is disabled and it is highlighted in the graph. +/// - Any other `p`: the two systems are independent and `g` is not forced. +pub const DEMO_SOURCE: &str = r#"sheet demo { + cell a: f64 = 2.0; + cell b: f64 = 3.0; + cell c: f64; + cell d: f64 = 4.0; + cell e: f64 = 5.0; + cell f: f64; + cell g: f64; + cell p: i32 = 0; + + relationship { + method [a, b] -> [c] { a * b } + method [b, c] -> [a] { c / b } + method [a, c] -> [b] { c / a } + } + + relationship { + method [d, e] -> [f] { d * e } + method [e, f] -> [d] { f / e } + method [d, f] -> [e] { f / d } + } + + conditional p { + 0i32 => { + method [f] -> [c] { f } + method [c] -> [f] { c } + } + 1i32 => { + method [f] -> [c] { f * 2.0 } + method [c] -> [f] { c / 2.0 } + method [c] -> [g] { c * 10.0 } + } + } +} +"#; +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `cargo test -p begin --no-default-features demo_source_g` +Expected: PASS (3 passed) + +Run: `cargo test -p begin --no-default-features` +Expected: all tests pass, including `app.rs`'s new tests and the existing `bridge.rs`/`inspector.rs`/`source_panel.rs` suites (none reference `DEMO_SOURCE`'s exact cell set, so none regress). + +- [ ] **Step 5: Manually re-verify the graph end-to-end** + +Repeat Task 3 Step 3 (`dx serve --platform desktop`) now that `g` actually exists: toggling `p` between `0` and `1` should disable/highlight `g` as described. + +- [ ] **Step 6: Commit** + +```bash +git add begin/src/app.rs +git commit -m "$(cat <<'EOF' +feat(begin): add a forced cell to the demo source + +DEMO_SOURCE's p == 1 branch now also activates a single-method +relationship g = c * 10, so g is forced (per Sheet::is_forced) only +while that branch is active — exercising the Inspector-disable and +graph-highlight behavior added in prior tasks. + +Co-Authored-By: Claude Sonnet 5 +EOF +)" +``` + +--- + +### Task 5: Full workspace verification + +**Files:** none (verification only) + +**Interfaces:** +- Consumes: everything from Tasks 1-4. +- Produces: nothing new; confirms the branch is ready to hand off per root `CLAUDE.md`'s "Before creating a PR" checklist. + +- [ ] **Step 1: Format** + +Run: `cargo fmt --all` +Expected: no changes (already formatted per-task), or if it does reformat something, stage and include it in the commit below. + +- [ ] **Step 2: Build the whole workspace** + +Run: `cargo build --workspace` +Expected: builds cleanly, zero warnings. + +- [ ] **Step 3: Run the full test suite** + +Run: `cargo test --workspace` +Run: `cargo test --doc --workspace` +Expected: all pass, no regressions anywhere in the workspace. + +- [ ] **Step 4: Lint the whole workspace** + +Run: `cargo clippy --workspace --exclude begin -- -D warnings` +Run: `cargo clippy -p begin --no-default-features -- -D warnings` +Expected: no warnings from either invocation. + +- [ ] **Step 5: Commit any formatting fixes (only if Step 1 produced changes)** + +```bash +git add -A +git commit -m "$(cat <<'EOF' +style: cargo fmt + +Co-Authored-By: Claude Sonnet 5 +EOF +)" +``` From 6dcaab18dd2f650b52f2d6fc2e0dc5842eee434d Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 21:10:23 -0700 Subject: [PATCH 7/9] docs(begin): correct forced-cell docs to match the shipped two-conditional form Final whole-branch review flagged that DEMO_SOURCE's doc comment and the design spec still describe folding g's method into the existing p == 1 branch, which the planner's forced-output intersection makes a no-op. Update both to describe and explain the two-conditional form actually shipped in the prior commit. Co-Authored-By: Claude Sonnet 5 --- begin/src/app.rs | 10 +++++++++- .../specs/2026-07-09-begin-forced-cells-ui-design.md | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/begin/src/app.rs b/begin/src/app.rs index 4dfb540..43c6b22 100644 --- a/begin/src/app.rs +++ b/begin/src/app.rs @@ -9,7 +9,7 @@ use crate::source_panel::{SourcePanel, build_sheet}; use crate::spectrum::SpTheme; /// Default pm-lang source: two independent bidirectional constraint systems -/// (`a × b = c` and `d × e = f`) linked by a conditional on `p`. +/// (`a × b = c` and `d × e = f`) linked by two conditionals on `p`. /// /// - `p = 0`: the relationship `c = f` (bidirectional) becomes active. /// - `p = 1`: the relationship `c = f × 2` (bidirectional) becomes active, and a @@ -17,6 +17,14 @@ use crate::spectrum::SpTheme; /// while this branch is active (see [`property_model::Sheet::is_forced`]), so its /// Inspector field is disabled and it is highlighted in the graph. /// - Any other `p`: the two systems are independent and `g` is not forced. +/// +/// `g`'s relationship is declared in its own `conditional p { .. }` block rather than +/// folded into the first: pm-lang groups every method in one branch into a single +/// relationship, and a relationship's forced outputs are the *intersection* of its +/// methods' pure outputs — mixing `[c] -> [g]` in with the `c`/`f` methods would make +/// that intersection empty, forcing nothing. Two conditionals sharing the same match +/// cell compose independently, so this is a distinct relationship gated on the same +/// `p == 1` condition. This also means the graph renders two diamond nodes for `p`. pub const DEMO_SOURCE: &str = r#"sheet demo { cell a: f64 = 2.0; cell b: f64 = 3.0; diff --git a/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md index 16292c1..0713d6d 100644 --- a/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md +++ b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md @@ -66,6 +66,14 @@ dimming (which only applies to relationships an active control link has switched ### Demo source: a conditional relationship that forces a cell +> **Superseded during implementation:** the form below (folding `[c] -> [g]` into the +> existing `1i32` branch) does not force `g`. pm-lang groups every method in one branch +> into a single relationship, and a relationship's forced outputs are the intersection +> of its methods' pure outputs — mixing `[c] -> [g]` in with the `c`/`f` methods makes +> that intersection empty. The shipped code instead declares `g`'s method in its own +> `conditional p { 1i32 => { .. } }` block, gated on the same match cell; see the doc +> comment on `DEMO_SOURCE` in `begin/src/app.rs` for the final form. + `DEMO_SOURCE` (`begin/src/app.rs`) adds one cell and one method to the existing `p` conditional's `1i32` branch, rather than introducing an unrelated second conditional: From 54f1eb74bd24a84890f8fb078784834ac5a3ad27 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 9 Jul 2026 21:23:18 -0700 Subject: [PATCH 8/9] fix(begin): highlight outgoing edges from a forced cell too A forced cell's value is just as guaranteed downstream as it is produced upstream, so the edges carrying it onward to other relationships should read as forced too, not just the edge that produces it. Co-Authored-By: Claude Sonnet 5 --- begin/assets/graph.js | 13 ++++++++----- .../2026-07-09-begin-forced-cells-ui-design.md | 7 ++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/begin/assets/graph.js b/begin/assets/graph.js index f21c904..261f7fb 100644 --- a/begin/assets/graph.js +++ b/begin/assets/graph.js @@ -250,18 +250,21 @@ }); }()); - // Highlight forced cells (see property_model::Sheet::is_forced) and the - // constraint edge that produces each one. Forced cells always belong to a - // currently active relationship, so this never overlaps with the inactive- - // relationship dimming above. + // Highlight forced cells (see property_model::Sheet::is_forced) and every + // constraint edge touching one: the incoming edge that produces it, and any + // outgoing edges carrying its (also guaranteed) value onward to other + // relationships. Forced cells always belong to a currently active + // relationship, so this never overlaps with the inactive-relationship + // dimming above. (function () { var forcedSet = new Set(data.forced || []); cellLayer.selectAll('rect') .classed('forced', function (d) { return forcedSet.has(d.id); }); linkLayer.selectAll('line') .classed('forced-edge', function (d) { + var srcId = typeof d.source === 'object' ? d.source.id : d.source; var tgtId = typeof d.target === 'object' ? d.target.id : d.target; - return forcedSet.has(tgtId); + return forcedSet.has(srcId) || forcedSet.has(tgtId); }); }()); diff --git a/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md index 0713d6d..b84977a 100644 --- a/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md +++ b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md @@ -48,9 +48,10 @@ let forced = sheet.forced_cells().map(cell_node_id).collect(); `graph.js`'s `update()` builds a `Set` from `data.forced` and: - toggles a `forced` CSS class on cell `` elements whose ID is in the set -- toggles a `forced-edge` CSS class on constraint `` elements whose *target* is a - forced cell (the edge from the relationship that produces it; direction is already - established by the existing directed-link logic when a plan is cached) +- toggles a `forced-edge` CSS class on constraint `` elements whose *source or + target* is a forced cell — both the incoming edge from the relationship that produces + it, and any outgoing edges carrying its (also guaranteed) value onward to other + relationships `graph.css` adds: From 19ffc25bd4404bed6eb3087bd49b275da7e02897 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Fri, 10 Jul 2026 10:27:58 -0700 Subject: [PATCH 9/9] docs(begin): resolve remaining doc/implementation mismatches on forced-cell edges The design spec still described highlighting only the incoming edge and still presented the superseded single-conditional demo-source form as current, contradicting the two-conditional structure and source-or-target edge highlighting actually shipped. Rewrite the spec's Demo Source/Testing/ Out of Scope sections to match, and mark the plan's now-stale JS/DEMO_SOURCE snippets as superseded (matching the existing convention) so they don't mislead readers about the final form. Co-Authored-By: Claude Sonnet 5 --- .../plans/2026-07-09-begin-forced-cells-ui.md | 14 ++++++++ ...2026-07-09-begin-forced-cells-ui-design.md | 33 ++++++++++--------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md b/docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md index c90b45a..4039cec 100644 --- a/docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md +++ b/docs/superpowers/plans/2026-07-09-begin-forced-cells-ui.md @@ -259,6 +259,12 @@ EOF In `begin/assets/graph.js`, inside `update()`, immediately after the existing inactive-relationship IIFE (the block ending at line 251, right before the `// NEW: Conditional diamond nodes` comment at line 253), add: +> **Superseded during implementation:** the snippet below only marks the edge whose +> *target* is forced (the incoming edge). The shipped code also marks edges whose +> *source* is forced, so a forced cell's outgoing edges (carrying its guaranteed value +> onward to other relationships) get highlighted too — see the forced-highlighting IIFE +> in `begin/assets/graph.js` for the final form. + ```javascript // Highlight forced cells (see property_model::Sheet::is_forced) and the // constraint edge that produces each one. Forced cells always belong to a @@ -399,6 +405,14 @@ Expected: FAIL — `called \`Option::unwrap()\` on a \`None\` value` (no cell na - [ ] **Step 3: Update `DEMO_SOURCE`** +> **Superseded during implementation:** the snippet below folds `[c] -> [g]` into the +> existing `1i32` branch, which does not force `g` — pm-lang groups every method in one +> branch into a single relationship, and a relationship's forced outputs are the +> intersection of its methods' pure outputs, so mixing `[c] -> [g]` in with the `c`/`f` +> methods makes that intersection empty. The shipped code instead declares `g`'s method +> in its own `conditional p { 1i32 => { .. } }` block, gated on the same match cell — +> see the doc comment on `DEMO_SOURCE` in `begin/src/app.rs` for the final form. + Replace `begin/src/app.rs:11-49` with: ```rust diff --git a/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md index b84977a..239d485 100644 --- a/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md +++ b/docs/superpowers/specs/2026-07-09-begin-forced-cells-ui-design.md @@ -67,16 +67,14 @@ dimming (which only applies to relationships an active control link has switched ### Demo source: a conditional relationship that forces a cell -> **Superseded during implementation:** the form below (folding `[c] -> [g]` into the -> existing `1i32` branch) does not force `g`. pm-lang groups every method in one branch -> into a single relationship, and a relationship's forced outputs are the intersection -> of its methods' pure outputs — mixing `[c] -> [g]` in with the `c`/`f` methods makes -> that intersection empty. The shipped code instead declares `g`'s method in its own -> `conditional p { 1i32 => { .. } }` block, gated on the same match cell; see the doc -> comment on `DEMO_SOURCE` in `begin/src/app.rs` for the final form. - -`DEMO_SOURCE` (`begin/src/app.rs`) adds one cell and one method to the existing `p` -conditional's `1i32` branch, rather than introducing an unrelated second conditional: +`DEMO_SOURCE` (`begin/src/app.rs`) adds cell `g` and a second +`conditional p { 1i32 => { .. } }` block, rather than folding `[c] -> [g]` into the +existing `1i32` branch: pm-lang groups every method in one branch into a single +relationship, and a relationship's forced outputs are the intersection of its methods' +pure outputs, so mixing `[c] -> [g]` in with the `c`/`f` methods would make that +intersection empty and force nothing. Two conditionals sharing the same match cell +compose independently, so `g`'s relationship is a separate relationship gated on the +same `p == 1` condition: ``` cell g: f64; @@ -86,6 +84,10 @@ conditional p { 1i32 => { method [f] -> [c] { f * 2.0 } method [c] -> [f] { c / 2.0 } + } +} +conditional p { + 1i32 => { method [c] -> [g] { c * 10.0 } } } @@ -94,8 +96,8 @@ conditional p { `[c] -> [g]` is a single-method relationship, so `g` is forced whenever branch `1i32` is active and not forced otherwise — directly exercising "forced only while its owning conditional branch is active." Setting `p` to `1` in the running demo disables `g`'s -Inspector field and highlights `g` and its incoming edge in the graph; setting `p` back -to `0` (or anything else) re-enables it. +Inspector field and highlights `g` plus every constraint edge touching it (incoming and +outgoing) in the graph; setting `p` back to `0` (or anything else) re-enables it. ## Testing @@ -108,12 +110,13 @@ to `0` (or anything else) re-enables it. exercised by `cargo test` for Dioxus components in this crate today, consistent with the rest of `inspector.rs`). - Manual verification: run the app, toggle `p` between `0` and `1`, confirm `g`'s field - disables/enables and the graph highlights `g` + its incoming edge accordingly. + disables/enables and the graph highlights `g` + every constraint edge touching it + accordingly. ## Out of Scope - No changes to `property-model`; `is_forced`/`forced_cells` already exist. - No tooltip or label annotation on forced fields — disabling the input is sufficient per current design; a label annotation can be added later if it proves necessary. -- No highlighting of the relationship node that produces a forced cell, only the cell - and its incoming edge, per the approved design. +- No highlighting of the relationship node that produces a forced cell — only the cell + and the constraint edges touching it are highlighted, per the approved design.