From c7903d5c542098740f46261e8db9d9dda9d046d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 10:37:14 +0000 Subject: [PATCH] =?UTF-8?q?contract(class=5Fview):=20execute=5Fcompute=5Fd?= =?UTF-8?q?ag=20=E2=80=94=20the=20ORDER=20half=20of=20the=20ActionDef=20va?= =?UTF-8?q?lue=20executor=20(lane-3a=20inc=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs a class's recompute DAG over a consumer-owned store in topological order (compute_dag_topo_order): Cyclic is refused before any target runs (store untouched); a failing consumer closure aborts at that target (earlier targets stay written, later targets never run — the same visibility contract as cycle-aware write_row gating). Value semantics live entirely in the consumer's compute_target closure — thinking/order upstream, values with the row owner; medcare's hand-ported sono Recal_* family is the designated first parity witness. Additive on #539's ComputeEdge machinery; zero new deps; +4 tests (41/41 class_view). Board: LATEST_STATE contract-inventory row in this commit. --- .claude/board/LATEST_STATE.md | 1 + crates/lance-graph-contract/src/class_view.rs | 153 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index d8a6e946..1c289577 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -62,6 +62,7 @@ Paired mirror of OGAR `2c8836f` (two-sided COUNT_FUSE: `lance-graph-ogar` compil ### Current Contract Inventory — new entry +- **`class_view::{execute_compute_dag, ExecuteComputeError}`** (NEW; one free fn + its error enum, additive, zero new deps). The ORDER half of the ActionDef value executor (medcare transpile lane-3a increment 1): runs a class's recompute DAG over a consumer-owned store in `compute_dag_topo_order` sequence — Cyclic refused before any target runs (store untouched), consumer compute errors abort at the failing target (earlier targets stay written, later never run — matches cycle-aware `write_row` gating). Value semantics stay consumer-side as the `compute_target` closure (commitment: thinking upstream, values with the row owner; medcare's hand-ported sono `Recal_*` family is the designated first parity witness). Builds directly on #539's `ComputeEdge`/`compute_dag_topo_order`. +4 class_view tests (in-order chain w/ precedent visibility; cycle-refusal-untouched-store; abort-at-failing-target; empty-noop); class_view 41/41. - **`class_view::{screens_reachable_from, nav_is_fully_connected}`** (NEW; two free fns, additive, zero new deps, zero signature changes). The **jump** half of the stackable-topology Lego kit — the `navigates_to` navigation graph (screens = `ClassView` nodes, clicks = edges). Reuses `ComputeEdge` as the edge *representation* (`target` = destination screen, `inputs` = source screens) and returns a plain `WideFieldMask` of reached positions — **one more brick, not a parallel path**. The invariant deliberately differs from the compute DAG: **stack** composition (a screen composes sub-views, in-family) is acyclic and reuses `compute_dag_is_acyclic` (#539) verbatim, with the stacked-fields mask minted via the sanctioned `WideFieldMask::from_universe_present(basis, skin)` brick (#669) so it is interchangeable across consumers + carries the 256-SoC guard; **jump** navigation (out-of-family) is a **reachability closure with cycles ALLOWED** (`A→B→A` = ordinary back-navigation), so `screens_reachable_from` is a forward `WideFieldMask` fixpoint (cycle-safe: the reached set only grows) and `nav_is_fully_connected(root, edges, screens)` = `screens ⊆ reached` (no orphan lane — the level-editor/Mario-editor validator at the Core layer; `screens` itself minted via `from_universe_present`). Paired with the ruff harvest half (`ruff_spo_triplet::Predicate::NavigatesTo` + `ruff_csharp_spo::EmitNavArm`, verified end-to-end on a synthetic WinForms fixture). +3 class_view tests (forward closure; cycle-does-not-reject-connectivity; orphan-fails-connectivity); class_view 33/33, contract lib green; clippy `-D warnings` clean. Consumer: medcare-rs Klickweg 1:1 parity check (`screens_reachable_from` over the harvested `navigates_to` edges vs the rendered nav graph). See EPIPHANIES `E-KLICKWEG-1`. ## 2026-07-06 — MERGED #651 (merge `16c9e0c`) — `contract::class_view::WideFieldMask`: backward-compatible >64-field masks (canonical form, repr-independent Eq/Hash) diff --git a/crates/lance-graph-contract/src/class_view.rs b/crates/lance-graph-contract/src/class_view.rs index c0a285f5..3e0e7200 100644 --- a/crates/lance-graph-contract/src/class_view.rs +++ b/crates/lance-graph-contract/src/class_view.rs @@ -750,6 +750,79 @@ pub fn nav_is_fully_connected(root: u8, edges: &[ComputeEdge], screens: &WideFie &screens_reachable_from(root, edges) == screens } +/// Why [`execute_compute_dag`] refused or aborted a recompute. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecuteComputeError { + /// The DAG is cyclic — no topological order exists. The registry-build + /// gate ([`compute_dag_is_acyclic`]) should have rejected this class; + /// hitting it at execute time means an unvalidated manifest. + Cyclic, + /// The consumer's compute closure failed at this target position. No + /// later targets were computed (abort-at-target — the partial recompute + /// is visible to the store owner, matching cycle-aware `write_row` + /// gating; earlier targets in the returned order remain written). + Compute { + /// The field position whose recompute failed. + target: u8, + /// The consumer's error. + source: E, + }, +} + +impl core::fmt::Display for ExecuteComputeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Cyclic => write!(f, "compute dag is cyclic — no topological order"), + Self::Compute { target, source } => { + write!(f, "recompute of field {target} failed: {source}") + } + } + } +} + +impl std::error::Error for ExecuteComputeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Cyclic => None, + Self::Compute { source, .. } => Some(source), + } + } +} + +/// Execute a class's **recompute DAG** over a consumer-owned store — the +/// ORDER half of the ActionDef value executor (the medcare transpile arc's +/// lane-3a increment 1). +/// +/// Split of responsibilities (commitment: thinking lives upstream, values +/// live with the row owner): this function owns *sequencing* — targets run +/// in a valid topological order ([`compute_dag_topo_order`]) so every +/// precedent is recomputed before its dependents — while `compute_target` +/// owns *value semantics*: it recomputes field position `t` on `store` +/// (e.g. a hand-ported score formula; the sono `Recal_*` family is the +/// first parity witness). Leaves are never passed to `compute_target` — +/// they are the already-present inputs. +/// +/// Returns the executed order (the same positions +/// [`compute_dag_topo_order`] yields). +/// +/// # Errors +/// +/// [`ExecuteComputeError::Cyclic`] when the DAG has no topological order; +/// [`ExecuteComputeError::Compute`] wrapping the consumer's error at the +/// failing target (later targets are not attempted). +pub fn execute_compute_dag( + edges: &[ComputeEdge], + store: &mut S, + mut compute_target: impl FnMut(&mut S, u8) -> Result<(), E>, +) -> Result, ExecuteComputeError> { + let order = compute_dag_topo_order(edges).ok_or(ExecuteComputeError::Cyclic)?; + for &t in &order { + compute_target(store, t) + .map_err(|source| ExecuteComputeError::Compute { target: t, source })?; + } + Ok(order) +} + /// The class as a **meta lookup that flies above the SoA** — the resolver trait. /// /// An implementor (in `lance-graph-ontology`, over the OGIT cache) is the @@ -1297,6 +1370,86 @@ mod tests { // ── screen-graph jump connector (navigates_to Klickweg) ────────────────── + // ── execute_compute_dag (lane-3a increment 1: the ORDER half) ──────────── + + /// Chain f0→f1→f2 executes in dependency order and the consumer closure + /// sees precedents already recomputed (value semantics stay consumer-side). + #[test] + fn execute_compute_dag_runs_targets_in_order() { + let mut row = [10i32, 0, 0]; // f0 = leaf input + let order = execute_compute_dag(SAMPLE_DAG, &mut row, |r, t| { + match t { + 1 => r[1] = r[0] + 1, // f1 = f0 + 1 + 2 => r[2] = r[1] * 2, // f2 = f1 * 2 + _ => unreachable!("leaves are never computed"), + } + Ok::<(), core::convert::Infallible>(()) + }) + .expect("acyclic dag executes"); + assert_eq!(order, compute_dag_topo_order(SAMPLE_DAG).unwrap()); + assert_eq!(row, [10, 11, 22], "each target saw its precedent's value"); + } + + /// A cyclic manifest is refused before any target runs. + #[test] + fn execute_compute_dag_refuses_cycle_untouched_store() { + let two_cycle = &[ + ComputeEdge { + target: 0, + inputs: &[1], + }, + ComputeEdge { + target: 1, + inputs: &[0], + }, + ]; + let mut row = [1i32, 2]; + let err = execute_compute_dag(two_cycle, &mut row, |_, _| { + Ok::<(), core::convert::Infallible>(()) + }) + .unwrap_err(); + assert_eq!(err, ExecuteComputeError::Cyclic); + assert_eq!(row, [1, 2], "store untouched on cycle refusal"); + } + + /// A failing consumer closure aborts at the failing target: earlier + /// targets stay written, later targets never run. + #[test] + fn execute_compute_dag_aborts_at_failing_target() { + let mut row = [10i32, 0, 0]; + let err = execute_compute_dag(SAMPLE_DAG, &mut row, |r, t| match t { + 1 => { + r[1] = r[0] + 1; + Ok(()) + } + _ => Err("boom"), + }) + .unwrap_err(); + assert_eq!( + err, + ExecuteComputeError::Compute { + target: 2, + source: "boom" + } + ); + assert_eq!(row, [10, 11, 0], "f1 written, f2 aborted"); + } + + /// Empty manifest = empty order (zero-fallback: nothing to recompute). + #[test] + fn execute_compute_dag_empty_is_noop() { + let mut row = [1i32]; + let order = + execute_compute_dag( + &[], + &mut row, + |_, _| Ok::<(), core::convert::Infallible>(()), + ) + .unwrap(); + assert!(order.is_empty()); + assert_eq!(row, [1]); + } + #[test] fn nav_reachability_is_a_forward_closure() { // root(0) -> orders(1) -> lines(2); root -> settings(3)