From 8578006ea375b3aacafeecda37fd6092d8df91d6 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 2 Oct 2026 08:38:20 +0200 Subject: [PATCH] fix: no early cutoff between documents in the same sub-decision cycle Inside a decision-call cycle a recorded signature depends on which member was cut off as recursive, so comparing it could skip a dependent (seen when undoing an edit in a 3-graph cycle). --- core/engine/src/workspace/affected.rs | 6 ++++ .../src/workspace/document_dependencies.rs | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/core/engine/src/workspace/affected.rs b/core/engine/src/workspace/affected.rs index 7c6e289c..15fc4fe7 100644 --- a/core/engine/src/workspace/affected.rs +++ b/core/engine/src/workspace/affected.rs @@ -7,6 +7,7 @@ use crate::workspace::reads::ReadView; impl Db { pub(crate) fn walk(&self, changed: &[Arc]) -> Vec> { let frozen = self.frozen_views(); + let cycles = self.document_dependencies().signature_cycles(); self.document_dependencies() .affected(changed, |user, dependency, kind| { let recorded = frozen @@ -20,8 +21,13 @@ impl Db { | (DependencyKind::Signature, ReadView::Signature(_)) ) }); + let cyclic = kind == DependencyKind::Signature + && cycles + .get(user) + .is_some_and(|component| cycles.get(dependency) == Some(component)); match (kind, recorded) { (DependencyKind::Import, _) | (_, None) => true, + _ if cyclic => true, (_, Some(view)) => { self.graph_stack.borrow_mut().push(user.clone()); let holds = self.view_holds(dependency, view); diff --git a/core/engine/src/workspace/document_dependencies.rs b/core/engine/src/workspace/document_dependencies.rs index fed335c3..e7889738 100644 --- a/core/engine/src/workspace/document_dependencies.rs +++ b/core/engine/src/workspace/document_dependencies.rs @@ -2,6 +2,8 @@ use std::collections::VecDeque; use std::sync::Arc; use ahash::{HashMap, HashSet}; +use petgraph::algo::tarjan_scc; +use petgraph::graph::{DiGraph, NodeIndex}; use zen_types::decision::DecisionNodeKind; use crate::model::DecisionContent; @@ -94,6 +96,38 @@ impl DependencyIndex { out } + pub fn signature_cycles(&self) -> HashMap, usize> { + let mut graph: DiGraph, ()> = DiGraph::new(); + let mut nodes: HashMap, NodeIndex> = HashMap::default(); + let mut node = |graph: &mut DiGraph, ()>, path: &Arc| { + *nodes + .entry(path.clone()) + .or_insert_with(|| graph.add_node(path.clone())) + }; + for (user, uses) in &self.uses { + for (dependency, kind) in uses { + if *kind == DependencyKind::Signature { + let from = node(&mut graph, user); + let to = node(&mut graph, dependency); + graph.add_edge(from, to, ()); + } + } + } + let mut out: HashMap, usize> = HashMap::default(); + for (id, component) in tarjan_scc(&graph).into_iter().enumerate() { + let cyclic = component.len() > 1 + || component + .first() + .is_some_and(|&n| graph.contains_edge(n, n)); + if cyclic { + for n in component { + out.insert(graph[n].clone(), id); + } + } + } + out + } + fn declared(content: &DecisionContent) -> Vec { let mut out: Vec = Vec::new(); let mut push = |path: &Arc, kind: DependencyKind| {