Skip to main content

graphrefly/
node.rs

1//! The node — the thinnest substrate primitive (D5 / R-node-thin).
2//!
3//! Holds a fn handle + deps + the wave state machine; **zero inspection cruft**
4//! (naming/find/describe are the graph layer). Single-thread (D22) ⇒ the shared
5//! state lives in a graph-local arena behind `Rc<RefCell<GraphCore>>` ([`Core`] =
6//! graph handle + node id), `!Send + !Sync`.
7//!
8//! Values cross the substrate **erased** as [`AnyValue`] (`Rc<dyn Any>`) — the
9//! Rust analogue of TS's `unknown` (per-language impl, `CLEAN-SLATE.md`). A typed
10//! [`Node<T>`] facade re-types the boundary (downcast at `cache`/`data`).
11//!
12//! ## Borrow discipline (the Rust-vs-TS divergence)
13//!
14//! TS has no runtime borrow check; here the wave engine must never hold a
15//! `RefCell` borrow across a call that re-enters a node — `ctx.down` re-enters
16//! the running node, and delivery re-enters downstream nodes. The rule throughout
17//! is **clone the callable out, drop the borrow, then call** (subscribers,
18//! dispatcher fns, dep handles). Short `borrow_mut` scopes mutate; calls happen
19//! borrow-free.
20//!
21//! ## Current substrate scope
22//!
23//! The active substrate covers state / producer / derived nodes, two-phase
24//! DIRTY-to-DATA delivery, diamond joins, first-run gating, push-on-subscribe,
25//! lazy activation, ROM/RAM, INVALIDATE, PAUSE/RESUME locksets and pause modes,
26//! COMPLETE/ERROR/TEARDOWN terminal behavior, batch commit/rollback, LocalAsync
27//! boundaries, runtime rewire, pull/routed-up demand, `ctx.rewire_next`, and
28//! panic-to-ERROR recovery at the wave boundary. Every occurrence remains DATA;
29//! no-emit functions settle with substrate-synthesized undirty RESOLVED (D49).
30
31use std::cell::{Cell, Ref, RefCell};
32use std::collections::{HashMap, HashSet, VecDeque};
33use std::marker::PhantomData;
34use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
35use std::rc::{Rc, Weak};
36
37use crate::batch::{
38    active_batch_committed_token, boundary_drains_blocked, collecting_batch,
39    committed_after_batch_for_target, defer_to_batch, register_boundary_root,
40};
41use crate::checkpoint::{
42    unregister_backend_state_contributor, unregister_backend_state_contributor_key,
43};
44use crate::ctx::{Ctx, DepRecord, DepTerminal, WaveData};
45use crate::dispatcher::{default_dispatcher, Dispatcher, NodeFn, PoolKind};
46use crate::environment::EnvironmentDrivers;
47use crate::host_boundary::is_host_boundary_abort_payload;
48use crate::protocol::{AnyValue, GraphError, Handle, LockId, Message, PullDemand, Tier, Wave};
49use crate::versioning::{
50    advance_node_version, assert_node_version_data_compatible, create_node_version,
51    resolve_node_versioning_policy, NodeVersion, NodeVersioningPolicy,
52    ResolvedNodeVersioningPolicy, RestoredNodeVersion,
53};
54
55/// Node lifecycle status (R-status-enum) — the source of truth for cache freshness.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum Status {
58    /// No DATA yet — cache absent (SENTINEL = `None`, D16). Also the
59    /// awaiting-first-run-gate state in this slice.
60    Sentinel,
61    /// Reserved by R-status-enum; not assigned in this slice (the awaiting-gate
62    /// state is represented as `Sentinel`, matching the TS reference).
63    Pending,
64    /// Phase-1 DIRTY received — the prior cached value is stale.
65    Dirty,
66    /// Settled with a fresh value this wave.
67    Settled,
68    /// Undirty settle (R-resolved-undirty / D49): DIRTY'd but produced no new
69    /// occurrence; a carried-over cached value stays fresh.
70    Resolved,
71    /// Terminal success — final (deferred slice).
72    Completed,
73    /// Terminal failure — final (deferred slice).
74    Errored,
75}
76
77impl Status {
78    /// Cached value is fresh (`settled` / `resolved`).
79    pub fn is_fresh(self) -> bool {
80        matches!(self, Status::Settled | Status::Resolved)
81    }
82    /// Terminal status (`completed` / `errored`) — D17 terminal-is-forever.
83    pub fn is_terminal(self) -> bool {
84        matches!(self, Status::Completed | Status::Errored)
85    }
86}
87
88/// PAUSE/RESUME backpressure mode (R-pause-modes). The OUTER gate over async-paused
89/// buffering (D44).
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub enum Pausable {
92    /// Default. Skip dep-driven fn recompute while paused, fire once on final-lock
93    /// RESUME with the latest dep values. A leaf source's (depless) OWN production —
94    /// sync or async — is NOT gated (delivered immediately); an async COMPUTE node's
95    /// (deps>0) in-flight result buffers (R-async-paused / C-2).
96    #[default]
97    True,
98    /// Production-gating: buffer the node's own tier-3/4 settle slice (sync OR async)
99    /// while paused and replay it on final-lock RESUME. B9 pre-pause baseline-equals
100    /// fidelity is deferred (same partial level as the TS arm).
101    ResumeAll,
102    /// Ignore PAUSE/RESUME ENTIRELY (timer/interval-class sources that must keep
103    /// producing): the lockset is never consulted, nothing is gated or buffered (D44,
104    /// resolves B20).
105    False,
106}
107
108/// Node construction options (substrate-level). Sugar/operator opts are the graph layer
109/// (D6/D24); this is the minimal substrate surface needed to select a pool (D20), a
110/// pause mode (R-pause-modes), and the dep-terminal propagation policy (R-deps-terminal).
111#[derive(Debug, Clone)]
112pub struct NodeOpts {
113    /// Real factory name for graph-less nodes that `describe` auto-discovers (D43/D51).
114    /// Registered graph entries still carry their own graph-layer factory string.
115    pub factory: Option<String>,
116    /// The dispatch pool (default LocalSync, R-sync-core).
117    pub pool: PoolKind,
118    /// The pause mode (default `true`, R-pause-modes).
119    pub pausable: Pausable,
120    /// First-run gate override: allow fn execution with SENTINEL deps (graph-layer
121    /// partial combinators / pull consumers). Default false.
122    pub partial: bool,
123    /// Pull-mode node id (R-pull / D269). When present, the node starts quiet; a
124    /// routed PULL({pullId, params?}) of the same id demands one delivery.
125    pub pull_id: Option<LockId>,
126    /// Auto-emit COMPLETE when ALL deps complete (default `true`; combineLatest
127    /// semantics — ALL not ANY). last/reduce/*Map set `false` to ABSORB inner
128    /// terminals (R-deps-terminal).
129    pub complete_when_deps_complete: bool,
130    /// Auto-emit ERROR when ANY dep errors (default `true`). rescue/catch set `false`.
131    pub error_when_deps_error: bool,
132    /// A dep's terminal is a REAL INPUT the fn reads (rescue/reduce/*Map) rather than an
133    /// absorbed settle — when set, the fn re-runs on a dep terminal and the first-run
134    /// gate treats a terminated dep as settled (R-deps-terminal). Default `false`.
135    pub terminal_as_real_input: bool,
136    /// D109 node runtime versioning policy. `None` means the package default V0; graph-owned
137    /// nodes may inherit a graph default before construction.
138    pub versioning: Option<NodeVersioningPolicy>,
139}
140
141/// Defaults: COMPLETE/ERROR auto-cascade ON (combineLatest-style), terminal-as-input OFF
142/// — the plain derived/effect behavior. (A manual impl, not `#[derive(Default)]`, so the
143/// two cascade flags default to `true`, not `false`.)
144impl Default for NodeOpts {
145    fn default() -> Self {
146        Self {
147            factory: None,
148            pool: PoolKind::default(),
149            pausable: Pausable::default(),
150            partial: false,
151            pull_id: None,
152            complete_when_deps_complete: true,
153            error_when_deps_error: true,
154            terminal_as_real_input: false,
155            versioning: None,
156        }
157    }
158}
159
160type Msg = Message<AnyValue>;
161type Sink = Rc<dyn Fn(&Msg)>;
162type Unsub = Box<dyn FnOnce()>;
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub(crate) enum SubscriberKind {
166    External,
167    GraphObserver,
168}
169
170struct SubscriberEntry {
171    id: u64,
172    kind: SubscriberKind,
173    sink: Sink,
174}
175
176enum SubscriberSnapshot {
177    Empty,
178    One(Sink),
179    Many(Vec<Sink>),
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
183enum MaybeRunDecision {
184    #[default]
185    Skip,
186    Passthrough,
187    Run,
188}
189
190fn snapshot_subscribers(a: &NodeAux) -> SubscriberSnapshot {
191    match a.subscribers.as_slice() {
192        [] => SubscriberSnapshot::Empty,
193        [entry] => SubscriberSnapshot::One(entry.sink.clone()),
194        many => SubscriberSnapshot::Many(many.iter().map(|entry| entry.sink.clone()).collect()),
195    }
196}
197
198fn deliver_subscriber_snapshot(subs: SubscriberSnapshot, msg: &Msg) {
199    match subs {
200        SubscriberSnapshot::Empty => {}
201        SubscriberSnapshot::One(s) => s(msg),
202        SubscriberSnapshot::Many(subs) => {
203            for s in subs {
204                s(msg);
205            }
206        }
207    }
208}
209
210/// A deferred self-rewire request (R-rewire-deferred / D47), issued via `ctx.rewire_next`
211/// and applied at the committed wave boundary. The new dep set is computed at APPLY time (so
212/// multiple requests in one wave compose against the live deps), then the surgical R-rewire
213/// (D42) runs as a fresh wave. The fn re-pairs the deps (SD-1 fn-deps pairing).
214pub(crate) enum RewireRequest {
215    /// Add a dep (idempotent if already present) + swap the fn.
216    Add(Core, NodeFn),
217    /// Remove a live dep identity + swap the fn. If that identity is already absent
218    /// at boundary-apply time, the request is a full no-op, including no fn swap, so
219    /// stale duplicate removes cannot desync the live dep/fn pairing.
220    Remove(CoreIdentity, NodeFn),
221    /// Replace the whole dep set + swap the fn.
222    Set(Vec<Core>, NodeFn),
223}
224
225impl RewireRequest {
226    pub(crate) fn remove(dep: &Core, f: NodeFn) -> Self {
227        Self::Remove(CoreIdentity::from_core(dep), f)
228    }
229
230    fn validate_deps_same_graph(&self, owner: &Core) {
231        match self {
232            RewireRequest::Set(deps, _) => {
233                for dep in deps {
234                    assert!(
235                        dep.same_graph(owner),
236                        "rewire: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
237                    );
238                }
239            }
240            RewireRequest::Add(dep, _) => {
241                assert!(
242                    dep.same_graph(owner),
243                    "rewire: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
244                );
245            }
246            RewireRequest::Remove(_, _) => {}
247        }
248    }
249
250    fn project_deps(&self, current: Vec<Core>) -> Vec<Core> {
251        match self {
252            RewireRequest::Set(deps, _) => deps.clone(),
253            RewireRequest::Add(dep, _) => {
254                let mut next = current;
255                if !next.iter().any(|d| d.ptr_eq(dep)) {
256                    next.push(dep.clone());
257                }
258                next
259            }
260            RewireRequest::Remove(dep, _) => {
261                current.into_iter().filter(|d| !dep.matches(d)).collect()
262            }
263        }
264    }
265
266    fn into_deps_and_fn(self, current: Vec<Core>) -> (Vec<Core>, NodeFn) {
267        match self {
268            RewireRequest::Set(deps, f) => (deps, f),
269            RewireRequest::Add(dep, f) => {
270                let mut next = current;
271                if !next.iter().any(|d| d.ptr_eq(&dep)) {
272                    next.push(dep);
273                }
274                (next, f)
275            }
276            RewireRequest::Remove(dep, f) => {
277                let next = current.into_iter().filter(|d| !dep.matches(d)).collect();
278                (next, f)
279            }
280        }
281    }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
285struct NodeId(usize);
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
288struct NodeKey {
289    id: NodeId,
290    generation: u64,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
294struct ArenaNodeKey {
295    graph: usize,
296    node: NodeKey,
297}
298
299pub(crate) struct CoreIdentity {
300    arena_key: ArenaNodeKey,
301}
302
303impl CoreIdentity {
304    fn from_core(core: &Core) -> Self {
305        Self {
306            arena_key: core.arena_node_key(),
307        }
308    }
309
310    fn matches(&self, core: &Core) -> bool {
311        self.arena_key == core.arena_node_key()
312    }
313}
314
315/// B49 / D71 transition slice: graph-local arena for node bookkeeping. The
316/// public [`Core`] is now a node id plus a shared graph arena, rather than a direct
317/// `Rc<RefCell<NodeInner>>`. Topology is already graph-owned in `NodeTopologySlot`;
318/// this keeps the public API stable while moving ownership toward arena-index execution.
319pub(crate) struct GraphArena(Rc<RefCell<GraphCore>>);
320
321impl GraphArena {
322    pub(crate) fn new() -> Self {
323        Self(Rc::new(RefCell::new(GraphCore::new())))
324    }
325
326    pub(crate) fn default() -> Self {
327        DEFAULT_GRAPH_CORE.with(|graph| Self(graph.clone()))
328    }
329}
330
331struct GraphCore {
332    slots: Vec<Option<NodeInner>>,
333    topology_slots: Vec<Option<NodeTopologySlot>>,
334    call_slots: Vec<Option<NodeCallSlot>>,
335    config_slots: Vec<Option<NodeConfigSlot>>,
336    run_slots: Vec<Option<NodeRunState>>,
337    edge_slots: Vec<Option<DepEdges>>,
338    version_slots: Vec<Option<NodeVersionState>>,
339    aux_slots: Vec<Option<NodeAux>>,
340    generations: Vec<u64>,
341    touched_waves: Vec<u64>,
342    pins: Vec<usize>,
343    free: Vec<usize>,
344    deferred_boundary: VecDeque<BoundaryTask>,
345    draining_boundary: bool,
346}
347
348type TakenNodeSlot = (
349    NodeInner,
350    NodeTopologySlot,
351    NodeCallSlot,
352    NodeConfigSlot,
353    NodeRunState,
354    DepEdges,
355    NodeVersionState,
356    NodeAux,
357);
358
359impl GraphCore {
360    fn new() -> Self {
361        Self {
362            slots: Vec::new(),
363            topology_slots: Vec::new(),
364            call_slots: Vec::new(),
365            config_slots: Vec::new(),
366            run_slots: Vec::new(),
367            edge_slots: Vec::new(),
368            version_slots: Vec::new(),
369            aux_slots: Vec::new(),
370            generations: Vec::new(),
371            touched_waves: Vec::new(),
372            pins: Vec::new(),
373            free: Vec::new(),
374            deferred_boundary: VecDeque::new(),
375            draining_boundary: false,
376        }
377    }
378
379    fn alloc(
380        &mut self,
381        inner: NodeInner,
382        topology: NodeTopologySlot,
383        call: NodeCallSlot,
384        config: NodeConfigSlot,
385        version: NodeVersionState,
386    ) -> NodeId {
387        let edges = DepEdges::new(topology.deps.len());
388        let aux = NodeAux::new();
389        let run = NodeRunState::new();
390        if let Some(id) = self.free.last().copied() {
391            let next_generation = self.generations[id]
392                .checked_add(1)
393                .expect("GraphCore generation overflow on slot reuse");
394            self.free.pop();
395            self.slots[id] = Some(inner);
396            self.topology_slots[id] = Some(topology);
397            self.call_slots[id] = Some(call);
398            self.config_slots[id] = Some(config);
399            self.run_slots[id] = Some(run);
400            self.edge_slots[id] = Some(edges);
401            self.version_slots[id] = Some(version);
402            self.aux_slots[id] = Some(aux);
403            self.generations[id] = next_generation;
404            self.touched_waves[id] = 0;
405            self.pins[id] = 0;
406            NodeId(id)
407        } else {
408            let id = self.slots.len();
409            self.slots.push(Some(inner));
410            self.topology_slots.push(Some(topology));
411            self.call_slots.push(Some(call));
412            self.config_slots.push(Some(config));
413            self.run_slots.push(Some(run));
414            self.edge_slots.push(Some(edges));
415            self.version_slots.push(Some(version));
416            self.aux_slots.push(Some(aux));
417            self.generations.push(0);
418            self.touched_waves.push(0);
419            self.pins.push(0);
420            NodeId(id)
421        }
422    }
423
424    fn key_for(&self, id: NodeId) -> NodeKey {
425        NodeKey {
426            id,
427            generation: self.generations[id.0],
428        }
429    }
430
431    fn get(&self, key: NodeKey) -> &NodeTopologySlot {
432        assert!(
433            self.is_live_key(key),
434            "Core points at a stale or freed GraphCore slot"
435        );
436        self.topology_slots
437            .get(key.id.0)
438            .and_then(Option::as_ref)
439            .expect("Core points at a live GraphCore topology slot")
440    }
441
442    fn get_node_and_edges_mut(&mut self, key: NodeKey) -> (&mut NodeTopologySlot, &mut DepEdges) {
443        assert!(
444            self.is_live_key(key),
445            "Core points at a stale or freed GraphCore slot"
446        );
447        let n = self
448            .topology_slots
449            .get_mut(key.id.0)
450            .and_then(Option::as_mut)
451            .expect("Core points at a live GraphCore topology slot");
452        let e = self
453            .edge_slots
454            .get_mut(key.id.0)
455            .and_then(Option::as_mut)
456            .expect("Core points at a live GraphCore edge slot");
457        (n, e)
458    }
459
460    fn get_node_call_config_run_edges(
461        &self,
462        key: NodeKey,
463    ) -> (
464        &NodeTopologySlot,
465        &NodeCallSlot,
466        &NodeConfigSlot,
467        &NodeRunState,
468        &DepEdges,
469    ) {
470        assert!(
471            self.is_live_key(key),
472            "Core points at a stale or freed GraphCore slot"
473        );
474        let n = self
475            .topology_slots
476            .get(key.id.0)
477            .and_then(Option::as_ref)
478            .expect("Core points at a live GraphCore topology slot");
479        let c = self
480            .call_slots
481            .get(key.id.0)
482            .and_then(Option::as_ref)
483            .expect("Core points at a live GraphCore call slot");
484        let cfg = self
485            .config_slots
486            .get(key.id.0)
487            .and_then(Option::as_ref)
488            .expect("Core points at a live GraphCore config slot");
489        let r = self
490            .run_slots
491            .get(key.id.0)
492            .and_then(Option::as_ref)
493            .expect("Core points at a live GraphCore run slot");
494        let e = self
495            .edge_slots
496            .get(key.id.0)
497            .and_then(Option::as_ref)
498            .expect("Core points at a live GraphCore edge slot");
499        (n, c, cfg, r, e)
500    }
501
502    fn get_call(&self, key: NodeKey) -> &NodeCallSlot {
503        assert!(
504            self.is_live_key(key),
505            "Core points at a stale or freed GraphCore slot"
506        );
507        self.call_slots
508            .get(key.id.0)
509            .and_then(Option::as_ref)
510            .expect("Core points at a live GraphCore call slot")
511    }
512
513    fn get_call_mut(&mut self, key: NodeKey) -> &mut NodeCallSlot {
514        assert!(
515            self.is_live_key(key),
516            "Core points at a stale or freed GraphCore slot"
517        );
518        self.call_slots
519            .get_mut(key.id.0)
520            .and_then(Option::as_mut)
521            .expect("Core points at a live GraphCore call slot")
522    }
523
524    fn get_config(&self, key: NodeKey) -> &NodeConfigSlot {
525        assert!(
526            self.is_live_key(key),
527            "Core points at a stale or freed GraphCore slot"
528        );
529        self.config_slots
530            .get(key.id.0)
531            .and_then(Option::as_ref)
532            .expect("Core points at a live GraphCore config slot")
533    }
534
535    fn get_version(&self, key: NodeKey) -> &NodeVersionState {
536        assert!(
537            self.is_live_key(key),
538            "Core points at a stale or freed GraphCore slot"
539        );
540        self.version_slots
541            .get(key.id.0)
542            .and_then(Option::as_ref)
543            .expect("Core points at a live GraphCore version slot")
544    }
545
546    fn get_version_mut(&mut self, key: NodeKey) -> &mut NodeVersionState {
547        assert!(
548            self.is_live_key(key),
549            "Core points at a stale or freed GraphCore slot"
550        );
551        self.version_slots
552            .get_mut(key.id.0)
553            .and_then(Option::as_mut)
554            .expect("Core points at a live GraphCore version slot")
555    }
556
557    fn get_node_edges_aux_mut(
558        &mut self,
559        key: NodeKey,
560    ) -> (&mut NodeTopologySlot, &mut DepEdges, &mut NodeAux) {
561        assert!(
562            self.is_live_key(key),
563            "Core points at a stale or freed GraphCore slot"
564        );
565        let topology_slots = &mut self.topology_slots;
566        let edge_slots = &mut self.edge_slots;
567        let aux_slots = &mut self.aux_slots;
568        let n = topology_slots
569            .get_mut(key.id.0)
570            .and_then(Option::as_mut)
571            .expect("Core points at a live GraphCore topology slot");
572        let e = edge_slots
573            .get_mut(key.id.0)
574            .and_then(Option::as_mut)
575            .expect("Core points at a live GraphCore edge slot");
576        let a = aux_slots
577            .get_mut(key.id.0)
578            .and_then(Option::as_mut)
579            .expect("Core points at a live GraphCore aux slot");
580        (n, e, a)
581    }
582
583    fn get_node_call_config_run_edges_mut(
584        &mut self,
585        key: NodeKey,
586    ) -> (
587        &mut NodeTopologySlot,
588        &mut NodeCallSlot,
589        &mut NodeConfigSlot,
590        &mut NodeRunState,
591        &mut DepEdges,
592    ) {
593        assert!(
594            self.is_live_key(key),
595            "Core points at a stale or freed GraphCore slot"
596        );
597        let topology_slots = &mut self.topology_slots;
598        let call_slots = &mut self.call_slots;
599        let config_slots = &mut self.config_slots;
600        let run_slots = &mut self.run_slots;
601        let edge_slots = &mut self.edge_slots;
602        let n = topology_slots
603            .get_mut(key.id.0)
604            .and_then(Option::as_mut)
605            .expect("Core points at a live GraphCore topology slot");
606        let c = call_slots
607            .get_mut(key.id.0)
608            .and_then(Option::as_mut)
609            .expect("Core points at a live GraphCore call slot");
610        let cfg = config_slots
611            .get_mut(key.id.0)
612            .and_then(Option::as_mut)
613            .expect("Core points at a live GraphCore config slot");
614        let r = run_slots
615            .get_mut(key.id.0)
616            .and_then(Option::as_mut)
617            .expect("Core points at a live GraphCore run slot");
618        let e = edge_slots
619            .get_mut(key.id.0)
620            .and_then(Option::as_mut)
621            .expect("Core points at a live GraphCore edge slot");
622        (n, c, cfg, r, e)
623    }
624
625    fn get_node_call_config_run_edges_aux_mut(
626        &mut self,
627        key: NodeKey,
628    ) -> (
629        &mut NodeTopologySlot,
630        &mut NodeCallSlot,
631        &mut NodeConfigSlot,
632        &mut NodeRunState,
633        &mut DepEdges,
634        &mut NodeAux,
635    ) {
636        assert!(
637            self.is_live_key(key),
638            "Core points at a stale or freed GraphCore slot"
639        );
640        let topology_slots = &mut self.topology_slots;
641        let call_slots = &mut self.call_slots;
642        let config_slots = &mut self.config_slots;
643        let run_slots = &mut self.run_slots;
644        let edge_slots = &mut self.edge_slots;
645        let aux_slots = &mut self.aux_slots;
646        let n = topology_slots
647            .get_mut(key.id.0)
648            .and_then(Option::as_mut)
649            .expect("Core points at a live GraphCore topology slot");
650        let c = call_slots
651            .get_mut(key.id.0)
652            .and_then(Option::as_mut)
653            .expect("Core points at a live GraphCore call slot");
654        let cfg = config_slots
655            .get_mut(key.id.0)
656            .and_then(Option::as_mut)
657            .expect("Core points at a live GraphCore config slot");
658        let r = run_slots
659            .get_mut(key.id.0)
660            .and_then(Option::as_mut)
661            .expect("Core points at a live GraphCore run slot");
662        let e = edge_slots
663            .get_mut(key.id.0)
664            .and_then(Option::as_mut)
665            .expect("Core points at a live GraphCore edge slot");
666        let a = aux_slots
667            .get_mut(key.id.0)
668            .and_then(Option::as_mut)
669            .expect("Core points at a live GraphCore aux slot");
670        (n, c, cfg, r, e, a)
671    }
672
673    fn get_aux(&self, key: NodeKey) -> &NodeAux {
674        assert!(
675            self.is_live_key(key),
676            "Core points at a stale or freed GraphCore slot"
677        );
678        self.aux_slots
679            .get(key.id.0)
680            .and_then(Option::as_ref)
681            .expect("Core points at a live GraphCore aux slot")
682    }
683
684    fn is_live(&self, id: NodeId) -> bool {
685        let live = self.slots.get(id.0).is_some_and(Option::is_some);
686        if live {
687            debug_assert!(self.topology_slots.get(id.0).is_some_and(Option::is_some));
688            debug_assert!(self.call_slots.get(id.0).is_some_and(Option::is_some));
689            debug_assert!(self.config_slots.get(id.0).is_some_and(Option::is_some));
690            debug_assert!(self.run_slots.get(id.0).is_some_and(Option::is_some));
691            debug_assert!(self.edge_slots.get(id.0).is_some_and(Option::is_some));
692            debug_assert!(self.version_slots.get(id.0).is_some_and(Option::is_some));
693            debug_assert!(self.aux_slots.get(id.0).is_some_and(Option::is_some));
694        }
695        live
696    }
697
698    fn is_live_key(&self, key: NodeKey) -> bool {
699        self.generations.get(key.id.0) == Some(&key.generation) && self.is_live(key.id)
700    }
701
702    fn pin_live_key(&mut self, key: NodeKey) -> bool {
703        if !self.is_live_key(key) {
704            return false;
705        }
706        self.pins[key.id.0] = self.pins[key.id.0]
707            .checked_add(1)
708            .expect("GraphCore pin count overflow");
709        true
710    }
711
712    fn release_pin(&mut self, key: NodeKey) {
713        if self.generations.get(key.id.0) != Some(&key.generation) {
714            return;
715        }
716        let Some(pin) = self.pins.get_mut(key.id.0) else {
717            return;
718        };
719        if *pin > 0 {
720            *pin -= 1;
721        }
722    }
723
724    fn pin_count(&self, key: NodeKey) -> usize {
725        if self.generations.get(key.id.0) != Some(&key.generation) {
726            return 0;
727        }
728        self.pins.get(key.id.0).copied().unwrap_or(0)
729    }
730
731    fn take_live(&mut self, key: NodeKey) -> Option<TakenNodeSlot> {
732        if !self.is_live_key(key) {
733            return None;
734        }
735        let inner = self.slots.get_mut(key.id.0)?.take()?;
736        let topology = self.topology_slots.get_mut(key.id.0)?.take()?;
737        let call = self.call_slots.get_mut(key.id.0)?.take()?;
738        let config = self.config_slots.get_mut(key.id.0)?.take()?;
739        let run = self.run_slots.get_mut(key.id.0)?.take()?;
740        let edges = self.edge_slots.get_mut(key.id.0)?.take()?;
741        let version = self.version_slots.get_mut(key.id.0)?.take()?;
742        let aux = self.aux_slots.get_mut(key.id.0)?.take()?;
743        Some((inner, topology, call, config, run, edges, version, aux))
744    }
745}
746
747thread_local! {
748    /// Until the Rust graph layer lands, public constructors allocate into the default
749    /// graph-local arena, mirroring the existing default dispatcher shape (D26). A later
750    /// graph-owned constructor can pass an explicit arena without changing [`Core`].
751    static DEFAULT_GRAPH_CORE: Rc<RefCell<GraphCore>> = Rc::new(RefCell::new(GraphCore::new()));
752}
753
754/// Per-dependency wave bookkeeping for one node slot.
755struct DepState {
756    batch: Vec<Option<Vec<AnyValue>>>,
757    batch_waves: Vec<Vec<Vec<WaveData>>>,
758    batch_wave_id: Vec<Option<u64>>,
759    prev: Vec<Option<AnyValue>>,
760    has_data: Vec<bool>,
761    dirty: Vec<bool>,
762    tier: Vec<u8>,
763    pending: i32,
764    terminal: Vec<Option<DepTerminal>>,
765    terminal_wave: Vec<Option<DepTerminal>>,
766}
767
768impl DepState {
769    fn new(dep_count: usize) -> Self {
770        Self {
771            batch: vec![None; dep_count],
772            batch_waves: vec![Vec::new(); dep_count],
773            batch_wave_id: vec![None; dep_count],
774            prev: vec![None; dep_count],
775            has_data: vec![false; dep_count],
776            dirty: vec![false; dep_count],
777            tier: vec![0; dep_count],
778            pending: 0,
779            terminal: vec![None; dep_count],
780            terminal_wave: vec![None; dep_count],
781        }
782    }
783
784    fn all_settled(&self, terminal_as_real_input: bool) -> bool {
785        for i in 0..self.has_data.len() {
786            if self.has_data[i] {
787                continue;
788            }
789            if terminal_as_real_input && self.terminal[i].is_some() {
790                continue;
791            }
792            return false;
793        }
794        true
795    }
796
797    fn current_wave_mut(&mut self, idx: usize) -> &mut Vec<WaveData> {
798        let delivery_id = current_delivery_id();
799        if self.batch_wave_id[idx] != Some(delivery_id) {
800            self.batch_waves[idx].push(Vec::new());
801            self.batch_wave_id[idx] = Some(delivery_id);
802        }
803        self.batch_waves[idx]
804            .last_mut()
805            .expect("current_wave_mut just pushed an entry")
806    }
807
808    fn record_wave_data(&mut self, idx: usize, value: AnyValue) {
809        self.current_wave_mut(idx).push(WaveData::Data(value));
810    }
811
812    fn record_wave_sentinel(&mut self, idx: usize) -> bool {
813        let had_current_projection = self.batch_wave_id[idx] == Some(current_delivery_id());
814        self.current_wave_mut(idx).push(WaveData::Sentinel);
815        had_current_projection
816    }
817
818    fn has_run_triggering_projection(&self) -> bool {
819        self.batch_waves.iter().any(|dep_waves| {
820            dep_waves.iter().any(|wave| {
821                wave.is_empty() || wave.iter().any(|item| matches!(item, WaveData::Data(_)))
822            })
823        })
824    }
825
826    fn clear_wave_projection(&mut self, idx: usize) {
827        self.batch_waves[idx].clear();
828        self.batch_wave_id[idx] = None;
829    }
830
831    fn record_resolved_wave(&mut self, idx: usize) {
832        let _ = self.current_wave_mut(idx);
833    }
834}
835
836/// Per-node transient wave/mutation flags.
837struct NodeWaveState {
838    emitted_dirty_this_wave: bool,
839    emitted_tier3_this_wave: bool,
840    inside_run_wave: bool,
841    in_dep_mutation: bool,
842    rewire_run_pending: bool,
843    batch_dirty_owed: bool,
844}
845
846impl NodeWaveState {
847    fn new() -> Self {
848        Self {
849            emitted_dirty_this_wave: false,
850            emitted_tier3_this_wave: false,
851            inside_run_wave: false,
852            in_dep_mutation: false,
853            rewire_run_pending: false,
854            batch_dirty_owed: false,
855        }
856    }
857}
858
859struct NodeValueState {
860    cache: Option<AnyValue>,
861    has_data: bool,
862    status: Status,
863    terminal: bool,
864}
865
866impl NodeValueState {
867    fn new() -> Self {
868        Self {
869            cache: None,
870            has_data: false,
871            status: Status::Sentinel,
872            terminal: false,
873        }
874    }
875}
876
877/// Per-dependency edge bookkeeping (subscription ownership + callback index indirection).
878/// Kept in GraphCore side-tables as part of the B49 arena transition.
879struct DepEdges {
880    value: NodeValueState,
881    wave: NodeWaveState,
882    state: DepState,
883    unsubs: Vec<Option<Unsub>>,
884    idx_boxes: Vec<Rc<Cell<i64>>>,
885    restored_activation_handshake: Vec<bool>,
886}
887
888impl DepEdges {
889    fn new(dep_count: usize) -> Self {
890        Self {
891            value: NodeValueState::new(),
892            wave: NodeWaveState::new(),
893            state: DepState::new(dep_count),
894            unsubs: (0..dep_count).map(|_| None).collect(),
895            idx_boxes: (0..dep_count).map(|_| Rc::new(Cell::new(-1i64))).collect(),
896            restored_activation_handshake: vec![false; dep_count],
897        }
898    }
899}
900
901/// Dispatcher-owned callable metadata for one node. Keeping this in GraphCore
902/// lets the hot wave path snapshot the call target by arena index, then invoke
903/// borrow-free through the dispatcher (R-dispatch-all).
904struct NodeCallSlot {
905    factory: Option<String>,
906    handle: Option<Handle>,
907    dispatcher: Dispatcher,
908    environment: EnvironmentDrivers,
909}
910
911/// Construction/configuration state for one node. This is side-table owned so the
912/// public `Core` handle can keep thinning toward a graph/id/generation token.
913struct NodeConfigSlot {
914    partial: bool,
915    pausable: Pausable,
916    pull_id: Option<LockId>,
917    complete_when_deps_complete: bool,
918    error_when_deps_error: bool,
919    terminal_as_real_input: bool,
920    versioning: Option<NodeVersioningPolicy>,
921}
922
923impl NodeConfigSlot {
924    fn from_opts(opts: &NodeOpts) -> Self {
925        Self {
926            partial: opts.partial,
927            pausable: opts.pausable,
928            pull_id: None,
929            complete_when_deps_complete: opts.complete_when_deps_complete,
930            error_when_deps_error: opts.error_when_deps_error,
931            terminal_as_real_input: opts.terminal_as_real_input,
932            versioning: opts.versioning.clone(),
933        }
934    }
935
936    fn all_deps_settled(&self, dep: &DepState) -> bool {
937        dep.all_settled(self.terminal_as_real_input)
938    }
939}
940
941struct NodeVersionState {
942    policy: ResolvedNodeVersioningPolicy,
943    value: Option<NodeVersion>,
944}
945
946impl NodeVersionState {
947    fn new(policy: Option<NodeVersioningPolicy>, initial: Option<&AnyValue>) -> Self {
948        let policy = resolve_node_versioning_policy(policy);
949        let value = create_node_version(&policy, initial)
950            .unwrap_or_else(|err| panic!("node versioning: {err}"));
951        Self { policy, value }
952    }
953}
954
955/// Lifecycle/run mutable state. Transient wave flags stay in [`NodeWaveState`];
956/// this side table holds state that persists across waves but resets on lifecycle
957/// teardown.
958struct NodeRunState {
959    has_called_fn_once: bool,
960}
961
962impl NodeRunState {
963    fn new() -> Self {
964        Self {
965            has_called_fn_once: false,
966        }
967    }
968}
969
970/// Node-local sidecars that are NOT part of dep-slot wave bookkeeping. Keeping these
971/// in a side-table continues the B49 thinning path without changing observable behavior.
972struct NodeAux {
973    subscribers: Vec<SubscriberEntry>,
974    next_sub_id: u64,
975    activated: bool,
976    state: Option<AnyValue>,
977    state_persist: bool,
978    on_deactivation: Vec<Box<dyn FnOnce()>>,
979    on_invalidate: Vec<Rc<dyn Fn()>>,
980    /// A PULL demand arrived but cannot yet fire because a dep is still pending,
981    /// the first-run gate is closed, or an external pause lock is held. Latest
982    /// params win while owed (D269/D272).
983    pull_demand_owed: Option<PullDemand>,
984    /// Holder-visible demand context installed only during a PULL delivery.
985    active_pull: Option<PullDemand>,
986    /// Guard against synchronous re-entrant demand while serving a demand.
987    in_deliver_demand: bool,
988    /// One-shot flag allowing a PULL-caused run to synthesize DIRTY when it
989    /// actually emits tier-3 while inside run_wave.
990    pull_dirty_owed: bool,
991    /// PAUSE lockset (R-pause-lockset). Paused iff non-empty.
992    pause_lockset: HashSet<LockId>,
993    /// A dep wave was skipped while paused; default true mode fires once on resume.
994    paused_dep_wave_occurred: bool,
995    /// Buffered tier-3/4 settle slices held while paused.
996    pause_buffer: Vec<Wave<AnyValue>>,
997}
998
999impl NodeAux {
1000    fn new() -> Self {
1001        Self {
1002            subscribers: Vec::new(),
1003            next_sub_id: 0,
1004            activated: false,
1005            state: None,
1006            state_persist: false,
1007            on_deactivation: Vec::new(),
1008            on_invalidate: Vec::new(),
1009            pull_demand_owed: None,
1010            active_pull: None,
1011            in_deliver_demand: false,
1012            pull_dirty_owed: false,
1013            pause_lockset: HashSet::new(),
1014            paused_dep_wave_occurred: false,
1015            pause_buffer: Vec::new(),
1016        }
1017    }
1018}
1019
1020pub(crate) struct NodeCheckpointRuntime {
1021    pub cache: Option<AnyValue>,
1022    pub has_data: bool,
1023    pub version: Option<NodeVersion>,
1024    pub status: Status,
1025    pub terminal: bool,
1026    pub activated: bool,
1027    pub has_called_fn_once: bool,
1028    pub ctx_state: Option<AnyValue>,
1029    pub ctx_state_persist: bool,
1030}
1031
1032pub(crate) struct NodeRestoreRuntime {
1033    pub cache: Option<AnyValue>,
1034    pub has_data: bool,
1035    pub version: RestoredNodeVersion,
1036    pub status: Status,
1037    pub terminal: bool,
1038    pub activated: bool,
1039    pub has_called_fn_once: bool,
1040    pub ctx_state: Option<AnyValue>,
1041    pub ctx_state_persist: bool,
1042}
1043
1044/// The mutable state of a node. Pure fields + non-reentrant helpers only; the
1045/// reentrant engine lives on [`Core`].
1046type TopologyDepsChangedObserver = Rc<dyn Fn(&[Core], &[Core])>;
1047
1048struct NodeTopologySlot {
1049    deps: Vec<Core>,
1050    topology_deps_changed: Option<TopologyDepsChangedObserver>,
1051}
1052
1053struct NodeInner;
1054
1055/// Release a node's upstream subscriptions + external resources when its inner is
1056/// finally dropped (D1).
1057///
1058/// Rust has no GC, so a `Node<T>` dropped without an explicit unsubscribe (and
1059/// with no subscriber-driven deactivation) would otherwise leave its sink in each
1060/// dep's `subscribers` list forever — the dep would never deactivate and would do
1061/// wasted work emitting to a dead `Weak`. Running the remaining `dep_unsubs` here
1062/// removes those entries, and firing `on_deactivation` releases external resources
1063/// (mirroring what GC + finalizers do in TS). Safe: at drop the refcount is 0 (no
1064/// live borrow on `self`); each unsub touches a *dep* node (a different `RefCell`),
1065/// recursively deactivating up the DAG (terminates); idempotent with `deactivate`
1066/// (which already drained both vecs, so this is then a no-op).
1067impl NodeInner {
1068    fn cleanup_before_free(
1069        &mut self,
1070        call: &mut NodeCallSlot,
1071        edges: &mut DepEdges,
1072        aux: &mut NodeAux,
1073    ) {
1074        // B32: free this node's fn slot so the dispatcher pool no longer holds the fn —
1075        // and thus the upstream `Core`s the fn captured (e.g. a `Node: Clone` handle, the
1076        // idiomatic feedback pattern) — alive for the whole process. Safe: invoke clones
1077        // the fn out before calling (the pool is unborrowed during the fn run), so a node
1078        // dropped from inside a fn can re-borrow the pool here. A fn that captures its OWN
1079        // node is a genuine Rc cycle this cannot break (the pool keeps that node alive →
1080        // Drop never runs) — that is a user-level self-cycle (use a Weak self-handle).
1081        // Run this before user cleanup callbacks so a panicking cleanup cannot strand the
1082        // dispatcher slot and its captures.
1083        if let Some(handle) = call.handle {
1084            call.dispatcher.unregister(handle);
1085        }
1086        for u in edges.unsubs.drain(..).flatten() {
1087            u();
1088        }
1089        for h in std::mem::take(&mut aux.on_deactivation) {
1090            h();
1091        }
1092    }
1093}
1094
1095/// A cloneable handle over a node slot in a shared graph arena. The reentrant
1096/// wave-engine methods live here; each manages its own short borrow scopes so calls
1097/// into other nodes (or back into this one via `ctx.down`) are never made while a
1098/// borrow is held.
1099pub struct Core {
1100    graph: Rc<RefCell<GraphCore>>,
1101    id: NodeId,
1102    generation: u64,
1103    /// Kept outside the arena so `Core::clone` can run while `GraphCore` is borrowed.
1104    refs: Rc<Cell<usize>>,
1105    /// Public/user handles are counted. Internal arena callbacks may construct an
1106    /// uncounted view for one synchronous dispatch to avoid refcount churn.
1107    counted: bool,
1108}
1109
1110impl Core {
1111    fn from_parts(
1112        graph: Rc<RefCell<GraphCore>>,
1113        id: NodeId,
1114        generation: u64,
1115        refs: Rc<Cell<usize>>,
1116    ) -> Self {
1117        assert!(
1118            refs.get() != 0,
1119            "Core counted promotion rejected: node is externally dead"
1120        );
1121        refs.set(refs.get().checked_add(1).expect("Core refcount overflow"));
1122        Self {
1123            graph,
1124            id,
1125            generation,
1126            refs,
1127            counted: true,
1128        }
1129    }
1130
1131    fn from_borrowed_parts(
1132        graph: Rc<RefCell<GraphCore>>,
1133        id: NodeId,
1134        generation: u64,
1135        refs: Rc<Cell<usize>>,
1136    ) -> Self {
1137        Self {
1138            graph,
1139            id,
1140            generation,
1141            refs,
1142            counted: false,
1143        }
1144    }
1145
1146    fn borrowed_view(&self) -> Self {
1147        Self::from_borrowed_parts(
1148            self.graph.clone(),
1149            self.id,
1150            self.generation,
1151            self.refs.clone(),
1152        )
1153    }
1154
1155    fn key(&self) -> NodeKey {
1156        NodeKey {
1157            id: self.id,
1158            generation: self.generation,
1159        }
1160    }
1161
1162    fn arena_node_key(&self) -> ArenaNodeKey {
1163        ArenaNodeKey {
1164            graph: Rc::as_ptr(&self.graph) as usize,
1165            node: self.key(),
1166        }
1167    }
1168
1169    fn boundary_root(&self) -> BoundaryRoot {
1170        BoundaryRoot {
1171            graph_id: Rc::as_ptr(&self.graph) as usize,
1172            graph: Rc::downgrade(&self.graph),
1173        }
1174    }
1175
1176    fn borrow(&self) -> Ref<'_, NodeTopologySlot> {
1177        Ref::map(self.graph.borrow(), |g| g.get(self.key()))
1178    }
1179
1180    fn with_inner_edges_mut<R>(
1181        &self,
1182        f: impl FnOnce(&mut NodeTopologySlot, &mut DepEdges) -> R,
1183    ) -> R {
1184        let mut g = self.graph.borrow_mut();
1185        let (n, e) = g.get_node_and_edges_mut(self.key());
1186        f(n, e)
1187    }
1188
1189    fn with_inner_edges_aux_mut<R>(
1190        &self,
1191        f: impl FnOnce(&mut NodeTopologySlot, &mut DepEdges, &mut NodeAux) -> R,
1192    ) -> R {
1193        let mut g = self.graph.borrow_mut();
1194        let (n, e, a) = g.get_node_edges_aux_mut(self.key());
1195        f(n, e, a)
1196    }
1197
1198    fn with_inner_edges<R>(&self, f: impl FnOnce(&NodeTopologySlot, &DepEdges) -> R) -> R {
1199        let g = self.graph.borrow();
1200        let n = g.get(self.key());
1201        let e = g
1202            .edge_slots
1203            .get(self.id.0)
1204            .and_then(Option::as_ref)
1205            .expect("Core points at a live GraphCore edge slot");
1206        f(n, e)
1207    }
1208
1209    fn with_node_state<R>(
1210        &self,
1211        f: impl FnOnce(&NodeTopologySlot, &NodeCallSlot, &NodeConfigSlot, &NodeRunState, &DepEdges) -> R,
1212    ) -> R {
1213        let g = self.graph.borrow();
1214        let (n, c, cfg, r, e) = g.get_node_call_config_run_edges(self.key());
1215        f(n, c, cfg, r, e)
1216    }
1217
1218    fn with_node_state_mut<R>(
1219        &self,
1220        f: impl FnOnce(
1221            &mut NodeTopologySlot,
1222            &mut NodeCallSlot,
1223            &mut NodeConfigSlot,
1224            &mut NodeRunState,
1225            &mut DepEdges,
1226        ) -> R,
1227    ) -> R {
1228        let mut g = self.graph.borrow_mut();
1229        let (n, c, cfg, r, e) = g.get_node_call_config_run_edges_mut(self.key());
1230        f(n, c, cfg, r, e)
1231    }
1232
1233    fn with_node_state_aux_mut<R>(
1234        &self,
1235        f: impl FnOnce(
1236            &mut NodeTopologySlot,
1237            &mut NodeCallSlot,
1238            &mut NodeConfigSlot,
1239            &mut NodeRunState,
1240            &mut DepEdges,
1241            &mut NodeAux,
1242        ) -> R,
1243    ) -> R {
1244        let mut g = self.graph.borrow_mut();
1245        let (n, c, cfg, r, e, a) = g.get_node_call_config_run_edges_aux_mut(self.key());
1246        f(n, c, cfg, r, e, a)
1247    }
1248
1249    fn with_call<R>(&self, f: impl FnOnce(&NodeCallSlot) -> R) -> R {
1250        let g = self.graph.borrow();
1251        f(g.get_call(self.key()))
1252    }
1253
1254    fn with_call_mut<R>(&self, f: impl FnOnce(&mut NodeCallSlot) -> R) -> R {
1255        let mut g = self.graph.borrow_mut();
1256        f(g.get_call_mut(self.key()))
1257    }
1258
1259    fn with_config<R>(&self, f: impl FnOnce(&NodeConfigSlot) -> R) -> R {
1260        let g = self.graph.borrow();
1261        f(g.get_config(self.key()))
1262    }
1263
1264    fn with_aux<R>(&self, f: impl FnOnce(&NodeAux) -> R) -> R {
1265        let g = self.graph.borrow();
1266        f(g.get_aux(self.key()))
1267    }
1268
1269    fn with_aux_mut<R>(&self, f: impl FnOnce(&mut NodeAux) -> R) -> R {
1270        let mut g = self.graph.borrow_mut();
1271        assert!(
1272            g.is_live_key(self.key()),
1273            "Core points at a stale or freed GraphCore slot"
1274        );
1275        let a = g
1276            .aux_slots
1277            .get_mut(self.id.0)
1278            .and_then(Option::as_mut)
1279            .expect("Core points at a live GraphCore aux slot");
1280        f(a)
1281    }
1282
1283    fn try_with_node_state_aux_mut<R>(
1284        &self,
1285        f: impl FnOnce(
1286            &mut NodeTopologySlot,
1287            &mut NodeCallSlot,
1288            &mut NodeConfigSlot,
1289            &mut NodeRunState,
1290            &mut DepEdges,
1291            &mut NodeAux,
1292        ) -> R,
1293    ) -> Option<R> {
1294        let mut g = self.graph.try_borrow_mut().ok()?;
1295        let (n, c, cfg, r, e, a) = g.get_node_call_config_run_edges_aux_mut(self.key());
1296        Some(f(n, c, cfg, r, e, a))
1297    }
1298
1299    fn downgrade(&self) -> CoreWeak {
1300        CoreWeak {
1301            graph: Rc::downgrade(&self.graph),
1302            key: self.key(),
1303            refs: Rc::downgrade(&self.refs),
1304        }
1305    }
1306}
1307
1308impl Clone for Core {
1309    fn clone(&self) -> Self {
1310        Self::from_parts(
1311            self.graph.clone(),
1312            self.id,
1313            self.generation,
1314            self.refs.clone(),
1315        )
1316    }
1317}
1318
1319impl Drop for Core {
1320    fn drop(&mut self) {
1321        if !self.counted {
1322            return;
1323        }
1324        let refs = self.refs.get();
1325        if refs > 1 {
1326            self.refs.set(refs - 1);
1327            return;
1328        }
1329        if refs == 0 {
1330            return;
1331        }
1332        self.refs.set(0);
1333        free_slot_if_unreferenced(&self.graph, self.key(), &self.refs);
1334    }
1335}
1336
1337fn free_slot_if_unreferenced(graph_ref: &Rc<RefCell<GraphCore>>, key: NodeKey, refs: &Cell<usize>) {
1338    if refs.get() != 0 {
1339        return;
1340    }
1341    let (mut inner, _topology, mut call, _config, _run, mut edges, _version, mut aux) = {
1342        let mut graph = graph_ref.borrow_mut();
1343        if graph.pin_count(key) != 0 {
1344            return;
1345        }
1346        let Some(pair) = graph.take_live(key) else {
1347            return;
1348        };
1349        pair
1350    };
1351    struct FreeSlotOnDrop {
1352        graph: Rc<RefCell<GraphCore>>,
1353        id: usize,
1354        armed: bool,
1355    }
1356    impl Drop for FreeSlotOnDrop {
1357        fn drop(&mut self) {
1358            if self.armed {
1359                self.graph.borrow_mut().free.push(self.id);
1360            }
1361        }
1362    }
1363    let mut free_slot = FreeSlotOnDrop {
1364        graph: graph_ref.clone(),
1365        id: key.id.0,
1366        armed: true,
1367    };
1368    unregister_backend_state_contributor_key((
1369        Rc::as_ptr(graph_ref) as usize,
1370        key.id.0,
1371        key.generation,
1372    ));
1373    inner.cleanup_before_free(&mut call, &mut edges, &mut aux);
1374    free_slot.armed = false;
1375    graph_ref.borrow_mut().free.push(key.id.0);
1376}
1377
1378struct ArenaNodePin {
1379    arena_key: ArenaNodeKey,
1380    graph: Rc<RefCell<GraphCore>>,
1381    refs: Rc<Cell<usize>>,
1382}
1383
1384impl ArenaNodePin {
1385    pub(crate) fn from_core(core: &Core) -> Option<Self> {
1386        {
1387            let mut graph = core.graph.borrow_mut();
1388            if !graph.pin_live_key(core.key()) {
1389                return None;
1390            }
1391        }
1392        Some(Self {
1393            arena_key: core.arena_node_key(),
1394            graph: core.graph.clone(),
1395            refs: core.refs.clone(),
1396        })
1397    }
1398
1399    fn borrowed_core(&self) -> Option<Core> {
1400        if !self.graph.borrow().is_live_key(self.arena_key.node) {
1401            return None;
1402        }
1403        Some(Core::from_borrowed_parts(
1404            self.graph.clone(),
1405            self.arena_key.node.id,
1406            self.arena_key.node.generation,
1407            self.refs.clone(),
1408        ))
1409    }
1410
1411    fn reset_wave_flags(&self) {
1412        let mut g = self.graph.borrow_mut();
1413        if !g.is_live_key(self.arena_key.node) {
1414            return;
1415        }
1416        let (n, e) = g.get_node_and_edges_mut(self.arena_key.node);
1417        reset_wave_flags_inner(n, e);
1418    }
1419
1420    fn boundary_root(&self) -> BoundaryRoot {
1421        BoundaryRoot {
1422            graph_id: self.arena_key.graph,
1423            graph: Rc::downgrade(&self.graph),
1424        }
1425    }
1426}
1427
1428impl Drop for ArenaNodePin {
1429    fn drop(&mut self) {
1430        self.graph.borrow_mut().release_pin(self.arena_key.node);
1431        free_slot_if_unreferenced(&self.graph, self.arena_key.node, &self.refs);
1432    }
1433}
1434
1435pub(crate) struct BatchTarget {
1436    pin: ArenaNodePin,
1437}
1438
1439impl BatchTarget {
1440    pub(crate) fn from_core(core: &Core) -> Option<Self> {
1441        Some(Self {
1442            pin: ArenaNodePin::from_core(core)?,
1443        })
1444    }
1445
1446    pub(crate) fn matches(&self, core: &Core) -> bool {
1447        self.pin.arena_key == core.arena_node_key()
1448    }
1449
1450    pub(crate) fn boundary_root(&self) -> BoundaryRoot {
1451        self.pin.boundary_root()
1452    }
1453
1454    pub(crate) fn commit_batched_wave(&self, wave: Wave<AnyValue>) {
1455        if let Some(core) = self.pin.borrowed_core() {
1456            core.commit_batched_wave(wave);
1457        }
1458    }
1459
1460    pub(crate) fn rollback_batched(&self) {
1461        if let Some(core) = self.pin.borrowed_core() {
1462            core.rollback_batched();
1463        }
1464    }
1465
1466    #[cfg(test)]
1467    fn borrowed_core(&self) -> Option<Core> {
1468        self.pin.borrowed_core()
1469    }
1470}
1471
1472#[derive(Clone)]
1473struct CoreWeak {
1474    graph: Weak<RefCell<GraphCore>>,
1475    key: NodeKey,
1476    refs: Weak<Cell<usize>>,
1477}
1478
1479#[derive(Default)]
1480struct InWaveDepReceiveAction {
1481    emit_teardown_subscribers: bool,
1482    emit_dirty: bool,
1483    do_invalidate: bool,
1484    invalidate_as_invalidate_msg: bool,
1485    had_node_data_before_invalidate: bool,
1486    clear_undirty_after_invalidate: bool,
1487    defer_maybe_run: bool,
1488    maybe_run_decision: MaybeRunDecision,
1489    down_msgs: Option<Vec<Msg>>,
1490    do_settle_after_absorbed_terminal: bool,
1491    fire_demand: bool,
1492}
1493
1494impl InWaveDepReceiveAction {
1495    fn has_work(&self) -> bool {
1496        self.emit_teardown_subscribers
1497            || self.emit_dirty
1498            || self.do_invalidate
1499            || self.defer_maybe_run
1500            || !matches!(self.maybe_run_decision, MaybeRunDecision::Skip)
1501            || self.do_settle_after_absorbed_terminal
1502            || self.fire_demand
1503            || self.down_msgs.as_ref().is_some_and(|msgs| !msgs.is_empty())
1504    }
1505
1506    fn apply(self, core: &Core) {
1507        if self.emit_teardown_subscribers {
1508            core.emit_to_subs(&Message::Teardown);
1509        }
1510        if self.emit_dirty {
1511            core.emit_to_subs(&Message::Dirty);
1512        }
1513        if self.do_invalidate {
1514            if self.invalidate_as_invalidate_msg && self.had_node_data_before_invalidate {
1515                core.down(vec![Message::Invalidate]);
1516            } else {
1517                core.invalidate();
1518            }
1519        }
1520        if self.clear_undirty_after_invalidate {
1521            if self.had_node_data_before_invalidate {
1522                core.with_inner_edges_mut(|_n, e| {
1523                    e.wave.emitted_dirty_this_wave = false;
1524                });
1525            } else {
1526                core.down(vec![Message::Resolved]);
1527            }
1528        }
1529        if self.defer_maybe_run {
1530            defer_run_until_delivery_boundary(core);
1531        }
1532        match self.maybe_run_decision {
1533            MaybeRunDecision::Skip => {}
1534            MaybeRunDecision::Passthrough => core.passthrough_emit(),
1535            MaybeRunDecision::Run => {
1536                core.run_wave();
1537            }
1538        }
1539        if let Some(msgs) = self.down_msgs {
1540            core.down(msgs);
1541        }
1542        if self.do_settle_after_absorbed_terminal {
1543            core.settle_after_absorbed_terminal();
1544        }
1545        if self.fire_demand {
1546            core.fire_owed_demand_if_ready();
1547        }
1548    }
1549}
1550
1551enum DownAction {
1552    Emit { msg: Msg, subs: SubscriberSnapshot },
1553    Invalidate { hooks: Vec<Rc<dyn Fn()>> },
1554}
1555
1556impl CoreWeak {
1557    #[cfg(test)]
1558    fn counted_core(&self) -> Option<Core> {
1559        let graph = self.graph.upgrade()?;
1560        let refs = self.refs.upgrade()?;
1561        if refs.get() == 0 {
1562            return None;
1563        }
1564        if !graph.borrow().is_live_key(self.key) {
1565            return None;
1566        }
1567        Some(Core::from_parts(
1568            graph,
1569            self.key.id,
1570            self.key.generation,
1571            refs,
1572        ))
1573    }
1574
1575    #[cfg(test)]
1576    fn borrowed_core(&self) -> Option<Core> {
1577        let graph = self.graph.upgrade()?;
1578        let refs = self.refs.upgrade()?;
1579        let can_borrow = {
1580            let g = graph.borrow();
1581            g.is_live_key(self.key) && (refs.get() != 0 || g.pin_count(self.key) != 0)
1582        };
1583        if !can_borrow {
1584            return None;
1585        }
1586        Some(Core::from_borrowed_parts(
1587            graph,
1588            self.key.id,
1589            self.key.generation,
1590            refs,
1591        ))
1592    }
1593
1594    fn pinned_borrowed_core(&self) -> Option<(ArenaNodePin, Core)> {
1595        let graph = self.graph.upgrade()?;
1596        let refs = self.refs.upgrade()?;
1597        let arena_key = ArenaNodeKey {
1598            graph: Rc::as_ptr(&graph) as usize,
1599            node: self.key,
1600        };
1601        {
1602            let mut g = graph.borrow_mut();
1603            if !g.pin_live_key(self.key) {
1604                return None;
1605            }
1606        }
1607        let pin = ArenaNodePin {
1608            arena_key,
1609            graph,
1610            refs,
1611        };
1612        let core = pin.borrowed_core()?;
1613        Some((pin, core))
1614    }
1615
1616    fn collect_in_wave_receive_from_dep_action(
1617        &self,
1618        idx: usize,
1619        msg: &Msg,
1620    ) -> Option<InWaveDepReceiveAction> {
1621        let graph = self.graph.upgrade()?;
1622        let refs = self.refs.upgrade()?;
1623        let arena_key = ArenaNodeKey {
1624            graph: Rc::as_ptr(&graph) as usize,
1625            node: self.key,
1626        };
1627        let registered = WAVE.with(|w| {
1628            let mut wave = w.borrow_mut();
1629            let Some(scope) = wave.as_mut() else {
1630                return false;
1631            };
1632            scope.pin_once(arena_key, &graph, &refs)
1633        });
1634        if !registered {
1635            return None;
1636        }
1637        let mut g = graph.borrow_mut();
1638        Core::collect_receive_from_dep_action_from_graph(&mut g, self.key, idx, msg)
1639    }
1640
1641    fn apply_in_wave_receive_action(&self, action: InWaveDepReceiveAction) {
1642        let graph = match self.graph.upgrade() {
1643            Some(graph) => graph,
1644            None => return,
1645        };
1646        let refs = match self.refs.upgrade() {
1647            Some(refs) => refs,
1648            None => return,
1649        };
1650        let can_apply = {
1651            let g = graph.borrow();
1652            g.is_live_key(self.key) && (refs.get() != 0 || g.pin_count(self.key) != 0)
1653        };
1654        if !can_apply {
1655            return;
1656        }
1657        let core = Core::from_borrowed_parts(graph, self.key.id, self.key.generation, refs);
1658        core.apply_receive_from_dep_action(action);
1659    }
1660
1661    fn receive_from_dep(&self, idx: usize, msg: &Msg) {
1662        // DR-8/B54 receive policy:
1663        // - active wave: use a borrowed arena view; `Core::receive_from_dep` immediately
1664        //   enrolls in the arena-pinned touched set before any callback-capable work;
1665        // - no active wave: pin first and run through a borrowed owner view, avoiding
1666        //   counted churn for ordinary callback delivery and for late cleanup/release
1667        //   windows.
1668        if wave_in_flight() {
1669            let Some(action) = self.collect_in_wave_receive_from_dep_action(idx, msg) else {
1670                return;
1671            };
1672            if action.has_work() {
1673                self.apply_in_wave_receive_action(action);
1674            }
1675            return;
1676        }
1677        if let Some((_pin, core)) = self.pinned_borrowed_core() {
1678            core.receive_from_dep_registered(idx, msg);
1679        }
1680    }
1681}
1682
1683struct DeferredNodeAction {
1684    arena_key: ArenaNodeKey,
1685    weak: CoreWeak,
1686}
1687
1688impl DeferredNodeAction {
1689    fn from_core(core: &Core) -> Self {
1690        Self {
1691            arena_key: core.arena_node_key(),
1692            weak: core.downgrade(),
1693        }
1694    }
1695
1696    fn matches(&self, core: &Core) -> bool {
1697        self.arena_key == core.arena_node_key()
1698    }
1699
1700    fn pinned_borrowed_core(&self) -> Option<(ArenaNodePin, Core)> {
1701        self.weak.pinned_borrowed_core()
1702    }
1703
1704    fn with_live_edges_mut<R>(&self, f: impl FnOnce(&mut DepEdges) -> R) -> Option<R> {
1705        let graph = self.weak.graph.upgrade()?;
1706        let refs = self.weak.refs.upgrade()?;
1707        let mut g = graph.borrow_mut();
1708        if !g.is_live_key(self.weak.key) || (refs.get() == 0 && g.pin_count(self.weak.key) == 0) {
1709            return None;
1710        }
1711        let (_n, e) = g.get_node_and_edges_mut(self.weak.key);
1712        Some(f(e))
1713    }
1714}
1715
1716#[derive(Clone, Copy, PartialEq, Eq)]
1717enum DeferredDeliveryKind {
1718    Run,
1719    AbsorbedSettle,
1720}
1721
1722struct DeferredDeliveryAction {
1723    kind: DeferredDeliveryKind,
1724    target: DeferredNodeAction,
1725}
1726
1727impl DeferredDeliveryAction {
1728    fn from_core(kind: DeferredDeliveryKind, core: &Core) -> Self {
1729        Self {
1730            kind,
1731            target: DeferredNodeAction::from_core(core),
1732        }
1733    }
1734
1735    fn matches(&self, kind: DeferredDeliveryKind, core: &Core) -> bool {
1736        self.kind == kind && self.target.matches(core)
1737    }
1738
1739    fn core_for_drain(&self) -> Option<(ArenaNodePin, Core)> {
1740        self.target.pinned_borrowed_core()
1741    }
1742
1743    fn apply(&self) {
1744        let Some((_pin, node)) = self.core_for_drain() else {
1745            return;
1746        };
1747        if node.with_inner_edges(|_, e| e.value.terminal) {
1748            return;
1749        }
1750        match self.kind {
1751            DeferredDeliveryKind::Run => node.maybe_run(),
1752            DeferredDeliveryKind::AbsorbedSettle => node.settle_after_absorbed_terminal(),
1753        }
1754    }
1755}
1756
1757#[derive(Clone)]
1758struct CoreToken {
1759    graph_id: usize,
1760    key: NodeKey,
1761    refs: Weak<Cell<usize>>,
1762}
1763
1764impl CoreToken {
1765    fn from_core(core: &Core) -> Self {
1766        Self {
1767            graph_id: Rc::as_ptr(&core.graph) as usize,
1768            key: core.key(),
1769            refs: Rc::downgrade(&core.refs),
1770        }
1771    }
1772
1773    fn pinned_borrowed_core(&self, graph: &Rc<RefCell<GraphCore>>) -> Option<PinnedBorrowedCore> {
1774        if Rc::as_ptr(graph) as usize != self.graph_id {
1775            return None;
1776        }
1777        let refs = self.refs.upgrade()?;
1778        {
1779            let mut g = graph.borrow_mut();
1780            if !(g.is_live_key(self.key) && (refs.get() != 0 || g.pin_count(self.key) != 0)) {
1781                return None;
1782            }
1783            if !g.pin_live_key(self.key) {
1784                return None;
1785            }
1786        }
1787        let arena_key = ArenaNodeKey {
1788            graph: Rc::as_ptr(graph) as usize,
1789            node: self.key,
1790        };
1791        Some(PinnedBorrowedCore {
1792            _pin: ArenaNodePin {
1793                arena_key,
1794                graph: graph.clone(),
1795                refs: refs.clone(),
1796            },
1797            core: Core::from_borrowed_parts(graph.clone(), self.key.id, self.key.generation, refs),
1798        })
1799    }
1800}
1801
1802struct BoundaryDrainGuard {
1803    graph: Rc<RefCell<GraphCore>>,
1804    active: bool,
1805}
1806
1807impl BoundaryDrainGuard {
1808    fn enter(graph: &Rc<RefCell<GraphCore>>) -> Self {
1809        graph.borrow_mut().draining_boundary = true;
1810        Self {
1811            graph: graph.clone(),
1812            active: true,
1813        }
1814    }
1815
1816    fn finish(mut self) {
1817        self.active = false;
1818        self.graph.borrow_mut().draining_boundary = false;
1819    }
1820}
1821
1822impl Drop for BoundaryDrainGuard {
1823    fn drop(&mut self) {
1824        if self.active {
1825            self.graph.borrow_mut().draining_boundary = false;
1826        }
1827    }
1828}
1829
1830struct PinnedBorrowedCore {
1831    _pin: ArenaNodePin,
1832    core: Core,
1833}
1834
1835enum BoundaryTask {
1836    Rewire {
1837        target: CoreToken,
1838        req: RewireRequest,
1839        committed: Rc<Cell<bool>>,
1840    },
1841    ExternalRewire {
1842        target: CoreToken,
1843        req: RewireRequest,
1844        committed: Rc<Cell<bool>>,
1845    },
1846    Up {
1847        target: CoreToken,
1848        msgs: Wave<AnyValue>,
1849        toward_dep: Option<usize>,
1850        committed: Rc<Cell<bool>>,
1851    },
1852    Down {
1853        target: CoreToken,
1854        msgs: Wave<AnyValue>,
1855        committed: Rc<Cell<bool>>,
1856    },
1857}
1858
1859impl BoundaryTask {
1860    fn committed(&self) -> bool {
1861        match self {
1862            BoundaryTask::Rewire { committed, .. }
1863            | BoundaryTask::ExternalRewire { committed, .. }
1864            | BoundaryTask::Up { committed, .. }
1865            | BoundaryTask::Down { committed, .. } => committed.get(),
1866        }
1867    }
1868}
1869
1870#[derive(Clone)]
1871pub(crate) struct BoundaryRoot {
1872    graph_id: usize,
1873    graph: Weak<RefCell<GraphCore>>,
1874}
1875
1876impl BoundaryRoot {
1877    fn from_core(core: &Core) -> Self {
1878        Self {
1879            graph_id: Rc::as_ptr(&core.graph) as usize,
1880            graph: Rc::downgrade(&core.graph),
1881        }
1882    }
1883
1884    pub(crate) fn same_graph(&self, core: &Core) -> bool {
1885        self.graph_id == Rc::as_ptr(&core.graph) as usize
1886    }
1887
1888    pub(crate) fn same_root(&self, other: &Self) -> bool {
1889        self.graph_id == other.graph_id
1890    }
1891
1892    fn upgrade(&self) -> Option<Rc<RefCell<GraphCore>>> {
1893        self.graph.upgrade()
1894    }
1895}
1896
1897pub(crate) fn boundary_root_for(target: &Core) -> BoundaryRoot {
1898    BoundaryRoot::from_core(target)
1899}
1900
1901/// Reset `inside_run_wave` on scope exit — including unwind, so the feedback-cycle
1902/// panic (D37) leaves the flag clean per frame (mirrors TS's try/finally around
1903/// `dispatcher.invoke`). The *other* stale wave-flags an unwinding panic leaves on
1904/// the source / intermediate frames (`emitted_dirty` / `dep_batch` / `dep_dirty` /
1905/// `pending`) are cleaned at the wave-owner catch via the [`WaveScope`] touched-set
1906/// (closes B25) — this guard only owns `inside_run_wave`.
1907struct WaveGuard(DeferredNodeAction);
1908impl Drop for WaveGuard {
1909    fn drop(&mut self) {
1910        let _ = self.0.with_live_edges_mut(|e| {
1911            e.wave.inside_run_wave = false;
1912        });
1913    }
1914}
1915
1916/// Clear `in_dep_mutation` on scope exit — including a panic unwind during a rewire
1917/// (QA-F1). A user fn can panic mid-rewire (an added dep's activation fn, or a removed
1918/// dep's onDeactivation hook); the wave-owner catch's wave-flag reset does NOT
1919/// cover `in_dep_mutation` (and may not include the rewiring node in its touched-set), so
1920/// without this RAII a caught rewire panic would wedge the node forever (every future
1921/// recompute deferred + every future rewire rejected as reentrant). Mirrors the TS arm's
1922/// `try { … } finally { _inDepMutation = false }`. `rewire_run_pending` needs no unwind
1923/// reset — it is reread/cleared at the start of the next `rewire` and has no other consumer.
1924struct DepMutationGuard(DeferredNodeAction);
1925impl Drop for DepMutationGuard {
1926    fn drop(&mut self) {
1927        let _ = self.0.with_live_edges_mut(|e| {
1928            e.wave.in_dep_mutation = false;
1929        });
1930    }
1931}
1932
1933/// The D30 catch boundary + B25 whole-cascade recovery (per-language impl, D24 —
1934/// the Rust analogue of TS's graph-layer value-fn `try/catch`; Rust has no graph
1935/// layer yet, so the catch lives at the substrate wave-owner).
1936///
1937/// A `panic!` is the Rust analogue of a value-level `throw` (the fn signature is
1938/// `Fn(&Ctx) -> ()` — no `Result` channel; R-reentrancy mandates an *unwind*, not a
1939/// graceful return). The **outermost** public mutating entry (`subscribe` / `down` /
1940/// `up` / `set`) becomes the wave OWNER: it installs a scope, runs the cascade under
1941/// `catch_unwind`, and on a caught panic resets every participating node's transient
1942/// wave-flags before emitting `[[ERROR,e]]` from the
1943/// `blamed` node. Nested re-entrant calls see the active scope and just run.
1944///
1945/// `blamed` is the innermost node whose fn actually ran ([`Core::run_wave`] records
1946/// it right before `invoke`), so a sync feedback cycle blames a node ON the cycle
1947/// (C-6) — the value-level catch "nearest the throw", impl-determined per
1948/// R-reentrancy. Falls back to the wave `owner` if no fn ran.
1949///
1950/// Forward-looking: this wave-owner boundary is the same "committed wave boundary"
1951/// that batch (D12) and the `ctx.rewire_next` deferred drain (D47) will reuse.
1952struct WaveScope {
1953    owner_graph_id: usize,
1954    /// Process-local wave id used to mark arena nodes once per wave without a
1955    /// per-touch hash lookup (DR-8/B54 owner-execution hot path).
1956    wave_id: u64,
1957    /// Graph roots that received committed-boundary tasks during this wave but are not
1958    /// necessarily the wave owner's graph (nested cross-arena public calls share WAVE).
1959    boundary_roots: Vec<BoundaryRoot>,
1960    /// Every node that mutated a transient wave-flag this wave. DR-8/B54: these are
1961    /// arena pins keyed by (graph, node id, generation), not counted Core clones.
1962    touched: Vec<ArenaNodePin>,
1963    /// Innermost node whose fn ran before the panic (the ERROR-bearing node).
1964    ///
1965    /// DR-8/B54: blame is a generation-keyed arena reference on the hot path. The
1966    /// node is already pinned by `touched` because `run_wave` registers before
1967    /// invoking the fn; the cold panic path reconstructs the counted handle from
1968    /// that pin when it needs to emit ERROR.
1969    blamed: Option<ArenaNodeKey>,
1970}
1971
1972impl WaveScope {
1973    fn pin_once(
1974        &mut self,
1975        arena_key: ArenaNodeKey,
1976        graph: &Rc<RefCell<GraphCore>>,
1977        refs: &Rc<Cell<usize>>,
1978    ) -> bool {
1979        {
1980            let mut g = graph.borrow_mut();
1981            if !g.is_live_key(arena_key.node) {
1982                return false;
1983            }
1984            let id = arena_key.node.id.0;
1985            if g.touched_waves.get(id) == Some(&self.wave_id) {
1986                return true;
1987            }
1988            g.pins[id] = g.pins[id]
1989                .checked_add(1)
1990                .expect("GraphCore pin count overflow");
1991            let touched = g
1992                .touched_waves
1993                .get_mut(id)
1994                .expect("Core points at a live GraphCore touch slot");
1995            *touched = self.wave_id;
1996        }
1997        self.touched.push(ArenaNodePin {
1998            arena_key,
1999            graph: graph.clone(),
2000            refs: refs.clone(),
2001        });
2002        true
2003    }
2004}
2005
2006thread_local! {
2007    /// The current wave's scope, if a wave is in flight (single-thread, D22 ⇒
2008    /// thread-local, like the default dispatcher D26). `Some` ⇔ inside a wave.
2009    static WAVE: RefCell<Option<WaveScope>> = const { RefCell::new(None) };
2010    static NEXT_WAVE_SCOPE_ID: Cell<u64> = const { Cell::new(1) };
2011    /// Downstream delivery depth for one `down(msgs)` wave. A dep may receive
2012    /// multiple DATA messages before its dirty contribution is cleared; the fn
2013    /// must see the full batch once, not run once per DATA occurrence.
2014    static DELIVERY_DEPTH: Cell<usize> = const { Cell::new(0) };
2015    static NEXT_DELIVERY_ID: Cell<u64> = const { Cell::new(1) };
2016    static CURRENT_DELIVERY_ID: Cell<u64> = const { Cell::new(0) };
2017    static DEFERRED_DELIVERY_ACTIONS: RefCell<Vec<DeferredDeliveryAction>> = const { RefCell::new(Vec::new()) };
2018}
2019
2020/// `true` while a wave is in flight on this thread (a scope is installed).
2021fn wave_in_flight() -> bool {
2022    WAVE.with(|w| w.borrow().is_some())
2023}
2024
2025fn next_wave_scope_id() -> u64 {
2026    NEXT_WAVE_SCOPE_ID.with(|next| {
2027        let id = next.get();
2028        next.set(id.checked_add(1).expect("wave scope id overflow"));
2029        id
2030    })
2031}
2032
2033/// Register a node as a wave participant (for the B25 touched-set reset). No-op if
2034/// no wave is in flight (e.g. a `cache` read outside any wave).
2035fn wave_register(core: &Core) {
2036    WAVE.with(|w| {
2037        if let Some(scope) = w.borrow_mut().as_mut() {
2038            let _ = scope.pin_once(core.arena_node_key(), &core.graph, &core.refs);
2039        }
2040    });
2041}
2042
2043fn wave_register_boundary_root(core: &Core) {
2044    WAVE.with(|w| {
2045        if let Some(scope) = w.borrow_mut().as_mut() {
2046            if scope.owner_graph_id == Rc::as_ptr(&core.graph) as usize
2047                || scope.boundary_roots.iter().any(|r| r.same_graph(core))
2048            {
2049                return;
2050            }
2051            scope.boundary_roots.push(BoundaryRoot::from_core(core));
2052        }
2053    });
2054}
2055
2056fn delivery_in_flight() -> bool {
2057    DELIVERY_DEPTH.with(|depth| depth.get() > 0)
2058}
2059
2060fn current_delivery_id() -> u64 {
2061    CURRENT_DELIVERY_ID.with(Cell::get)
2062}
2063
2064fn defer_run_until_delivery_boundary(core: &Core) {
2065    defer_delivery_action_until_boundary(core, DeferredDeliveryKind::Run);
2066}
2067
2068fn defer_absorbed_settle_until_delivery_boundary(core: &Core) {
2069    defer_delivery_action_until_boundary(core, DeferredDeliveryKind::AbsorbedSettle);
2070}
2071
2072fn defer_delivery_action_until_boundary(core: &Core, kind: DeferredDeliveryKind) {
2073    DEFERRED_DELIVERY_ACTIONS.with(|actions| {
2074        let mut actions = actions.borrow_mut();
2075        if !actions.iter().any(|action| action.matches(kind, core)) {
2076            actions.push(DeferredDeliveryAction::from_core(kind, core));
2077        }
2078    });
2079}
2080
2081struct DeliveryGuard {
2082    active: bool,
2083    prev_id: u64,
2084}
2085
2086impl DeliveryGuard {
2087    fn enter() -> Self {
2088        let prev_id = CURRENT_DELIVERY_ID.with(Cell::get);
2089        let id = NEXT_DELIVERY_ID.with(|next| {
2090            let id = next.get();
2091            next.set(id.wrapping_add(1).max(1));
2092            id
2093        });
2094        CURRENT_DELIVERY_ID.with(|current| current.set(id));
2095        DELIVERY_DEPTH.with(|depth| depth.set(depth.get() + 1));
2096        Self {
2097            active: true,
2098            prev_id,
2099        }
2100    }
2101
2102    fn finish(mut self) {
2103        self.active = false;
2104        CURRENT_DELIVERY_ID.with(|current| current.set(self.prev_id));
2105        let outer = DELIVERY_DEPTH.with(|depth| {
2106            let current = depth.get();
2107            debug_assert!(current > 0);
2108            depth.set(current - 1);
2109            current == 1
2110        });
2111        if outer {
2112            drain_deferred_runs();
2113        }
2114    }
2115}
2116
2117impl Drop for DeliveryGuard {
2118    fn drop(&mut self) {
2119        if self.active {
2120            CURRENT_DELIVERY_ID.with(|current| current.set(self.prev_id));
2121            DELIVERY_DEPTH.with(|depth| {
2122                let current = depth.get();
2123                if current > 0 {
2124                    depth.set(current - 1);
2125                }
2126            });
2127            if !std::thread::panicking() && !delivery_in_flight() {
2128                drain_deferred_runs();
2129            }
2130        }
2131    }
2132}
2133
2134fn with_delivery_scope(body: impl FnOnce()) {
2135    let guard = DeliveryGuard::enter();
2136    body();
2137    guard.finish();
2138}
2139
2140fn drain_deferred_runs() {
2141    loop {
2142        let actions = DEFERRED_DELIVERY_ACTIONS.with(|r| std::mem::take(&mut *r.borrow_mut()));
2143        if actions.is_empty() {
2144            break;
2145        }
2146        for kind in [
2147            DeferredDeliveryKind::Run,
2148            DeferredDeliveryKind::AbsorbedSettle,
2149        ] {
2150            for action in &actions {
2151                if action.kind == kind {
2152                    action.apply();
2153                }
2154            }
2155        }
2156    }
2157}
2158
2159fn clear_deferred_delivery_actions() {
2160    DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
2161}
2162
2163/// Record the node whose fn is about to run — the ERROR-bearing node on a panic.
2164fn wave_set_blamed(core: &Core) {
2165    WAVE.with(|w| {
2166        if let Some(scope) = w.borrow_mut().as_mut() {
2167            scope.blamed = Some(core.arena_node_key());
2168        }
2169    });
2170}
2171
2172/// Turn a caught panic payload into the untyped `GraphError` (D31).
2173fn panic_to_error(payload: Box<dyn std::any::Any + Send>) -> GraphError {
2174    let msg = payload
2175        .downcast_ref::<&str>()
2176        .map(|s| (*s).to_owned())
2177        .or_else(|| payload.downcast_ref::<String>().cloned())
2178        .unwrap_or_else(|| "node fn panicked".to_owned());
2179    msg.into()
2180}
2181
2182/// Run `body` as a wave, with `owner` the wave-owner if no wave is yet in flight.
2183/// The outermost caller installs a [`WaveScope`] + `catch_unwind`; on a caught
2184/// panic it resets every touched node's wave-flags (B25) and emits `[[ERROR,e]]`
2185/// from the blamed cycle node (D30), then returns `on_error()`. Nested calls (a
2186/// re-entrant `subscribe`/`down` during the cascade) just run `body` — the outer
2187/// owner owns the single catch.
2188fn with_wave_owner<R>(owner: &Core, body: impl FnOnce() -> R, on_error: impl FnOnce() -> R) -> R {
2189    if wave_in_flight() {
2190        return body();
2191    }
2192    WAVE.with(|w| {
2193        *w.borrow_mut() = Some(WaveScope {
2194            owner_graph_id: Rc::as_ptr(&owner.graph) as usize,
2195            wave_id: next_wave_scope_id(),
2196            boundary_roots: Vec::new(),
2197            touched: Vec::new(),
2198            blamed: None,
2199        });
2200    });
2201    let result = catch_unwind(AssertUnwindSafe(body));
2202    let mut scope = WAVE
2203        .with(|w| w.borrow_mut().take())
2204        .expect("wave owner installed a scope");
2205    let boundary_roots = std::mem::take(&mut scope.boundary_roots);
2206    let ret = match result {
2207        Ok(r) => r,
2208        Err(payload) => {
2209            // The scope is taken (we are outside the wave again). Recover every node
2210            // the aborted wave corrupted (B25). D431 host-boundary aborts are not graph
2211            // value-level failures: clear stale wave/boundary work, then rethrow without
2212            // emitting protocol ERROR.
2213            for n in &scope.touched {
2214                n.reset_wave_flags();
2215            }
2216            // Delivery-boundary run/settle queues are wave-local. If the delivery
2217            // aborted before its guard could drain them, carrying those actions into a
2218            // later unrelated wave would resurrect stale dep projections after B25 has
2219            // already reset the touched nodes.
2220            clear_deferred_delivery_actions();
2221            if is_host_boundary_abort_payload(payload.as_ref()) {
2222                clear_all_deferred_boundary_root(&owner.boundary_root());
2223                for root in &boundary_roots {
2224                    clear_all_deferred_boundary_root(root);
2225                }
2226                std::panic::resume_unwind(payload);
2227            }
2228            // Non-host Rust/value failures still surface as ERROR on a node ON the
2229            // failure path (D30) — a fresh terminal wave.
2230            let blamed = scope
2231                .blamed
2232                .and_then(|key| {
2233                    scope
2234                        .touched
2235                        .iter()
2236                        .find(|n| n.arena_key == key)
2237                        .and_then(ArenaNodePin::borrowed_core)
2238                })
2239                .unwrap_or_else(|| owner.borrowed_view());
2240            let err = panic_to_error(payload);
2241            // The recovery ERROR emit runs OUTSIDE any catch (the scope is taken). A
2242            // user subscriber sink that itself panics while handling this ERROR must not
2243            // escape the public API / fault the recovery — the node is going terminal
2244            // anyway. Swallow a sink-panic here (delivery may be partial; acceptable for
2245            // a node being torn down).
2246            let _ = catch_unwind(AssertUnwindSafe(move || {
2247                blamed.down(vec![Message::Error(err)]);
2248            }));
2249            clear_deferred_delivery_actions();
2250            on_error()
2251        }
2252    };
2253    drop(std::mem::take(&mut scope.touched));
2254    // Committed wave boundary (R-rewire-deferred / D47): drain any `ctx.rewire_next` queued
2255    // during this wave, each applied as a FRESH wave. Batch holds this drain until AFTER
2256    // the outermost commit/rollback, so topology never mutates on an uncommitted view
2257    // (R-rewire-batch-boundary / D67).
2258    if !boundary_drains_blocked() {
2259        drain_committed_boundaries(std::slice::from_ref(owner), &boundary_roots);
2260    }
2261    ret
2262}
2263
2264struct UpRouteState {
2265    demand_fired: Vec<(LockId, ArenaNodeKey)>,
2266}
2267
2268impl UpRouteState {
2269    fn new() -> Self {
2270        Self {
2271            demand_fired: Vec::new(),
2272        }
2273    }
2274
2275    fn mark_demand(&mut self, lock: &LockId, holder: &Core) -> bool {
2276        let holder_key = holder.arena_node_key();
2277        if self
2278            .demand_fired
2279            .iter()
2280            .any(|(l, h)| l == lock && *h == holder_key)
2281        {
2282            return true;
2283        }
2284        self.demand_fired.push((lock.clone(), holder_key));
2285        false
2286    }
2287}
2288
2289/// Queue a deferred boundary task (R-rewire-deferred / D47). Applied at the committed
2290/// wave boundary by [`drain_deferred_rewires`], never in place.
2291fn defer_boundary(owner: &Core, task: BoundaryTask) {
2292    wave_register_boundary_root(owner);
2293    register_boundary_root(owner);
2294    owner.graph.borrow_mut().deferred_boundary.push_back(task);
2295}
2296
2297pub(crate) fn drain_committed_boundary(target: &Core) {
2298    drain_deferred_rewires(target);
2299}
2300
2301pub(crate) fn drain_committed_boundary_root(root: &BoundaryRoot) {
2302    if let Some(graph) = root.upgrade() {
2303        drain_deferred_rewires_for_graph(&graph);
2304    }
2305}
2306
2307pub(crate) fn drain_committed_boundaries(core_roots: &[Core], task_roots: &[BoundaryRoot]) {
2308    let mut escaped: Option<Box<dyn std::any::Any + Send>> = None;
2309    for root in core_roots {
2310        let result = catch_unwind(AssertUnwindSafe(|| drain_committed_boundary(root)));
2311        if let Err(payload) = result {
2312            if is_host_boundary_abort_payload(payload.as_ref()) {
2313                clear_all_deferred_boundaries(core_roots, task_roots);
2314                std::panic::resume_unwind(payload);
2315            }
2316            remember_first_boundary_panic(&mut escaped, Err(payload));
2317        }
2318    }
2319    for root in task_roots {
2320        let result = catch_unwind(AssertUnwindSafe(|| drain_committed_boundary_root(root)));
2321        if let Err(payload) = result {
2322            if is_host_boundary_abort_payload(payload.as_ref()) {
2323                clear_all_deferred_boundaries(core_roots, task_roots);
2324                std::panic::resume_unwind(payload);
2325            }
2326            remember_first_boundary_panic(&mut escaped, Err(payload));
2327        }
2328    }
2329    if let Some(e) = escaped {
2330        std::panic::resume_unwind(e);
2331    }
2332}
2333
2334fn clear_all_deferred_boundaries(core_roots: &[Core], task_roots: &[BoundaryRoot]) {
2335    for root in core_roots {
2336        clear_all_deferred_boundary_root(&root.boundary_root());
2337    }
2338    for root in task_roots {
2339        clear_all_deferred_boundary_root(root);
2340    }
2341}
2342
2343fn remember_first_boundary_panic(
2344    escaped: &mut Option<Box<dyn std::any::Any + Send>>,
2345    result: std::thread::Result<()>,
2346) {
2347    if let Err(e) = result {
2348        if escaped.is_none() {
2349            *escaped = Some(e);
2350        }
2351    }
2352}
2353
2354pub(crate) fn clear_deferred_boundary_root(root: &BoundaryRoot) {
2355    if let Some(graph) = root.upgrade() {
2356        graph
2357            .borrow_mut()
2358            .deferred_boundary
2359            .retain(BoundaryTask::committed);
2360    }
2361}
2362
2363pub(crate) fn clear_all_deferred_boundary_root(root: &BoundaryRoot) {
2364    if let Some(graph) = root.upgrade() {
2365        graph.borrow_mut().deferred_boundary.clear();
2366    }
2367}
2368
2369/// Drain the deferred-rewire FIFO at the committed boundary. Each thunk applies one queued
2370/// self-rewire as a FRESH wave (its own wave-owner) which may enqueue MORE — appended and
2371/// drained by this same loop (DrainExactlyOnce). A single global FIFO yields the drain order
2372/// for free: issue order during a synchronous cascade IS causal order (a dep settles before
2373/// its dependent's fn runs), so global-FIFO == per-node FIFO + causal-node order. Per-thunk
2374/// isolation: a thunk that panics does NOT abandon the rest of the queue (which would strand
2375/// thunks to mis-fire at an unrelated later wave); the first escape re-surfaces once the
2376/// queue is empty. Process-global is correct per D22 (one sync cascade per causal domain;
2377/// cross-domain is the async wire bridge, which never shares this stack — same basis as the
2378/// module-global `batch.active`).
2379fn drain_deferred_rewires(owner: &Core) {
2380    drain_deferred_rewires_for_graph(&owner.graph);
2381}
2382
2383fn drain_deferred_rewires_for_graph(graph: &Rc<RefCell<GraphCore>>) {
2384    if graph.borrow().draining_boundary {
2385        return; // a nested wave-owner exit during the drain — the outer loop owns draining
2386    }
2387    if graph.borrow().deferred_boundary.is_empty() {
2388        return; // F-PERF: one empty-queue check per outermost wave when unused (behavior-neutral)
2389    }
2390    let drain_guard = BoundaryDrainGuard::enter(graph);
2391    let mut escaped: Option<Box<dyn std::any::Any + Send>> = None;
2392    let mut blocked = 0usize;
2393    loop {
2394        let task = {
2395            let mut g = graph.borrow_mut();
2396            if g.deferred_boundary.is_empty() {
2397                None
2398            } else {
2399                g.deferred_boundary.pop_front()
2400            }
2401        };
2402        let Some(task) = task else { break };
2403        match catch_unwind(AssertUnwindSafe(|| run_boundary_task(graph, task))) {
2404            Ok(Some(task)) => {
2405                let queued = {
2406                    let mut g = graph.borrow_mut();
2407                    g.deferred_boundary.push_back(task);
2408                    g.deferred_boundary.len()
2409                };
2410                blocked += 1;
2411                if blocked >= queued {
2412                    break;
2413                }
2414            }
2415            Ok(None) => {
2416                blocked = 0;
2417            }
2418            Err(e) => {
2419                blocked = 0;
2420                if is_host_boundary_abort_payload(e.as_ref()) {
2421                    graph.borrow_mut().deferred_boundary.clear();
2422                    drain_guard.finish();
2423                    std::panic::resume_unwind(e);
2424                }
2425                if escaped.is_none() {
2426                    escaped = Some(e);
2427                }
2428            }
2429        }
2430    }
2431    drain_guard.finish();
2432    if let Some(e) = escaped {
2433        std::panic::resume_unwind(e);
2434    }
2435}
2436
2437fn run_boundary_task(graph: &Rc<RefCell<GraphCore>>, task: BoundaryTask) -> Option<BoundaryTask> {
2438    match task {
2439        BoundaryTask::Rewire {
2440            target,
2441            req,
2442            committed,
2443        } => {
2444            if !committed.get() {
2445                return None;
2446            }
2447            if let Some(node) = target.pinned_borrowed_core(graph) {
2448                if node.core.is_paused() {
2449                    return Some(BoundaryTask::Rewire {
2450                        target,
2451                        req,
2452                        committed,
2453                    });
2454                }
2455                node.core.apply_rewire_next(req);
2456            }
2457        }
2458        BoundaryTask::ExternalRewire {
2459            target,
2460            req,
2461            committed,
2462        } => {
2463            if committed.get() {
2464                if let Some(node) = target.pinned_borrowed_core(graph) {
2465                    node.core.apply_external_rewire(req);
2466                }
2467            }
2468        }
2469        BoundaryTask::Up {
2470            target,
2471            msgs,
2472            toward_dep,
2473            committed,
2474        } => {
2475            if !committed.get() {
2476                return None;
2477            }
2478            if let Some(node) = target.pinned_borrowed_core(graph) {
2479                if node.core.is_paused() {
2480                    return Some(BoundaryTask::Up {
2481                        target,
2482                        msgs,
2483                        toward_dep,
2484                        committed,
2485                    });
2486                }
2487                node.core.owned_up(msgs, toward_dep);
2488            }
2489        }
2490        BoundaryTask::Down {
2491            target,
2492            msgs,
2493            committed,
2494        } => {
2495            if committed.get() {
2496                if let Some(node) = target.pinned_borrowed_core(graph) {
2497                    node.core.owned_down(msgs);
2498                }
2499            }
2500        }
2501    }
2502    None
2503}
2504
2505impl Core {
2506    pub(crate) fn ptr_eq(&self, other: &Core) -> bool {
2507        Rc::ptr_eq(&self.graph, &other.graph)
2508            && self.id == other.id
2509            && self.generation == other.generation
2510    }
2511
2512    pub(crate) fn same_graph(&self, other: &Core) -> bool {
2513        Rc::ptr_eq(&self.graph, &other.graph)
2514    }
2515
2516    pub(crate) fn same_graph_arena(&self, arena: &GraphArena) -> bool {
2517        Rc::ptr_eq(&self.graph, &arena.0)
2518    }
2519
2520    pub(crate) fn arena(&self) -> GraphArena {
2521        GraphArena(self.graph.clone())
2522    }
2523
2524    pub(crate) fn dispatcher(&self) -> Dispatcher {
2525        self.with_call(|c| c.dispatcher.clone())
2526    }
2527
2528    pub(crate) fn deps(&self) -> Vec<Core> {
2529        self.borrow().deps.clone()
2530    }
2531
2532    pub(crate) fn set_topology_deps_changed_observer(&self, observer: TopologyDepsChangedObserver) {
2533        self.with_node_state_mut(|n, _c, _cfg, _r, _e| {
2534            n.topology_deps_changed = Some(observer);
2535        });
2536    }
2537
2538    fn notify_topology_deps_changed(&self, old_deps: &[Core], new_deps: &[Core]) {
2539        let observer = self.with_inner_edges(|n, _e| n.topology_deps_changed.clone());
2540        if let Some(observer) = observer {
2541            observer(old_deps, new_deps);
2542        }
2543    }
2544
2545    pub(crate) fn cache_any(&self) -> Option<AnyValue> {
2546        self.with_inner_edges(|_n, e| e.value.cache.clone())
2547    }
2548
2549    pub(crate) fn status(&self) -> Status {
2550        self.with_inner_edges(|_n, e| e.value.status)
2551    }
2552
2553    pub(crate) fn version(&self) -> Option<NodeVersion> {
2554        self.graph.borrow().get_version(self.key()).value.clone()
2555    }
2556
2557    pub(crate) fn versioning_policy(&self) -> ResolvedNodeVersioningPolicy {
2558        self.graph.borrow().get_version(self.key()).policy.clone()
2559    }
2560
2561    pub(crate) fn handle(&self) -> Option<Handle> {
2562        self.with_call(|c| c.handle)
2563    }
2564
2565    pub(crate) fn runtime_is_quiescent_for_release(&self) -> bool {
2566        if wave_in_flight() || delivery_in_flight() {
2567            return false;
2568        }
2569        let g = self.graph.borrow();
2570        let key = self.key();
2571        if !g.is_live_key(key) {
2572            return true;
2573        }
2574        if g.pin_count(key) != 0 || g.draining_boundary || !g.deferred_boundary.is_empty() {
2575            return false;
2576        }
2577        let e = g
2578            .edge_slots
2579            .get(key.id.0)
2580            .and_then(Option::as_ref)
2581            .expect("Core points at a live GraphCore edge slot");
2582        let a = g.get_aux(key);
2583        e.value.status != Status::Dirty
2584            && e.value.status != Status::Pending
2585            && e.state.pending == 0
2586            && !e.wave.inside_run_wave
2587            && !e.wave.in_dep_mutation
2588            && !e.wave.rewire_run_pending
2589            && !e.wave.batch_dirty_owed
2590            && a.pull_demand_owed.is_none()
2591            && a.active_pull.is_none()
2592            && a.pause_buffer.is_empty()
2593            && a.pause_lockset.is_empty()
2594    }
2595
2596    pub(crate) fn is_quiescent_for_release(&self) -> bool {
2597        self.runtime_is_quiescent_for_release() && self.subscriber_count() == 0
2598    }
2599
2600    pub(crate) fn release_runtime_for_graph(&self) -> bool {
2601        if !self.is_quiescent_for_release() {
2602            return false;
2603        }
2604        let key = self.key();
2605        let slot = {
2606            let mut graph = self.graph.borrow_mut();
2607            if graph.pin_count(key) != 0 {
2608                return false;
2609            }
2610            let Some(slot) = graph.take_live(key) else {
2611                self.refs.set(0);
2612                return true;
2613            };
2614            slot
2615        };
2616        self.refs.set(0);
2617        struct FreeSlotOnDrop {
2618            graph: Rc<RefCell<GraphCore>>,
2619            id: usize,
2620            armed: bool,
2621        }
2622        impl Drop for FreeSlotOnDrop {
2623            fn drop(&mut self) {
2624                if self.armed {
2625                    self.graph.borrow_mut().free.push(self.id);
2626                }
2627            }
2628        }
2629        let mut free_slot = FreeSlotOnDrop {
2630            graph: self.graph.clone(),
2631            id: key.id.0,
2632            armed: true,
2633        };
2634        unregister_backend_state_contributor(self);
2635        let (mut inner, _topology, mut call, _config, _run, mut edges, _version, mut aux) = slot;
2636        inner.cleanup_before_free(&mut call, &mut edges, &mut aux);
2637        free_slot.armed = false;
2638        self.graph.borrow_mut().free.push(key.id.0);
2639        true
2640    }
2641
2642    pub(crate) fn checkpoint_runtime(&self) -> NodeCheckpointRuntime {
2643        let version = self.version();
2644        self.with_node_state_aux_mut(|_n, _c, _cfg, r, e, a| NodeCheckpointRuntime {
2645            cache: e.value.cache.clone(),
2646            has_data: e.value.has_data,
2647            version,
2648            status: e.value.status,
2649            terminal: e.value.terminal,
2650            activated: a.activated,
2651            has_called_fn_once: r.has_called_fn_once,
2652            ctx_state: a.state.clone(),
2653            ctx_state_persist: a.state_persist,
2654        })
2655    }
2656
2657    pub(crate) fn restore_runtime(&self, state: NodeRestoreRuntime) {
2658        let should_activate = state.activated;
2659        let deps = self.with_node_state_aux_mut(|n, _c, _cfg, r, e, a| {
2660            e.value.cache = state.cache;
2661            e.value.has_data = state.has_data;
2662            e.value.status = state.status;
2663            e.value.terminal = state.terminal;
2664            e.wave = NodeWaveState::new();
2665            e.state = DepState::new(e.unsubs.len());
2666            e.restored_activation_handshake =
2667                vec![state.has_data || state.has_called_fn_once; e.unsubs.len()];
2668            r.has_called_fn_once = state.has_called_fn_once;
2669            a.activated = should_activate;
2670            a.state = state.ctx_state;
2671            a.state_persist = state.ctx_state_persist;
2672            a.on_deactivation.clear();
2673            a.on_invalidate.clear();
2674            a.pull_demand_owed = None;
2675            a.active_pull = None;
2676            a.in_deliver_demand = false;
2677            a.pull_dirty_owed = false;
2678            a.pause_lockset.clear();
2679            a.paused_dep_wave_occurred = false;
2680            a.pause_buffer.clear();
2681            n.deps.clone()
2682        });
2683        if should_activate {
2684            for (idx, dep) in deps.iter().enumerate() {
2685                self.subscribe_dep(idx, dep);
2686            }
2687        }
2688        let mut graph = self.graph.borrow_mut();
2689        let version_state = graph.get_version_mut(self.key());
2690        match state.version {
2691            RestoredNodeVersion::Disabled => {
2692                version_state.policy = ResolvedNodeVersioningPolicy::Disabled;
2693                version_state.value = None;
2694            }
2695            RestoredNodeVersion::Version(version) => {
2696                if matches!(version, NodeVersion::V0 { .. }) {
2697                    version_state.policy = ResolvedNodeVersioningPolicy::Level0;
2698                }
2699                version_state.value = Some(version);
2700            }
2701        }
2702    }
2703
2704    pub(crate) fn local_async_driver(
2705        &self,
2706    ) -> Option<Rc<dyn crate::async_driver::LocalAsyncDriver>> {
2707        self.with_call(|c| c.environment.local_async_driver())
2708    }
2709
2710    pub(crate) fn environment(&self) -> EnvironmentDrivers {
2711        self.with_call(|c| c.environment.clone())
2712    }
2713
2714    pub(crate) fn set_environment(&self, environment: EnvironmentDrivers) {
2715        self.with_call_mut(|c| {
2716            c.environment = environment;
2717        });
2718    }
2719
2720    pub(crate) fn factory(&self) -> Option<String> {
2721        self.with_call(|c| c.factory.clone())
2722    }
2723
2724    pub(crate) fn identity_key(&self) -> (usize, usize, u64) {
2725        (Rc::as_ptr(&self.graph) as usize, self.id.0, self.generation)
2726    }
2727
2728    fn new_in_arena(
2729        arena: &GraphArena,
2730        deps: Vec<Core>,
2731        handle: Option<Handle>,
2732        dispatcher: Dispatcher,
2733        initial: Option<AnyValue>,
2734        opts: NodeOpts,
2735    ) -> Core {
2736        for dep in &deps {
2737            assert!(
2738                dep.same_graph_arena(arena),
2739                "node construction: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
2740            );
2741        }
2742        let topology = NodeTopologySlot {
2743            deps,
2744            topology_deps_changed: None,
2745        };
2746        let inner = NodeInner;
2747        let mut environment = EnvironmentDrivers::default();
2748        environment.set_local_async_driver(dispatcher.local_async_driver());
2749        let call = NodeCallSlot {
2750            factory: None,
2751            handle,
2752            dispatcher,
2753            environment,
2754        };
2755        let config = NodeConfigSlot::from_opts(&opts);
2756        let version = NodeVersionState::new(config.versioning.clone(), initial.as_ref());
2757        {
2758            let mut g = arena.0.borrow_mut();
2759            let id = g.alloc(inner, topology, call, config, version);
2760            let generation = g.key_for(id).generation;
2761            // R-initial: a provided initial pre-populates the cache.
2762            if let Some(v) = initial {
2763                let (_n, e) = g.get_node_and_edges_mut(NodeKey { id, generation });
2764                e.value.cache = Some(v);
2765                e.value.has_data = true;
2766                e.value.status = Status::Settled;
2767            }
2768            Core {
2769                graph: arena.0.clone(),
2770                id,
2771                generation,
2772                refs: Rc::new(Cell::new(1)),
2773                counted: true,
2774            }
2775        }
2776    }
2777
2778    fn configure_pull(&self, pull_id: Option<LockId>) {
2779        if let Some(id) = pull_id {
2780            self.with_node_state_aux_mut(|_n, _c, cfg, _r, _e, a| {
2781                cfg.pull_id = Some(id);
2782                a.pull_demand_owed = None;
2783                a.active_pull = None;
2784            });
2785        }
2786    }
2787
2788    // ── subscription (R-push-subscribe) ──
2789
2790    /// Register a sink, push START/cached state, and lazily activate on first subscriber.
2791    /// Records the new subscriber's id into `id_out` **immediately after registration**
2792    /// — before the push + activation cascade that may panic. The public [`Node::subscribe`]
2793    /// runs this under the wave-owner catch; if `activate()` panics (a feedback cycle),
2794    /// the sink is already registered, so the caller can still detach it (the error path
2795    /// reconstructs the real unsub from `id_out`) — no orphaned subscriber.
2796    fn subscribe_recording_id(&self, sink: Sink, id_out: &Cell<Option<u64>>) -> Unsub {
2797        self.subscribe_recording_id_with_kind(sink, SubscriberKind::External, id_out)
2798    }
2799
2800    fn subscribe_recording_id_with_kind(
2801        &self,
2802        sink: Sink,
2803        kind: SubscriberKind,
2804        id_out: &Cell<Option<u64>>,
2805    ) -> Unsub {
2806        let (id, push) = {
2807            self.with_node_state_aux_mut(|_n, _c, cfg, _r, e, a| {
2808                let id = a.next_sub_id;
2809                a.next_sub_id += 1;
2810                a.subscribers.push(SubscriberEntry {
2811                    id,
2812                    kind,
2813                    sink: sink.clone(),
2814                });
2815                let push = if cfg.pull_id.is_some() {
2816                    None
2817                } else if e.value.has_data {
2818                    Some(Message::Data(
2819                        e.value.cache.clone().expect("has_data ⇒ cache present"),
2820                    ))
2821                } else if e.value.status == Status::Dirty {
2822                    Some(Message::Dirty)
2823                } else {
2824                    None
2825                };
2826                (id, push)
2827            })
2828        };
2829        id_out.set(Some(id));
2830        sink(&Message::Start);
2831        if let Some(m) = push {
2832            sink(&m);
2833        }
2834        if !self.with_aux(|a| a.activated) {
2835            self.activate();
2836        }
2837        let core = self.clone();
2838        Box::new(move || core.unsubscribe(id))
2839    }
2840
2841    fn unsubscribe(&self, id: u64) {
2842        if !self.graph.borrow().is_live_key(self.key()) {
2843            return;
2844        }
2845        let became_empty = self.with_aux_mut(|a| {
2846            let before = a.subscribers.len();
2847            a.subscribers.retain(|entry| entry.id != id);
2848            a.subscribers.len() < before && a.subscribers.is_empty()
2849        });
2850        if became_empty {
2851            self.deactivate();
2852        }
2853    }
2854
2855    pub(crate) fn external_subscriber_count_for_release(&self) -> usize {
2856        self.with_aux(|a| {
2857            a.subscribers
2858                .iter()
2859                .filter(|entry| entry.kind != SubscriberKind::GraphObserver)
2860                .count()
2861        })
2862    }
2863
2864    pub(crate) fn detach_graph_observer_subscribers_for_release(&self) -> usize {
2865        if !self.graph.borrow().is_live_key(self.key()) {
2866            return 0;
2867        }
2868        self.with_aux_mut(|a| {
2869            let before = a.subscribers.len();
2870            a.subscribers
2871                .retain(|entry| entry.kind != SubscriberKind::GraphObserver);
2872            before - a.subscribers.len()
2873        })
2874    }
2875
2876    // ── activation / deactivation (lazy; R-rom-ram) ──
2877
2878    fn activate(&self) {
2879        let deps = self.with_node_state_aux_mut(|n, _c, _cfg, _r, e, a| {
2880            a.activated = true;
2881            e.unsubs = (0..n.deps.len()).map(|_| None).collect();
2882            // placeholder boxes (distinct Rcs); subscribe_dep overwrites each with the
2883            // real box its callback captures.
2884            e.idx_boxes = (0..n.deps.len())
2885                .map(|_| Rc::new(Cell::new(-1i64)))
2886                .collect();
2887            n.deps.clone()
2888        });
2889        for (idx, dep) in deps.iter().enumerate() {
2890            self.subscribe_dep(idx, dep);
2891        }
2892        // Depless producer (fn, no deps): run once on activation.
2893        let run = self.with_node_state(|n, c, _cfg, r, _e| {
2894            n.deps.is_empty() && c.handle.is_some() && !r.has_called_fn_once
2895        });
2896        if run {
2897            self.run_wave();
2898        }
2899    }
2900
2901    fn subscribe_dep(&self, idx: usize, dep: &Core) {
2902        let weak = self.downgrade();
2903        // R-rewire Option-C: the callback reads the dep's CURRENT index from a shared box
2904        // so a surgical reorder reroutes in O(1); -1 means the dep was removed (drain).
2905        let idx_box: Rc<Cell<i64>> = Rc::new(Cell::new(idx as i64));
2906        let cb_box = idx_box.clone();
2907        let sink: Sink = Rc::new(move |msg: &Msg| {
2908            let i = cb_box.get();
2909            if i < 0 {
2910                return; // dep removed — stale in-flight callback drops (drain)
2911            }
2912            weak.receive_from_dep(i as usize, msg);
2913        });
2914        // dep.subscribe synchronously pushes START (+ cached DATA) into the sink,
2915        // re-entering receive_from_dep — done before we re-borrow self below.
2916        let dep_sub_id = Cell::new(None);
2917        let subscribed = catch_unwind(AssertUnwindSafe(|| {
2918            dep.subscribe_recording_id(sink, &dep_sub_id)
2919        }));
2920        let unsub = match subscribed {
2921            Ok(unsub) => unsub,
2922            Err(payload) => {
2923                if let Some(id) = dep_sub_id.get() {
2924                    if self.with_inner_edges(|_, e| e.wave.in_dep_mutation) {
2925                        let dep = dep.clone();
2926                        let _ = catch_unwind(AssertUnwindSafe(move || dep.unsubscribe(id)));
2927                    }
2928                }
2929                std::panic::resume_unwind(payload);
2930            }
2931        };
2932        self.with_inner_edges_mut(|_, e| {
2933            e.unsubs[idx] = Some(unsub);
2934            e.idx_boxes[idx] = idx_box;
2935            e.restored_activation_handshake[idx] = false;
2936        });
2937    }
2938
2939    fn deactivate(&self) {
2940        let (unsubs, hooks) = self.with_node_state_aux_mut(|n, c, _cfg, r, e, a| {
2941            a.activated = false;
2942            let unsubs: Vec<Unsub> = e.unsubs.drain(..).flatten().collect();
2943            e.idx_boxes.clear();
2944            let hooks: Vec<Box<dyn FnOnce()>> = std::mem::take(&mut a.on_deactivation);
2945            // RAM: a compute node (fn and/or deps) clears its cache on deactivation;
2946            // a depless state node retains it (ROM).
2947            let is_compute = c.handle.is_some() || !n.deps.is_empty();
2948            if is_compute {
2949                e.value.cache = None;
2950                e.value.has_data = false;
2951                e.value.status = Status::Sentinel;
2952            }
2953            for i in 0..n.deps.len() {
2954                e.state.batch[i] = None;
2955                e.state.batch_waves[i].clear();
2956                e.state.batch_wave_id[i] = None;
2957                e.state.prev[i] = None;
2958                e.state.has_data[i] = false;
2959                e.state.dirty[i] = false;
2960                e.state.tier[i] = 0;
2961                e.state.terminal[i] = None;
2962                e.state.terminal_wave[i] = None;
2963            }
2964            e.restored_activation_handshake.fill(false);
2965            e.state.pending = 0;
2966            e.wave.emitted_dirty_this_wave = false;
2967            r.has_called_fn_once = false;
2968            // Control + INVALIDATE hooks are fresh-lifecycle (R-cleanup-hooks / D28):
2969            // the next activation re-registers them from the fn body.
2970            a.on_invalidate.clear();
2971            a.pause_lockset.clear();
2972            a.pull_demand_owed = None;
2973            a.active_pull = None;
2974            a.in_deliver_demand = false;
2975            a.pull_dirty_owed = false;
2976            a.paused_dep_wave_occurred = false;
2977            a.pause_buffer.clear();
2978            if !a.state_persist {
2979                a.state = None;
2980            }
2981            (unsubs, hooks)
2982        });
2983        for u in unsubs {
2984            u();
2985        }
2986        for h in hooks {
2987            h();
2988        }
2989    }
2990
2991    // ── upstream wave receive (two-phase + diamond) ──
2992
2993    #[cfg(test)]
2994    fn receive_from_dep(&self, idx: usize, msg: &Msg) {
2995        // B25: enroll in the wave touched-set so a panic-abort can reset our flags.
2996        // DR-8/B54: the internal dep-callback entry may be an uncounted arena view;
2997        // under a real wave this generation-keyed touched action pins the node in the
2998        // owner arena while borrow-free subscriber callbacks run.
2999        wave_register(self);
3000        self.receive_from_dep_registered(idx, msg);
3001    }
3002
3003    fn receive_from_dep_registered(&self, idx: usize, msg: &Msg) {
3004        let Some(action) = self.collect_receive_from_dep_action(idx, msg) else {
3005            return;
3006        };
3007        if action.has_work() {
3008            self.apply_receive_from_dep_action(action);
3009        }
3010    }
3011
3012    fn collect_receive_from_dep_action(
3013        &self,
3014        idx: usize,
3015        msg: &Msg,
3016    ) -> Option<InWaveDepReceiveAction> {
3017        let mut g = self.graph.borrow_mut();
3018        Self::collect_receive_from_dep_action_from_graph(&mut g, self.key(), idx, msg)
3019    }
3020
3021    fn decide_maybe_run_from_graph(g: &mut GraphCore, key: NodeKey) -> MaybeRunDecision {
3022        let (_n, c, cfg, r, e, a) = g.get_node_call_config_run_edges_aux_mut(key);
3023
3024        // R-rewire (D42): an added cached dep's push-on-subscribe lands here mid-mutation.
3025        // Defer fn-run to ONE atomic settle after every added dep is wired.
3026        if e.wave.in_dep_mutation {
3027            e.wave.rewire_run_pending = true;
3028            return MaybeRunDecision::Skip;
3029        }
3030
3031        // R-pause-modes: only the default "true" mode coalesces dep-driven recompute
3032        // (skip the fn while any lock is held → fire once with latest dep values on
3033        // final-lock RESUME). `resumeAll` RUNS the fn while paused but buffers output
3034        // (`down` → `pause_buffer`). `false` runs + emits immediately.
3035        let pull_quiet = cfg.pull_id.is_some() && a.active_pull.is_none();
3036        if matches!(cfg.pausable, Pausable::True) && (!a.pause_lockset.is_empty() || pull_quiet) {
3037            a.paused_dep_wave_occurred = true;
3038            return MaybeRunDecision::Skip;
3039        }
3040        if e.state.pending > 0 {
3041            return MaybeRunDecision::Skip;
3042        }
3043        if c.handle.is_none() {
3044            return MaybeRunDecision::Passthrough;
3045        }
3046        if !(r.has_called_fn_once || cfg.partial || cfg.all_deps_settled(&e.state)) {
3047            return MaybeRunDecision::Skip;
3048        }
3049
3050        MaybeRunDecision::Run
3051    }
3052
3053    fn set_maybe_run_action(g: &mut GraphCore, key: NodeKey, action: &mut InWaveDepReceiveAction) {
3054        if delivery_in_flight() {
3055            action.defer_maybe_run = true;
3056        } else {
3057            action.maybe_run_decision = Self::decide_maybe_run_from_graph(g, key);
3058        }
3059    }
3060
3061    fn apply_receive_from_dep_action(&self, action: InWaveDepReceiveAction) {
3062        action.apply(self);
3063    }
3064
3065    fn collect_receive_from_dep_action_from_graph(
3066        g: &mut GraphCore,
3067        key: NodeKey,
3068        idx: usize,
3069        msg: &Msg,
3070    ) -> Option<InWaveDepReceiveAction> {
3071        let (pausable, pull_id, auto_error, auto_complete, terminal_as_real_input) = {
3072            let cfg = g.get_config(key);
3073            (
3074                cfg.pausable,
3075                cfg.pull_id.clone(),
3076                cfg.error_when_deps_error,
3077                cfg.complete_when_deps_complete,
3078                cfg.terminal_as_real_input,
3079            )
3080        };
3081        if g.get_node_and_edges_mut(key).1.value.terminal {
3082            if matches!(msg, Message::Teardown) {
3083                return Some(InWaveDepReceiveAction {
3084                    emit_teardown_subscribers: true,
3085                    ..Default::default()
3086                });
3087            }
3088            return None;
3089        }
3090
3091        let mut action = InWaveDepReceiveAction::default();
3092        match msg {
3093            Message::Start => {}
3094            Message::Invalidate => {
3095                let (had_data, undirty_no_settle, paused) = {
3096                    let (_n, e, a) = g.get_node_edges_aux_mut(key);
3097                    let pull_quiet = pull_id.is_some() && a.active_pull.is_none();
3098                    e.state.prev[idx] = None;
3099                    e.state.has_data[idx] = false;
3100                    e.state.batch[idx] = None;
3101                    let had_current_projection = e.state.record_wave_sentinel(idx);
3102                    if !had_current_projection && !e.state.has_run_triggering_projection() {
3103                        e.state.clear_wave_projection(idx);
3104                    }
3105                    if e.state.dirty[idx] {
3106                        e.state.dirty[idx] = false;
3107                        e.state.pending -= 1;
3108                    }
3109                    if a.paused_dep_wave_occurred && e.state.batch.iter().all(|b| b.is_none()) {
3110                        a.paused_dep_wave_occurred = false;
3111                    }
3112                    (
3113                        e.value.has_data,
3114                        e.state.pending == 0 && e.wave.emitted_dirty_this_wave,
3115                        !a.pause_lockset.is_empty() || pull_quiet,
3116                    )
3117                };
3118                let buffer_own_invalidate = paused && matches!(pausable, Pausable::ResumeAll);
3119                action.had_node_data_before_invalidate = had_data;
3120                action.do_invalidate = true;
3121                action.invalidate_as_invalidate_msg = buffer_own_invalidate && had_data;
3122                if undirty_no_settle {
3123                    if !had_data {
3124                        action.down_msgs = Some(vec![Message::Resolved]);
3125                    } else {
3126                        action.clear_undirty_after_invalidate = true;
3127                    }
3128                }
3129                action.fire_demand = true;
3130            }
3131            Message::Dirty => {
3132                action.emit_dirty = {
3133                    let (_n, e, a) = g.get_node_edges_aux_mut(key);
3134                    if e.state.dirty[idx] {
3135                        false
3136                    } else {
3137                        e.state.dirty[idx] = true;
3138                        e.state.pending += 1;
3139                        e.state.tier[idx] = 2;
3140                        let pull_quiet = pull_id.is_some() && a.active_pull.is_none();
3141                        if pull_quiet || e.wave.emitted_dirty_this_wave {
3142                            false
3143                        } else {
3144                            e.wave.emitted_dirty_this_wave = true;
3145                            e.value.status = Status::Dirty;
3146                            true
3147                        }
3148                    }
3149                };
3150            }
3151            Message::Data(v) => {
3152                if g.get_node_and_edges_mut(key)
3153                    .1
3154                    .restored_activation_handshake[idx]
3155                {
3156                    let (_n, e) = g.get_node_and_edges_mut(key);
3157                    e.state.prev[idx] = Some(v.clone());
3158                    e.state.has_data[idx] = true;
3159                    e.state.tier[idx] = 3;
3160                    e.state.batch[idx] = None;
3161                    return None;
3162                }
3163                let pending_drained = {
3164                    let (_n, e, a) = g.get_node_edges_aux_mut(key);
3165                    match &mut e.state.batch[idx] {
3166                        Some(b) => b.push(v.clone()),
3167                        none => *none = Some(vec![v.clone()]),
3168                    }
3169                    e.state.record_wave_data(idx, v.clone());
3170                    e.state.has_data[idx] = true;
3171                    e.state.tier[idx] = 3;
3172                    if e.state.dirty[idx] {
3173                        e.state.dirty[idx] = false;
3174                        e.state.pending -= 1;
3175                    }
3176                    let pending_drained = e.state.pending == 0;
3177                    if !pending_drained
3178                        && matches!(pausable, Pausable::True)
3179                        && !e.wave.in_dep_mutation
3180                        && (!a.pause_lockset.is_empty()
3181                            || (pull_id.is_some() && a.active_pull.is_none()))
3182                    {
3183                        a.paused_dep_wave_occurred = true;
3184                    }
3185                    pending_drained
3186                };
3187                if pending_drained {
3188                    Self::set_maybe_run_action(g, key, &mut action);
3189                    action.fire_demand = true;
3190                }
3191            }
3192            Message::Resolved => {
3193                let pending_drained = {
3194                    let (_n, e, a) = g.get_node_edges_aux_mut(key);
3195                    e.state.record_resolved_wave(idx);
3196                    e.state.tier[idx] = 3;
3197                    if e.state.dirty[idx] {
3198                        e.state.dirty[idx] = false;
3199                        e.state.pending -= 1;
3200                    }
3201                    let pending_drained = e.state.pending == 0;
3202                    if !pending_drained
3203                        && matches!(pausable, Pausable::True)
3204                        && !e.wave.in_dep_mutation
3205                        && (!a.pause_lockset.is_empty()
3206                            || (pull_id.is_some() && a.active_pull.is_none()))
3207                    {
3208                        a.paused_dep_wave_occurred = true;
3209                    }
3210                    pending_drained
3211                };
3212                if pending_drained {
3213                    Self::set_maybe_run_action(g, key, &mut action);
3214                    action.fire_demand = true;
3215                }
3216            }
3217            m if m.tier() == Tier::Terminal => {
3218                // Box<dyn Error> is not Clone and `m` is a borrow (D31): keep the error as its
3219                // Display message in the DepTerminal record (Rc<str>, which IS Clone).
3220                let dep_term = match m {
3221                    Message::Error(e) => DepTerminal::Error(format!("{e}").into()),
3222                    _ => DepTerminal::Complete,
3223                };
3224                let is_error = matches!(dep_term, DepTerminal::Error(_));
3225                let all_deps_terminal = {
3226                    let (n, e) = g.get_node_and_edges_mut(key);
3227                    e.state.terminal[idx] = Some(dep_term.clone());
3228                    e.state.terminal_wave[idx] = Some(dep_term.clone());
3229                    if e.state.dirty[idx] {
3230                        e.state.dirty[idx] = false;
3231                        e.state.pending -= 1;
3232                    }
3233                    !n.deps.is_empty() && e.state.terminal.iter().all(|t| t.is_some())
3234                };
3235                if is_error && auto_error {
3236                    if let DepTerminal::Error(s) = &dep_term {
3237                        action.down_msgs = Some(vec![Message::Error(s.to_string().into())]);
3238                    }
3239                } else if terminal_as_real_input {
3240                    Self::set_maybe_run_action(g, key, &mut action);
3241                } else if auto_complete && all_deps_terminal {
3242                    action.down_msgs = Some(vec![Message::Complete]);
3243                } else {
3244                    action.do_settle_after_absorbed_terminal = true;
3245                }
3246                action.fire_demand = true;
3247            }
3248            Message::Teardown => {
3249                action.down_msgs = Some(vec![Message::Complete, Message::Teardown]);
3250            }
3251            // PAUSE/RESUME are never delivered to a dep-subscriber (a node is paused via its
3252            // own up(), not by an upstream dep).
3253            _ => {}
3254        }
3255        if action.has_work() {
3256            Some(action)
3257        } else {
3258            None
3259        }
3260    }
3261
3262    /// R-terminal-settles-dirty (B35): settle a node whose dirtied dep was released by an
3263    /// ABSORBED terminal that is NOT a real input (a plain derived/effect — one of several
3264    /// deps completing while others stay live). Runs only when the release drained `pending`
3265    /// while the node still owes a downstream settle (it broadcast DIRTY this wave):
3266    ///   - some OTHER dep delivered real DATA this wave → recompute (→ DATA);
3267    ///   - else no value materialised (or the recompute is gated) → one undirty RESOLVED
3268    ///     (R-resolved-undirty), keeping the cache (a terminal, unlike INVALIDATE, leaves it).
3269    fn settle_after_absorbed_terminal(&self) {
3270        if delivery_in_flight() {
3271            defer_absorbed_settle_until_delivery_boundary(self);
3272            return;
3273        }
3274        if !self.with_inner_edges(|_, e| e.state.pending == 0 && e.wave.emitted_dirty_this_wave) {
3275            return;
3276        }
3277        // A real value occurred this wave (some OTHER dep delivered DATA) → recompute.
3278        // maybe_run runs the fn ONLY if ungated (gate open, not paused); it may emit DATA, a
3279        // fn-synthesized undirty RESOLVED, or nothing (gated / gate still holds).
3280        let saw_data = self.with_inner_edges(|_n, e| {
3281            e.state
3282                .batch
3283                .iter()
3284                .any(|b| b.as_ref().is_some_and(|v| !v.is_empty()))
3285        });
3286        if saw_data {
3287            self.maybe_run();
3288        }
3289        // If the node STILL owes a downstream settle (no DATA occurred, OR the recompute was
3290        // gated — e.g. the first-run gate holds because the terminated dep never delivered and
3291        // terminal_as_real_input is false), balance the broadcast DIRTY with one undirty
3292        // RESOLVED, keeping the cache. Without this fallback a DIRTY-then-terminal-without-DATA
3293        // dep on a pre-first-run multi-dep node strands the DIRTY → downstream wedged (the B35
3294        // gate-holds corner, C-15(d)). Route through `down` so D64/R-undirty-settle-timing
3295        // honors resumeAll and batch timing.
3296        let still_owes = self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave);
3297        if still_owes {
3298            self.down(vec![Message::Resolved]);
3299        }
3300    }
3301
3302    fn mark_dirty(&self) {
3303        let subs = self.with_inner_edges_aux_mut(|_n, e, a| {
3304            e.value.status = Status::Dirty;
3305            if !e.wave.emitted_dirty_this_wave {
3306                e.wave.emitted_dirty_this_wave = true;
3307                snapshot_subscribers(a)
3308            } else {
3309                SubscriberSnapshot::Empty
3310            }
3311        });
3312        deliver_subscriber_snapshot(subs, &Message::Dirty);
3313    }
3314
3315    fn maybe_run(&self) {
3316        if delivery_in_flight() {
3317            defer_run_until_delivery_boundary(self);
3318            return;
3319        }
3320        let decision = {
3321            let mut g = self.graph.borrow_mut();
3322            Self::decide_maybe_run_from_graph(&mut g, self.key())
3323        };
3324        match decision {
3325            MaybeRunDecision::Skip => {}
3326            MaybeRunDecision::Passthrough => self.passthrough_emit(),
3327            MaybeRunDecision::Run => self.run_wave(),
3328        }
3329    }
3330
3331    fn try_run(&self) {
3332        let (pending, has_handle, has_called, partial, all_settled) =
3333            self.with_node_state(|_n, c, cfg, r, e| {
3334                (
3335                    e.state.pending,
3336                    c.handle.is_some(),
3337                    r.has_called_fn_once,
3338                    cfg.partial,
3339                    cfg.all_deps_settled(&e.state),
3340                )
3341            });
3342        if pending > 0 {
3343            return;
3344        }
3345        if !has_handle {
3346            self.passthrough_emit();
3347            return;
3348        }
3349        if !has_called {
3350            // first-run gate: hold the fn until every dep has settled (R-first-run-gate).
3351            if partial || all_settled {
3352                self.run_wave();
3353            }
3354            return;
3355        }
3356        self.run_wave();
3357    }
3358
3359    fn passthrough_emit(&self) {
3360        // Single-dep wire (deps, no fn): relay dep 0's latest DATA downstream.
3361        let v = self.with_inner_edges_mut(|_n, e| {
3362            let v = e.state.batch[0].as_ref().and_then(|b| b.last().cloned());
3363            if let Some(v) = &v {
3364                e.state.prev[0] = Some(v.clone());
3365            }
3366            e.state.batch[0] = None;
3367            e.state.batch_waves[0].clear();
3368            e.state.batch_wave_id[0] = None;
3369            v
3370        });
3371        if let Some(v) = v {
3372            self.down(vec![Message::Data(v)]);
3373        }
3374    }
3375
3376    // ── runtime topology rewire (R-rewire / D42, C-8) ──
3377
3378    /// Runtime topology rewire — the public substrate entry (called by `Node::replace_deps`/
3379    /// `subscribe_dep`/`unsubscribe_dep`). The validation REJECTS run first and panic: called
3380    /// EXTERNALLY (no wave in flight) the panic propagates to the caller (idiomatic
3381    /// error); called MID-FN (inside a wave) the panic is caught by the running wave-owner
3382    /// → `[[ERROR,e]]` (R-reentrancy/D37 — a fn mutating its own topology mid-wave is the
3383    /// feedback cycle). The surgical Option-C mutation + atomic settle then run under a
3384    /// FRESH wave-owner so a user-fn panic during dep-activation/settle becomes ERROR
3385    /// (D30). INTRA-graph only (D22). Requires an explicit fn (SD-1 fn-deps pairing).
3386    pub(crate) fn rewire(&self, new_deps: Vec<Core>, fn_: NodeFn) {
3387        self.external_rewire(RewireRequest::Set(new_deps, fn_));
3388    }
3389
3390    fn external_rewire(&self, req: RewireRequest) {
3391        req.validate_deps_same_graph(self);
3392        if let Some(committed) = committed_after_batch_for_target(self) {
3393            defer_boundary(
3394                self,
3395                BoundaryTask::ExternalRewire {
3396                    target: CoreToken::from_core(self),
3397                    req,
3398                    committed,
3399                },
3400            );
3401            return;
3402        }
3403        self.apply_external_rewire(req);
3404    }
3405
3406    fn apply_external_rewire(&self, req: RewireRequest) {
3407        let current = self.deps();
3408        let (new_deps, fn_) = req.into_deps_and_fn(current);
3409        self.rewire_inner(new_deps, fn_, false);
3410    }
3411
3412    fn project_pending_external_rewire_deps(&self) -> Vec<Core> {
3413        let target = CoreToken::from_core(self);
3414        let mut deps = self.deps();
3415        let graph = self.graph.borrow();
3416        for task in &graph.deferred_boundary {
3417            let BoundaryTask::ExternalRewire {
3418                target: queued_target,
3419                req,
3420                ..
3421            } = task
3422            else {
3423                continue;
3424            };
3425            if queued_target.graph_id == target.graph_id && queued_target.key == target.key {
3426                deps = req.project_deps(deps);
3427            }
3428        }
3429        deps
3430    }
3431
3432    fn rewire_inner(&self, new_deps: Vec<Core>, fn_: NodeFn, allow_terminal_owner: bool) {
3433        let new_deps = dedup_cores(new_deps);
3434        for dep in &new_deps {
3435            assert!(
3436                dep.same_graph(self),
3437                "rewire: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
3438            );
3439        }
3440        if !allow_terminal_owner {
3441            if let Some(committed) = committed_after_batch_for_target(self) {
3442                defer_boundary(
3443                    self,
3444                    BoundaryTask::ExternalRewire {
3445                        target: CoreToken::from_core(self),
3446                        req: RewireRequest::Set(new_deps, fn_),
3447                        committed,
3448                    },
3449                );
3450                return;
3451            }
3452        }
3453        {
3454            let (terminal, inside_run_wave, in_dep_mutation) = self.with_inner_edges(|_n, e| {
3455                (
3456                    e.value.terminal,
3457                    e.wave.inside_run_wave,
3458                    e.wave.in_dep_mutation,
3459                )
3460            });
3461            assert!(
3462                allow_terminal_owner || !terminal,
3463                "rewire: node is terminal (completed/errored) — cannot rewire (R-rewire / D42)"
3464            );
3465            assert!(
3466                !inside_run_wave,
3467                "rewire: mid-fn topology mutation — a fn mutating its own deps mid-wave is the feedback cycle (R-rewire / D37)"
3468            );
3469            assert!(
3470                !in_dep_mutation,
3471                "rewire: reentrant dep mutation — another replace_deps/subscribe_dep/unsubscribe_dep is in flight (R-rewire)"
3472            );
3473        }
3474        assert!(
3475            !new_deps.iter().any(|d| d.ptr_eq(self)),
3476            "rewire: self-dependency rejected (R-rewire / D42)"
3477        );
3478        let old_deps = self.borrow().deps.clone();
3479        for d in new_deps
3480            .iter()
3481            .filter(|d| !old_deps.iter().any(|o| o.ptr_eq(d)))
3482        {
3483            assert!(
3484                !reachable_upstream(d, self),
3485                "rewire: would create a cycle — dep already transitively depends on this node (R-rewire / D42)"
3486            );
3487            // Rust has no resubscribable-terminal opt-in yet (R-terminal, later slice) ⇒
3488            // ALL terminal deps are non-resubscribable → adding any terminal dep is rejected.
3489            assert!(
3490                !d.with_inner_edges(|_n, e| e.value.terminal),
3491                "rewire: cannot add a non-resubscribable terminal dep — would wedge (R-rewire / D42)"
3492            );
3493        }
3494        with_wave_owner(self, || self.rewire_apply(new_deps, fn_), || {});
3495    }
3496
3497    // ── deferred self-rewire (R-rewire-deferred / D47): ctx.rewire_next ──
3498
3499    /// Enqueue a deferred self-rewire (`ctx.rewire_next`). Applied at the committed wave
3500    /// boundary by the wave-owner drain, NEVER in place — the immediate `replace_deps`/`subscribe_dep`/
3501    /// `unsubscribe_dep` still panics mid-fn (D37/R-reentrancy). The drain runs each as a fresh
3502    /// wave; this is the substrate affordance the higher-order *Map operators wire inners with.
3503    pub(crate) fn request_rewire_next(&self, req: RewireRequest) {
3504        defer_boundary(
3505            self,
3506            BoundaryTask::Rewire {
3507                target: CoreToken::from_core(self),
3508                req,
3509                committed: active_batch_committed_token()
3510                    .unwrap_or_else(|| Rc::new(Cell::new(true))),
3511            },
3512        );
3513    }
3514
3515    pub(crate) fn request_up_next(&self, msgs: Wave<AnyValue>, toward_dep: Option<usize>) {
3516        defer_boundary(
3517            self,
3518            BoundaryTask::Up {
3519                target: CoreToken::from_core(self),
3520                msgs,
3521                toward_dep,
3522                committed: active_batch_committed_token()
3523                    .unwrap_or_else(|| Rc::new(Cell::new(true))),
3524            },
3525        );
3526    }
3527
3528    pub(crate) fn request_down_next(&self, msgs: Wave<AnyValue>) {
3529        defer_boundary(
3530            self,
3531            BoundaryTask::Down {
3532                target: CoreToken::from_core(self),
3533                msgs,
3534                committed: active_batch_committed_token()
3535                    .unwrap_or_else(|| Rc::new(Cell::new(true))),
3536            },
3537        );
3538    }
3539
3540    /// Apply one queued self-rewire at the boundary (a drain thunk). D62: terminal seals output
3541    /// but does NOT cancel queued topology intent. The dep set is composed
3542    /// against the LIVE deps at apply time (so several requests in one wave compose). A rewire
3543    /// reject (cycle/self/non-resubscribable terminal dep) panics — caught here and surfaced as
3544    /// `[[ERROR,e]]` on this node (D30-consistent) so it does not strand the rest of the drain.
3545    fn apply_rewire_next(&self, req: RewireRequest) {
3546        let (new_deps, fn_) = match req {
3547            RewireRequest::Set(deps, f) => (deps, f),
3548            RewireRequest::Add(dep, f) => {
3549                let mut next = self.deps();
3550                if !next.iter().any(|d| d.ptr_eq(&dep)) {
3551                    next.push(dep);
3552                }
3553                (next, f)
3554            }
3555            RewireRequest::Remove(dep, f) => {
3556                let current = self.borrow().deps.clone();
3557                if !current.iter().any(|d| dep.matches(d)) {
3558                    return;
3559                }
3560                let next: Vec<Core> = current.into_iter().filter(|d| !dep.matches(d)).collect();
3561                (next, f)
3562            }
3563        };
3564        // rewire() dedups + validates + applies under its own wave-owner (a fn-panic during the
3565        // apply becomes ERROR on the blamed node there). Only the PRE-apply validation rejects
3566        // panic OUT of rewire() — caught here → ERROR on this node, via owned_down (a fresh
3567        // wave-owner; the drain runs outside any live wave).
3568        let outcome = catch_unwind(AssertUnwindSafe(|| self.rewire_inner(new_deps, fn_, true)));
3569        if let Err(payload) = outcome {
3570            if is_host_boundary_abort_payload(payload.as_ref()) {
3571                resume_unwind(payload);
3572            }
3573            let err = panic_to_error(payload);
3574            self.owned_down(vec![Message::Error(err)]);
3575        }
3576    }
3577
3578    /// The surgical Option-C mutation + atomic settle (D42), run under a wave-owner. Kept
3579    /// deps keep their subscription + per-dep state + idx-box (rerouted O(1)); removed deps
3580    /// drain (box→-1, unsub) + drop their dirty contribution; added deps fresh-subscribe
3581    /// (push-on-subscribe). The first-run gate + cache are PRESERVED (Q2/Q7). One atomic
3582    /// settle if any added dep delivered data or the sole dirty contributor was removed.
3583    fn rewire_apply(&self, new_deps: Vec<Core>, fn_: NodeFn) {
3584        let old_deps = self.borrow().deps.clone();
3585        if same_ordered_deps(&old_deps, &new_deps) {
3586            self.with_inner_edges_mut(|_, e| {
3587                e.wave.in_dep_mutation = true;
3588                e.wave.rewire_run_pending = false;
3589            });
3590            let _mut_guard = DepMutationGuard(DeferredNodeAction::from_core(self));
3591            self.swap_fn_preserving_pool(fn_);
3592            self.with_inner_edges_mut(|_, e| {
3593                e.wave.in_dep_mutation = false;
3594            });
3595            return;
3596        }
3597        if old_deps.len() == new_deps.len() && Self::same_graph_no_overlap(&old_deps, &new_deps) {
3598            // DR-8/B54 no-kept fast path: full dep replacement (same arity, no shared
3599            // identity) keeps this in the same O(n) class as the old keep-path, but bypasses
3600            // full O(n²) matching when no dep survives.
3601            let n = new_deps.len();
3602            self.with_inner_edges_mut(|_, e| {
3603                e.wave.in_dep_mutation = true;
3604                e.wave.rewire_run_pending = false;
3605            });
3606            let _mut_guard = DepMutationGuard(DeferredNodeAction::from_core(self));
3607            let activated = self.with_aux(|a| a.activated);
3608            let mut removed_dirty_contributor = false;
3609
3610            self.swap_fn_preserving_pool(fn_);
3611
3612            for old_idx in 0..old_deps.len() {
3613                let unsub = {
3614                    let mut graph = self.graph.borrow_mut();
3615                    let (_nn, edges) = graph.get_node_and_edges_mut(self.key());
3616                    if edges.state.dirty[old_idx] {
3617                        removed_dirty_contributor = true;
3618                        edges.state.pending -= 1;
3619                    }
3620                    if let Some(box_ref) = edges.idx_boxes.get(old_idx) {
3621                        box_ref.set(-1);
3622                    }
3623                    edges.unsubs.get_mut(old_idx).and_then(|u| u.take())
3624                };
3625                if let Some(unsub) = unsub {
3626                    unsub();
3627                }
3628            }
3629
3630            let new_batch: Vec<Option<Vec<AnyValue>>> = (0..n).map(|_| None).collect();
3631            let new_batch_waves: Vec<Vec<Vec<WaveData>>> = (0..n).map(|_| Vec::new()).collect();
3632            let new_batch_wave_id: Vec<Option<u64>> = (0..n).map(|_| None).collect();
3633            let new_prev: Vec<Option<AnyValue>> = (0..n).map(|_| None).collect();
3634            let new_tier: Vec<u8> = (0..n).map(|_| 0).collect();
3635            let new_terminal: Vec<Option<DepTerminal>> = (0..n).map(|_| None).collect();
3636            let new_terminal_wave: Vec<Option<DepTerminal>> = (0..n).map(|_| None).collect();
3637            let new_has: Vec<bool> = (0..n).map(|_| false).collect();
3638            let new_dirty: Vec<bool> = (0..n).map(|_| false).collect();
3639            let new_unsubs: Vec<Option<Unsub>> = (0..n).map(|_| None).collect();
3640            let new_boxes: Vec<Rc<Cell<i64>>> = (0..n).map(|_| Rc::new(Cell::new(-1i64))).collect();
3641            let new_restored_handshake: Vec<bool> = vec![false; n];
3642            self.with_inner_edges_mut(|nn, e| {
3643                nn.deps = new_deps.clone();
3644                e.state.batch = new_batch;
3645                e.state.batch_waves = new_batch_waves;
3646                e.state.batch_wave_id = new_batch_wave_id;
3647                e.state.prev = new_prev;
3648                e.state.has_data = new_has;
3649                e.state.dirty = new_dirty;
3650                e.state.tier = new_tier;
3651                e.state.terminal = new_terminal;
3652                e.state.terminal_wave = new_terminal_wave;
3653                e.unsubs = new_unsubs;
3654                e.idx_boxes = new_boxes;
3655                e.restored_activation_handshake = new_restored_handshake;
3656            });
3657
3658            if activated {
3659                for (idx, dep) in new_deps.iter().enumerate() {
3660                    self.subscribe_dep(idx, dep);
3661                }
3662            }
3663            self.notify_topology_deps_changed(&old_deps, &new_deps);
3664
3665            if self.with_inner_edges(|_, e| e.value.terminal) {
3666                return;
3667            }
3668
3669            let should_rewire_settle = self.with_inner_edges(|_, e| {
3670                removed_dirty_contributor && e.state.pending == 0 && e.value.status == Status::Dirty
3671            });
3672            let mut zero_dep_undirty = false;
3673            if should_rewire_settle {
3674                if n == 0 {
3675                    zero_dep_undirty = true;
3676                } else {
3677                    self.with_inner_edges_mut(|_, e| {
3678                        e.wave.rewire_run_pending = true;
3679                    });
3680                }
3681            }
3682
3683            self.with_inner_edges_mut(|_, e| {
3684                e.wave.in_dep_mutation = false;
3685            });
3686
3687            let run_pending =
3688                self.with_inner_edges_mut(|_, e| std::mem::take(&mut e.wave.rewire_run_pending));
3689            if run_pending {
3690                self.settle_rewire();
3691            } else if zero_dep_undirty {
3692                let emitted = self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave);
3693                if emitted {
3694                    self.down(vec![Message::Resolved]);
3695                } else {
3696                    self.with_inner_edges_mut(|_n, e| {
3697                        e.value.status = if e.value.has_data {
3698                            Status::Settled
3699                        } else {
3700                            Status::Sentinel
3701                        };
3702                    });
3703                }
3704            }
3705            return;
3706        }
3707        let n = new_deps.len();
3708        let mut old_to_new: Vec<Option<usize>> = vec![None; old_deps.len()];
3709        let mut new_to_old: Vec<Option<usize>> = vec![None; n];
3710        if old_deps.len().saturating_mul(n) <= 64 {
3711            for (old_idx, old_dep) in old_deps.iter().enumerate() {
3712                for (new_idx, new_dep) in new_deps.iter().enumerate() {
3713                    if old_dep.ptr_eq(new_dep) && new_to_old[new_idx].is_none() {
3714                        old_to_new[old_idx] = Some(new_idx);
3715                        new_to_old[new_idx] = Some(old_idx);
3716                        break;
3717                    }
3718                }
3719            }
3720        } else {
3721            let mut first_old_by_key: HashMap<ArenaNodeKey, usize> =
3722                HashMap::with_capacity(old_deps.len());
3723            for (old_idx, old_dep) in old_deps.iter().enumerate() {
3724                first_old_by_key
3725                    .entry(old_dep.arena_node_key())
3726                    .or_insert(old_idx);
3727            }
3728            for (new_idx, new_dep) in new_deps.iter().enumerate() {
3729                if let Some(old_idx) = first_old_by_key.remove(&new_dep.arena_node_key()) {
3730                    old_to_new[old_idx] = Some(new_idx);
3731                    new_to_old[new_idx] = Some(old_idx);
3732                }
3733            }
3734        }
3735        let added: Vec<(usize, Core)> = new_deps
3736            .iter()
3737            .enumerate()
3738            .filter(|(idx, _)| new_to_old[*idx].is_none())
3739            .map(|(idx, dep)| (idx, dep.clone()))
3740            .collect();
3741        let removed: Vec<usize> = old_deps
3742            .iter()
3743            .enumerate()
3744            .filter(|(idx, _)| old_to_new[*idx].is_none())
3745            .map(|(idx, _)| idx)
3746            .collect();
3747
3748        self.with_inner_edges_mut(|_, e| {
3749            e.wave.in_dep_mutation = true;
3750            e.wave.rewire_run_pending = false;
3751        });
3752        // QA-F1 (critical): clear `in_dep_mutation` on EVERY exit incl. a panic unwind during
3753        // the mutation. A user fn CAN panic in this window — an added dep's activation fn
3754        // (subscribe_dep → dep.activate → run_wave) or a removed dep's onDeactivation hook
3755        // (u() → deactivate → user closure). The wave-owner catch (with_wave_owner) then runs
3756        // reset_wave_flags, which does NOT cover in_dep_mutation and may not even include `self`
3757        // in `touched` — so without this guard a caught rewire panic leaves the node permanently
3758        // in_dep_mutation=true: every future maybe_run defers (never recomputes) + every future
3759        // rewire is rejected as reentrant. Mirrors the TS `finally { _inDepMutation = false }`.
3760        let _mut_guard = DepMutationGuard(DeferredNodeAction::from_core(self));
3761        let activated = self.with_aux(|a| a.activated);
3762        let mut zero_dep_undirty = false;
3763
3764        // fn swap (SD-1 + B32 GC): register the new fn in the SAME pool, then unregister the
3765        // old handle so the rewired-away closure (and its captures) is freed and its slot
3766        // reused. Register-first keeps `handle` never pointing at a freed slot.
3767        self.swap_fn_preserving_pool(fn_);
3768
3769        // removed deps: clear dirty contribution + drain (box→-1, unsubscribe the edge).
3770        let mut removed_dirty_contributor = false;
3771        for old_idx in &removed {
3772            let old_idx = *old_idx;
3773            let unsub = {
3774                let mut graph = self.graph.borrow_mut();
3775                let (_nn, edges) = graph.get_node_and_edges_mut(self.key());
3776                if edges.state.dirty[old_idx] {
3777                    removed_dirty_contributor = true;
3778                    edges.state.pending -= 1;
3779                }
3780                if let Some(b) = edges.idx_boxes.get(old_idx) {
3781                    b.set(-1); // drain: any stale in-flight callback drops
3782                }
3783                edges.unsubs.get_mut(old_idx).and_then(|u| u.take())
3784            };
3785            if let Some(u) = unsub {
3786                u(); // stops the removed dep's edge — no further delivery
3787            }
3788        }
3789
3790        // rebuild the per-dep parallel arrays in new order; kept deps carry their state +
3791        // subscription + idx-box (rerouted to the new index), added deps start fresh.
3792        {
3793            let mut graph = self.graph.borrow_mut();
3794            let (nn, edges) = graph.get_node_and_edges_mut(self.key());
3795            let mut new_batch: Vec<Option<Vec<AnyValue>>> = vec![None; n];
3796            let mut new_batch_waves: Vec<Vec<Vec<WaveData>>> = vec![Vec::new(); n];
3797            let mut new_batch_wave_id: Vec<Option<u64>> = vec![None; n];
3798            let mut new_prev: Vec<Option<AnyValue>> = vec![None; n];
3799            let mut new_has = vec![false; n];
3800            let mut new_dirty = vec![false; n];
3801            let mut new_tier = vec![0u8; n];
3802            let mut new_terminal: Vec<Option<DepTerminal>> = vec![None; n];
3803            let mut new_terminal_wave: Vec<Option<DepTerminal>> = vec![None; n];
3804            let mut new_unsubs: Vec<Option<Unsub>> = (0..n).map(|_| None).collect();
3805            let mut new_boxes: Vec<Rc<Cell<i64>>> =
3806                (0..n).map(|_| Rc::new(Cell::new(-1i64))).collect();
3807            for (j, old_idx) in new_to_old.iter().enumerate() {
3808                if let Some(old_idx) = *old_idx {
3809                    new_batch[j] = edges.state.batch[old_idx].take();
3810                    new_batch_waves[j] = std::mem::take(&mut edges.state.batch_waves[old_idx]);
3811                    new_batch_wave_id[j] = edges.state.batch_wave_id[old_idx].take();
3812                    new_prev[j] = edges.state.prev[old_idx].clone();
3813                    new_has[j] = edges.state.has_data[old_idx];
3814                    new_dirty[j] = edges.state.dirty[old_idx];
3815                    new_tier[j] = edges.state.tier[old_idx];
3816                    new_terminal[j] = edges.state.terminal[old_idx].clone();
3817                    new_terminal_wave[j] = edges.state.terminal_wave[old_idx].clone();
3818                    if let Some(slot) = edges.unsubs.get_mut(old_idx) {
3819                        new_unsubs[j] = slot.take();
3820                    }
3821                    if let Some(b) = edges.idx_boxes.get(old_idx) {
3822                        b.set(j as i64); // O(1) reroute of the kept dep's callback index
3823                        new_boxes[j] = b.clone();
3824                    }
3825                }
3826            }
3827            nn.deps = new_deps.clone();
3828            edges.state.batch = new_batch;
3829            edges.state.batch_waves = new_batch_waves;
3830            edges.state.batch_wave_id = new_batch_wave_id;
3831            edges.state.prev = new_prev;
3832            edges.state.has_data = new_has;
3833            edges.state.dirty = new_dirty;
3834            edges.state.tier = new_tier;
3835            edges.state.terminal = new_terminal;
3836            edges.state.terminal_wave = new_terminal_wave;
3837            edges.unsubs = new_unsubs;
3838            edges.idx_boxes = new_boxes;
3839            edges.restored_activation_handshake = vec![false; n];
3840        }
3841
3842        // subscribe added deps — push-on-subscribe delivers a cached dep's DATA (driving
3843        // maybe_run, which DEFERS to the atomic settle via in_dep_mutation); a SENTINEL dep
3844        // delivers START only. Only when activated (else activation will subscribe later).
3845        if activated {
3846            for (idx, d) in &added {
3847                self.subscribe_dep(*idx, d);
3848            }
3849        }
3850        self.notify_topology_deps_changed(&old_deps, &new_deps);
3851
3852        // D62 / R-rewire-deferred: a terminal owner still drains queued topology intent,
3853        // but terminal-is-forever remains an output guard. Apply the dep-set/fn cleanup
3854        // above, then skip every post-mutation settle path so no post-terminal DIRTY,
3855        // DATA, RESOLVED, INVALIDATE, COMPLETE, or ERROR can escape.
3856        if self.with_inner_edges(|_, e| e.value.terminal) {
3857            return;
3858        }
3859
3860        // Q6 auto-settle: removing the sole dirty contributor closes the wave. With deps
3861        // remaining → request the atomic settle; with zero deps → just un-dirty downstream
3862        // (degenerate fn-no-deps). Cache is preserved either way (Q7).
3863        {
3864            let should_rewire_settle = self.with_inner_edges(|_n, e| {
3865                removed_dirty_contributor && e.state.pending == 0 && e.value.status == Status::Dirty
3866            });
3867            if should_rewire_settle {
3868                if new_deps.is_empty() {
3869                    zero_dep_undirty = true;
3870                } else {
3871                    self.with_inner_edges_mut(|_, e| {
3872                        e.wave.rewire_run_pending = true;
3873                    });
3874                }
3875            }
3876        }
3877
3878        self.with_inner_edges_mut(|_, e| {
3879            e.wave.in_dep_mutation = false;
3880        });
3881
3882        // Atomic post-mutation settle (a fresh wave): ONE two-phase DIRTY→DATA if an added
3883        // dep delivered data or a sole-dirty dep was removed; else the zero-dep un-dirty.
3884        let run_pending =
3885            self.with_inner_edges_mut(|_, e| std::mem::take(&mut e.wave.rewire_run_pending));
3886        if run_pending {
3887            self.settle_rewire();
3888        } else if zero_dep_undirty {
3889            let emitted = self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave);
3890            if emitted {
3891                self.down(vec![Message::Resolved]); // un-dirty downstream (pause/batch-safe)
3892            } else {
3893                self.with_inner_edges_mut(|_n, e| {
3894                    e.value.status = if e.value.has_data {
3895                        Status::Settled
3896                    } else {
3897                        Status::Sentinel
3898                    };
3899                });
3900            }
3901        }
3902    }
3903
3904    fn same_graph_no_overlap(old_deps: &[Core], new_deps: &[Core]) -> bool {
3905        let mut old_ids = HashSet::with_capacity(old_deps.len());
3906        for dep in old_deps {
3907            old_ids.insert(dep.arena_node_key());
3908        }
3909        !new_deps
3910            .iter()
3911            .any(|dep| old_ids.contains(&dep.arena_node_key()))
3912    }
3913
3914    /// R-rewire atomic settle (D42 + the D1 /qa fix): emit ONE proper two-phase
3915    /// DIRTY→DATA wave after a rewire that warrants a recompute. Mirrors `try_run`'s
3916    /// pause + gate + pending guards, then injects the phase-1 DIRTY the added dep's
3917    /// [START,DATA] handshake did not carry (R-dirty-before-data).
3918    fn settle_rewire(&self) {
3919        if self.with_config(|cfg| matches!(cfg.pausable, Pausable::True)) && self.is_paused() {
3920            self.with_aux_mut(|a| a.paused_dep_wave_occurred = true);
3921            return;
3922        }
3923        let (pending, has_handle, has_called, partial, all_settled) =
3924            self.with_node_state(|_n, c, cfg, r, e| {
3925                (
3926                    e.state.pending,
3927                    c.handle.is_some(),
3928                    r.has_called_fn_once,
3929                    cfg.partial,
3930                    cfg.all_deps_settled(&e.state),
3931                )
3932            });
3933        if pending > 0 {
3934            return;
3935        }
3936        if !has_handle {
3937            self.passthrough_emit();
3938            return;
3939        }
3940        if !(has_called || partial || all_settled) {
3941            return; // first-run gate still holds
3942        }
3943        self.mark_dirty(); // phase 1 (no-op if already dirty, e.g. unsubscribe_dep auto-settle)
3944        self.run_wave(); // phase 2: fn → DATA / undirty RESOLVED
3945    }
3946
3947    fn swap_fn_preserving_pool(&self, fn_: NodeFn) {
3948        let (old_handle, disp) = { self.with_call(|call| (call.handle, call.dispatcher.clone())) };
3949        let kind = old_handle
3950            .map(|h| disp.pool_kind(h.pool_id))
3951            .unwrap_or(PoolKind::Sync);
3952        let new_handle = register_with(&disp, kind, fn_);
3953        self.with_node_state_mut(|_n, call, _cfg, _r, _e| {
3954            call.handle = Some(new_handle);
3955        });
3956        if let Some(oh) = old_handle {
3957            disp.unregister(oh);
3958        }
3959    }
3960
3961    fn run_wave(&self) {
3962        // B25: enroll in the wave touched-set before any mutation so a panic-abort
3963        // can reset our flags.
3964        wave_register(self);
3965        if self.with_inner_edges(|_, e| e.wave.inside_run_wave) {
3966            // R-reentrancy (D37): a fn re-driving its own dep mid-wave is the
3967            // synchronous feedback cycle. Reject by panicking (the Rust analogue of a
3968            // value-level throw) — the wave-owner's catch_unwind converts it to
3969            // [[ERROR,e]] on a node ON the cycle (D30 / C-6, see `with_wave_owner`),
3970            // rather than recurse to a stack overflow.
3971            panic!(
3972                "synchronous feedback cycle: node fn re-entered its own wave (R-reentrancy / D37)"
3973            );
3974        }
3975        // Build ctx + call snapshot under one short arena borrow, then invoke
3976        // borrow-free through the dispatcher (R-dispatch-all / R-sync-core).
3977        let (handle, dispatcher, is_async, ctx, old_on_invalidate, old_on_deactivation) = self
3978            .with_node_state_aux_mut(|n, c, _cfg, r, e, a| {
3979                let dep_records: Vec<DepRecord> = (0..n.deps.len())
3980                    .map(|i| {
3981                        let latest = e.state.batch[i]
3982                            .as_ref()
3983                            .and_then(|b| b.last().cloned())
3984                            .or_else(|| e.state.prev[i].clone());
3985                        let wave_data = std::mem::take(&mut e.state.batch_waves[i]);
3986                        e.state.batch_wave_id[i] = None;
3987                        DepRecord {
3988                            wave_data,
3989                            prev_data: e.state.prev[i].clone(),
3990                            latest,
3991                            terminal: e.state.terminal_wave[i].take(),
3992                        }
3993                    })
3994                    .collect();
3995                r.has_called_fn_once = true;
3996                e.wave.inside_run_wave = true;
3997                e.wave.emitted_tier3_this_wave = false;
3998                (
3999                    c.handle.expect("run_wave ⇒ handle present"),
4000                    c.dispatcher.clone(),
4001                    c.handle
4002                        .map(|h| c.dispatcher.pool_kind(h.pool_id) == PoolKind::Async)
4003                        .unwrap_or(false),
4004                    Ctx::new(self.borrowed_view(), dep_records, a.active_pull.clone()),
4005                    std::mem::take(&mut a.on_invalidate),
4006                    std::mem::take(&mut a.on_deactivation),
4007                )
4008            });
4009        // Dropping user hooks may drop captured `Core`s. Do that after releasing the
4010        // graph-arena borrow, or the captured handle's `Drop` can re-enter `GraphCore`.
4011        drop(old_on_invalidate);
4012        drop(old_on_deactivation);
4013        // D30: this is the innermost node whose fn runs — blame it if the fn (or a
4014        // re-entry it triggers) panics, so the ERROR lands "nearest the throw" (C-6).
4015        wave_set_blamed(self);
4016        let _guard = WaveGuard(DeferredNodeAction::from_core(self));
4017        // borrow-free: the fn calls ctx.down → re-enters this node's _down.
4018        dispatcher.invoke(handle, &ctx);
4019        drop(_guard); // clears inside_run_wave
4020                      // R-resolved-undirty (D49): a node DIRTY'd downstream in phase 1
4021                      // that produced NO tier-3 value this run (filter-reject / no-emit
4022                      // fn) emits exactly one balancing RESOLVED to clear the downstream
4023                      // dirty — the substrate synthesizes it so the operator body stays
4024                      // clean (R-primary-api-clean).
4025                      // EXEMPT async-pool nodes: an async fn that returns WITHOUT emitting has DEFERRED
4026                      // its result (it emits later via a stashed DeferredCtx), NOT rejected — synthesizing
4027                      // an undirty RESOLVED here would prematurely settle a still-pending diamond leg
4028                      // (R-async-paused / C-4). The eventual deferred emit carries its own DIRTY balance.
4029        let undirty = self.with_inner_edges(|_n, e| {
4030            // EXEMPT a TERMINAL wave (the fn emitted COMPLETE/ERROR): the terminal IS the settle,
4031            // and R-terminal-settles-dirty releases the downstream dirty — synthesizing an undirty
4032            // RESOLVED here would overwrite the terminal status + emit a spurious post-terminal
4033            // RESOLVED (mirrors the TS `_terminal === undefined` guard, node.ts). Pre-existing
4034            // parity gap surfaced by C-11 (a fn emitting a bare COMPLETE).
4035            e.wave.emitted_dirty_this_wave
4036                && !e.wave.emitted_tier3_this_wave
4037                && !e.value.terminal
4038                && !is_async
4039        });
4040        if undirty {
4041            // Route the balancing RESOLVED through the normal delivery waist so
4042            // resumeAll and batch timing obey R-undirty-settle-timing.
4043            self.down(vec![Message::Resolved]);
4044        }
4045        let keep_dirty_for_deferred_undirty = undirty
4046            && self.with_inner_edges(|_n, e| {
4047                e.wave.batch_dirty_owed || e.wave.emitted_tier3_this_wave
4048            });
4049
4050        // Roll wave-local state forward after the settle path sees this wave's
4051        // dirty/tier flags. Subscriber callbacks above run borrow-free.
4052        self.with_inner_edges_mut(|_n, e| {
4053            for (i, b) in e.state.batch.iter_mut().enumerate() {
4054                if let Some(last) = b.as_ref().and_then(|batch| batch.last()).cloned() {
4055                    e.state.prev[i] = Some(last);
4056                }
4057                *b = None;
4058            }
4059            if !keep_dirty_for_deferred_undirty {
4060                e.wave.emitted_dirty_this_wave = false;
4061                e.wave.emitted_tier3_this_wave = false;
4062            }
4063        });
4064    }
4065
4066    // ── downstream emission (the unified waist) ──
4067
4068    /// Emit a wave toward sinks. Synthesizes the leading DIRTY for an external
4069    /// tier-3 emit (R-dirty-before-data), updates cache/status, and broadcasts.
4070    ///
4071    /// D49 (supersedes D15): every value-occurrence is emitted as **DATA** — the
4072    /// substrate does NOT substitute DATA→RESOLVED on value-equality and never
4073    /// inspects the per-wave DATA count for substitution (dedup is opt-in at the
4074    /// operator layer). A RESOLVED reaching here is an explicit operator escape
4075    /// hatch; the undirty RESOLVED is synthesized in [`Core::run_wave`].
4076    pub(crate) fn down(&self, msgs: Vec<Msg>) {
4077        // Terminal-is-forever (D17 / R-terminal): a terminated node emits nothing
4078        // further — including a self-emit DATA via `set()` / `ctx.down`. (The
4079        // COMPLETE/ERROR arms below also self-guard against a double terminal.)
4080        let (terminal, inside) =
4081            self.with_inner_edges(|_n, e| (e.value.terminal, e.wave.inside_run_wave));
4082        if terminal {
4083            return;
4084        }
4085        // B25: enroll in the wave touched-set before any mutation.
4086        wave_register(self);
4087        let mut sorted = msgs;
4088        sorted.sort_by_key(|m| m.tier().as_u8()); // stable: preserves intra-tier order
4089
4090        // R-invalidate-idempotent: collapse repeated INVALIDATE in one wave so the
4091        // cleanup hook + downstream broadcast fire at most once.
4092        if sorted
4093            .iter()
4094            .filter(|m| matches!(m, Message::Invalidate))
4095            .count()
4096            > 1
4097        {
4098            let mut seen = false;
4099            sorted.retain(|m| {
4100                if matches!(m, Message::Invalidate) {
4101                    let keep = !seen;
4102                    seen = true;
4103                    keep
4104                } else {
4105                    true
4106                }
4107            });
4108        }
4109
4110        let versioning_policy = self.versioning_policy();
4111        for m in &sorted {
4112            if let Message::Data(value) = m {
4113                assert_node_version_data_compatible(&versioning_policy, value)
4114                    .unwrap_or_else(|err| panic!("{err}"));
4115            }
4116        }
4117
4118        if !inside && collecting_batch() {
4119            let (deferred, rest): (Vec<Msg>, Vec<Msg>) = sorted
4120                .into_iter()
4121                .partition(|m| m.tier().is_batch_deferred());
4122            if !deferred.is_empty() && defer_to_batch(self, deferred) {
4123                if !self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave) {
4124                    self.emit_dirty_once();
4125                }
4126                self.with_inner_edges_mut(|_, e| {
4127                    e.wave.batch_dirty_owed = true;
4128                });
4129                return;
4130            }
4131            sorted = rest;
4132            if sorted.is_empty() {
4133                return;
4134            }
4135        }
4136
4137        // R-pause-modes / R-async-paused (D44): while paused, defer the tier-3/4 settle
4138        // slice (DATA/RESOLVED/INVALIDATE) into the pause buffer; tier<3 (DIRTY/PAUSE/
4139        // RESUME) and tier-5/6 (terminal/TEARDOWN) bypass so control + end-of-stream
4140        // always reach observers. Replayed in arrival order on final-lock RESUME
4141        // (on_resume). MUST run before the tier-3 counting + DIRTY synthesis below so a
4142        // buffered wave emits nothing while paused (matches the TS ordering).
4143        if self.should_buffer_on_pause() {
4144            let (buffered, rest): (Vec<Msg>, Vec<Msg>) = sorted
4145                .into_iter()
4146                .partition(|m| m.tier().is_pause_buffered());
4147            if !buffered.is_empty() {
4148                self.with_inner_edges_aux_mut(|_n, e, a| {
4149                    // A buffered tier-3 IS a produced settle — mark emitted_tier3 so run_wave
4150                    // does NOT synthesize a spurious undirty RESOLVED for a fn whose DATA was
4151                    // merely deferred into the buffer (per-language correctness, D24: TS sets
4152                    // this flag only in the post-buffer emit loop, leaving a resumeAll recompute
4153                    // prone to that spurious RESOLVED; recognizing the settle at buffer time is
4154                    // the more-correct reading of R-resolved-undirty).
4155                    e.wave.emitted_tier3_this_wave = true;
4156                    a.pause_buffer.push(buffered);
4157                });
4158            }
4159            sorted = rest;
4160            if sorted.is_empty() {
4161                return;
4162            }
4163        }
4164
4165        let mut data_count = 0usize;
4166        let mut has_resolved = false;
4167        let mut has_tier3 = false;
4168        for m in &sorted {
4169            match m {
4170                Message::Data(_) => {
4171                    data_count += 1;
4172                    has_tier3 = true;
4173                }
4174                Message::Resolved => {
4175                    has_resolved = true;
4176                    has_tier3 = true;
4177                }
4178                _ => {}
4179            }
4180        }
4181        // tier-3 exclusivity (R-resolved-undirty / D49): a wave's tier-3 slot is
4182        // ≥1 DATA XOR 1 RESOLVED (occurrence vs undirty), never mixed.
4183        assert!(
4184            !(data_count >= 1 && has_resolved),
4185            "down: a wave cannot mix DATA and RESOLVED (tier-3 exclusivity, R-resolved-undirty / D49)"
4186        );
4187
4188        // Synthesize a leading DIRTY for an EXTERNAL tier-3 emit, and for a PULL
4189        // demand fn only when it actually emits tier-3 (D269/D272).
4190        let pull_dirty_owed = self.with_aux(|a| a.pull_dirty_owed);
4191        if has_tier3 && (!inside || pull_dirty_owed) {
4192            self.emit_dirty_once();
4193        }
4194
4195        with_delivery_scope(|| {
4196            for m in sorted {
4197                if let Some(action) = self.collect_down_action(m) {
4198                    self.apply_down_action(action);
4199                }
4200            }
4201        });
4202
4203        if !inside {
4204            self.with_inner_edges_mut(|_, e| {
4205                e.wave.emitted_dirty_this_wave = false;
4206                e.wave.emitted_tier3_this_wave = false;
4207            });
4208        }
4209    }
4210
4211    fn collect_down_action(&self, msg: Msg) -> Option<DownAction> {
4212        let next_version = if let Message::Data(value) = &msg {
4213            let (policy, current) = {
4214                let graph = self.graph.borrow();
4215                let version = graph.get_version(self.key());
4216                (version.policy.clone(), version.value.clone())
4217            };
4218            Some(
4219                advance_node_version(current.as_ref(), &policy, value)
4220                    .unwrap_or_else(|err| panic!("{err}")),
4221            )
4222        } else {
4223            None
4224        };
4225        let mut g = self.graph.borrow_mut();
4226        Self::collect_down_action_from_graph(&mut g, self.key(), msg, next_version)
4227    }
4228
4229    fn apply_down_action(&self, action: DownAction) {
4230        match action {
4231            DownAction::Emit { msg, subs } => deliver_subscriber_snapshot(subs, &msg),
4232            DownAction::Invalidate { hooks } => {
4233                // Invalidate hooks may unsubscribe current observers. Preserve the
4234                // existing hook-before-subscriber-snapshot order (R-cleanup-hooks).
4235                for f in hooks {
4236                    f();
4237                }
4238                self.emit_to_subs(&Message::Invalidate);
4239            }
4240        }
4241    }
4242
4243    fn collect_down_action_from_graph(
4244        g: &mut GraphCore,
4245        key: NodeKey,
4246        msg: Msg,
4247        next_version: Option<Option<NodeVersion>>,
4248    ) -> Option<DownAction> {
4249        match msg {
4250            Message::Dirty => {
4251                let (_n, e, a) = g.get_node_edges_aux_mut(key);
4252                if e.wave.emitted_dirty_this_wave {
4253                    return None;
4254                }
4255                e.wave.emitted_dirty_this_wave = true;
4256                e.value.status = Status::Dirty;
4257                Some(DownAction::Emit {
4258                    msg: Message::Dirty,
4259                    subs: snapshot_subscribers(a),
4260                })
4261            }
4262            Message::Data(v) => {
4263                // D49: every occurrence is DATA — no equals-substitution.
4264                {
4265                    let version = g.get_version_mut(key);
4266                    version.value =
4267                        next_version.expect("DATA precomputed next node runtime version");
4268                }
4269                let (_n, e, a) = g.get_node_edges_aux_mut(key);
4270                e.value.cache = Some(v.clone());
4271                e.value.has_data = true;
4272                e.value.status = Status::Settled;
4273                e.wave.emitted_tier3_this_wave = true;
4274                Some(DownAction::Emit {
4275                    msg: Message::Data(v),
4276                    subs: snapshot_subscribers(a),
4277                })
4278            }
4279            Message::Resolved => {
4280                // Explicit operator escape hatch (D49); the substrate-synthesized
4281                // undirty RESOLVED usually goes through run_wave. Dirty-balance paths
4282                // that must honor pause/batch timing also route through here, so keep
4283                // the no-cache case at SENTINEL while still emitting RESOLVED.
4284                let (_n, e, a) = g.get_node_edges_aux_mut(key);
4285                e.value.status = if e.value.has_data {
4286                    Status::Resolved
4287                } else {
4288                    Status::Sentinel
4289                };
4290                e.wave.emitted_tier3_this_wave = true;
4291                Some(DownAction::Emit {
4292                    msg: Message::Resolved,
4293                    subs: snapshot_subscribers(a),
4294                })
4295            }
4296            Message::Invalidate => {
4297                // Honor the invalidate-request: clear cache → SENTINEL, fire
4298                // onInvalidate, broadcast downstream (idempotent, no-op if unpopulated).
4299                let (_n, e, a) = g.get_node_edges_aux_mut(key);
4300                if !e.value.has_data {
4301                    return None;
4302                }
4303                e.value.cache = None;
4304                e.value.has_data = false;
4305                e.value.status = Status::Sentinel;
4306                Some(DownAction::Invalidate {
4307                    hooks: a.on_invalidate.clone(),
4308                })
4309            }
4310            Message::Complete => {
4311                let (_n, e, a) = g.get_node_edges_aux_mut(key);
4312                if e.value.terminal {
4313                    return None;
4314                }
4315                e.value.terminal = true;
4316                e.value.status = Status::Completed;
4317                a.pull_demand_owed = None;
4318                a.active_pull = None;
4319                a.in_deliver_demand = false;
4320                a.pull_dirty_owed = false;
4321                a.paused_dep_wave_occurred = false;
4322                a.pause_buffer.clear();
4323                Some(DownAction::Emit {
4324                    msg: Message::Complete,
4325                    subs: snapshot_subscribers(a),
4326                })
4327            }
4328            Message::Error(e) => {
4329                let (_n, edges, a) = g.get_node_edges_aux_mut(key);
4330                if edges.value.terminal {
4331                    return None;
4332                }
4333                edges.value.terminal = true;
4334                edges.value.status = Status::Errored;
4335                a.pull_demand_owed = None;
4336                a.active_pull = None;
4337                a.in_deliver_demand = false;
4338                a.pull_dirty_owed = false;
4339                a.paused_dep_wave_occurred = false;
4340                a.pause_buffer.clear();
4341                Some(DownAction::Emit {
4342                    msg: Message::Error(e),
4343                    subs: snapshot_subscribers(a),
4344                })
4345            }
4346            Message::Teardown => Some(DownAction::Emit {
4347                msg: Message::Teardown,
4348                subs: snapshot_subscribers(g.get_aux(key)),
4349            }),
4350            // PAUSE / RESUME are control-only and are delivered via up().
4351            _ => None,
4352        }
4353    }
4354
4355    /// External deferred emit — the async-pool late-emit path used by
4356    /// [`crate::ctx::DeferredCtx`] (R-sync-core: async lives in the fn body, the emit
4357    /// serializes back onto the single thread). Establishes a FRESH wave-owner boundary
4358    /// (like [`Node::down`]) so a panic in the cascade becomes `[[ERROR,e]]` (D30) and the
4359    /// leading DIRTY is synthesized (`inside_run_wave` is false here). Nested under a live
4360    /// wave-owner (e.g. a deferred emit fired during a RESUME cascade) it just runs.
4361    pub(crate) fn owned_down(&self, msgs: Wave<AnyValue>) {
4362        with_wave_owner(self, || self.down(msgs), || {});
4363    }
4364
4365    pub(crate) fn owned_up(&self, msgs: Wave<AnyValue>, toward_dep: Option<usize>) {
4366        with_wave_owner(self, || self.up(msgs, toward_dep), || {});
4367    }
4368
4369    pub(crate) fn commit_batched_wave(&self, wave: Wave<AnyValue>) {
4370        self.with_inner_edges_mut(|_, e| {
4371            e.wave.batch_dirty_owed = false;
4372        });
4373        self.owned_down(wave);
4374    }
4375
4376    pub(crate) fn rollback_batched(&self) {
4377        let should_balance = self.with_inner_edges_mut(|_, e| {
4378            if e.wave.batch_dirty_owed {
4379                e.wave.batch_dirty_owed = false;
4380                true
4381            } else {
4382                false
4383            }
4384        });
4385        if should_balance {
4386            self.owned_down(vec![Message::Resolved]);
4387        }
4388    }
4389
4390    fn emit_dirty_once(&self) {
4391        let subs = self.with_inner_edges_aux_mut(|_n, e, a| {
4392            if !e.wave.emitted_dirty_this_wave {
4393                e.wave.emitted_dirty_this_wave = true;
4394                e.value.status = Status::Dirty;
4395                snapshot_subscribers(a)
4396            } else {
4397                SubscriberSnapshot::Empty
4398            }
4399        });
4400        deliver_subscriber_snapshot(subs, &Message::Dirty);
4401    }
4402
4403    fn emit_to_subs(&self, msg: &Msg) {
4404        // Clone the sink handles out, drop the borrow, then deliver — guards against
4405        // subscribe/unsubscribe (and any re-entry) during iteration. The 0/1-sink
4406        // cases avoid allocating a Vec while preserving the same snapshot boundary.
4407        let subs = self.with_aux(snapshot_subscribers);
4408        deliver_subscriber_snapshot(subs, msg);
4409    }
4410
4411    /// Emit upstream toward deps — control tiers only (R-ctx-up). PAUSE/RESUME act on
4412    /// this node's own lockset (R-pause-lockset). For the other up-allowed kinds
4413    /// (INVALIDATE/DIRTY/TEARDOWN) a node is either the TERMINUS (depless source) or a
4414    /// pass-through INTERMEDIATE (R-up-at-source / D38, C-7):
4415    /// - depless source = terminus: INVALIDATE → HONOR (route through `down` → clear
4416    ///   cache to SENTINEL, fire onInvalidate, broadcast INVALIDATE downstream); routing
4417    ///   through `down` (not a direct `invalidate()`) keeps it batch/pause-consistent with
4418    ///   a downstream-originated INVALIDATE. DIRTY/TEARDOWN → DROP (no coherent terminus
4419    ///   action: self-DIRTY would wedge downstream awaiting a settle that never comes;
4420    ///   source lifecycle is source/graph-owned).
4421    /// - dep-bearing intermediate: forward toward deps only, never self-act (the source
4422    ///   is the single actor, the down-cascade is the effect).
4423    pub(crate) fn up(&self, msgs: Vec<Msg>, toward_dep: Option<usize>) {
4424        let mut route = UpRouteState::new();
4425        self.up_route(msgs, toward_dep, &mut route);
4426    }
4427
4428    fn up_route(&self, msgs: Vec<Msg>, toward_dep: Option<usize>, route: &mut UpRouteState) {
4429        for m in &msgs {
4430            assert!(
4431                m.is_up_allowed(),
4432                "ctx.up: {m:?} is down-only; up carries control tiers only (R-ctx-up)"
4433            );
4434        }
4435        for m in msgs {
4436            self.up_msg(m, toward_dep, route);
4437        }
4438    }
4439
4440    fn up_msg(&self, m: Msg, toward_dep: Option<usize>, route: &mut UpRouteState) {
4441        let is_depless = self.borrow().deps.is_empty();
4442        match m {
4443            Message::Pause(lock) => self.pause_acquire(lock),
4444            Message::Resume(lock) => {
4445                if self.with_aux(|a| a.pause_lockset.contains(&lock)) {
4446                    self.pause_release(lock);
4447                } else {
4448                    self.forward_up(Message::Resume(lock), toward_dep, route);
4449                }
4450            }
4451            Message::Pull(demand) => {
4452                let is_pull_holder =
4453                    self.with_config(|cfg| cfg.pull_id.as_ref() == Some(&demand.pull_id));
4454                if is_pull_holder {
4455                    if !route.mark_demand(&demand.pull_id, self) {
4456                        self.on_demand(demand);
4457                    }
4458                } else {
4459                    self.forward_up(Message::Pull(demand), toward_dep, route);
4460                }
4461            }
4462            // R-up-at-source (D38): INVALIDATE/DIRTY/TEARDOWN.
4463            other if is_depless => {
4464                // terminus: honor INVALIDATE, drop DIRTY/TEARDOWN.
4465                if matches!(other, Message::Invalidate) {
4466                    self.down(vec![Message::Invalidate]);
4467                }
4468            }
4469            other => {
4470                // dep-bearing intermediate: forward toward deps (reconstruct the
4471                // payload-free control kind — Msg is not Clone, but only the unit
4472                // control kinds reach here per is_up_allowed minus PAUSE/RESUME).
4473                self.forward_up(other, toward_dep, route);
4474            }
4475        }
4476    }
4477
4478    fn forward_up(&self, m: Msg, toward_dep: Option<usize>, route: &mut UpRouteState) {
4479        // DR-8/B54: route control fanout over borrowed dep keys under wave safety.
4480        // In-wave and out-of-wave share the same path: resolve dep projections as
4481        // arena keys and only materialize a borrowed Core when the target is live and
4482        // can be re-entered this slice (counted or pinned).
4483        let deps = self.with_inner_edges(|n, _| {
4484            n.deps
4485                .iter()
4486                .map(|dep| (dep.id, dep.generation, dep.refs.clone()))
4487                .collect::<Vec<_>>()
4488        });
4489        let mut to_target = |target: &(NodeId, u64, Rc<Cell<usize>>), msg: Msg| {
4490            let (id, generation, refs) = target;
4491            let key = NodeKey {
4492                id: *id,
4493                generation: *generation,
4494            };
4495            let arena_key = ArenaNodeKey {
4496                graph: Rc::as_ptr(&self.graph) as usize,
4497                node: key,
4498            };
4499            {
4500                let mut graph = self.graph.borrow_mut();
4501                if !graph.is_live_key(key) || (refs.get() == 0 && graph.pin_count(key) == 0) {
4502                    return;
4503                }
4504                if !graph.pin_live_key(key) {
4505                    return;
4506                }
4507            }
4508            let pin = ArenaNodePin {
4509                arena_key,
4510                graph: self.graph.clone(),
4511                refs: refs.clone(),
4512            };
4513            let Some(dep_core) = pin.borrowed_core() else {
4514                return;
4515            };
4516            dep_core.up_msg(msg, None, route);
4517        };
4518        if let Some(i) = toward_dep {
4519            if let Some(dep) = deps.get(i) {
4520                to_target(dep, m);
4521            }
4522            return;
4523        }
4524        for dep in deps {
4525            to_target(&dep, dup_control(&m));
4526        }
4527    }
4528
4529    // ── PAUSE/RESUME lockset (R-pause-lockset) + default mode (R-pause-modes) ──
4530
4531    fn is_paused(&self) -> bool {
4532        !self.with_aux(|a| a.pause_lockset.is_empty())
4533    }
4534
4535    fn pause_acquire(&self, lock: LockId) {
4536        // Set ⇒ a same-id repeat PAUSE is idempotent.
4537        self.with_aux_mut(|a| {
4538            a.pause_lockset.insert(lock);
4539        });
4540    }
4541
4542    fn pause_release(&self, lock: LockId) {
4543        let Some(resumed) = self.with_aux_mut(|a| {
4544            if !a.pause_lockset.remove(&lock) {
4545                return None; // unknown id ⇒ no-op
4546            }
4547            Some(a.pause_lockset.is_empty()) // another lock still held ⇒ stay paused
4548        }) else {
4549            return;
4550        };
4551        if resumed && self.with_config(|cfg| cfg.pull_id.is_none()) {
4552            self.on_resume();
4553            if boundary_drains_blocked() {
4554                register_boundary_root(self);
4555            }
4556        } else if resumed && boundary_drains_blocked() {
4557            register_boundary_root(self);
4558        }
4559        self.fire_owed_demand_if_ready();
4560    }
4561
4562    fn can_fire_demand(&self) -> bool {
4563        self.with_node_state_aux_mut(|_n, c, cfg, r, e, a| {
4564            if cfg.pull_id.is_none()
4565                || e.value.terminal
4566                || e.state.pending != 0
4567                || !a.pause_lockset.is_empty()
4568            {
4569                return false;
4570            }
4571            if c.handle.is_some()
4572                && !r.has_called_fn_once
4573                && !cfg.partial
4574                && !cfg.all_deps_settled(&e.state)
4575            {
4576                return false;
4577            }
4578            true
4579        })
4580    }
4581
4582    fn on_demand(&self, demand: PullDemand) {
4583        let should_deliver = self.with_node_state_aux_mut(|_n, _c, cfg, _r, _e, a| {
4584            if cfg.pull_id.is_none() || a.in_deliver_demand {
4585                return false;
4586            }
4587            a.pull_demand_owed = Some(demand);
4588            true
4589        });
4590        if should_deliver {
4591            self.fire_owed_demand_if_ready();
4592        }
4593    }
4594
4595    fn fire_owed_demand_if_ready(&self) {
4596        if self.with_aux(|a| a.in_deliver_demand) || !self.can_fire_demand() {
4597            return;
4598        }
4599        let Some(demand) = self.with_aux_mut(|a| a.pull_demand_owed.take()) else {
4600            return;
4601        };
4602        self.deliver_pull_demand(demand);
4603    }
4604
4605    fn deliver_pull_demand(&self, demand: PullDemand) {
4606        self.with_aux_mut(|a| {
4607            a.active_pull = Some(demand);
4608            a.in_deliver_demand = true;
4609        });
4610        struct PullDelivery<'a> {
4611            core: &'a Core,
4612        }
4613        impl Drop for PullDelivery<'_> {
4614            fn drop(&mut self) {
4615                let _ = self
4616                    .core
4617                    .try_with_node_state_aux_mut(|_n, _c, _cfg, _r, _e, a| {
4618                        a.active_pull = None;
4619                        a.in_deliver_demand = false;
4620                        a.pull_dirty_owed = false;
4621                    });
4622            }
4623        }
4624        let _guard = PullDelivery { core: self };
4625        self.fire_pull_demand();
4626    }
4627
4628    fn fire_pull_demand(&self) {
4629        let (mut buf, pausable) = self.with_node_state_aux_mut(|_n, _c, cfg, _r, _e, a| {
4630            (std::mem::take(&mut a.pause_buffer), cfg.pausable)
4631        });
4632        if !buf.is_empty() {
4633            if matches!(pausable, Pausable::True) {
4634                if let Some(wave) = buf.pop() {
4635                    self.down(wave);
4636                }
4637            } else {
4638                for wave in buf {
4639                    self.down(wave);
4640                }
4641            }
4642            return;
4643        }
4644
4645        let should_run_paused_dep = self.with_aux(|a| a.paused_dep_wave_occurred);
4646        if should_run_paused_dep {
4647            let gated = self.with_node_state(|_n, c, cfg, r, e| {
4648                e.state.pending != 0
4649                    || (c.handle.is_some()
4650                        && !r.has_called_fn_once
4651                        && !cfg.partial
4652                        && !cfg.all_deps_settled(&e.state))
4653            });
4654            if gated {
4655                return;
4656            }
4657            self.with_node_state_aux_mut(|_n, _c, _cfg, _r, e, a| {
4658                a.paused_dep_wave_occurred = false;
4659                a.pull_dirty_owed = true;
4660                e.wave.emitted_dirty_this_wave = false;
4661            });
4662            self.try_run();
4663            return;
4664        }
4665
4666        let has_handle = self.with_call(|c| c.handle.is_some());
4667        if has_handle {
4668            self.with_inner_edges_aux_mut(|_n, e, a| {
4669                a.pull_dirty_owed = true;
4670                e.wave.emitted_dirty_this_wave = false;
4671            });
4672            self.try_run();
4673        }
4674    }
4675
4676    fn on_resume(&self) {
4677        // terminal-is-forever: a node that terminated while paused discards its buffer
4678        // and never replays/recomputes (BH3).
4679        if self.with_inner_edges(|_n, e| e.value.terminal) {
4680            self.with_aux_mut(|a| {
4681                a.pull_demand_owed = None;
4682                a.active_pull = None;
4683                a.in_deliver_demand = false;
4684                a.pull_dirty_owed = false;
4685                a.paused_dep_wave_occurred = false;
4686                a.pause_buffer.clear();
4687            });
4688            return;
4689        }
4690        // Drain buffered settle slices (resumeAll / async-at-paused, R-async-paused). The
4691        // lockset is already empty (pause_release removed the final lock before calling
4692        // us), so each replayed wave emits normally — DIRTY synth + DATA, no longer
4693        // buffered. Drain BEFORE the default-mode recompute (matches the TS arm order).
4694        let buf = { self.with_aux_mut(|a| std::mem::take(&mut a.pause_buffer)) };
4695        for wave in buf {
4696            self.down(wave);
4697        }
4698        // Default ("true") mode: a dep wave skipped while paused fires the fn once now,
4699        // with the latest dep values.
4700        let fire = { self.with_aux_mut(|a| std::mem::take(&mut a.paused_dep_wave_occurred)) };
4701        if fire {
4702            self.try_run();
4703        }
4704    }
4705
4706    /// Async-pool check that takes an existing borrow. The pool kind is a dispatcher
4707    /// property of the handle.
4708    fn call_is_async(c: &NodeCallSlot) -> bool {
4709        c.handle
4710            .map(|h| c.dispatcher.pool_kind(h.pool_id) == PoolKind::Async)
4711            .unwrap_or(false)
4712    }
4713
4714    /// Should an outgoing settle slice be deferred into the pause buffer? `pausable` mode
4715    /// is the OUTER gate over R-async-paused buffering (D44).
4716    fn should_buffer_on_pause(&self) -> bool {
4717        self.with_node_state(|n, c, cfg, _r, e| match cfg.pausable {
4718            // false: ignore PAUSE/RESUME ENTIRELY — never buffer, keep producing (B20).
4719            Pausable::False => false,
4720            _ if self.with_aux(|a| {
4721                a.pause_lockset.is_empty() && !(cfg.pull_id.is_some() && a.active_pull.is_none())
4722            }) =>
4723            {
4724                false
4725            }
4726            // resumeAll: production-gating — buffer the own (sync/async) settle slice too.
4727            Pausable::ResumeAll => true,
4728            // true (default): PAUSE gates recomputation/propagation, NOT a leaf source's
4729            // own production. An async COMPUTE node's (deps>0) in-flight result buffers
4730            // (C-2); a depless async leaf source delivers immediately (C-10). The
4731            // leaf-vs-compute discriminator is deps.is_empty().
4732            Pausable::True => {
4733                let pull_quiet =
4734                    cfg.pull_id.is_some() && self.with_aux(|a| a.active_pull.is_none());
4735                (pull_quiet && !e.wave.inside_run_wave && n.deps.is_empty())
4736                    || (!e.wave.inside_run_wave && Self::call_is_async(c) && !n.deps.is_empty())
4737            }
4738        })
4739    }
4740
4741    /// R-invalidate-idempotent: clear cache → SENTINEL, flush onInvalidate, broadcast
4742    /// INVALIDATE downstream. A no-op if nothing is cached (never-populated /
4743    /// already-reset are observationally equivalent — prevents double-cleanup).
4744    fn invalidate(&self) {
4745        let hooks = {
4746            self.with_inner_edges_aux_mut(|_n, e, a| {
4747                if !e.value.has_data {
4748                    return None; // idempotent no-op
4749                }
4750                e.value.cache = None;
4751                e.value.has_data = false;
4752                e.value.status = Status::Sentinel;
4753                Some(a.on_invalidate.clone()) // re-callable; clone out so we fire borrow-free
4754            })
4755        };
4756        let Some(hooks) = hooks else {
4757            return;
4758        };
4759        for f in hooks {
4760            f();
4761        }
4762        self.emit_to_subs(&Message::Invalidate);
4763    }
4764
4765    // ── ctx.state + hooks (called from Ctx) ──
4766
4767    pub(crate) fn get_state(&self) -> Option<AnyValue> {
4768        self.with_aux(|a| a.state.clone())
4769    }
4770    pub(crate) fn set_state(&self, v: AnyValue) {
4771        self.with_aux_mut(|a| a.state = Some(v));
4772    }
4773    pub(crate) fn set_state_persist(&self, on: bool) {
4774        self.with_aux_mut(|a| a.state_persist = on);
4775    }
4776    pub(crate) fn register_on_deactivation(&self, f: Box<dyn FnOnce()>) {
4777        self.with_aux_mut(|a| a.on_deactivation.push(f));
4778    }
4779    pub(crate) fn register_on_invalidate(&self, f: Rc<dyn Fn()>) {
4780        self.with_aux_mut(|a| a.on_invalidate.push(f));
4781    }
4782
4783    pub(crate) fn subscriber_count(&self) -> usize {
4784        self.with_aux(|a| a.subscribers.len())
4785    }
4786
4787    pub(crate) fn is_active(&self) -> bool {
4788        self.with_aux(|a| a.activated)
4789    }
4790}
4791
4792fn reset_wave_flags_inner(n: &mut NodeTopologySlot, e: &mut DepEdges) {
4793    e.wave.inside_run_wave = false;
4794    e.wave.emitted_dirty_this_wave = false;
4795    e.wave.emitted_tier3_this_wave = false;
4796    e.state.pending = 0;
4797    for i in 0..n.deps.len() {
4798        e.state.batch[i] = None;
4799        e.state.batch_waves[i].clear();
4800        e.state.batch_wave_id[i] = None;
4801        e.state.terminal_wave[i] = None;
4802        e.state.dirty[i] = false;
4803    }
4804    // `dep_terminal` is intentionally NOT reset here: it is a PERSISTED cross-wave fact
4805    // (the dep really did COMPLETE/ERROR — terminal-is-forever, D17), not a wave-transient
4806    // like `dep_dirty`/`dep_batch`. `deactivate`/`rewire_apply` refresh it on a fresh
4807    // lifecycle; a panic-aborted wave must leave a terminated dep marked terminal.
4808}
4809
4810/// Reconstruct a payload-free upstream control message for forwarding toward deps
4811/// (R-up-at-source / D38). [`Msg`] is not `Clone` (the `Error` variant carries a
4812/// non-clonable `GraphError`), but the only kinds that reach the intermediate-forward
4813/// path are the unit control kinds DIRTY/INVALIDATE/TEARDOWN (per `is_up_allowed`, minus
4814/// PAUSE/RESUME which self-handle), so a fresh copy is always constructible.
4815fn dup_control(m: &Msg) -> Msg {
4816    match m {
4817        Message::Pause(lock) => Message::Pause(lock.clone()),
4818        Message::Resume(lock) => Message::Resume(lock.clone()),
4819        Message::Pull(demand) => Message::Pull(demand.clone()),
4820        Message::Dirty => Message::Dirty,
4821        Message::Invalidate => Message::Invalidate,
4822        Message::Teardown => Message::Teardown,
4823        other => unreachable!("up forwards only control messages, got {other:?}"),
4824    }
4825}
4826
4827/// Is `target` reachable upstream from `from` (following deps)? The rewire cycle-
4828/// prevention DFS (R-rewire / D42). Node identity = (graph arena, node id).
4829fn reachable_upstream(from: &Core, target: &Core) -> bool {
4830    if !from.same_graph(target) {
4831        return false;
4832    }
4833    let target_key = target.key();
4834    let mut seen: HashSet<NodeKey> = HashSet::new();
4835    let mut stack: Vec<NodeKey> = vec![from.key()];
4836    let graph = from.graph.borrow();
4837    if !graph.is_live_key(target_key) {
4838        return false;
4839    }
4840    while let Some(key) = stack.pop() {
4841        if !seen.insert(key) {
4842            continue;
4843        }
4844        if !graph.is_live_key(key) {
4845            continue;
4846        }
4847        if key == target_key {
4848            return true;
4849        }
4850        let topology = graph.get(key);
4851        for dep in &topology.deps {
4852            stack.push(dep.key());
4853        }
4854    }
4855    false
4856}
4857
4858/// Dedup a dep list by node identity, preserving first-seen order — a dep
4859/// appearing twice in a rewire collapses to one (Option-C; matches the TS `_dedupDeps`).
4860fn dedup_cores(deps: Vec<Core>) -> Vec<Core> {
4861    let mut out: Vec<Core> = Vec::with_capacity(deps.len());
4862    let mut seen: HashSet<ArenaNodeKey> = HashSet::with_capacity(deps.len());
4863    for d in deps {
4864        if seen.insert(d.arena_node_key()) {
4865            out.push(d);
4866        }
4867    }
4868    out
4869}
4870
4871fn same_ordered_deps(old_deps: &[Core], new_deps: &[Core]) -> bool {
4872    old_deps.len() == new_deps.len()
4873        && old_deps
4874            .iter()
4875            .zip(new_deps)
4876            .all(|(old_dep, new_dep)| old_dep.ptr_eq(new_dep))
4877}
4878
4879/// Register a fn into the pool selected by `pool` (R-dispatch-all). The async pool's
4880/// invoke is still sync void (R-sync-core); the pool kind only labels the node's
4881/// deferred-emit / pause-buffering behavior.
4882fn register_with(disp: &Dispatcher, pool: PoolKind, f: NodeFn) -> Handle {
4883    match pool {
4884        PoolKind::Sync => disp.register(f),
4885        PoolKind::Async => disp.register_async(f),
4886    }
4887}
4888
4889/// The typed facade over the erased substrate node (D5). Re-types the boundary:
4890/// `cache()`/`set()` downcast to `T`; deps are erased via [`Node::erased`].
4891///
4892/// No `PartialEq` bound (D49: the substrate does no value-equality — dedup is
4893/// opt-in at the operator layer, e.g. `distinctUntilChanged`).
4894pub struct Node<T> {
4895    core: Core,
4896    _t: PhantomData<T>,
4897}
4898
4899impl<T: 'static> Node<T> {
4900    /// A state node pre-populated with `initial`; a new subscriber gets `[DATA]`
4901    /// (R-initial). Manual source — push new values with [`Node::set`].
4902    pub fn state(initial: T) -> Node<T> {
4903        Self::state_in_arena(&GraphArena::default(), initial)
4904    }
4905
4906    pub(crate) fn state_in_arena(arena: &GraphArena, initial: T) -> Node<T> {
4907        Self::state_in_arena_with_dispatcher(arena, default_dispatcher(), initial)
4908    }
4909
4910    pub(crate) fn state_in_arena_with_dispatcher(
4911        arena: &GraphArena,
4912        dispatcher: Dispatcher,
4913        initial: T,
4914    ) -> Node<T> {
4915        Self::state_opts_in_arena_with_dispatcher(arena, dispatcher, initial, NodeOpts::default())
4916    }
4917
4918    pub(crate) fn state_opts_in_arena_with_dispatcher(
4919        arena: &GraphArena,
4920        dispatcher: Dispatcher,
4921        initial: T,
4922        opts: NodeOpts,
4923    ) -> Node<T> {
4924        Node {
4925            core: Core::new_in_arena(
4926                arena,
4927                vec![],
4928                None,
4929                dispatcher,
4930                Some(Rc::new(initial)),
4931                opts,
4932            ),
4933            _t: PhantomData,
4934        }
4935    }
4936
4937    /// A SENTINEL state node — no value until the first [`Node::set`].
4938    pub fn state_empty() -> Node<T> {
4939        Self::state_empty_in_arena(&GraphArena::default())
4940    }
4941
4942    pub(crate) fn state_empty_in_arena(arena: &GraphArena) -> Node<T> {
4943        Self::state_empty_in_arena_with_dispatcher(arena, default_dispatcher())
4944    }
4945
4946    pub(crate) fn state_empty_in_arena_with_dispatcher(
4947        arena: &GraphArena,
4948        dispatcher: Dispatcher,
4949    ) -> Node<T> {
4950        Self::state_empty_opts_in_arena_with_dispatcher(arena, dispatcher, NodeOpts::default())
4951    }
4952
4953    pub(crate) fn state_empty_opts_in_arena_with_dispatcher(
4954        arena: &GraphArena,
4955        dispatcher: Dispatcher,
4956        opts: NodeOpts,
4957    ) -> Node<T> {
4958        Node {
4959            core: Core::new_in_arena(arena, vec![], None, dispatcher, None, opts),
4960            _t: PhantomData,
4961        }
4962    }
4963
4964    /// A producer node (fn, no deps): runs once on activation, emits via `ctx.emit`.
4965    pub fn producer<F: Fn(&Ctx) + 'static>(f: F) -> Node<T> {
4966        Self::producer_opts(NodeOpts::default(), f)
4967    }
4968
4969    /// A producer on the LocalAsync pool (D20): the fn runs once on activation and may
4970    /// DEFER its emission — stash `ctx.defer()` and emit later via the [`DeferredCtx`]
4971    /// (the async source pattern, R-sync-core / R-no-raw-async). Default pause mode
4972    /// (`true`); a depless leaf source's own production is delivered immediately even
4973    /// while paused (R-pause-modes / C-10).
4974    ///
4975    /// [`DeferredCtx`]: crate::ctx::DeferredCtx
4976    pub fn producer_async<F: Fn(&Ctx) + 'static>(f: F) -> Node<T> {
4977        Self::producer_opts(
4978            NodeOpts {
4979                factory: None,
4980                pool: PoolKind::Async,
4981                pausable: Pausable::True,
4982                ..NodeOpts::default()
4983            },
4984            f,
4985        )
4986    }
4987
4988    /// A producer with explicit [`NodeOpts`] (pool + pause mode).
4989    pub fn producer_opts<F: Fn(&Ctx) + 'static>(opts: NodeOpts, f: F) -> Node<T> {
4990        Self::producer_opts_in_arena(&GraphArena::default(), opts, f)
4991    }
4992
4993    pub(crate) fn producer_opts_in_arena<F: Fn(&Ctx) + 'static>(
4994        arena: &GraphArena,
4995        opts: NodeOpts,
4996        f: F,
4997    ) -> Node<T> {
4998        Self::producer_opts_in_arena_with_dispatcher(arena, default_dispatcher(), opts, f)
4999    }
5000
5001    pub(crate) fn producer_opts_in_arena_with_dispatcher<F: Fn(&Ctx) + 'static>(
5002        arena: &GraphArena,
5003        dispatcher: Dispatcher,
5004        opts: NodeOpts,
5005        f: F,
5006    ) -> Node<T> {
5007        assert!(
5008            !(opts.pull_id.is_some() && matches!(opts.pausable, Pausable::False)),
5009            "pullId + pausable:false is invalid (R-pull / R-pause-modes)"
5010        );
5011        let factory = opts.factory.clone();
5012        let handle = register_with(&dispatcher, opts.pool, Rc::new(f));
5013        let node = Node {
5014            core: Core::new_in_arena(arena, vec![], Some(handle), dispatcher, None, opts.clone()),
5015            _t: PhantomData,
5016        };
5017        node.core.with_node_state_mut(|_n, call, _cfg, _r, _e| {
5018            call.factory = factory;
5019        });
5020        node.core.configure_pull(opts.pull_id);
5021        node
5022    }
5023
5024    /// A derived node over erased deps. The fn reads deps positionally via
5025    /// `ctx.data::<U>(i)` and emits via `ctx.emit`. The first-run gate holds the fn
5026    /// until every dep has settled (R-first-run-gate). A fn that returns WITHOUT
5027    /// emitting (filter-reject) makes the substrate synthesize an undirty RESOLVED
5028    /// (D49 / R-resolved-undirty).
5029    pub fn derived<F: Fn(&Ctx) + 'static>(deps: Vec<Core>, f: F) -> Node<T> {
5030        Self::derived_opts(deps, NodeOpts::default(), f)
5031    }
5032
5033    /// A derived node on the LocalAsync pool (D20): the fn may DEFER its emission (stash
5034    /// `ctx.defer()`, emit later). An async COMPUTE node (deps>0) that returns without
5035    /// emitting has DEFERRED (not rejected) — no undirty RESOLVED is synthesized, and its
5036    /// in-flight result buffers if the node is paused (R-async-paused / C-2/C-4). Default
5037    /// pause mode (`true`).
5038    pub fn derived_async<F: Fn(&Ctx) + 'static>(deps: Vec<Core>, f: F) -> Node<T> {
5039        Self::derived_opts(
5040            deps,
5041            NodeOpts {
5042                factory: None,
5043                pool: PoolKind::Async,
5044                pausable: Pausable::True,
5045                ..NodeOpts::default()
5046            },
5047            f,
5048        )
5049    }
5050
5051    /// A derived node with explicit [`NodeOpts`] (pool + pause mode + dep-terminal policy).
5052    pub fn derived_opts<F: Fn(&Ctx) + 'static>(deps: Vec<Core>, opts: NodeOpts, f: F) -> Node<T> {
5053        Self::derived_opts_in_arena(&GraphArena::default(), deps, opts, f)
5054    }
5055
5056    pub(crate) fn derived_opts_in_arena<F: Fn(&Ctx) + 'static>(
5057        arena: &GraphArena,
5058        deps: Vec<Core>,
5059        opts: NodeOpts,
5060        f: F,
5061    ) -> Node<T> {
5062        Self::derived_opts_in_arena_with_dispatcher(arena, default_dispatcher(), deps, opts, f)
5063    }
5064
5065    pub(crate) fn derived_opts_in_arena_with_dispatcher<F: Fn(&Ctx) + 'static>(
5066        arena: &GraphArena,
5067        dispatcher: Dispatcher,
5068        deps: Vec<Core>,
5069        opts: NodeOpts,
5070        f: F,
5071    ) -> Node<T> {
5072        Self::derived_opts_initial_in_arena_with_dispatcher(arena, dispatcher, deps, opts, None, f)
5073    }
5074
5075    pub(crate) fn derived_opts_initial_in_arena_with_dispatcher<F: Fn(&Ctx) + 'static>(
5076        arena: &GraphArena,
5077        dispatcher: Dispatcher,
5078        deps: Vec<Core>,
5079        opts: NodeOpts,
5080        initial: Option<T>,
5081        f: F,
5082    ) -> Node<T> {
5083        assert!(
5084            !(opts.pull_id.is_some() && matches!(opts.pausable, Pausable::False)),
5085            "pullId + pausable:false is invalid (R-pull / R-pause-modes)"
5086        );
5087        let factory = opts.factory.clone();
5088        let handle = register_with(&dispatcher, opts.pool, Rc::new(f));
5089        let initial = initial.map(|value| Rc::new(value) as AnyValue);
5090        let node = Node {
5091            core: Core::new_in_arena(arena, deps, Some(handle), dispatcher, initial, opts.clone()),
5092            _t: PhantomData,
5093        };
5094        {
5095            // Thread the dep-terminal propagation policy (R-deps-terminal) into the inner —
5096            // `Core::new` defaults to the plain derived behavior (auto-cascade, not an input).
5097            node.core.with_node_state_mut(|_n, call, cfg, _r, _e| {
5098                cfg.partial = opts.partial;
5099                cfg.complete_when_deps_complete = opts.complete_when_deps_complete;
5100                cfg.error_when_deps_error = opts.error_when_deps_error;
5101                cfg.terminal_as_real_input = opts.terminal_as_real_input;
5102                cfg.versioning = opts.versioning;
5103                call.factory = factory;
5104            });
5105        }
5106        node.core.configure_pull(opts.pull_id);
5107        node
5108    }
5109
5110    /// Push a new value from a state node (one DATA wave). Always emits DATA — the
5111    /// substrate never absorbs to RESOLVED on value-equality (D49).
5112    pub fn set(&self, v: T) {
5113        self.down(vec![Message::Data(Rc::new(v))]);
5114    }
5115
5116    /// Emit a raw wave downstream (R-node-iface). The wave-owner boundary for the D30
5117    /// catch (a panicking fn the cascade triggers becomes `[[ERROR,e]]`, not an escape).
5118    pub fn down(&self, msgs: Wave<AnyValue>) {
5119        with_wave_owner(&self.core, || self.core.down(msgs), || {});
5120    }
5121
5122    /// Emit a raw control wave upstream — control tiers only (R-ctx-up / R-node-iface).
5123    /// PAUSE/RESUME act on this node's lockset (R-pause-lockset).
5124    pub fn up(&self, msgs: Wave<AnyValue>) {
5125        with_wave_owner(&self.core, || self.core.up(msgs, None), || {});
5126    }
5127
5128    /// Directed upstream control along one declared dep edge (R-up-routing).
5129    pub fn up_toward(&self, toward_dep: usize, msgs: Wave<AnyValue>) {
5130        with_wave_owner(&self.core, || self.core.up(msgs, Some(toward_dep)), || {});
5131    }
5132
5133    /// The current cached value (downcast), or `None` for SENTINEL.
5134    pub fn cache(&self) -> Option<T>
5135    where
5136        T: Clone,
5137    {
5138        self.core
5139            .with_inner_edges(|_n, e| e.value.cache.clone())
5140            .as_ref()
5141            .and_then(|a| a.downcast_ref::<T>().cloned())
5142    }
5143
5144    /// The node's lifecycle status (R-status-enum).
5145    pub fn status(&self) -> Status {
5146        self.core.with_inner_edges(|_n, e| e.value.status)
5147    }
5148
5149    /// Read-only node runtime version metadata (D109).
5150    pub fn version(&self) -> Option<NodeVersion> {
5151        self.core.version()
5152    }
5153
5154    /// Updates or reads `pull_id`.
5155    pub fn pull_id(&self) -> Option<LockId> {
5156        self.core.with_config(|cfg| cfg.pull_id.clone())
5157    }
5158
5159    /// Subscribe a sink; returns an unsubscribe handle (call it to detach; the node
5160    /// deactivates when the last subscriber leaves, R-rom-ram).
5161    ///
5162    /// This is also a wave-owner boundary: the activation cascade it drives runs under
5163    /// the D30 catch, so a synchronous feedback cycle surfaces as `[[ERROR,e]]` on a
5164    /// cycle node instead of escaping the subscribe call (C-6). Even on that error path
5165    /// the returned handle is a REAL unsubscribe (the sink is registered before the
5166    /// panicking activation, so the caller can always detach it).
5167    ///
5168    /// Panics (R-terminal / D17): subscribing to a non-resubscribable **terminal** node
5169    /// is rejected — the stream is permanently over. (Resubscribable opt-in reset is a
5170    /// later slice.)
5171    pub fn subscribe(&self, sink: impl Fn(&Msg) + 'static) -> Unsub {
5172        self.subscribe_with_kind(sink, SubscriberKind::External)
5173    }
5174
5175    pub(crate) fn subscribe_graph_observer(&self, sink: impl Fn(&Msg) + 'static) -> Unsub {
5176        self.subscribe_with_kind(sink, SubscriberKind::GraphObserver)
5177    }
5178
5179    fn subscribe_with_kind(&self, sink: impl Fn(&Msg) + 'static, kind: SubscriberKind) -> Unsub {
5180        assert!(
5181            !self.core.with_inner_edges(|_n, e| e.value.terminal),
5182            "subscribe: node is terminal and non-resubscribable — the stream is permanently over (R-terminal / D17)"
5183        );
5184        let sink = Rc::new(sink);
5185        // Record the subscriber id before activation so the error path hands back a real
5186        // unsubscribe instead of leaking the just-registered sink (no orphaned sink).
5187        let id_cell: Rc<Cell<Option<u64>>> = Rc::new(Cell::new(None));
5188        let body_cell = id_cell.clone();
5189        let abort_cell = id_cell.clone();
5190        let result = catch_unwind(AssertUnwindSafe(|| {
5191            with_wave_owner(
5192                &self.core,
5193                || {
5194                    self.core
5195                        .subscribe_recording_id_with_kind(sink, kind, &body_cell)
5196                },
5197                move || match id_cell.get() {
5198                    Some(id) => {
5199                        let err_core = self.core.clone();
5200                        Box::new(move || err_core.unsubscribe(id)) as Unsub
5201                    }
5202                    None => Box::new(|| {}) as Unsub,
5203                },
5204            )
5205        }));
5206        match result {
5207            Ok(unsub) => unsub,
5208            Err(payload) => {
5209                if is_host_boundary_abort_payload(payload.as_ref()) {
5210                    if let Some(id) = abort_cell.get() {
5211                        self.core.unsubscribe(id);
5212                    }
5213                }
5214                resume_unwind(payload);
5215            }
5216        }
5217    }
5218
5219    /// R-rewire (D42): replace this node's deps atomically (surgical Option-C). Kept deps
5220    /// keep their subscription + per-dep state; only removed deps unsubscribe + drain, only
5221    /// added deps fresh-subscribe (push-on-subscribe for an added cached dep). The first-run
5222    /// gate + cache are PRESERVED. Requires an explicit fn (SD-1 fn-deps pairing — user fns
5223    /// read deps positionally). INTRA-graph only (D22). Deps are erased ([`Core`]).
5224    ///
5225    /// Rejects (R-rewire): self-dep, a cycle, a terminal `self`, a (non-resubscribable)
5226    /// terminal added dep, a reentrant rewire — these panic (propagate to the caller). A
5227    /// mid-fn rewire (during this node's own fn run) is the D37 feedback cycle → caught by
5228    /// the running wave-owner as `[[ERROR,e]]`, not a propagated panic.
5229    pub fn replace_deps<F: Fn(&Ctx) + 'static>(&self, new_deps: Vec<Core>, f: F) {
5230        self.core.rewire(new_deps, Rc::new(f));
5231    }
5232
5233    /// Subscribe to one dep (special case of [`Node::replace_deps`]); returns its index. fn required (SD-1).
5234    pub fn subscribe_dep<F: Fn(&Ctx) + 'static>(&self, dep: Core, f: F) -> usize {
5235        let mut next = self.core.project_pending_external_rewire_deps();
5236        if !next.iter().any(|d| d.ptr_eq(&dep)) {
5237            next.push(dep.clone());
5238        }
5239        let index = next
5240            .iter()
5241            .position(|d| d.ptr_eq(&dep))
5242            .expect("subscribe_dep next shape contains dep");
5243        self.core
5244            .external_rewire(RewireRequest::Add(dep, Rc::new(f)));
5245        index
5246    }
5247
5248    /// Unsubscribe from one dep (special case of [`Node::replace_deps`]); idempotent if absent (the fn
5249    /// swap still applies). fn required (SD-1).
5250    pub fn unsubscribe_dep<F: Fn(&Ctx) + 'static>(&self, dep: Core, f: F) {
5251        self.core
5252            .external_rewire(RewireRequest::remove(&dep, Rc::new(f)));
5253    }
5254
5255    /// The erased core, for wiring this node as a dep of a `derived` node.
5256    pub fn erased(&self) -> Core {
5257        self.core.clone()
5258    }
5259
5260    pub(crate) fn from_core(core: Core) -> Node<T> {
5261        Node {
5262            core,
5263            _t: PhantomData,
5264        }
5265    }
5266}
5267
5268/// A cheap handle clone — both `Node`s address the SAME underlying node (the shared
5269/// `Rc<RefCell<…>>`), like cloning a [`Core`]. Manual (not `#[derive]`) so it does
5270/// NOT require `T: Clone` — the value type need not be cloneable for the handle to be.
5271impl<T> Clone for Node<T> {
5272    fn clone(&self) -> Self {
5273        Node {
5274            core: self.core.clone(),
5275            _t: PhantomData,
5276        }
5277    }
5278}
5279
5280#[cfg(test)]
5281mod tests {
5282    use super::*;
5283    use std::cell::Cell;
5284
5285    use serde_json::json;
5286
5287    /// Collect a subscriber's message-kind tags into a shared Vec for shape asserts.
5288    fn recorder() -> (Rc<RefCell<Vec<String>>>, impl Fn(&Msg) + 'static) {
5289        let log = Rc::new(RefCell::new(Vec::new()));
5290        let l2 = log.clone();
5291        (log, move |m: &Msg| l2.borrow_mut().push(format!("{m:?}")))
5292    }
5293
5294    #[test]
5295    fn status_freshness_and_terminality() {
5296        assert!(Status::Settled.is_fresh());
5297        assert!(Status::Resolved.is_fresh());
5298        assert!(!Status::Dirty.is_fresh());
5299        assert!(Status::Completed.is_terminal());
5300        assert!(!Status::Settled.is_terminal());
5301    }
5302
5303    #[test]
5304    fn state_node_push_on_subscribe_and_set() {
5305        let s = Node::<i32>::state(1);
5306        let (log, sink) = recorder();
5307        let _u = s.subscribe(sink);
5308        // push-on-subscribe: START then cached DATA (R-push-subscribe).
5309        assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5310        assert_eq!(s.cache(), Some(1));
5311
5312        s.set(2);
5313        // external tier-3 emit synthesizes a leading DIRTY (R-dirty-before-data).
5314        assert_eq!(*log.borrow(), vec!["START", "DATA", "DIRTY", "DATA"]);
5315        assert_eq!(s.cache(), Some(2));
5316        assert_eq!(s.status(), Status::Settled);
5317    }
5318
5319    #[test]
5320    fn batch_commits_last_settle_and_rollback_balances_dirty() {
5321        let s = Node::<i32>::state(0);
5322        let (log, sink) = recorder();
5323        let _u = s.subscribe(sink);
5324        log.borrow_mut().clear();
5325
5326        crate::batch::batch(|_| {
5327            s.set(1);
5328            s.set(2);
5329            assert_eq!(*log.borrow(), vec!["DIRTY"]);
5330            assert_eq!(s.cache(), Some(0));
5331        });
5332        assert_eq!(*log.borrow(), vec!["DIRTY", "DATA"]);
5333        assert_eq!(s.cache(), Some(2));
5334
5335        log.borrow_mut().clear();
5336        crate::batch::batch(|bctx| {
5337            s.set(3);
5338            bctx.rollback();
5339            assert_eq!(*log.borrow(), vec!["DIRTY"]);
5340        });
5341        assert_eq!(*log.borrow(), vec!["DIRTY", "RESOLVED"]);
5342        assert_eq!(s.cache(), Some(2));
5343    }
5344
5345    #[test]
5346    fn batch_rollback_resolved_respects_resumeall_pause() {
5347        let n = Node::<i32>::producer_opts(
5348            NodeOpts {
5349                pausable: Pausable::ResumeAll,
5350                ..NodeOpts::default()
5351            },
5352            |_| {},
5353        );
5354        let (log, sink) = recorder();
5355        let _u = n.subscribe(sink);
5356        log.borrow_mut().clear();
5357
5358        let pause = LockId::new("rollback");
5359        n.up(vec![Message::Pause(pause.clone())]);
5360        crate::batch::batch(|bctx| {
5361            n.down(vec![Message::Data(Rc::new(1i32))]);
5362            bctx.rollback();
5363            assert_eq!(*log.borrow(), vec!["DIRTY"]);
5364        });
5365
5366        assert_eq!(*log.borrow(), vec!["DIRTY"]);
5367        assert_eq!(n.cache(), None);
5368        n.up(vec![Message::Resume(pause)]);
5369        assert_eq!(*log.borrow(), vec!["DIRTY", "RESOLVED"]);
5370        assert_eq!(n.status(), Status::Sentinel);
5371    }
5372
5373    #[test]
5374    fn d49_repeated_equal_value_stays_data() {
5375        // D49 (supersedes D15): every occurrence is DATA — NO equals-substitution.
5376        // Probe: fromIter([1,1,1]) must be [DATA,DATA,DATA] so take/scan/count work.
5377        let s = Node::<i32>::state(5);
5378        let (log, sink) = recorder();
5379        let _u = s.subscribe(sink);
5380        s.set(5); // unchanged value → still DATA (never absorbed to RESOLVED)
5381        assert_eq!(*log.borrow(), vec!["START", "DATA", "DIRTY", "DATA"]);
5382        assert_eq!(s.status(), Status::Settled);
5383        s.set(5); // again → still DATA
5384        assert_eq!(
5385            *log.borrow(),
5386            vec!["START", "DATA", "DIRTY", "DATA", "DIRTY", "DATA"]
5387        );
5388        assert_eq!(s.status(), Status::Settled);
5389    }
5390
5391    #[test]
5392    fn d49_filter_no_emit_synthesizes_undirty_resolved() {
5393        // R-resolved-undirty (D49): a derived fn DIRTY'd in phase 1 that returns
5394        // WITHOUT emitting (filter-reject) → the substrate synthesizes exactly one
5395        // RESOLVED to clear the downstream dirty (no wedge).
5396        let a = Node::<i32>::state_empty();
5397        let evens: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5398            let v = *ctx.data::<i32>(0).unwrap();
5399            if v % 2 == 0 {
5400                ctx.emit(v);
5401            } // odd → emit nothing (filter-reject)
5402        });
5403        let (log, sink) = recorder();
5404        let seen_refs = Rc::new(RefCell::new(Vec::<usize>::new()));
5405        let evens_refs = evens.core.refs.clone();
5406        let seen_ref_count = seen_refs.clone();
5407        let _u = evens.subscribe({
5408            move |m| {
5409                if matches!(m, Message::Resolved) {
5410                    seen_ref_count.borrow_mut().push(evens_refs.get());
5411                }
5412                sink(m)
5413            }
5414        });
5415        let base_refs = evens.core.refs.get();
5416        assert_eq!(*log.borrow(), vec!["START"]); // first-run gate holds (a SENTINEL)
5417
5418        a.set(3); // odd → fn runs, no emit → substrate undirties with one RESOLVED
5419        assert_eq!(*log.borrow(), vec!["START", "DIRTY", "RESOLVED"]);
5420        assert_eq!(evens.cache(), None); // filtered → never valued
5421        assert_eq!(evens.status(), Status::Sentinel);
5422        assert!(
5423            seen_refs.borrow().iter().all(|count| *count == base_refs),
5424            "post-run RESOLVED must not promote a counted owner Core"
5425        );
5426        let (prev, batch) = evens.core.with_inner_edges(|_n, e| {
5427            (
5428                e.state.prev[0]
5429                    .as_ref()
5430                    .and_then(|v| v.downcast_ref::<i32>().copied()),
5431                e.state.batch[0].is_none(),
5432            )
5433        });
5434        assert_eq!(prev, Some(3));
5435        assert!(batch);
5436
5437        a.set(4); // even → real DATA (downstream was NOT wedged by the prior RESOLVED)
5438        assert_eq!(
5439            *log.borrow(),
5440            vec!["START", "DIRTY", "RESOLVED", "DIRTY", "DATA"]
5441        );
5442        assert_eq!(evens.cache(), Some(4));
5443        assert_eq!(evens.status(), Status::Settled);
5444        let prev = evens.core.with_inner_edges(|_n, e| {
5445            e.state.prev[0]
5446                .as_ref()
5447                .and_then(|v| v.downcast_ref::<i32>().copied())
5448        });
5449        assert_eq!(prev, Some(4));
5450
5451        a.set(5); // odd → filter; node KEEPS its value 4, status → Resolved (no new occurrence)
5452        assert_eq!(
5453            *log.borrow(),
5454            vec!["START", "DIRTY", "RESOLVED", "DIRTY", "DATA", "DIRTY", "RESOLVED"]
5455        );
5456        assert_eq!(evens.cache(), Some(4));
5457        assert_eq!(evens.status(), Status::Resolved);
5458        let (seen_refs, prev_batch) = evens.core.with_inner_edges(|_n, e| {
5459            (
5460                seen_refs.borrow().clone(),
5461                e.state.prev[0]
5462                    .as_ref()
5463                    .and_then(|v| v.downcast_ref::<i32>().copied()),
5464            )
5465        });
5466        assert_eq!(
5467            seen_refs,
5468            vec![base_refs, base_refs],
5469            "RESOLVED callbacks should not increment counted Core refs"
5470        );
5471        assert_eq!(prev_batch, Some(5));
5472    }
5473
5474    #[test]
5475    fn run_wave_post_dispatch_resolved_is_borrow_free_and_rolls_dep_state() {
5476        // DR-8/B54 owner-execution: post-dispatch run_wave cleanup should route the
5477        // synthesized undirty RESOLVED through the delivery waist, deliver callbacks
5478        // borrow-free, and then roll dep state without counted owner promotion.
5479        let arena = GraphArena::new();
5480        let source = Node::<i32>::state_in_arena(&arena, 2);
5481        type SeenRuns = Rc<RefCell<Vec<(Option<i32>, Vec<i32>)>>>;
5482        let seen: SeenRuns = Rc::new(RefCell::new(Vec::new()));
5483        let evens: Node<i32> =
5484            Node::derived_opts_in_arena(&arena, vec![source.erased()], NodeOpts::default(), {
5485                let seen = seen.clone();
5486                move |ctx| {
5487                    let prev = ctx.dep_records()[0]
5488                        .prev_data
5489                        .as_ref()
5490                        .and_then(|v| v.downcast_ref::<i32>().copied());
5491                    let batch = ctx
5492                        .batch::<i32>(0)
5493                        .into_iter()
5494                        .map(|v| *v)
5495                        .collect::<Vec<_>>();
5496                    seen.borrow_mut().push((prev, batch));
5497                    let v = *ctx.data::<i32>(0).unwrap();
5498                    if v % 2 == 0 {
5499                        ctx.emit(v);
5500                    }
5501                }
5502            });
5503        let held: Rc<RefCell<Option<Node<i32>>>> =
5504            Rc::new(RefCell::new(Some(Node::<i32>::state_in_arena(&arena, 99))));
5505        let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
5506        let resolved_refs = Rc::new(Cell::new(usize::MAX));
5507        let _u = evens.subscribe({
5508            let held = held.clone();
5509            let refs_slot = refs_slot.clone();
5510            let resolved_refs = resolved_refs.clone();
5511            move |msg| {
5512                if matches!(msg, Message::Resolved) {
5513                    if let Some(refs) = refs_slot.borrow().as_ref() {
5514                        resolved_refs.set(refs.get());
5515                    }
5516                    let _ = held.borrow_mut().take();
5517                }
5518            }
5519        });
5520        *refs_slot.borrow_mut() = Some(evens.core.refs.clone());
5521        let refs_before = evens.core.refs.get();
5522
5523        assert_eq!(
5524            &*seen.borrow(),
5525            &[(None, vec![2])],
5526            "activation run sees the cached source wave once"
5527        );
5528        source.set(3);
5529
5530        assert!(held.borrow().is_none(), "RESOLVED callback ran borrow-free");
5531        assert_eq!(
5532            resolved_refs.get(),
5533            refs_before,
5534            "post-run RESOLVED delivery must not temporarily promote the owner Core"
5535        );
5536        assert_eq!(evens.cache(), Some(2));
5537        assert_eq!(evens.status(), Status::Resolved);
5538
5539        source.set(4);
5540        assert_eq!(
5541            &*seen.borrow(),
5542            &[(None, vec![2]), (Some(2), vec![3]), (Some(3), vec![4])],
5543            "the rejected odd wave must still roll dep prev_data before the next run"
5544        );
5545        assert_eq!(evens.cache(), Some(4));
5546        let (res_refs, rolled_prev) = evens.core.with_inner_edges(|_n, e| {
5547            (
5548                resolved_refs.get(),
5549                e.state.prev[0]
5550                    .as_ref()
5551                    .and_then(|v| v.downcast_ref::<i32>().copied()),
5552            )
5553        });
5554        assert_eq!(
5555            rolled_prev,
5556            Some(4),
5557            "run_wave should carry latest settled data forward in prev"
5558        );
5559        assert!(
5560            evens
5561                .core
5562                .with_inner_edges(|_n, e| e.state.batch[0].is_none()),
5563            "post-wave state must clear batch"
5564        );
5565        assert_eq!(
5566            res_refs, refs_before,
5567            "post-run RESOLVED delivery should remain borrow-free and ref-neutral"
5568        );
5569    }
5570
5571    #[test]
5572    fn run_wave_post_dispatch_panic_keeps_wave_guard() {
5573        // DR-8/B54: if a synthesized RESOLVED callback panics, WaveGuard must
5574        // clear `inside_run_wave` on unwind so D30 recovery can proceed.
5575        let source = Node::<i32>::state_empty();
5576        let resolved = Rc::new(Cell::new(false));
5577        let resolved_flag = resolved.clone();
5578        let derived: Node<i32> = Node::derived(vec![source.erased()], |ctx| {
5579            let _ = ctx.data::<i32>(0).unwrap();
5580        });
5581        let _u = derived.subscribe(move |msg| {
5582            if matches!(msg, Message::Resolved) {
5583                resolved_flag.set(true);
5584                panic!("resolved callback panic");
5585            }
5586        });
5587
5588        source.set(1);
5589
5590        assert!(
5591            resolved.get(),
5592            "post-run RESOLVED should still be delivered before callback-panic recovery"
5593        );
5594        assert_eq!(
5595            derived.status(),
5596            Status::Errored,
5597            "panic during callback should land ERROR"
5598        );
5599        assert!(
5600            !derived.core.with_inner_edges(|_, e| e.wave.inside_run_wave),
5601            "WaveGuard must clear inside_run_wave during unwind"
5602        );
5603    }
5604
5605    #[test]
5606    fn unarmed_host_boundary_abort_is_ordinary_graph_error() {
5607        let source = Node::<i32>::state(1);
5608        let bad: Node<i32> = Node::derived(vec![source.erased()], |_| {
5609            crate::host_boundary::abort_host_boundary();
5610        });
5611
5612        let _u = bad.subscribe(|_| {});
5613
5614        assert_eq!(
5615            bad.status(),
5616            Status::Errored,
5617            "D431 marker helper is active only inside native host-boundary guards"
5618        );
5619    }
5620
5621    #[test]
5622    fn d1_drop_releases_upstream_subscription_and_runs_cleanup() {
5623        // D1: dropping an active node (no explicit unsubscribe) must detach from its
5624        // deps and fire its cleanup hooks — Rust has no GC to reap the dead sink.
5625        let deactivated = Rc::new(Cell::new(false));
5626        let a = Node::<i32>::state(1);
5627        {
5628            let flag = deactivated.clone();
5629            let mid: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
5630                let f = flag.clone();
5631                ctx.on_deactivation(move || f.set(true));
5632                ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
5633            });
5634            let _sub = mid.subscribe(|_| {}); // activates mid: runs fn (registers hook) + subscribes to a
5635            assert!(!deactivated.get());
5636            // `mid` + `_sub` drop here → mid's Core refcount hits 0 → Drop runs cleanup.
5637        }
5638        assert!(
5639            deactivated.get(),
5640            "dropping an active node fires its on_deactivation + detaches from deps (D1)"
5641        );
5642        // `a` survived (still held), its sink was removed; a fresh subscribe still works.
5643        let (log, sink) = recorder();
5644        let _u = a.subscribe(sink);
5645        assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5646    }
5647
5648    #[test]
5649    fn producer_runs_once_on_activation() {
5650        let p = Node::<i32>::producer(|ctx| ctx.emit(42i32));
5651        let (log, sink) = recorder();
5652        let _u = p.subscribe(sink);
5653        // activation-exempt: the first run during subscribe needs no preceding DIRTY.
5654        assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5655        assert_eq!(p.cache(), Some(42));
5656    }
5657
5658    #[test]
5659    fn derived_first_run_gate_then_recompute() {
5660        let a = Node::<i32>::state_empty();
5661        let b = Node::<i32>::state_empty();
5662        let runs = Rc::new(Cell::new(0usize));
5663        let r2 = runs.clone();
5664        let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5665            r2.set(r2.get() + 1);
5666            let x = *ctx.data::<i32>(0).unwrap();
5667            let y = *ctx.data::<i32>(1).unwrap();
5668            ctx.emit(x + y);
5669        });
5670        let (log, sink) = recorder();
5671        let _u = sum.subscribe(sink);
5672        // first-run gate: neither dep has settled → fn has not fired.
5673        assert_eq!(runs.get(), 0);
5674        assert_eq!(*log.borrow(), vec!["START"]);
5675
5676        a.set(3); // only one dep settled → gate still holds
5677        assert_eq!(runs.get(), 0);
5678
5679        b.set(4); // both settled → first run, sum = 7
5680        assert_eq!(runs.get(), 1);
5681        assert_eq!(sum.cache(), Some(7));
5682        assert_eq!(*log.borrow(), vec!["START", "DIRTY", "DATA"]);
5683
5684        a.set(10); // recompute (gate already passed), sum = 14
5685        assert_eq!(runs.get(), 2);
5686        assert_eq!(sum.cache(), Some(14));
5687    }
5688
5689    #[test]
5690    fn data_waits_while_another_dep_is_pending() {
5691        let a = Node::<i32>::state_empty();
5692        let b = Node::<i32>::state_empty();
5693        let runs = Rc::new(Cell::new(0usize));
5694        let r2 = runs.clone();
5695        let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5696            r2.set(r2.get() + 1);
5697            ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
5698        });
5699        let (log, sink) = recorder();
5700        let _u = sum.subscribe(sink);
5701        assert_eq!(*log.borrow(), vec!["START"]);
5702
5703        with_wave_owner(
5704            &a.core,
5705            || sum.core.receive_from_dep(0, &Message::Dirty),
5706            || {},
5707        );
5708        with_wave_owner(
5709            &b.core,
5710            || sum.core.receive_from_dep(1, &Message::Dirty),
5711            || {},
5712        );
5713        assert_eq!(*log.borrow(), vec!["START", "DIRTY"]);
5714
5715        with_wave_owner(
5716            &a.core,
5717            || sum.core.receive_from_dep(0, &Message::Data(Rc::new(3i32))),
5718            || {},
5719        );
5720        assert_eq!(runs.get(), 0, "dep 1 is still pending");
5721
5722        with_wave_owner(
5723            &b.core,
5724            || sum.core.receive_from_dep(1, &Message::Data(Rc::new(4i32))),
5725            || {},
5726        );
5727        assert_eq!(runs.get(), 1);
5728        assert_eq!(sum.cache(), Some(7));
5729        assert_eq!(*log.borrow(), vec!["START", "DIRTY", "DATA"]);
5730    }
5731
5732    #[test]
5733    fn data_without_prior_dirty_still_satisfies_first_run_gate() {
5734        let a = Node::<i32>::state_empty();
5735        let b = Node::<i32>::state_empty();
5736        let runs = Rc::new(Cell::new(0usize));
5737        let r2 = runs.clone();
5738        let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5739            r2.set(r2.get() + 1);
5740            ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
5741        });
5742        let (log, sink) = recorder();
5743        let _u = sum.subscribe(sink);
5744        assert_eq!(*log.borrow(), vec!["START"]);
5745
5746        with_wave_owner(
5747            &a.core,
5748            || sum.core.receive_from_dep(0, &Message::Data(Rc::new(3i32))),
5749            || {},
5750        );
5751        assert_eq!(runs.get(), 0, "first-run gate still waits for dep 1");
5752
5753        with_wave_owner(
5754            &b.core,
5755            || sum.core.receive_from_dep(1, &Message::Data(Rc::new(4i32))),
5756            || {},
5757        );
5758        assert_eq!(runs.get(), 1);
5759        assert_eq!(sum.cache(), Some(7));
5760        assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5761    }
5762
5763    #[test]
5764    fn paused_data_survives_until_later_invalidate_drains_pending() {
5765        let a = Node::<i32>::state(1);
5766        let b = Node::<i32>::state(10);
5767        let runs = Rc::new(Cell::new(0usize));
5768        let r2 = runs.clone();
5769        let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5770            r2.set(r2.get() + 1);
5771            let x = ctx.data::<i32>(0).map(|v| *v).unwrap_or(0);
5772            let y = ctx.data::<i32>(1).map(|v| *v).unwrap_or(0);
5773            ctx.emit(x + y);
5774        });
5775        let (log, sink) = recorder();
5776        let _u = sum.subscribe(sink);
5777        assert_eq!(runs.get(), 1);
5778        assert_eq!(sum.cache(), Some(11));
5779        log.borrow_mut().clear();
5780
5781        let pause = LockId::new("coalesce");
5782        sum.up(vec![Message::Pause(pause.clone())]);
5783        a.down(vec![Message::Dirty]);
5784        b.down(vec![Message::Dirty]);
5785        a.down(vec![Message::Data(Rc::new(2i32))]);
5786        assert_eq!(runs.get(), 1, "dep 1 is still pending while paused");
5787
5788        b.down(vec![Message::Invalidate]);
5789        assert_eq!(runs.get(), 1, "still paused after pending drains");
5790        sum.up(vec![Message::Resume(pause)]);
5791
5792        assert_eq!(runs.get(), 2);
5793        assert_eq!(sum.cache(), Some(2));
5794        assert_eq!(*log.borrow(), vec!["DIRTY", "INVALIDATE", "DATA"]);
5795    }
5796
5797    #[test]
5798    fn diamond_recomputes_exactly_once_two_phase() {
5799        // A → B, A → C, B → D, C → D. D must join once per A update, glitch-free.
5800        let a = Node::<i32>::state_empty();
5801        let b: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5802            ctx.emit(*ctx.data::<i32>(0).unwrap() + 1)
5803        });
5804        let c: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5805            ctx.emit(*ctx.data::<i32>(0).unwrap() * 10)
5806        });
5807        let d_runs = Rc::new(Cell::new(0usize));
5808        let dr = d_runs.clone();
5809        let d: Node<i32> = Node::derived(vec![b.erased(), c.erased()], move |ctx| {
5810            dr.set(dr.get() + 1);
5811            ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
5812        });
5813        let (log, sink) = recorder();
5814        let _u = d.subscribe(sink);
5815
5816        a.set(2); // B=3, C=20, D=23 — D fires EXACTLY once (R-diamond)
5817        assert_eq!(d_runs.get(), 1);
5818        assert_eq!(d.cache(), Some(23));
5819        // two-phase: DIRTY (phase 1, from the cascade) precedes DATA (phase 2). No glitch.
5820        assert_eq!(*log.borrow(), vec!["START", "DIRTY", "DATA"]);
5821
5822        a.set(5); // B=6, C=50, D=56 — once more
5823        assert_eq!(d_runs.get(), 2);
5824        assert_eq!(d.cache(), Some(56));
5825    }
5826
5827    #[test]
5828    fn rom_ram_deactivation_clears_compute_cache() {
5829        let a = Node::<i32>::state(7); // ROM: state node retains across disconnect
5830        let dbl: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5831            ctx.emit(*ctx.data::<i32>(0).unwrap() * 2)
5832        });
5833        let (_log, sink) = recorder();
5834        let u = dbl.subscribe(sink);
5835        assert_eq!(dbl.cache(), Some(14));
5836        assert_eq!(dbl.status(), Status::Settled);
5837
5838        u(); // last subscriber leaves → dbl deactivates
5839             // RAM: compute node cleared its cache; ROM: state node kept its value.
5840        assert_eq!(dbl.cache(), None);
5841        assert_eq!(dbl.status(), Status::Sentinel);
5842        assert_eq!(a.cache(), Some(7));
5843    }
5844
5845    #[test]
5846    #[should_panic(expected = "terminal")]
5847    fn subscribe_to_terminal_node_is_rejected() {
5848        // R-terminal (D17): a late subscribe to a non-resubscribable terminal node is
5849        // REJECTED (idiomatic error) — the stream is permanently over.
5850        let s = Node::<i32>::state(1);
5851        let _u = s.subscribe(|_| {}); // keep alive (so the node doesn't deactivate)
5852        s.down(vec![Message::Complete]); // S goes terminal
5853        assert_eq!(s.status(), Status::Completed);
5854        let _u2 = s.subscribe(|_| {}); // late subscribe to a terminal node → panics
5855    }
5856
5857    #[test]
5858    fn set_on_terminal_node_is_a_noop() {
5859        // Terminal-is-forever (D17 / R-terminal): a self-emit (set/ctx.down) on a
5860        // terminated node emits nothing and does not resurrect its value.
5861        let s = Node::<i32>::state(1);
5862        let (log, sink) = recorder();
5863        let _u = s.subscribe(sink);
5864        assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5865
5866        s.down(vec![Message::Complete]); // S terminal
5867        assert_eq!(s.status(), Status::Completed);
5868        assert_eq!(*log.borrow(), vec!["START", "DATA", "COMPLETE"]);
5869
5870        s.set(5); // self-emit after terminal → no-op (no DATA, value unchanged)
5871        assert_eq!(*log.borrow(), vec!["START", "DATA", "COMPLETE"]);
5872        assert_eq!(s.cache(), Some(1));
5873        assert_eq!(s.status(), Status::Completed);
5874    }
5875
5876    #[test]
5877    fn b32_dropped_node_unregisters_its_fn_releasing_captures() {
5878        // B32: the dispatcher pool no longer holds a dropped node's fn forever. Before the
5879        // slotmap + unregister-on-Drop, a registered fn lived for the process, pinning the
5880        // upstream `Core`s it captured (the no-GC leak). Probe via a guard captured by the
5881        // fn: when the node drops, its fn slot is freed → the fn (and the guard) drop.
5882        struct DropFlag(Rc<Cell<bool>>);
5883        impl Drop for DropFlag {
5884            fn drop(&mut self) {
5885                self.0.set(true);
5886            }
5887        }
5888        let fn_dropped = Rc::new(Cell::new(false));
5889        {
5890            let guard = DropFlag(fn_dropped.clone());
5891            let p = Node::<i32>::producer(move |ctx| {
5892                let _hold = &guard; // the fn captures the guard (stands in for a captured Core)
5893                ctx.emit(1i32);
5894            });
5895            let _u = p.subscribe(|_| {});
5896            assert!(
5897                !fn_dropped.get(),
5898                "the fn (and its capture) is live while the node is"
5899            );
5900            // `p` + `_u` drop here → p's Core refcount → 0 → NodeInner::Drop → dispatcher
5901            // .unregister → the pool drops the fn → the captured guard drops.
5902        }
5903        assert!(
5904            fn_dropped.get(),
5905            "a dropped node unregisters its fn from the pool, releasing the fn's captures (B32)"
5906        );
5907    }
5908
5909    #[test]
5910    fn run_wave_hook_clear_drops_captured_core_borrow_free() {
5911        // B49/D71 regression: clearing per-run hooks must drop the old user closures
5912        // AFTER releasing the GraphCore borrow. A hook may capture the last handle to
5913        // another node; dropping that handle re-enters Core::drop on the same arena.
5914        let a = Node::<i32>::state(1);
5915        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5916            let held = Node::<i32>::state(99);
5917            ctx.on_invalidate(move || {
5918                let _ = held.status();
5919            });
5920            ctx.emit(*ctx.data::<i32>(0).unwrap());
5921        });
5922        let _u = d.subscribe(|_| {});
5923
5924        a.set(2);
5925        assert_eq!(d.cache(), Some(2));
5926    }
5927
5928    #[test]
5929    fn on_invalidate_unsubscribe_affects_current_broadcast_snapshot() {
5930        // R-cleanup-hooks / R-invalidate-idempotent: onInvalidate runs before the
5931        // downstream INVALIDATE broadcast. If the hook detaches a subscriber, the
5932        // current broadcast must snapshot after that detach rather than deliver to
5933        // a stale pre-hook subscriber list.
5934        let src = Node::<i32>::state(1);
5935        let victim_unsub: Rc<RefCell<Option<Unsub>>> = Rc::new(RefCell::new(None));
5936        let d: Node<i32> = {
5937            let victim_unsub = victim_unsub.clone();
5938            Node::derived(vec![src.erased()], move |ctx| {
5939                let victim_unsub = victim_unsub.clone();
5940                ctx.on_invalidate(move || {
5941                    if let Some(unsub) = victim_unsub.borrow_mut().take() {
5942                        unsub();
5943                    }
5944                });
5945                ctx.emit(*ctx.data::<i32>(0).unwrap());
5946            })
5947        };
5948        let _keepalive = d.subscribe(|_| {});
5949        let (log, sink) = recorder();
5950        let victim = d.subscribe(sink);
5951        *victim_unsub.borrow_mut() = Some(victim);
5952        log.borrow_mut().clear();
5953
5954        src.down(vec![Message::Invalidate]);
5955
5956        assert_eq!(
5957            log.borrow().iter().filter(|m| *m == "INVALIDATE").count(),
5958            0,
5959            "a subscriber detached by onInvalidate must not receive the same INVALIDATE"
5960        );
5961    }
5962
5963    #[test]
5964    fn subscriber_callback_drops_handle_borrow_free() {
5965        // DR-8/B54 owner-execution invariant: callbacks run only after the owner arena
5966        // mutation borrow has been released. A subscriber may drop the last handle to
5967        // another same-arena node, which re-enters Core::drop; holding the owner borrow
5968        // across the callback would panic.
5969        let held: Rc<RefCell<Option<Node<i32>>>> =
5970            Rc::new(RefCell::new(Some(Node::<i32>::producer(|_| {}))));
5971        let source = Node::<i32>::state_empty();
5972        let h = held.clone();
5973        let _u = source.subscribe(move |m| {
5974            if matches!(m, Message::Data(_)) {
5975                let _ = h.borrow_mut().take();
5976            }
5977        });
5978
5979        source.set(1);
5980
5981        assert!(held.borrow().is_none());
5982        assert_eq!(source.cache(), Some(1));
5983    }
5984
5985    #[test]
5986    fn drop_returns_arena_slot_when_cleanup_hook_panics() {
5987        // B49/D71 regression: Core::drop removes the NodeInner from the arena before
5988        // running cleanup. Even if a user cleanup hook panics, the slot id must be
5989        // returned to the free list.
5990        let free_before = DEFAULT_GRAPH_CORE.with(|g| g.borrow().free.len());
5991        let result = catch_unwind(AssertUnwindSafe(|| {
5992            let p = Node::<i32>::producer(|ctx| {
5993                ctx.on_deactivation(|| panic!("cleanup boom"));
5994                ctx.emit(1i32);
5995            });
5996            let _u = p.subscribe(|_| {});
5997        }));
5998        assert!(result.is_err());
5999        let free_after = DEFAULT_GRAPH_CORE.with(|g| g.borrow().free.len());
6000        assert!(
6001            free_after > free_before,
6002            "panicking cleanup still returns the arena slot to the free list"
6003        );
6004    }
6005
6006    #[test]
6007    fn drop_unregisters_fn_even_when_cleanup_hook_panics() {
6008        // DR-8/B54 side-table cleanup regression: the call slot must unregister before
6009        // user cleanup hooks run, or a panicking onDeactivation strands the dispatcher
6010        // fn slot and every Core/resource captured by the fn.
6011        struct DropFlag(Rc<Cell<bool>>);
6012        impl Drop for DropFlag {
6013            fn drop(&mut self) {
6014                self.0.set(true);
6015            }
6016        }
6017
6018        let fn_dropped = Rc::new(Cell::new(false));
6019        let result = catch_unwind(AssertUnwindSafe({
6020            let fn_dropped = fn_dropped.clone();
6021            move || {
6022                let guard = DropFlag(fn_dropped);
6023                let p = Node::<i32>::producer(move |ctx| {
6024                    let _keep = &guard;
6025                    ctx.on_deactivation(|| panic!("cleanup boom"));
6026                    ctx.emit(1i32);
6027                });
6028                let _u = p.subscribe(|_| {});
6029            }
6030        }));
6031
6032        assert!(result.is_err());
6033        assert!(
6034            fn_dropped.get(),
6035            "dispatcher unregister must drop fn captures even if user cleanup panics"
6036        );
6037    }
6038
6039    #[test]
6040    fn identity_key_changes_when_arena_slot_is_reused() {
6041        let first = Node::<i32>::producer_opts(
6042            NodeOpts {
6043                factory: Some("first".to_owned()),
6044                ..NodeOpts::default()
6045            },
6046            |_| {},
6047        );
6048        let first_key = first.erased().identity_key();
6049        drop(first);
6050
6051        let second = Node::<i32>::producer_opts(
6052            NodeOpts {
6053                factory: Some("second".to_owned()),
6054                ..NodeOpts::default()
6055            },
6056            |_| {},
6057        );
6058        let second_key = second.erased().identity_key();
6059
6060        assert_ne!(
6061            first_key, second_key,
6062            "D51 synthetic describe ids must not inherit across reused arena slots"
6063        );
6064    }
6065
6066    #[test]
6067    fn arena_generation_key_rejects_reused_slot() {
6068        // DR-8/B54 owner-execution prep: arena entries are addressed by slot +
6069        // generation, so a stale key cannot hit a later node that reused the slot.
6070        let arena = GraphArena::new();
6071        let first = Node::<i32>::state_in_arena(&arena, 1);
6072        let old_key = first.core.key();
6073        let old_weak = first.core.downgrade();
6074        assert!(old_weak.borrowed_core().is_some());
6075        drop(first);
6076
6077        let second = Node::<i32>::state_in_arena(&arena, 2);
6078        let new_key = second.core.key();
6079        assert_eq!(
6080            old_key.id, new_key.id,
6081            "isolated arena should reuse the just-freed slot"
6082        );
6083        assert_ne!(old_key.generation, new_key.generation);
6084        assert!(
6085            !arena.0.borrow().is_live_key(old_key),
6086            "the old generation key is stale after slot reuse"
6087        );
6088
6089        // A stale weak dep-callback entry is a no-op and must not disturb the new node.
6090        old_weak.receive_from_dep(0, &Message::Start);
6091        assert_eq!(second.cache(), Some(2));
6092        assert_eq!(second.status(), Status::Settled);
6093    }
6094
6095    #[test]
6096    fn deferred_delivery_actions_do_not_pin_core_while_queued() {
6097        // DR-8/B54: delivery-boundary queues carry generation-keyed weak arena actions,
6098        // not counted Core clones. The wave touched-set owns the in-wave pin; the queued
6099        // action should not add hot-path refcount churn while it waits for the boundary.
6100        let source = Node::<i32>::state(1);
6101        let derived: Node<i32> = Node::derived(vec![source.erased()], |ctx| {
6102            ctx.emit(*ctx.data::<i32>(0).unwrap() + 1)
6103        });
6104        let _u = derived.subscribe(|_| {});
6105        let refs_before = derived.core.refs.get();
6106
6107        with_delivery_scope(|| {
6108            defer_run_until_delivery_boundary(&derived.core);
6109            defer_run_until_delivery_boundary(&derived.core);
6110            defer_absorbed_settle_until_delivery_boundary(&derived.core);
6111            defer_absorbed_settle_until_delivery_boundary(&derived.core);
6112
6113            assert_eq!(
6114                derived.core.refs.get(),
6115                refs_before,
6116                "queued delivery-boundary actions must not hold counted Core refs"
6117            );
6118            DEFERRED_DELIVERY_ACTIONS.with(|actions| {
6119                let actions = actions.borrow();
6120                assert_eq!(
6121                    actions.len(),
6122                    2,
6123                    "delivery-boundary actions dedupe by arena key and action kind"
6124                );
6125                assert_eq!(
6126                    actions
6127                        .iter()
6128                        .filter(|action| action.kind == DeferredDeliveryKind::Run)
6129                        .count(),
6130                    1,
6131                    "run actions dedupe by arena key"
6132                );
6133                assert_eq!(
6134                    actions
6135                        .iter()
6136                        .filter(|action| action.kind == DeferredDeliveryKind::AbsorbedSettle)
6137                        .count(),
6138                    1,
6139                    "absorbed-settle actions dedupe by arena key"
6140                );
6141            });
6142        });
6143
6144        assert_eq!(
6145            derived.core.refs.get(),
6146            refs_before,
6147            "delivery-boundary drain does not retain queued counted Core refs"
6148        );
6149    }
6150
6151    #[test]
6152    fn active_wave_deferred_delivery_drain_uses_borrowed_core() {
6153        // DR-8/B54: when a DATA delivery queues a fn run until the delivery boundary,
6154        // the owner wave's arena pin is still live while the drain executes. Rehydrate
6155        // that target as a borrowed Core, not a transient counted Core.
6156        let source = Node::<i32>::state_empty();
6157        let observed_refs = Rc::new(Cell::new(usize::MAX));
6158        let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
6159        let derived: Node<i32> = Node::derived(vec![source.erased()], {
6160            let observed_refs = observed_refs.clone();
6161            let refs_slot = refs_slot.clone();
6162            move |ctx| {
6163                if let Some(refs) = refs_slot.borrow().as_ref() {
6164                    observed_refs.set(refs.get());
6165                }
6166                ctx.emit(*ctx.data::<i32>(0).unwrap());
6167            }
6168        });
6169        *refs_slot.borrow_mut() = Some(derived.core.refs.clone());
6170        let _u = derived.subscribe(|_| {});
6171        let refs_before = derived.core.refs.get();
6172
6173        source.set(1);
6174
6175        assert_eq!(
6176            observed_refs.get(),
6177            refs_before,
6178            "active-wave delivery-boundary drain should run through a borrowed arena view"
6179        );
6180        assert_eq!(derived.core.refs.get(), refs_before);
6181    }
6182
6183    #[test]
6184    fn deferred_delivery_drains_run_before_absorbed_settle() {
6185        // DR-8/B54: delivery boundary actions must process Run before AbsorbedSettle
6186        // so a deferred recompute is not starved behind its follow-up settle.
6187        let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
6188        let derived: Node<i32> = Node::derived(vec![], {
6189            let events = events.clone();
6190            move |_ctx| {
6191                events.borrow_mut().push("run");
6192            }
6193        });
6194        let _u = derived.subscribe({
6195            let events = events.clone();
6196            move |msg| {
6197                if let Message::Resolved = msg {
6198                    events.borrow_mut().push("resolved");
6199                }
6200            }
6201        });
6202        events.borrow_mut().clear();
6203
6204        // Seed settle eligibility for the same wave as an absorbed terminal path would do.
6205        derived
6206            .core
6207            .with_inner_edges_mut(|_n, e| e.wave.emitted_dirty_this_wave = true);
6208
6209        with_delivery_scope(|| {
6210            defer_run_until_delivery_boundary(&derived.core);
6211            defer_absorbed_settle_until_delivery_boundary(&derived.core);
6212        });
6213
6214        assert_eq!(
6215            &*events.borrow(),
6216            &["run", "resolved"],
6217            "deferred Run must drain before AbsorbedSettle"
6218        );
6219    }
6220
6221    #[test]
6222    fn deferred_delivery_outside_wave_run_does_not_increment_refs() {
6223        // DR-8/B54: when a queued delivery-boundary run drains outside a wave, the
6224        // borrowed pin is used for execution, so no temporary counted ref is added.
6225        let source = Node::<i32>::state(1);
6226        let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
6227        let observed_refs = Rc::new(Cell::new(usize::MAX));
6228        let run_count = Rc::new(Cell::new(0usize));
6229
6230        let derived: Node<i32> = Node::derived_opts(
6231            vec![source.erased()],
6232            NodeOpts {
6233                partial: true,
6234                ..NodeOpts::default()
6235            },
6236            {
6237                let refs_slot = refs_slot.clone();
6238                let observed_refs = observed_refs.clone();
6239                let run_count = run_count.clone();
6240                move |_ctx| {
6241                    if let Some(refs) = refs_slot.borrow().as_ref() {
6242                        observed_refs.set(refs.get());
6243                    }
6244                    run_count.set(run_count.get() + 1);
6245                }
6246            },
6247        );
6248
6249        *refs_slot.borrow_mut() = Some(derived.core.refs.clone());
6250        let before = derived.core.refs.get();
6251
6252        with_delivery_scope(|| {
6253            defer_run_until_delivery_boundary(&derived.core);
6254        });
6255
6256        assert_eq!(
6257            run_count.get(),
6258            1,
6259            "queued delivery-boundary run must execute"
6260        );
6261        assert_eq!(
6262            observed_refs.get(),
6263            before,
6264            "outside-wave delivery-boundary run must execute from a borrowed pin"
6265        );
6266    }
6267
6268    #[test]
6269    fn stale_deferred_delivery_action_does_not_run_after_slot_free() {
6270        // DR-8/B54: once a node's slot is released, queued deferred actions must be
6271        // generation-checked and become inert, even before the slot is reused.
6272        DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
6273
6274        let arena = GraphArena::new();
6275        let old = Node::<i32>::state_in_arena(&arena, 1);
6276        let old_key = old.core.key();
6277        let weak = old.core.downgrade();
6278        let stale_run = DeferredDeliveryAction::from_core(DeferredDeliveryKind::Run, &old.core);
6279        let stale_settle =
6280            DeferredDeliveryAction::from_core(DeferredDeliveryKind::AbsorbedSettle, &old.core);
6281
6282        drop(old);
6283
6284        assert!(
6285            weak.borrowed_core().is_none(),
6286            "old slot is not live after drop"
6287        );
6288        assert_eq!(weak.key.generation, old_key.generation);
6289
6290        DEFERRED_DELIVERY_ACTIONS.with(|actions| {
6291            let mut actions = actions.borrow_mut();
6292            actions.push(stale_run);
6293            actions.push(stale_settle);
6294        });
6295        drain_deferred_runs();
6296
6297        assert!(
6298            weak.borrowed_core().is_none(),
6299            "freed slot remains out-of-scope for stale deferred work"
6300        );
6301    }
6302
6303    #[test]
6304    fn weak_borrowed_core_uses_arena_pin_after_external_death() {
6305        // DR-8/B54: a wave pin, not the counted Core refcount, owns liveness for
6306        // in-wave borrowed execution. This must fail closed once the pin releases.
6307        let arena = GraphArena::new();
6308        let graph = arena.0.clone();
6309        let victim = Node::<i32>::state_in_arena(&arena, 1);
6310        let key = victim.core.key();
6311        let weak = victim.core.downgrade();
6312        let refs = victim.core.refs.clone();
6313        let pin = ArenaNodePin::from_core(&victim.core).expect("live slot pins");
6314        drop(victim);
6315
6316        assert_eq!(refs.get(), 0);
6317        assert!(graph.borrow().is_live_key(key));
6318        assert!(
6319            weak.borrowed_core().is_some(),
6320            "active arena pin should allow borrowed weak rehydration without reviving refs"
6321        );
6322        drop(pin);
6323        assert!(!graph.borrow().is_live_key(key));
6324        assert!(
6325            weak.borrowed_core().is_none(),
6326            "borrowed weak rehydration must fail once the pin releases"
6327        );
6328    }
6329
6330    #[test]
6331    fn deferred_delivery_run_skips_same_wave_terminalized_target() {
6332        // R-terminal: a DATA may queue a delivery-boundary run, then a later terminal in
6333        // the same upstream wave can complete the target before that boundary drains.
6334        // The stale run must not invoke user code after terminal-is-forever seals output.
6335        let source = Node::<i32>::state_empty();
6336        let runs = Rc::new(Cell::new(0usize));
6337        let derived: Node<i32> = Node::derived(vec![source.erased()], {
6338            let runs = runs.clone();
6339            move |ctx| {
6340                runs.set(runs.get() + 1);
6341                ctx.emit(*ctx.data::<i32>(0).unwrap());
6342            }
6343        });
6344        let _u = derived.subscribe(|_| {});
6345
6346        source.down(vec![Message::Data(Rc::new(1i32)), Message::Complete]);
6347
6348        assert_eq!(
6349            runs.get(),
6350            0,
6351            "queued delivery-boundary run must not execute after same-wave terminal"
6352        );
6353        assert_eq!(derived.status(), Status::Completed);
6354    }
6355
6356    #[test]
6357    fn stale_deferred_delivery_action_does_not_target_reused_slot() {
6358        // A queued weak arena action may outlive the original slot after an unwind/drop.
6359        // Rehydration must check the generation and old ref token, so a later occupant of
6360        // the same slot is a no-op target, not a run/settle target.
6361        DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
6362
6363        let arena = GraphArena::new();
6364        let old = Node::<i32>::state_in_arena(&arena, 1);
6365        let stale_run = DeferredDeliveryAction::from_core(DeferredDeliveryKind::Run, &old.core);
6366        let stale_settle =
6367            DeferredDeliveryAction::from_core(DeferredDeliveryKind::AbsorbedSettle, &old.core);
6368        let old_key = old.core.key();
6369        drop(old);
6370
6371        let reused = Node::<i32>::state_in_arena(&arena, 2);
6372        assert_eq!(old_key.id, reused.core.key().id);
6373        assert_ne!(old_key.generation, reused.core.key().generation);
6374
6375        DEFERRED_DELIVERY_ACTIONS.with(|actions| {
6376            let mut actions = actions.borrow_mut();
6377            actions.push(stale_run);
6378            actions.push(stale_settle);
6379        });
6380        drain_deferred_runs();
6381
6382        assert_eq!(reused.cache(), Some(2));
6383        assert_eq!(
6384            reused.status(),
6385            Status::Settled,
6386            "stale deferred generation must not disturb the reused slot occupant"
6387        );
6388    }
6389
6390    #[test]
6391    fn panic_aborted_delivery_clears_deferred_delivery_actions() {
6392        // B25/DR-8: delivery-boundary run/settle queues are wave-local. If a subscriber
6393        // panics after a dep callback queued a run, the wave-owner catch resets touched
6394        // flags AND must discard those queued actions. A later unrelated delivery must
6395        // not run stale work against reset dep projections.
6396        DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
6397
6398        let source = Node::<i32>::state_empty();
6399        let runs = Rc::new(Cell::new(0usize));
6400        let d: Node<i32> = Node::derived(vec![source.erased()], {
6401            let runs = runs.clone();
6402            move |_ctx| {
6403                runs.set(runs.get() + 1);
6404            }
6405        });
6406        let _d_sub = d.subscribe(|_| {});
6407        let _panic_sub = source.subscribe(|m| {
6408            if matches!(m, Message::Data(_)) {
6409                panic!("subscriber aborts delivery after dep callback queued run");
6410            }
6411        });
6412
6413        source.set(1);
6414
6415        assert_eq!(runs.get(), 0, "aborted delivery must not run the queued fn");
6416        DEFERRED_DELIVERY_ACTIONS.with(|queued| {
6417            assert!(
6418                queued.borrow().is_empty(),
6419                "aborted wave must clear queued deferred delivery actions"
6420            );
6421        });
6422
6423        let unrelated = Node::<i32>::state_empty();
6424        let _u = unrelated.subscribe(|_| {});
6425        unrelated.set(1);
6426        assert_eq!(
6427            runs.get(),
6428            0,
6429            "later unrelated delivery must not drain stale work from the aborted wave"
6430        );
6431    }
6432
6433    #[test]
6434    fn internal_dep_callback_entry_does_not_increment_core_refcount() {
6435        // The B54 first slice keeps closure transport but avoids constructing a counted
6436        // Core for every internal dep message. START is enough to exercise the entry
6437        // without mutating dep-slot arrays. This path is only uncounted while a real
6438        // wave owner is active; outside a wave, the callback entry must pin the slot.
6439        let arena = GraphArena::new();
6440        let source = Node::<i32>::state_in_arena(&arena, 1);
6441        let derived: Node<i32> = Node::derived_opts_in_arena(
6442            &arena,
6443            vec![source.erased()],
6444            NodeOpts::default(),
6445            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6446        );
6447        let weak = derived.core.downgrade();
6448        let refs_before = derived.core.refs.get();
6449
6450        let borrowed = with_wave_owner(&source.core, || weak.borrowed_core(), || None)
6451            .expect("borrowed core should exist while the node is live");
6452        assert_eq!(
6453            derived.core.refs.get(),
6454            refs_before,
6455            "borrowed weak dep entry should not increment Core refs while held"
6456        );
6457        drop(borrowed);
6458
6459        assert_eq!(
6460            derived.core.refs.get(),
6461            refs_before,
6462            "internal weak dep entry should borrow by arena key, not create a counted Core"
6463        );
6464    }
6465
6466    #[test]
6467    fn internal_dep_callback_entry_uses_borrowed_pin_outside_wave() {
6468        // DR-8/B54: the off-wave weak dep callback path should also use a temporary
6469        // arena pin + borrowed Core, not a counted Core promotion.
6470        let arena = GraphArena::new();
6471        let source = Node::<i32>::state_in_arena(&arena, 1);
6472        let derived: Node<i32> = Node::derived_opts_in_arena(
6473            &arena,
6474            vec![source.erased()],
6475            NodeOpts::default(),
6476            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6477        );
6478        let weak = derived.core.downgrade();
6479        let refs_before = derived.core.refs.get();
6480
6481        weak.receive_from_dep(0, &Message::Start);
6482        assert_eq!(
6483            derived.core.refs.get(),
6484            refs_before,
6485            "outside a wave, weak dep entry must not promote a counted Core"
6486        );
6487    }
6488
6489    #[test]
6490    fn internal_dep_callback_entry_outside_wave_uses_pinned_borrow_when_refcount_dead() {
6491        // DR-8/B54: when a node is already arena-pinned (eg. by a temporary
6492        // batch/closure token) and `refs == 0`, we can process an off-wave dep callback
6493        // without creating a temporary counted Core clone.
6494        let arena = GraphArena::new();
6495        let source = Node::<i32>::state_in_arena(&arena, 1);
6496        let derived: Node<i32> = Node::derived_opts_in_arena(
6497            &arena,
6498            vec![source.erased()],
6499            NodeOpts::default(),
6500            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6501        );
6502        let weak = derived.core.downgrade();
6503        let borrowed = derived.core.borrowed_view();
6504        let pin = ArenaNodePin::from_core(&derived.core).expect("node must be pinnable while live");
6505        let refs = derived.core.refs.clone();
6506
6507        drop(derived);
6508        assert_eq!(
6509            refs.get(),
6510            0,
6511            "setup drops the final public node handle so receive path is stress-tested with no counted refs"
6512        );
6513
6514        weak.receive_from_dep(0, &Message::Data(Rc::new(4i32)));
6515        assert_eq!(
6516            refs.get(),
6517            0,
6518            "pinned receive should not promote a counted Core when the slot is already externally dead"
6519        );
6520        assert_eq!(
6521            borrowed
6522                .cache_any()
6523                .and_then(|v| v.downcast_ref::<i32>().copied()),
6524            Some(4),
6525            "borrowed owner execution should still run the dep callback while pinned"
6526        );
6527
6528        with_wave_owner(
6529            &source.core,
6530            || weak.receive_from_dep(0, &Message::Data(Rc::new(5i32))),
6531            || {},
6532        );
6533        assert_eq!(
6534            refs.get(),
6535            0,
6536            "in-wave pinned receive should also avoid counted Core promotion"
6537        );
6538        assert_eq!(
6539            borrowed
6540                .cache_any()
6541                .and_then(|v| v.downcast_ref::<i32>().copied()),
6542            Some(5),
6543            "in-wave collect/apply must not half-apply dep bookkeeping then skip the action"
6544        );
6545        drop(pin);
6546    }
6547
6548    #[test]
6549    fn internal_dep_callback_entry_run_decision_executes_from_borrowed_action() {
6550        // A precomputed in-wave Run decision must execute from the borrowed action
6551        // path and avoid perturbing the owner Core refcount during fn execution.
6552        let arena = GraphArena::new();
6553        let source = Node::<i32>::state_empty_in_arena(&arena);
6554        let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
6555        let observed_refs_in_fn = Rc::new(Cell::new(usize::MAX));
6556        let run_count = Rc::new(Cell::new(0usize));
6557
6558        let derived = Node::<i32>::derived_opts_in_arena(
6559            &arena,
6560            vec![source.erased()],
6561            NodeOpts::default(),
6562            {
6563                let refs_slot = refs_slot.clone();
6564                let observed_refs_in_fn = observed_refs_in_fn.clone();
6565                let run_count = run_count.clone();
6566                move |ctx| {
6567                    run_count.set(run_count.get() + 1);
6568                    let refs = refs_slot
6569                        .borrow()
6570                        .as_ref()
6571                        .expect("test installed ref counter before run")
6572                        .clone();
6573                    observed_refs_in_fn.set(refs.get());
6574                    ctx.emit(*ctx.data::<i32>(0).unwrap());
6575                }
6576            },
6577        );
6578        *refs_slot.borrow_mut() = Some(derived.core.refs.clone());
6579        let _u = derived.subscribe(|_| {});
6580        let baseline_refs = derived.core.refs.get();
6581        assert_eq!(
6582            run_count.get(),
6583            0,
6584            "first-run gate should hold before source DATA"
6585        );
6586
6587        let weak = derived.core.downgrade();
6588        let action = with_wave_owner(
6589            &source.core,
6590            || {
6591                weak.collect_in_wave_receive_from_dep_action(0, &Message::Data(Rc::new(4i32)))
6592                    .expect("in-wave receive should collect a runnable action")
6593            },
6594            InWaveDepReceiveAction::default,
6595        );
6596
6597        assert!(
6598            matches!(action.maybe_run_decision, MaybeRunDecision::Run),
6599            "first in-wave settle for a ready dep should materialize as Run"
6600        );
6601
6602        weak.apply_in_wave_receive_action(action);
6603
6604        assert_eq!(
6605            run_count.get(),
6606            1,
6607            "borrowed in-wave action must still execute one fn run"
6608        );
6609        assert_eq!(
6610            observed_refs_in_fn.get(),
6611            baseline_refs,
6612            "fn execution from borrowed action must not re-count owner refs"
6613        );
6614        assert_eq!(derived.cache(), Some(4));
6615    }
6616
6617    #[test]
6618    fn stale_in_wave_receive_action_is_generation_checked() {
6619        // A collected in-wave action is still keyed by the generation-scoped weak token.
6620        // Once the slot is reused, rehydrating the stale action must be a no-op.
6621        let arena = GraphArena::new();
6622        let source = Node::<i32>::state_in_arena(&arena, 1);
6623        let run_count = Rc::new(Cell::new(0usize));
6624
6625        let (weak, old_key, action) = {
6626            let derived = Node::<i32>::derived_opts_in_arena(
6627                &arena,
6628                vec![source.erased()],
6629                NodeOpts::default(),
6630                {
6631                    let run_count = run_count.clone();
6632                    move |ctx| {
6633                        run_count.set(run_count.get() + 1);
6634                        ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
6635                    }
6636                },
6637            );
6638            let old_key = derived.core.key();
6639            let weak = derived.core.downgrade();
6640            let action = with_wave_owner(
6641                &source.core,
6642                || {
6643                    weak.collect_in_wave_receive_from_dep_action(0, &Message::Data(Rc::new(9i32)))
6644                        .expect("in-wave receive should collect a runnable action")
6645                },
6646                InWaveDepReceiveAction::default,
6647            );
6648            (weak, old_key, action)
6649        };
6650
6651        let replacement = Node::<i32>::state_in_arena(&arena, 2);
6652        assert_eq!(old_key.id, replacement.core.key().id);
6653        assert_ne!(old_key.generation, replacement.core.key().generation);
6654
6655        weak.apply_in_wave_receive_action(action);
6656
6657        assert_eq!(
6658            run_count.get(),
6659            0,
6660            "stale in-wave action must not execute after generation drift"
6661        );
6662        assert_eq!(
6663            replacement.cache(),
6664            Some(2),
6665            "reused slot should keep its own initial cache when action is stale"
6666        );
6667    }
6668
6669    #[test]
6670    fn in_flight_passthrough_decision_waits_for_delivery_boundary() {
6671        // R-diamond/D77: a single upstream msgs array may carry multiple DATA
6672        // occurrences. A no-fn passthrough must wait until the delivery boundary so
6673        // it relays the wave's latest DATA once instead of clearing its dep batch on
6674        // the first occurrence.
6675        let arena = GraphArena::new();
6676        let source = Node::<i32>::state_empty_in_arena(&arena);
6677        let wire = Node::<i32>::from_core(Core::new_in_arena(
6678            &arena,
6679            vec![source.erased()],
6680            None,
6681            default_dispatcher(),
6682            None,
6683            NodeOpts::default(),
6684        ));
6685        let seen = Rc::new(RefCell::new(Vec::<i32>::new()));
6686        let seen_sink = seen.clone();
6687        let _u = wire.subscribe(move |msg| {
6688            if let Message::Data(v) = msg {
6689                seen_sink
6690                    .borrow_mut()
6691                    .push(*v.downcast_ref::<i32>().expect("wire emits i32"));
6692            }
6693        });
6694
6695        source.down(vec![
6696            Message::Data(Rc::new(1i32)),
6697            Message::Data(Rc::new(2i32)),
6698        ]);
6699
6700        assert_eq!(
6701            seen.borrow().as_slice(),
6702            &[2],
6703            "passthrough must coalesce one upstream wave to its latest DATA"
6704        );
6705        assert_eq!(wire.cache(), Some(2));
6706    }
6707
6708    #[test]
6709    fn in_flight_pause_resume_preserves_single_wave_projection() {
6710        // C-23/D77: deciding a paused skip during receive collection used to split
6711        // a multi-DATA upstream wave if another subscriber RESUMEd the node before
6712        // the delivery finished. The in-flight path must defer the whole maybe-run
6713        // decision until the delivery boundary so ctx.wave_data keeps [1, 2] in one
6714        // projection and the fn runs once.
6715        let arena = GraphArena::new();
6716        let source = Node::<i32>::state_empty_in_arena(&arena);
6717        let lock = LockId::from("qa-in-flight-resume");
6718        let batches_seen = Rc::new(RefCell::new(Vec::<Vec<i32>>::new()));
6719        let runs = Rc::new(Cell::new(0usize));
6720        let derived: Node<i32> =
6721            Node::derived_opts_in_arena(&arena, vec![source.erased()], NodeOpts::default(), {
6722                let batches_seen = batches_seen.clone();
6723                let runs = runs.clone();
6724                move |ctx| {
6725                    runs.set(runs.get() + 1);
6726                    batches_seen
6727                        .borrow_mut()
6728                        .push(ctx.batch::<i32>(0).into_iter().map(|v| *v).collect());
6729                    ctx.emit(*ctx.data::<i32>(0).expect("latest source DATA"));
6730                }
6731            });
6732        let _derived_sub = derived.subscribe(|_| {});
6733        derived.up(vec![Message::Pause(lock.clone())]);
6734        let derived_for_resume = derived.clone();
6735        let lock_for_resume = lock.clone();
6736        let _controller = source.subscribe(move |msg| {
6737            if matches!(msg, Message::Data(_)) {
6738                derived_for_resume.up(vec![Message::Resume(lock_for_resume.clone())]);
6739            }
6740        });
6741
6742        source.down(vec![
6743            Message::Data(Rc::new(1i32)),
6744            Message::Data(Rc::new(2i32)),
6745        ]);
6746
6747        assert_eq!(runs.get(), 1, "receiver fn should run once at boundary");
6748        assert_eq!(
6749            batches_seen.borrow().as_slice(),
6750            &[vec![1, 2]],
6751            "receiver must see one upstream wave projection, not two split runs"
6752        );
6753        assert_eq!(derived.cache(), Some(2));
6754    }
6755
6756    #[test]
6757    fn internal_dep_callback_entry_out_of_wave_does_not_inflate_live_refcount() {
6758        // When the target is still publicly owned, a borrowed owner execution should be used
6759        // without increasing the temporary Core refcount visible to user callbacks.
6760        let arena = GraphArena::new();
6761        let source = Node::<i32>::state_empty_in_arena(&arena);
6762        let observed_refs = Rc::new(Cell::new(0usize));
6763        let derived: Node<i32> = Node::derived_opts_in_arena(
6764            &arena,
6765            vec![source.erased()],
6766            NodeOpts::default(),
6767            |ctx| {
6768                if let Some(v) = ctx.data::<i32>(0) {
6769                    ctx.emit(*v);
6770                }
6771            },
6772        );
6773        let weak = derived.core.downgrade();
6774
6775        let _sub = derived.subscribe({
6776            let observed_refs = observed_refs.clone();
6777            let live_refs = derived.core.refs.clone();
6778            move |msg| {
6779                if matches!(msg, Message::Data(_)) {
6780                    observed_refs.set(live_refs.get());
6781                }
6782            }
6783        });
6784        let live_refs = derived.core.refs.clone();
6785        let baseline = live_refs.get();
6786
6787        weak.receive_from_dep(0, &Message::Data(Rc::new(7i32)));
6788
6789        assert_eq!(
6790            observed_refs.get(),
6791            baseline,
6792            "out-of-wave receive should not promote/ref-bump while still borrowed by arena pin"
6793        );
6794        assert_eq!(derived.cache(), Some(7));
6795        assert_eq!(
6796            derived.core.refs.get(),
6797            baseline,
6798            "out-of-wave borrowed execution should preserve live counted handle count"
6799        );
6800    }
6801
6802    #[test]
6803    fn internal_dep_callback_entry_out_of_wave_refcount_dead_pin_stays_fail_closed() {
6804        // DR-8/B54 pin-first execution should work with refcount-dead live pins,
6805        // then fail closed once that temporary pin is released.
6806        let arena = GraphArena::new();
6807        let graph = arena.0.clone();
6808        let source = Node::<i32>::state_in_arena(&arena, 1);
6809        let derived: Node<i32> = Node::derived_opts_in_arena(
6810            &arena,
6811            vec![source.erased()],
6812            NodeOpts::default(),
6813            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6814        );
6815        let weak = derived.core.downgrade();
6816        let key = derived.core.key();
6817        let refs = derived.core.refs.clone();
6818        let borrowed = derived.core.borrowed_view();
6819        let pin = ArenaNodePin::from_core(&derived.core).expect("node must be pinnable while live");
6820
6821        drop(derived);
6822
6823        assert_eq!(
6824            refs.get(),
6825            0,
6826            "setup leaves no counted owner so this only exercises fail-closed pin behavior"
6827        );
6828        weak.receive_from_dep(0, &Message::Data(Rc::new(4i32)));
6829        assert_eq!(
6830            borrowed
6831                .cache_any()
6832                .and_then(|v| v.downcast_ref::<i32>().copied()),
6833            Some(4),
6834            "pinned borrowed receive should still execute while the slot is temporarily alive"
6835        );
6836
6837        drop(pin);
6838        assert!(
6839            !graph.borrow().is_live_key(key),
6840            "pin release should allow slot cleanup"
6841        );
6842        assert!(
6843            weak.counted_core().is_none(),
6844            "fail-closed should not revive a dead counted handle after pin release"
6845        );
6846        assert!(
6847            weak.borrowed_core().is_none(),
6848            "fail-closed should not rehydrate from dead refs with no pin"
6849        );
6850        assert!(
6851            weak.pinned_borrowed_core().is_none(),
6852            "fail-closed should not construct a borrowed pin path after release"
6853        );
6854        weak.receive_from_dep(0, &Message::Data(Rc::new(5i32)));
6855        assert_eq!(
6856            refs.get(),
6857            0,
6858            "receive from a dead slot must not resurrect refs"
6859        );
6860    }
6861
6862    #[test]
6863    fn internal_up_route_broadcast_uses_borrowed_deps() {
6864        // DR-8/B54: control fanout from an in-wave `up` path should traverse the
6865        // hot deps as borrowed arena views, so callback-visible refcounts must not
6866        // bump by one per forwarded target.
6867        let arena = GraphArena::new();
6868        let a = Node::<i32>::state_in_arena(&arena, 1);
6869        let b = Node::<i32>::state_in_arena(&arena, 2);
6870        let a_refs = a.core.refs.clone();
6871        let b_refs = b.core.refs.clone();
6872        let graph = arena.0.clone();
6873        let a_key = a.core.key();
6874        let b_key = b.core.key();
6875        let observed: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6876        let observed_pins: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6877        let _ua = a.subscribe({
6878            let observed = observed.clone();
6879            let observed_pins = observed_pins.clone();
6880            let a_refs = a_refs.clone();
6881            let graph = graph.clone();
6882            move |msg| {
6883                if matches!(msg, Message::Invalidate) {
6884                    observed.borrow_mut().push(a_refs.get());
6885                    observed_pins
6886                        .borrow_mut()
6887                        .push(graph.borrow().pin_count(a_key));
6888                }
6889            }
6890        });
6891        let _ub = b.subscribe({
6892            let observed = observed.clone();
6893            let observed_pins = observed_pins.clone();
6894            let b_refs = b_refs.clone();
6895            let graph = graph.clone();
6896            move |msg| {
6897                if matches!(msg, Message::Invalidate) {
6898                    observed.borrow_mut().push(b_refs.get());
6899                    observed_pins
6900                        .borrow_mut()
6901                        .push(graph.borrow().pin_count(b_key));
6902                }
6903            }
6904        });
6905
6906        let parent = Node::<i32>::derived_opts_in_arena(
6907            &arena,
6908            vec![a.erased(), b.erased()],
6909            NodeOpts::default(),
6910            |_| {},
6911        );
6912        let baseline = (a_refs.get(), b_refs.get());
6913        parent.up(vec![Message::Invalidate]);
6914
6915        let observed = observed.borrow();
6916        assert_eq!(
6917            observed.len(),
6918            2,
6919            "both deps should receive one in-wave INVALIDATE control wave"
6920        );
6921        assert_eq!(
6922            observed[0], baseline.0,
6923            "dep a must not pick up a temporary counted up-route ref bump"
6924        );
6925        assert_eq!(
6926            observed[1], baseline.1,
6927            "dep b must not pick up a temporary counted up-route ref bump"
6928        );
6929        assert_eq!(
6930            *observed_pins.borrow(),
6931            vec![2, 2],
6932            "in-wave borrowed up-route should keep the wave pin and add a scoped route pin around each dep callback"
6933        );
6934    }
6935
6936    #[test]
6937    fn out_of_wave_up_route_broadcast_uses_borrowed_deps() {
6938        // DR-8/B54: direct `Core::up` control routing (no owning wave) should use
6939        // borrowed dep views rather than counted clones when forwarding out-of-wave.
6940        let arena = GraphArena::new();
6941        let a = Node::<i32>::state_in_arena(&arena, 1);
6942        let b = Node::<i32>::state_in_arena(&arena, 2);
6943        let a_refs = a.core.refs.clone();
6944        let b_refs = b.core.refs.clone();
6945        let graph = arena.0.clone();
6946        let a_key = a.core.key();
6947        let b_key = b.core.key();
6948        let observed: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6949        let observed_pins: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6950        let _ua = a.subscribe({
6951            let observed = observed.clone();
6952            let observed_pins = observed_pins.clone();
6953            let a_refs = a_refs.clone();
6954            let graph = graph.clone();
6955            move |msg| {
6956                if matches!(msg, Message::Invalidate) {
6957                    observed.borrow_mut().push(a_refs.get());
6958                    observed_pins
6959                        .borrow_mut()
6960                        .push(graph.borrow().pin_count(a_key));
6961                }
6962            }
6963        });
6964        let _ub = b.subscribe({
6965            let observed = observed.clone();
6966            let observed_pins = observed_pins.clone();
6967            let b_refs = b_refs.clone();
6968            let graph = graph.clone();
6969            move |msg| {
6970                if matches!(msg, Message::Invalidate) {
6971                    observed.borrow_mut().push(b_refs.get());
6972                    observed_pins
6973                        .borrow_mut()
6974                        .push(graph.borrow().pin_count(b_key));
6975                }
6976            }
6977        });
6978
6979        let parent = Node::<i32>::derived_opts_in_arena(
6980            &arena,
6981            vec![a.erased(), b.erased()],
6982            NodeOpts::default(),
6983            |_| {},
6984        );
6985        let baseline = (a_refs.get(), b_refs.get());
6986        parent.core.up(vec![Message::Invalidate], None);
6987
6988        let observed = observed.borrow();
6989        assert_eq!(
6990            observed.len(),
6991            2,
6992            "both deps should receive one out-of-wave INVALIDATE control wave"
6993        );
6994        assert_eq!(
6995            observed[0], baseline.0,
6996            "dep a must not pick up a temporary counted up-route ref bump"
6997        );
6998        assert_eq!(
6999            observed[1], baseline.1,
7000            "dep b must not pick up a temporary counted up-route ref bump"
7001        );
7002        assert_eq!(
7003            *observed_pins.borrow(),
7004            vec![1, 1],
7005            "out-of-wave borrowed up-route should hold an arena pin around each dep callback"
7006        );
7007    }
7008
7009    #[test]
7010    fn public_wave_owner_entry_does_not_preclone_core() {
7011        // DR-8/B54 owner-execution prep: public down/up entry should enter the
7012        // wave-owner boundary with the existing owner handle instead of creating an
7013        // extra counted Core clone before the hot wave path runs. The scope itself
7014        // also stores only the owner's graph id; the touched-set remains the only
7015        // in-wave arena safety pin.
7016        let source = Node::<i32>::state_empty();
7017        let refs = source.core.refs.clone();
7018        let seen_refs = Rc::new(Cell::new(0usize));
7019        let seen = seen_refs.clone();
7020        let _u = source.subscribe(move |m| {
7021            if matches!(m, Message::Data(_)) {
7022                seen.set(refs.get());
7023            }
7024        });
7025
7026        source.set(1);
7027
7028        assert_eq!(
7029            seen_refs.get(),
7030            2,
7031            "expected source handle + unsubscribe handle; wave-owner/touched arena pins must not increment Core refs"
7032        );
7033    }
7034
7035    #[test]
7036    fn touched_arena_pin_defers_slot_free_without_core_refcount_pin() {
7037        // DR-8/B54 owner-execution: a touched node is pinned by graph arena slot,
7038        // not by a counted Core clone. If the last public handle drops mid-wave,
7039        // cleanup/free waits until the arena pin releases at the wave boundary.
7040        let arena = GraphArena::new();
7041        let graph = arena.0.clone();
7042        let owner = Node::<i32>::state_in_arena(&arena, 0);
7043        let victim = Node::<i32>::state_in_arena(&arena, 1);
7044        let victim_key = victim.core.key();
7045        let victim_refs = victim.core.refs.clone();
7046        let graph_in_wave = graph.clone();
7047
7048        with_wave_owner(
7049            &owner.core,
7050            move || {
7051                assert_eq!(victim_refs.get(), 1);
7052                wave_register(&victim.core);
7053                assert_eq!(
7054                    victim_refs.get(),
7055                    1,
7056                    "arena touched pin must not increment counted Core refs"
7057                );
7058                drop(victim);
7059                assert_eq!(
7060                    victim_refs.get(),
7061                    0,
7062                    "dropping the final public handle marks the slot externally dead"
7063                );
7064                assert!(
7065                    graph_in_wave.borrow().is_live_key(victim_key),
7066                    "arena pin keeps the touched slot live until wave exit"
7067                );
7068            },
7069            || {},
7070        );
7071
7072        assert!(
7073            !graph.borrow().is_live_key(victim_key),
7074            "wave exit releases the arena pin and frees the externally-dead slot"
7075        );
7076        let reused = Node::<i32>::state_in_arena(&arena, 2);
7077        assert_eq!(victim_key.id, reused.core.key().id);
7078        assert_ne!(victim_key.generation, reused.core.key().generation);
7079    }
7080
7081    #[test]
7082    fn touched_arena_pin_dedupes_same_node_within_wave() {
7083        // DR-8/B54 owner-execution: repeated callbacks to one node in a single wave
7084        // need one arena lifetime pin + one unwind-reset entry. DIRTY, DATA, and
7085        // run_wave may all touch the same node; duplicate pins only add hot-path tax.
7086        let arena = GraphArena::new();
7087        let graph = arena.0.clone();
7088        let owner = Node::<i32>::state_in_arena(&arena, 0);
7089        let victim = Node::<i32>::state_in_arena(&arena, 1);
7090        let victim_key = victim.core.key();
7091        let victim_refs = victim.core.refs.clone();
7092        let graph_in_wave = graph.clone();
7093
7094        with_wave_owner(
7095            &owner.core,
7096            move || {
7097                wave_register(&victim.core);
7098                wave_register(&victim.core);
7099                wave_register(&victim.core);
7100                WAVE.with(|w| {
7101                    let wave = w.borrow();
7102                    let scope = wave.as_ref().expect("wave installed");
7103                    assert_eq!(scope.touched.len(), 1);
7104                });
7105                assert_eq!(
7106                    graph_in_wave.borrow().pin_count(victim_key),
7107                    1,
7108                    "duplicate touches should share one arena pin"
7109                );
7110                drop(victim);
7111                assert_eq!(
7112                    victim_refs.get(),
7113                    0,
7114                    "the deduped arena pin still allows external refs to reach zero"
7115                );
7116                assert!(
7117                    graph_in_wave.borrow().is_live_key(victim_key),
7118                    "one deduped pin must still keep the slot live through wave exit"
7119                );
7120            },
7121            || {},
7122        );
7123
7124        assert!(
7125            !graph.borrow().is_live_key(victim_key),
7126            "deduped pin releases at wave exit and frees externally-dead slot"
7127        );
7128    }
7129
7130    #[test]
7131    fn touched_arena_pins_release_before_committed_boundary_drain() {
7132        // R-rewire-deferred/D47 boundary tasks are fresh waves. A touched node whose
7133        // final public handle dropped during the prior wave must be cleaned up before
7134        // queued boundary tasks run, not kept alive until after the drain.
7135        let arena = GraphArena::new();
7136        let graph = arena.0.clone();
7137        let owner = Node::<i32>::state_in_arena(&arena, 1);
7138        let victim = Node::<i32>::state_in_arena(&arena, 2);
7139        let victim_key = victim.core.key();
7140        let saw_boundary = Rc::new(Cell::new(false));
7141        let saw = saw_boundary.clone();
7142        let graph_for_sink = graph.clone();
7143        let _u = owner.subscribe(move |m| {
7144            if matches!(m, Message::Invalidate) {
7145                saw.set(true);
7146                assert!(
7147                    !graph_for_sink.borrow().is_live_key(victim_key),
7148                    "touched pins must release before committed-boundary tasks run"
7149                );
7150            }
7151        });
7152        let owner_core = owner.core.clone();
7153
7154        with_wave_owner(
7155            &owner.core,
7156            move || {
7157                wave_register(&victim.core);
7158                drop(victim);
7159                owner_core.request_up_next(vec![Message::Invalidate], None);
7160            },
7161            || {},
7162        );
7163
7164        assert!(saw_boundary.get(), "queued boundary INVALIDATE should run");
7165        let reused = Node::<i32>::state_in_arena(&arena, 3);
7166        assert_eq!(victim_key.id, reused.core.key().id);
7167        assert_ne!(victim_key.generation, reused.core.key().generation);
7168    }
7169
7170    #[test]
7171    fn run_wave_ctx_borrows_core_until_defer_promotes() {
7172        // DR-8/B54 delivery-waist follow-up: a synchronous fn-body Ctx is used only
7173        // while the wave touched-set already pins the owner in the arena, so it can be
7174        // an uncounted arena view. The async escape hatch ctx.defer() must promote to a
7175        // counted Core because DeferredCtx can outlive the wave.
7176        let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
7177        let sync_refs = Rc::new(Cell::new(0usize));
7178        let deferred_refs = Rc::new(Cell::new(0usize));
7179        let deferred_ctx: Rc<RefCell<Option<crate::ctx::DeferredCtx>>> =
7180            Rc::new(RefCell::new(None));
7181
7182        let p = Node::<i32>::producer({
7183            let refs_slot = refs_slot.clone();
7184            let sync_refs = sync_refs.clone();
7185            let deferred_refs = deferred_refs.clone();
7186            let deferred_ctx = deferred_ctx.clone();
7187            move |ctx| {
7188                let refs = refs_slot
7189                    .borrow()
7190                    .as_ref()
7191                    .expect("test installed ref counter before activation")
7192                    .clone();
7193                sync_refs.set(refs.get());
7194                *deferred_ctx.borrow_mut() = Some(ctx.defer());
7195                deferred_refs.set(refs.get());
7196                ctx.emit(1i32);
7197            }
7198        });
7199        *refs_slot.borrow_mut() = Some(p.core.refs.clone());
7200
7201        let _u = p.subscribe(|_| {});
7202
7203        assert_eq!(
7204            sync_refs.get(),
7205            1,
7206            "expected only the public handle; subscribe owner execution, touched arena pin, and Ctx itself must not count"
7207        );
7208        assert_eq!(
7209            deferred_refs.get(),
7210            2,
7211            "ctx.defer() promotes the borrowed fn-body Ctx to a counted async handle"
7212        );
7213        assert_eq!(
7214            p.core.refs.get(),
7215            3,
7216            "after activation, public handle + unsubscribe handle + stashed DeferredCtx remain"
7217        );
7218        drop(deferred_ctx.borrow_mut().take());
7219        assert_eq!(
7220            p.core.refs.get(),
7221            2,
7222            "dropping the deferred async handle releases its counted Core pin"
7223        );
7224    }
7225
7226    #[test]
7227    fn run_state_slot_resets_on_deactivation_while_call_slot_persists() {
7228        // DR-8/B54 next slice: first-run lifecycle state lives in GraphCore run_slots,
7229        // while the dispatcher handle lives in call_slots. Deactivation must re-arm the
7230        // producer without unregistering/re-registering its callable.
7231        let runs = Rc::new(Cell::new(0usize));
7232        let p = Node::<i32>::producer({
7233            let runs = runs.clone();
7234            move |ctx| {
7235                runs.set(runs.get() + 1);
7236                ctx.emit(runs.get() as i32);
7237            }
7238        });
7239        let handle_before = p.core.handle().expect("producer registered a call slot");
7240
7241        let u = {
7242            let u = p.subscribe(|_| {});
7243            assert_eq!(runs.get(), 1);
7244            assert!(p
7245                .core
7246                .with_node_state(|_n, _c, _cfg, r, _e| r.has_called_fn_once));
7247            u
7248        };
7249        u();
7250
7251        assert!(
7252            !p.core
7253                .with_node_state(|_n, _c, _cfg, r, _e| r.has_called_fn_once),
7254            "deactivation resets run_slots.has_called_fn_once"
7255        );
7256        assert_eq!(
7257            p.core.handle(),
7258            Some(handle_before),
7259            "deactivation keeps the call slot registered for the next lifecycle"
7260        );
7261
7262        let _u2 = p.subscribe(|_| {});
7263        assert_eq!(
7264            runs.get(),
7265            2,
7266            "run state reset re-arms the depless producer on the next activation"
7267        );
7268        assert_eq!(p.cache(), Some(2));
7269    }
7270
7271    #[test]
7272    fn config_slot_partial_gate_runs_with_sentinel_dep() {
7273        // DR-8/B54 next slice: first-run config moved out of NodeInner. The partial
7274        // flag still opens the gate for a SENTINEL dep once the node is asked to run;
7275        // without config-slot wiring this derived node would wait for source DATA.
7276        let source = Node::<i32>::state_empty();
7277        let d: Node<i32> = Node::derived_opts(
7278            vec![source.erased()],
7279            NodeOpts {
7280                partial: true,
7281                ..NodeOpts::default()
7282            },
7283            |ctx| {
7284                assert!(ctx.data::<i32>(0).is_none());
7285                ctx.emit(7i32);
7286            },
7287        );
7288        assert!(d.core.with_config(|cfg| cfg.partial));
7289
7290        let _u = d.subscribe(|_| {});
7291        d.core.try_run();
7292
7293        assert_eq!(d.cache(), Some(7));
7294        assert!(d
7295            .core
7296            .with_node_state(|_n, _c, _cfg, r, _e| r.has_called_fn_once));
7297    }
7298
7299    #[test]
7300    fn reachable_upstream_uses_arena_keys_without_refcount_churn() {
7301        // DR-8/B54 retained-topology safe subset: the cycle-prevention DFS should walk
7302        // graph-owned generation keys from the topology side-table without allocating
7303        // extra counted Core clones for traversal.
7304        let a = Node::<i32>::state(1);
7305        let b: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7306            ctx.emit(*ctx.data::<i32>(0).unwrap())
7307        });
7308        let c: Node<i32> = Node::derived(vec![b.erased()], |ctx| {
7309            ctx.emit(*ctx.data::<i32>(0).unwrap())
7310        });
7311        let before = (a.core.refs.get(), b.core.refs.get(), c.core.refs.get());
7312
7313        assert!(reachable_upstream(&c.core, &a.core));
7314        assert!(!reachable_upstream(&a.core, &c.core));
7315
7316        assert_eq!(
7317            (a.core.refs.get(), b.core.refs.get(), c.core.refs.get()),
7318            before,
7319            "reachable_upstream must not allocate extra counted Core clones for traversal"
7320        );
7321    }
7322
7323    #[test]
7324    fn reachable_upstream_stale_key_fails_closed() {
7325        // DR-8/B54 fail-closed safety: stale/liveness-checked keys in cycle-prevention DFS
7326        // should be inert and never follow dead arena slots.
7327        let arena = GraphArena::new();
7328        let graph = arena.0.clone();
7329
7330        let victim = Node::<i32>::state_in_arena(&arena, 1);
7331        let victim_key = victim.core.key();
7332        drop(victim);
7333
7334        let reused = Node::<i32>::state_in_arena(&arena, 2);
7335        assert_eq!(
7336            victim_key.id,
7337            reused.core.key().id,
7338            "slot should be reused so stale generation is visible"
7339        );
7340        assert_ne!(
7341            victim_key.generation,
7342            reused.core.key().generation,
7343            "reused slot must advance generation"
7344        );
7345        assert!(
7346            !graph.borrow().is_live_key(victim_key),
7347            "old generation key is dead after slot reuse"
7348        );
7349
7350        let stale_source = Core::from_borrowed_parts(
7351            graph.clone(),
7352            victim_key.id,
7353            victim_key.generation,
7354            Rc::new(Cell::new(0)),
7355        );
7356        assert!(
7357            !reachable_upstream(&stale_source, &reused.core),
7358            "stale source key should never reach a live target"
7359        );
7360        assert!(
7361            !reachable_upstream(&reused.core, &stale_source),
7362            "stale target key should never be considered reachable"
7363        );
7364        assert!(
7365            !reachable_upstream(&stale_source, &stale_source),
7366            "stale source and target keys should still fail closed"
7367        );
7368    }
7369
7370    #[test]
7371    fn topology_slot_retains_inactive_declared_deps() {
7372        // DR-8/B54 final topology contract: declared deps are graph-owned topology
7373        // until explicit rewire/remove/drop. An inactive derived node must not lose
7374        // an edge merely because the caller drops the original dep handle.
7375        let arena = GraphArena::new();
7376        let source = Node::<i32>::state_in_arena(&arena, 1);
7377        let source_key = source.core.key();
7378        let refs = source.core.refs.clone();
7379        let derived: Node<i32> = Node::derived_opts_in_arena(
7380            &arena,
7381            vec![source.erased()],
7382            NodeOpts::default(),
7383            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
7384        );
7385
7386        assert_eq!(
7387            refs.get(),
7388            2,
7389            "inactive topology retains the declared dep in addition to the caller handle"
7390        );
7391        assert_eq!(derived.core.borrow().deps.len(), 1);
7392        drop(source);
7393
7394        assert!(
7395            arena.0.borrow().is_live_key(source_key),
7396            "declared topology keeps the dep live after the original handle is dropped"
7397        );
7398        assert_eq!(refs.get(), 1);
7399        assert_eq!(derived.core.deps().len(), 1);
7400
7401        let u = derived.subscribe(|_| {});
7402        assert_eq!(
7403            derived.cache(),
7404            Some(1),
7405            "activation must still subscribe the retained declared dep"
7406        );
7407        u();
7408        drop(derived);
7409        assert!(
7410            !arena.0.borrow().is_live_key(source_key),
7411            "dropping the owner topology releases the last retained dep handle"
7412        );
7413        assert_eq!(refs.get(), 0);
7414    }
7415
7416    #[test]
7417    fn activation_subscribes_retained_topology_deps() {
7418        // Once activated, the subscription edge owns an unsubscribe closure that keeps
7419        // the dep alive until deactivation. Topology itself also retains the declared edge.
7420        let arena = GraphArena::new();
7421        let source = Node::<i32>::state_in_arena(&arena, 1);
7422        let refs = source.core.refs.clone();
7423        let derived: Node<i32> = Node::derived_opts_in_arena(
7424            &arena,
7425            vec![source.erased()],
7426            NodeOpts::default(),
7427            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap() + 1),
7428        );
7429        assert_eq!(refs.get(), 2);
7430
7431        let u = derived.subscribe(|_| {});
7432        assert!(
7433            refs.get() > 2,
7434            "activation should add a subscription-owned counted dep handle"
7435        );
7436        assert_eq!(derived.cache(), Some(2));
7437        u();
7438        assert_eq!(
7439            refs.get(),
7440            2,
7441            "deactivation releases only the subscription-owned dep handle"
7442        );
7443    }
7444
7445    #[test]
7446    fn borrowed_core_cannot_promote_after_external_death() {
7447        // DR-8/B54 fail-closed lifetime rule: an arena pin may keep a slot live for
7448        // the current synchronous wave after the last public handle drops, but a
7449        // borrowed Core must not promote that externally-dead slot into a new counted
7450        // handle.
7451        let arena = GraphArena::new();
7452        let graph = arena.0.clone();
7453        let victim = Node::<i32>::state_in_arena(&arena, 1);
7454        let victim_key = victim.core.key();
7455        let victim_refs = victim.core.refs.clone();
7456        let pin = ArenaNodePin::from_core(&victim.core).expect("live slot pins");
7457        let borrowed = pin.borrowed_core().expect("pin can create borrowed view");
7458        drop(victim);
7459
7460        assert_eq!(victim_refs.get(), 0);
7461        assert!(
7462            graph.borrow().is_live_key(victim_key),
7463            "arena pin keeps the externally-dead slot live only for the current wave"
7464        );
7465        let result = catch_unwind(AssertUnwindSafe(|| {
7466            let _revived = borrowed.clone();
7467        }));
7468        assert!(
7469            result.is_err(),
7470            "borrowed Core clone must fail closed instead of reviving refs from zero"
7471        );
7472        drop(borrowed);
7473        drop(pin);
7474        assert!(!graph.borrow().is_live_key(victim_key));
7475        let reused = Node::<i32>::state_in_arena(&arena, 2);
7476        assert_eq!(victim_key.id, reused.core.key().id);
7477        assert_ne!(victim_key.generation, reused.core.key().generation);
7478    }
7479
7480    #[test]
7481    fn boundary_owner_actions_do_not_pin_core_while_queued() {
7482        // DR-8/B54: `ctx.up_next` / `ctx.rewire_next` boundary tasks store a
7483        // generation-checked owner token while queued, not a counted owner `Core`.
7484        // The drain upgrades to a counted handle only for execution, preserving
7485        // callback/drop safety without retaining the owner for the queue lifetime.
7486        let source = Node::<i32>::state(1);
7487        let refs = source.core.refs.clone();
7488
7489        with_wave_owner(
7490            &source.core,
7491            || {
7492                let owner_refs = refs.get();
7493                source.core.request_up_next(vec![Message::Dirty], None);
7494                assert_eq!(
7495                    refs.get(),
7496                    owner_refs,
7497                    "queued up_next stores an owner token, not a counted Core"
7498                );
7499
7500                source
7501                    .core
7502                    .request_rewire_next(RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(2i32))));
7503                assert_eq!(
7504                    refs.get(),
7505                    owner_refs,
7506                    "queued rewire_next stores an owner token, not a counted Core"
7507                );
7508            },
7509            || {},
7510        );
7511
7512        assert_eq!(
7513            refs.get(),
7514            1,
7515            "boundary execution releases its temporary counted handle"
7516        );
7517    }
7518
7519    #[test]
7520    fn boundary_up_task_executes_with_borrowed_pinned_owner() {
7521        // DR-8/B54: committed-boundary owner actions execute via an arena pin +
7522        // borrowed Core view. The queue still does not retain the owner, but the drain
7523        // also avoids a temporary counted promotion while running the task.
7524        let source = Node::<i32>::state(1);
7525        let refs = source.core.refs.clone();
7526        let seen_refs = Rc::new(Cell::new(usize::MAX));
7527        let seen = seen_refs.clone();
7528        let refs_for_sink = refs.clone();
7529        let _u = source.subscribe(move |m| {
7530            if matches!(m, Message::Invalidate) {
7531                seen.set(refs_for_sink.get());
7532            }
7533        });
7534        let refs_before = refs.get();
7535
7536        with_wave_owner(
7537            &source.core,
7538            || source.core.request_up_next(vec![Message::Invalidate], None),
7539            || {},
7540        );
7541
7542        assert_eq!(
7543            seen_refs.get(),
7544            refs_before,
7545            "boundary up task should not add a counted owner Core while executing"
7546        );
7547        assert_eq!(refs.get(), refs_before);
7548    }
7549
7550    #[test]
7551    fn boundary_rewire_task_executes_with_borrowed_pinned_owner() {
7552        // Same owner-execution invariant for the rewire boundary task. The added cached
7553        // dep drives the queued fn during boundary drain, where old code had both a
7554        // task-level counted promotion and a rewire_inner pre-clone.
7555        let a = Node::<i32>::state(1);
7556        let b = Node::<i32>::state(10);
7557        let observed_refs = Rc::new(Cell::new(usize::MAX));
7558        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7559            ctx.emit(*ctx.data::<i32>(0).unwrap())
7560        });
7561        let _u = d.subscribe(|_| {});
7562        let refs = d.core.refs.clone();
7563        let refs_before = refs.get();
7564
7565        with_wave_owner(
7566            &d.core,
7567            || {
7568                let observed_refs = observed_refs.clone();
7569                let refs = refs.clone();
7570                d.core.request_rewire_next(RewireRequest::Set(
7571                    vec![b.erased()],
7572                    Rc::new(move |ctx| {
7573                        observed_refs.set(refs.get());
7574                        ctx.emit(*ctx.data::<i32>(0).unwrap());
7575                    }),
7576                ));
7577            },
7578            || {},
7579        );
7580
7581        assert_eq!(
7582            observed_refs.get(),
7583            refs_before,
7584            "boundary rewire task should run without counted owner promotions"
7585        );
7586        assert_eq!(refs.get(), refs_before);
7587        assert_eq!(d.cache(), Some(10));
7588    }
7589
7590    #[test]
7591    fn deferred_rewire_remove_does_not_pin_removed_dep_while_queued() {
7592        // DR-8/B54: a queued remove only needs dep identity. Add/Set must retain
7593        // runtime-created deps until the boundary, but Remove should not add another
7594        // counted dep pin while waiting to apply.
7595        let dep = Node::<i32>::state(1);
7596        let derived: Node<i32> = Node::derived(vec![dep.erased()], |ctx| {
7597            ctx.emit(*ctx.data::<i32>(0).unwrap())
7598        });
7599        let _u = derived.subscribe(|_| {});
7600        let refs = dep.core.refs.clone();
7601
7602        with_wave_owner(
7603            &derived.core,
7604            || {
7605                let before = refs.get();
7606                derived
7607                    .core
7608                    .request_rewire_next(RewireRequest::remove(&dep.core, Rc::new(|_| {})));
7609                assert_eq!(
7610                    refs.get(),
7611                    before,
7612                    "queued remove stores dep identity, not a counted dep Core"
7613                );
7614            },
7615            || {},
7616        );
7617
7618        assert!(
7619            derived.core.deps().is_empty(),
7620            "queued remove still applies at the committed boundary"
7621        );
7622    }
7623
7624    #[test]
7625    fn core_identity_does_not_match_reused_slot_generation() {
7626        // CoreIdentity is used by queued remove requests: it must be graph+generation
7627        // identity, not just a slot id, or a stale remove could target a later occupant.
7628        let arena = GraphArena::new();
7629        let old = Node::<i32>::state_in_arena(&arena, 1);
7630        let old_identity = CoreIdentity::from_core(&old.core);
7631        let old_key = old.core.key();
7632        drop(old);
7633
7634        let reused = Node::<i32>::state_in_arena(&arena, 2);
7635        assert_eq!(old_key.id, reused.core.key().id);
7636        assert_ne!(old_key.generation, reused.core.key().generation);
7637        assert!(
7638            !old_identity.matches(&reused.core),
7639            "stale identity must not match a reused arena slot"
7640        );
7641    }
7642
7643    #[test]
7644    fn queued_remove_identity_noops_after_prior_remove_and_preserves_later_add() {
7645        // Multiple ctx.rewire_next requests compose at apply time. A later remove of
7646        // an already-removed dep must be a no-op and must not remove a replacement dep
7647        // that was added in between.
7648        let old = Node::<i32>::state(1);
7649        let replacement = Node::<i32>::state(2);
7650        let derived: Node<i32> = Node::derived(vec![old.erased()], |ctx| {
7651            ctx.emit(*ctx.data::<i32>(0).unwrap())
7652        });
7653        let _u = derived.subscribe(|_| {});
7654
7655        with_wave_owner(
7656            &derived.core,
7657            || {
7658                derived
7659                    .core
7660                    .request_rewire_next(RewireRequest::remove(&old.core, Rc::new(|_| {})));
7661                derived.core.request_rewire_next(RewireRequest::Add(
7662                    replacement.erased(),
7663                    Rc::new(|ctx| {
7664                        if let Some(v) = ctx.data::<i32>(0) {
7665                            ctx.emit(*v);
7666                        }
7667                    }),
7668                ));
7669                derived
7670                    .core
7671                    .request_rewire_next(RewireRequest::remove(&old.core, Rc::new(|_| {})));
7672            },
7673            || {},
7674        );
7675
7676        let deps = derived.core.deps();
7677        assert_eq!(deps.len(), 1);
7678        assert!(
7679            deps[0].ptr_eq(&replacement.core),
7680            "second stale remove must not disturb the replacement dep"
7681        );
7682        assert_eq!(
7683            derived.cache(),
7684            Some(2),
7685            "replacement add should install the replacement fn"
7686        );
7687
7688        replacement.set(3);
7689        assert_eq!(
7690            derived.cache(),
7691            Some(3),
7692            "stale remove must not swap in its stale fn after the replacement add"
7693        );
7694    }
7695
7696    #[test]
7697    fn stale_boundary_owner_action_does_not_target_reused_slot() {
7698        // A queued boundary action may outlive its owner if the owner is dropped before a
7699        // later explicit drain. The weak owner token + generation check must turn that
7700        // action into a no-op, never an operation on a later node that reused the slot.
7701        let arena = GraphArena::new();
7702        let old = Node::<i32>::state_in_arena(&arena, 1);
7703        let old_key = old.core.key();
7704        old.core.request_up_next(vec![Message::Invalidate], None);
7705        drop(old);
7706
7707        let reused = Node::<i32>::state_in_arena(&arena, 2);
7708        assert_eq!(old_key.id, reused.core.key().id);
7709        assert_ne!(old_key.generation, reused.core.key().generation);
7710
7711        drain_committed_boundary(&reused.core);
7712
7713        assert_eq!(reused.status(), Status::Settled);
7714        assert_eq!(
7715            reused.cache(),
7716            Some(2),
7717            "stale queued owner action must not invalidate the reused slot occupant"
7718        );
7719    }
7720
7721    #[test]
7722    fn nested_cross_arena_boundary_task_drains_at_outer_committed_boundary() {
7723        // Nested public calls into another GraphArena share the thread-local WAVE scope.
7724        // A boundary task queued by the inner arena must still drain at the outer
7725        // committed boundary, not wait for a later wave in that inner arena.
7726        let arena_a = GraphArena::new();
7727        let arena_b = GraphArena::new();
7728        let outer = Node::<i32>::state_empty_in_arena(&arena_a);
7729        let inner_src = Node::<i32>::state_empty_in_arena(&arena_b);
7730        let inner_dep = inner_src.erased();
7731        let replacement_dep = inner_dep.clone();
7732        let inner: Node<i32> =
7733            Node::derived_opts_in_arena(&arena_b, vec![inner_dep.clone()], NodeOpts::default(), {
7734                let replacement_dep = replacement_dep.clone();
7735                move |ctx| {
7736                    ctx.rewire_next_replace_deps(vec![replacement_dep.clone()], |next| {
7737                        next.emit(*next.data::<i32>(0).unwrap() * 10)
7738                    });
7739                    ctx.emit(*ctx.data::<i32>(0).unwrap());
7740                }
7741            });
7742        let _inner_sub = inner.subscribe(|_| {});
7743        let inner_src_for_outer = inner_src.clone();
7744        let _outer_sub = outer.subscribe(move |m| {
7745            if matches!(m, Message::Data(_)) {
7746                inner_src_for_outer.set(1);
7747            }
7748        });
7749
7750        outer.set(1);
7751        assert_eq!(inner.cache(), Some(1));
7752
7753        inner_src.set(2);
7754        assert_eq!(
7755            inner.cache(),
7756            Some(20),
7757            "inner arena rewire_next must have drained before the next inner wave"
7758        );
7759    }
7760
7761    #[test]
7762    fn batch_boundary_root_tracking_does_not_pin_queued_owner() {
7763        // DR-8/B54: batch needs to remember which graph queues boundary work, but that
7764        // graph-root bookkeeping must not retain the node owner. Owner liveness remains
7765        // exclusively in the queued task's weak CoreToken.
7766        let source = Node::<i32>::state(1);
7767        let refs = source.core.refs.clone();
7768
7769        crate::batch::batch(|_| {
7770            let before = refs.get();
7771            source.core.request_up_next(vec![Message::Dirty], None);
7772            assert_eq!(
7773                refs.get(),
7774                before,
7775                "batch boundary root is a weak graph root, not a counted Core"
7776            );
7777        });
7778
7779        assert_eq!(refs.get(), 1);
7780    }
7781
7782    #[test]
7783    fn batch_deferred_external_rewire_does_not_pin_owner_while_queued() {
7784        // D67 queues an external rewire until after the target's batched settle commits.
7785        // DR-8/B54: that queued owner action uses a weak generation token, not a counted
7786        // owner Core captured by a generic thunk.
7787        let a = Node::<i32>::state(1);
7788        let b = Node::<i32>::state(2);
7789        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7790            ctx.emit(*ctx.data::<i32>(0).unwrap())
7791        });
7792        let _u = d.subscribe(|_| {});
7793
7794        crate::batch::batch(|_| {
7795            a.set(3);
7796            let refs_after_batched_settle = d.core.refs.get();
7797            d.replace_deps(vec![b.erased()], |ctx| {
7798                ctx.emit(*ctx.data::<i32>(0).unwrap() * 10)
7799            });
7800            assert_eq!(
7801                d.core.refs.get(),
7802                refs_after_batched_settle,
7803                "queued batch-deferred external rewire must not retain a counted owner Core"
7804            );
7805        });
7806
7807        assert_eq!(
7808            d.cache(),
7809            Some(20),
7810            "batch-deferred external rewire still drains at the committed boundary"
7811        );
7812    }
7813
7814    #[test]
7815    fn batch_deferred_subscribe_dep_returns_intended_index_while_queued() {
7816        let a = Node::<i32>::state(1);
7817        let b = Node::<i32>::state(2);
7818        let c = Node::<i32>::state(3);
7819        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7820            ctx.emit(*ctx.data::<i32>(0).unwrap())
7821        });
7822        let _u = d.subscribe(|_| {});
7823
7824        crate::batch::batch(|_| {
7825            d.down(vec![Message::Data(Rc::new(5i32))]);
7826            let index = d.subscribe_dep(b.erased(), |ctx| {
7827                ctx.emit(*ctx.data::<i32>(1).unwrap() * 10)
7828            });
7829            assert_eq!(
7830                index, 1,
7831                "queued subscribe_dep returns the requested next-shape index"
7832            );
7833            let c_index = d.subscribe_dep(c.erased(), |ctx| {
7834                ctx.emit(*ctx.data::<i32>(2).unwrap() * 100)
7835            });
7836            assert_eq!(
7837                c_index, 2,
7838                "queued subscribe_dep composes with earlier queued external rewire intents"
7839            );
7840            assert_eq!(
7841                d.core.deps().len(),
7842                1,
7843                "D67 keeps the live dep shape old until batch commit"
7844            );
7845        });
7846
7847        assert_eq!(
7848            d.cache(),
7849            Some(300),
7850            "old-shape batch commit precedes fresh subscribeDep boundary waves in FIFO order"
7851        );
7852        assert_eq!(
7853            d.core.deps().len(),
7854            3,
7855            "queued subscribeDep requests compose instead of replacing one another"
7856        );
7857    }
7858
7859    #[test]
7860    fn batch_deferred_subscribe_then_unsubscribe_composes_while_queued() {
7861        let a = Node::<i32>::state(1);
7862        let b = Node::<i32>::state(2);
7863        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7864            ctx.emit(*ctx.data::<i32>(0).unwrap())
7865        });
7866        let _u = d.subscribe(|_| {});
7867
7868        crate::batch::batch(|_| {
7869            d.down(vec![Message::Data(Rc::new(5i32))]);
7870            d.subscribe_dep(b.erased(), |ctx| {
7871                ctx.emit(*ctx.data::<i32>(1).unwrap() * 10)
7872            });
7873            d.unsubscribe_dep(b.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
7874            assert_eq!(
7875                d.core.deps().len(),
7876                1,
7877                "D67 keeps the live dep shape old until batch commit"
7878            );
7879        });
7880
7881        assert_eq!(
7882            d.core.deps().len(),
7883            1,
7884            "queued unsubscribeDep composes with an earlier queued subscribeDep"
7885        );
7886        let before_removed_dep_set = d.cache();
7887        b.set(9);
7888        assert_eq!(
7889            d.cache(),
7890            before_removed_dep_set,
7891            "removed queued dep no longer drives"
7892        );
7893        a.set(6);
7894        assert_eq!(
7895            d.cache(),
7896            Some(6),
7897            "original dep remains live after composed drain"
7898        );
7899    }
7900
7901    #[test]
7902    fn batch_boundary_task_executes_after_last_public_owner_drops() {
7903        // DR-8/B54: a queued boundary task can run while the public handle is gone,
7904        // as long as the batch target pin keeps the arena slot live during commit.
7905        let arena = GraphArena::new();
7906        let graph = arena.0.clone();
7907        let dep = Node::<i32>::state_in_arena(&arena, 10);
7908        let source = Node::<i32>::state_in_arena(&arena, 1);
7909        source.core.activate();
7910        let key = source.core.key();
7911        let refs = source.core.refs.clone();
7912        let seen_refs = Rc::new(Cell::new(usize::MAX));
7913
7914        crate::batch::batch({
7915            let dep = dep.erased();
7916            let graph = graph.clone();
7917            let refs = refs.clone();
7918            let seen_refs = seen_refs.clone();
7919            move |_| {
7920                let owned = source;
7921                owned.set(2);
7922                owned.replace_deps(vec![dep], {
7923                    let refs = refs.clone();
7924                    let seen_refs = seen_refs.clone();
7925                    move |ctx| {
7926                        seen_refs.set(refs.get());
7927                        ctx.emit(*ctx.data::<i32>(0).expect("cached added dep data"));
7928                    }
7929                });
7930                drop(owned);
7931                assert_eq!(
7932                    refs.get(),
7933                    0,
7934                    "batch-deferred external rewire owner can drop to zero while queued"
7935                );
7936                assert!(
7937                    graph.borrow().pin_count(key) >= 1,
7938                    "batch target pin keeps the arena slot live during commit"
7939                );
7940                assert!(
7941                    graph.borrow().is_live_key(key),
7942                    "batch target pin keeps slot live during committed-boundary drain"
7943                );
7944            }
7945        });
7946
7947        assert_eq!(
7948            seen_refs.get(),
7949            0,
7950            "batch-deferred external rewire executes from borrowed pin when refs reach zero"
7951        );
7952        assert!(
7953            !graph.borrow().is_live_key(key),
7954            "batch pin should release when commit finishes"
7955        );
7956    }
7957
7958    #[test]
7959    fn defer_to_batch_does_not_increment_refs() {
7960        let source = Node::<i32>::state(1);
7961        let refs = source.core.refs.clone();
7962        let before = refs.get();
7963
7964        crate::batch::batch(|_| {
7965            source.set(2);
7966            source.set(3);
7967            source.set(4);
7968            assert_eq!(
7969                refs.get(),
7970                before,
7971                "defer_to_batch should use generation-checked batch pins, not counted Core handles"
7972            );
7973        });
7974
7975        assert_eq!(
7976            refs.get(),
7977            before,
7978            "collecting batch should not permanently retain extra refs"
7979        );
7980        assert_eq!(source.cache(), Some(4));
7981    }
7982
7983    #[test]
7984    fn dropping_last_public_handle_inside_batch_keeps_slot_live_for_commit_and_rollback() {
7985        let arena = GraphArena::new();
7986        let graph = arena.0.clone();
7987
7988        {
7989            let source = Node::<i32>::state_in_arena(&arena, 0);
7990            let refs = source.core.refs.clone();
7991            let key = source.core.key();
7992            let initial_refs = refs.get();
7993            assert_eq!(initial_refs, 1);
7994
7995            crate::batch::batch({
7996                let graph = graph.clone();
7997                let refs = refs.clone();
7998                move |_| {
7999                    let owned = source;
8000                    owned.set(1);
8001                    drop(owned);
8002
8003                    assert_eq!(
8004                        refs.get(),
8005                        0,
8006                        "explicitly dropping the public handle should leave no counted owners"
8007                    );
8008                    assert!(
8009                        graph.borrow().is_live_key(key),
8010                        "arena pin keeps the node slot live for open batch finish"
8011                    );
8012                }
8013            });
8014
8015            assert_eq!(refs.get(), 0);
8016            assert!(
8017                !graph.borrow().is_live_key(key),
8018                "batch pin should release when commit finishes"
8019            );
8020        };
8021
8022        let source = Node::<i32>::state_in_arena(&arena, 0);
8023        let refs = source.core.refs.clone();
8024        let key = source.core.key();
8025        crate::batch::batch({
8026            let graph = graph.clone();
8027            let refs = refs.clone();
8028            move |bctx| {
8029                let owned = source;
8030                owned.set(2);
8031                drop(owned);
8032
8033                assert_eq!(
8034                    refs.get(),
8035                    0,
8036                    "explicit rollback should not restore a counted batch owner reference"
8037                );
8038                assert!(
8039                    graph.borrow().is_live_key(key),
8040                    "rollback also runs with a pinned slot while the batch remains open"
8041                );
8042                bctx.rollback();
8043            }
8044        });
8045
8046        assert_eq!(refs.get(), 0);
8047        assert!(
8048            !graph.borrow().is_live_key(key),
8049            "batch pin should release after rollback too"
8050        );
8051    }
8052
8053    #[test]
8054    fn batch_target_stale_generation_does_not_borrow_reused_slot() {
8055        let arena = GraphArena::new();
8056        let graph = arena.0.clone();
8057        let stale_target: BatchTarget;
8058        let old_key;
8059        {
8060            let node = Node::<i32>::state_in_arena(&arena, 1);
8061            old_key = node.core.key();
8062            stale_target =
8063                BatchTarget::from_core(&node.core).expect("pin tracks live batch target");
8064            let borrowed = stale_target
8065                .borrowed_core()
8066                .expect("pin may borrow while owner is alive");
8067            assert_eq!(
8068                borrowed
8069                    .cache_any()
8070                    .and_then(|value| value.downcast_ref::<i32>().copied()),
8071                Some(1)
8072            );
8073            drop(node);
8074            assert!(
8075                graph.borrow().is_live_key(old_key),
8076                "pin keeps dead owner slot alive while batch-target lives"
8077            );
8078        }
8079
8080        drop(stale_target);
8081        assert!(!graph.borrow().is_live_key(old_key));
8082
8083        let reused = Node::<i32>::state_in_arena(&arena, 2);
8084        assert_eq!(old_key.id, reused.core.key().id);
8085        assert_ne!(old_key.generation, reused.core.key().generation);
8086        assert!(
8087            graph.borrow().is_live_key(reused.core.key()),
8088            "reused slot is live with the new generation"
8089        );
8090    }
8091
8092    #[test]
8093    fn stale_external_rewire_token_does_not_target_reused_slot() {
8094        // DR-8/B54 boundary freshness: ExternalRewire queues carry a weak generation
8095        // token for the owner. A stale queued owner must no-op after slot reuse, never
8096        // apply a topology/fn swap to the new occupant.
8097        let arena = GraphArena::new();
8098        let old = Node::<i32>::state_in_arena(&arena, 1);
8099        let old_key = old.core.key();
8100        let committed = Rc::new(Cell::new(true));
8101        defer_boundary(
8102            &old.core,
8103            BoundaryTask::ExternalRewire {
8104                target: CoreToken::from_core(&old.core),
8105                req: RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(999i32))),
8106                committed,
8107            },
8108        );
8109        drop(old);
8110
8111        let reused = Node::<i32>::state_in_arena(&arena, 2);
8112        assert_eq!(old_key.id, reused.core.key().id);
8113        assert_ne!(old_key.generation, reused.core.key().generation);
8114
8115        drain_committed_boundary(&reused.core);
8116
8117        assert_eq!(reused.cache(), Some(2));
8118        assert_eq!(
8119            reused.status(),
8120            Status::Settled,
8121            "stale ExternalRewire token must not disturb the reused slot occupant"
8122        );
8123        assert!(
8124            reused.core.deps().is_empty(),
8125            "state node topology remains unchanged after stale external rewire drains"
8126        );
8127    }
8128
8129    #[test]
8130    fn boundary_task_queued_during_batch_commit_drains_before_next_wave() {
8131        // A batch commit wave can synchronously enter another arena while ACTIVE_BATCH is
8132        // still installed (`collecting=false`). Roots registered during that commit must
8133        // be merged before clear/drain, or the inner queued rewire waits for a later wave.
8134        let arena_a = GraphArena::new();
8135        let arena_b = GraphArena::new();
8136        let outer = Node::<i32>::state_empty_in_arena(&arena_a);
8137        let inner_src = Node::<i32>::state_empty_in_arena(&arena_b);
8138        let inner_dep = inner_src.erased();
8139        let replacement_dep = inner_dep.clone();
8140        let inner: Node<i32> =
8141            Node::derived_opts_in_arena(&arena_b, vec![inner_dep.clone()], NodeOpts::default(), {
8142                let replacement_dep = replacement_dep.clone();
8143                move |ctx| {
8144                    ctx.rewire_next_replace_deps(vec![replacement_dep.clone()], |next| {
8145                        next.emit(*next.data::<i32>(0).unwrap() * 10)
8146                    });
8147                    ctx.emit(*ctx.data::<i32>(0).unwrap());
8148                }
8149            });
8150        let _inner_sub = inner.subscribe(|_| {});
8151        let inner_src_for_commit = inner_src.clone();
8152        let _outer_sub = outer.subscribe(move |m| {
8153            if matches!(m, Message::Data(_)) {
8154                inner_src_for_commit.set(1);
8155            }
8156        });
8157
8158        crate::batch::batch(|_| {
8159            outer.set(1);
8160        });
8161        assert_eq!(inner.cache(), Some(1));
8162
8163        inner_src.set(2);
8164        assert_eq!(
8165            inner.cache(),
8166            Some(20),
8167            "inner rewire queued during batch commit must drain before the next inner wave"
8168        );
8169    }
8170
8171    #[test]
8172    fn wave_boundary_drains_later_roots_after_earlier_root_panics() {
8173        // Boundary drains isolate panics per queued task and per graph root: the first
8174        // panic still escapes, but not before every registered root reaches its boundary.
8175        let arena_a = GraphArena::new();
8176        let arena_b = GraphArena::new();
8177        let a = Node::<i32>::state_in_arena(&arena_a, 1);
8178        let b_src = Node::<i32>::state_in_arena(&arena_b, 2);
8179        let b: Node<i32> = Node::derived_opts_in_arena(
8180            &arena_b,
8181            vec![b_src.erased()],
8182            NodeOpts::default(),
8183            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
8184        );
8185        let _u = b.subscribe(|_| {});
8186        let committed = Rc::new(Cell::new(true));
8187
8188        let result = catch_unwind(AssertUnwindSafe(|| {
8189            with_wave_owner(
8190                &a.core,
8191                || {
8192                    defer_boundary(
8193                        &a.core,
8194                        BoundaryTask::ExternalRewire {
8195                            target: CoreToken::from_core(&a.core),
8196                            req: RewireRequest::Set(vec![a.erased()], Rc::new(|_| {})),
8197                            committed: committed.clone(),
8198                        },
8199                    );
8200                    defer_boundary(
8201                        &b.core,
8202                        BoundaryTask::ExternalRewire {
8203                            target: CoreToken::from_core(&b.core),
8204                            req: RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(99i32))),
8205                            committed: committed.clone(),
8206                        },
8207                    );
8208                },
8209                || {},
8210            );
8211        }));
8212
8213        assert!(result.is_err());
8214        assert!(
8215            b.core.borrow().deps.is_empty(),
8216            "a panic in the owner graph's boundary queue must not strand later graph roots"
8217        );
8218    }
8219
8220    #[test]
8221    fn batch_boundary_drains_later_roots_after_earlier_root_panics() {
8222        // Same isolation as the wave boundary, but through the batch committed-boundary
8223        // root set. A panicking root must not skip later roots before the panic escapes.
8224        let arena_a = GraphArena::new();
8225        let arena_b = GraphArena::new();
8226        let a = Node::<i32>::state_in_arena(&arena_a, 1);
8227        let b_src = Node::<i32>::state_in_arena(&arena_b, 2);
8228        let b: Node<i32> = Node::derived_opts_in_arena(
8229            &arena_b,
8230            vec![b_src.erased()],
8231            NodeOpts::default(),
8232            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
8233        );
8234        let _u = b.subscribe(|_| {});
8235        let committed = Rc::new(Cell::new(true));
8236
8237        let result = catch_unwind(AssertUnwindSafe(|| {
8238            crate::batch::batch(|_| {
8239                defer_boundary(
8240                    &a.core,
8241                    BoundaryTask::ExternalRewire {
8242                        target: CoreToken::from_core(&a.core),
8243                        req: RewireRequest::Set(vec![a.erased()], Rc::new(|_| {})),
8244                        committed: committed.clone(),
8245                    },
8246                );
8247                defer_boundary(
8248                    &b.core,
8249                    BoundaryTask::ExternalRewire {
8250                        target: CoreToken::from_core(&b.core),
8251                        req: RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(99i32))),
8252                        committed: committed.clone(),
8253                    },
8254                );
8255            });
8256        }));
8257
8258        assert!(result.is_err());
8259        assert!(
8260            b.core.borrow().deps.is_empty(),
8261            "a panic in one batch boundary root must not strand later graph roots"
8262        );
8263    }
8264
8265    #[test]
8266    fn terminal_owner_rewire_drains_topology_without_post_terminal_settle() {
8267        // D62: terminal drains queued topology but terminal-is-forever still seals output.
8268        // Force the old hazard shape: a terminal owner also has a stale dirty contribution
8269        // from a dep being removed. The rewire must apply cleanup but skip settle_rewire /
8270        // zero-dep undirty so no post-terminal DIRTY/RESOLVED/DATA escapes.
8271        let a = Node::<i32>::state(1);
8272        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
8273            ctx.emit(*ctx.data::<i32>(0).unwrap())
8274        });
8275        let (log, sink) = recorder();
8276        let _u = d.subscribe(sink);
8277        log.borrow_mut().clear();
8278
8279        d.core.with_inner_edges_mut(|_n, e| {
8280            e.value.terminal = true;
8281            e.value.status = Status::Completed;
8282            e.wave.emitted_dirty_this_wave = true;
8283            e.state.dirty[0] = true;
8284            e.state.pending = 1;
8285        });
8286
8287        d.core
8288            .apply_rewire_next(RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(999i32))));
8289
8290        assert_eq!(d.status(), Status::Completed);
8291        assert!(
8292            log.borrow().is_empty(),
8293            "terminal-owner topology drain must not emit post-terminal messages: {:?}",
8294            *log.borrow()
8295        );
8296        assert!(
8297            d.core.borrow().deps.is_empty(),
8298            "queued topology still drains on the terminal owner"
8299        );
8300        assert!(
8301            !d.core.with_inner_edges(|_, e| e.wave.in_dep_mutation),
8302            "terminal-owner early return must still clear the dep-mutation guard"
8303        );
8304    }
8305
8306    #[test]
8307    fn wave_scope_blamed_does_not_add_ref_pin() {
8308        // DR-8/B54 next slice: `blamed` is a generation-keyed arena reference rather
8309        // than another counted Core clone. The touched-set now pins live nodes in the
8310        // owner arena for drop/unwind safety; neither touch nor blame increments Core refs.
8311        let source = Node::<i32>::state(1);
8312        let refs = source.core.refs.clone();
8313
8314        with_wave_owner(
8315            &source.core,
8316            || {
8317                let owner_pinned = refs.get();
8318                wave_register(&source.core);
8319                let touched_pinned = refs.get();
8320                assert_eq!(touched_pinned, owner_pinned);
8321
8322                wave_set_blamed(&source.core);
8323                assert_eq!(
8324                    refs.get(),
8325                    touched_pinned,
8326                    "blame records a generation key, not a counted Core clone"
8327                );
8328            },
8329            || {},
8330        );
8331
8332        assert_eq!(
8333            refs.get(),
8334            1,
8335            "all wave-owner/touched arena pins are released after the wave"
8336        );
8337    }
8338
8339    #[test]
8340    fn generation_keyed_blame_recovers_after_callback_drop_and_fn_panic() {
8341        // DR-8/B54: the fn-running node is pinned by the touched set, while `blamed`
8342        // itself is only a generation key. If a subscriber callback drops another
8343        // same-arena node and the fn then panics, recovery still emits ERROR on the
8344        // blamed node without holding a GraphCore borrow across the callback.
8345        let held: Rc<RefCell<Option<Node<i32>>>> =
8346            Rc::new(RefCell::new(Some(Node::<i32>::producer(|_| {}))));
8347        let source = Node::<i32>::state_empty();
8348        let derived: Node<i32> = Node::derived(vec![source.erased()], |ctx| {
8349            ctx.emit(*ctx.data::<i32>(0).unwrap());
8350            panic!("panic after subscriber callback");
8351        });
8352        let log = Rc::new(RefCell::new(Vec::<String>::new()));
8353        let h = held.clone();
8354        let l = log.clone();
8355        let _u = derived.subscribe(move |m| {
8356            l.borrow_mut().push(format!("{m:?}"));
8357            if matches!(m, Message::Data(_)) {
8358                let _ = h.borrow_mut().take();
8359            }
8360        });
8361
8362        source.set(1);
8363
8364        assert!(held.borrow().is_none());
8365        assert_eq!(derived.status(), Status::Errored);
8366        assert!(
8367            log.borrow().iter().any(|k| k == "ERROR"),
8368            "panic recovery should emit ERROR on the generation-keyed blamed node"
8369        );
8370    }
8371
8372    #[test]
8373    fn stale_blamed_generation_does_not_target_reused_slot() {
8374        // A stale blamed key must not match a later node that reused the same arena slot.
8375        // This is an artificial cold-path probe for the panic recovery lookup: generation
8376        // is part of the key, so the old slot id cannot reset or ERROR the new occupant.
8377        let arena = GraphArena::new();
8378        let old = Node::<i32>::state_in_arena(&arena, 1);
8379        let old_key = old.core.key();
8380        let old_arena_key = old.core.arena_node_key();
8381        drop(old);
8382
8383        let reused = Node::<i32>::state_in_arena(&arena, 2);
8384        assert_eq!(old_key.id, reused.core.key().id);
8385        assert_ne!(old_key.generation, reused.core.key().generation);
8386
8387        let owner = Node::<i32>::state_in_arena(&arena, 3);
8388        with_wave_owner(
8389            &owner.core,
8390            || {
8391                wave_register(&reused.core);
8392                WAVE.with(|w| {
8393                    w.borrow_mut().as_mut().expect("wave installed").blamed = Some(old_arena_key);
8394                });
8395                panic!("stale blame");
8396            },
8397            || {},
8398        );
8399
8400        assert_eq!(
8401            reused.status(),
8402            Status::Settled,
8403            "stale generation key must not ERROR/reset the reused slot occupant"
8404        );
8405        assert_eq!(
8406            owner.status(),
8407            Status::Errored,
8408            "stale blame falls back to the live wave owner"
8409        );
8410    }
8411
8412    #[test]
8413    fn blame_key_is_arena_qualified_for_nested_cross_arena_panic() {
8414        // DR-8/B54: WAVE is thread-local, so a nested public call into another
8415        // GraphArena shares the outer wave-owner scope. Two independent arenas can
8416        // both have slot 0/generation 0; blame lookup must include arena identity or
8417        // the recovery ERROR can land on the wrong same-key node.
8418        let arena_a = GraphArena::new();
8419        let arena_b = GraphArena::new();
8420        let outer = Node::<i32>::state_empty_in_arena(&arena_a);
8421        let panicker = Node::<i32>::producer_opts_in_arena(&arena_b, NodeOpts::default(), |_| {
8422            panic!("nested cross-arena panic");
8423        });
8424
8425        assert_eq!(outer.core.key(), panicker.core.key());
8426        assert_ne!(outer.core.arena_node_key(), panicker.core.arena_node_key());
8427
8428        let p = panicker.clone();
8429        let _u = outer.subscribe(move |m| {
8430            if matches!(m, Message::Data(_)) {
8431                let _ = p.subscribe(|_| {});
8432            }
8433        });
8434
8435        outer.set(1);
8436
8437        assert_eq!(
8438            outer.status(),
8439            Status::Settled,
8440            "outer same-slot node must not receive the nested arena's recovery ERROR"
8441        );
8442        assert_eq!(
8443            panicker.status(),
8444            Status::Errored,
8445            "panic recovery should blame the nested arena node that ran the fn"
8446        );
8447    }
8448
8449    #[test]
8450    #[should_panic(expected = "GraphCore generation overflow")]
8451    fn arena_generation_overflow_fails_closed() {
8452        // A stale NodeKey must never become live again via generation wraparound.
8453        let arena = GraphArena::new();
8454        let first = Node::<i32>::state_in_arena(&arena, 1);
8455        let slot = first.core.id.0;
8456        drop(first);
8457        arena.0.borrow_mut().generations[slot] = u64::MAX;
8458
8459        let _second = Node::<i32>::state_in_arena(&arena, 2);
8460    }
8461
8462    #[test]
8463    fn resumeall_buffers_each_recompute_and_replays_all_on_resume() {
8464        // R-pause-modes resumeAll (D44): unlike `true` (coalesce → fire ONCE on resume),
8465        // resumeAll RUNS the fn on each dep wave while paused and BUFFERS the output,
8466        // replaying every buffered settle in arrival order on final-lock RESUME. No
8467        // conformance scenario covers resumeAll (TS is also only B9-partial — the pre-pause
8468        // baseline-equals fidelity is deferred); this pins the buffer+replay property.
8469        let runs = Rc::new(Cell::new(0usize));
8470        let src = Node::<i32>::state(1);
8471        let doubled = {
8472            let r = runs.clone();
8473            Node::<i32>::derived_opts(
8474                vec![src.erased()],
8475                NodeOpts {
8476                    pool: PoolKind::Sync,
8477                    pausable: Pausable::ResumeAll,
8478                    ..NodeOpts::default()
8479                },
8480                move |ctx| {
8481                    r.set(r.get() + 1);
8482                    ctx.emit(*ctx.data::<i32>(0).unwrap() * 2);
8483                },
8484            )
8485        };
8486        let (log, sink) = recorder();
8487        let _u = doubled.subscribe(sink); // activation (not paused): 1*2 = 2, delivered
8488        assert_eq!(doubled.cache(), Some(2));
8489        runs.set(0);
8490
8491        let l = LockId::new("p");
8492        doubled.up(vec![Message::Pause(l.clone())]);
8493        log.borrow_mut().clear();
8494
8495        src.set(5); // recompute (10) while paused → fn RUNS, output buffered
8496        src.set(6); // recompute (12) → buffered
8497        src.set(7); // recompute (14) → buffered
8498        let data_while_paused = log.borrow().iter().filter(|k| *k == "DATA").count();
8499        assert_eq!(
8500            runs.get(),
8501            3,
8502            "resumeAll runs the fn on each dep wave (NOT coalesced to one like `true`)"
8503        );
8504        assert_eq!(
8505            data_while_paused, 0,
8506            "no DATA delivered while paused (output buffered)"
8507        );
8508        assert_eq!(doubled.cache(), Some(2), "cache not advanced while paused");
8509
8510        doubled.up(vec![Message::Resume(l)]); // replay all three buffered settles in order
8511        let data_after_resume = log.borrow().iter().filter(|k| *k == "DATA").count();
8512        assert_eq!(
8513            data_after_resume, 3,
8514            "all three buffered settles replay on final-lock RESUME (arrival order)"
8515        );
8516        assert_eq!(doubled.cache(), Some(14)); // last replayed value (7 * 2)
8517    }
8518
8519    #[test]
8520    fn resumeall_buffers_dep_invalidate_until_resume() {
8521        // R-pause/R-undirty-settle timing: resumeAll buffers a node's own tier-4
8522        // INVALIDATE output while paused. Default pausable:true still propagates dep
8523        // INVALIDATE immediately (R-paused-invalidate); resumeAll replays it on RESUME.
8524        let src = Node::<i32>::state(1);
8525        let flushes = Rc::new(Cell::new(0usize));
8526        let d = {
8527            let flushes = flushes.clone();
8528            Node::<i32>::derived_opts(
8529                vec![src.erased()],
8530                NodeOpts {
8531                    pausable: Pausable::ResumeAll,
8532                    ..NodeOpts::default()
8533                },
8534                move |ctx| {
8535                    let f = flushes.clone();
8536                    ctx.on_invalidate(move || f.set(f.get() + 1));
8537                    ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
8538                },
8539            )
8540        };
8541        let (log, sink) = recorder();
8542        let _u = d.subscribe(sink);
8543        assert_eq!(d.cache(), Some(2));
8544        log.borrow_mut().clear();
8545
8546        let lock = LockId::new("resumeall-invalidate");
8547        d.up(vec![Message::Pause(lock.clone())]);
8548        src.down(vec![Message::Invalidate]);
8549
8550        assert_eq!(flushes.get(), 0, "onInvalidate waits for buffered replay");
8551        assert_eq!(
8552            d.cache(),
8553            Some(2),
8554            "cache is not cleared until RESUME replay"
8555        );
8556        assert_eq!(
8557            log.borrow().iter().filter(|k| *k == "INVALIDATE").count(),
8558            0,
8559            "INVALIDATE is not visible while resumeAll-paused"
8560        );
8561
8562        d.up(vec![Message::Resume(lock)]);
8563        assert_eq!(flushes.get(), 1);
8564        assert_eq!(d.cache(), None);
8565        assert_eq!(
8566            log.borrow().iter().filter(|k| *k == "INVALIDATE").count(),
8567            1,
8568            "buffered INVALIDATE replays once on final RESUME"
8569        );
8570    }
8571
8572    // ── rewire (R-rewire / D42, C-8) ──
8573
8574    #[test]
8575    fn rewire_subscribe_dep_sentinel_dep_does_not_rearm_gate() {
8576        // Q2: adding a never-emitted (SENTINEL) dep delivers START only — no recompute — and
8577        // does NOT re-arm the first-run gate (a alone re-drives d, not waiting for b).
8578        let a = Node::<i32>::state(1);
8579        let b = Node::<i32>::state_empty(); // SENTINEL, never emits
8580        let runs = Rc::new(Cell::new(0usize));
8581        let d = {
8582            let r = runs.clone();
8583            Node::<i32>::derived(vec![a.erased()], move |ctx| {
8584                r.set(r.get() + 1);
8585                ctx.emit(*ctx.data::<i32>(0).unwrap() * 10);
8586            })
8587        };
8588        let (_log, sink) = recorder();
8589        let _u = d.subscribe(sink);
8590        assert_eq!(d.cache(), Some(10));
8591        runs.set(0);
8592
8593        let r2 = runs.clone();
8594        d.subscribe_dep(b.erased(), move |ctx| {
8595            r2.set(r2.get() + 1);
8596            let bv = ctx.data::<i32>(1).map(|v| *v).unwrap_or(0); // SENTINEL guard
8597            ctx.emit(*ctx.data::<i32>(0).unwrap() * 10 + bv);
8598        });
8599        assert_eq!(
8600            runs.get(),
8601            0,
8602            "a SENTINEL added dep delivers START only — no recompute"
8603        );
8604
8605        a.set(2); // gate NOT re-armed: a alone re-drives d
8606        assert_eq!(runs.get(), 1);
8607        assert_eq!(d.cache(), Some(20)); // 2*10 + 0 (b still SENTINEL)
8608    }
8609
8610    #[test]
8611    fn rewire_unsubscribe_dep_drains_and_preserves_cache() {
8612        // Q3/Q7: unsubscribe_dep of a non-dirty dep preserves cache (no recompute) + drains the
8613        // removed edge (it no longer drives the node); the swapped fn runs on the next wave.
8614        let a = Node::<i32>::state(1);
8615        let b = Node::<i32>::state(2);
8616        let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], |ctx| {
8617            ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap())
8618        });
8619        let (_log, sink) = recorder();
8620        let _u = d.subscribe(sink);
8621        assert_eq!(d.cache(), Some(3));
8622
8623        d.unsubscribe_dep(b.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
8624        assert_eq!(d.cache(), Some(3)); // preserved (unsubscribe_dep of a non-dirty dep: no recompute)
8625
8626        b.set(99); // drained — must NOT drive d
8627        assert_eq!(d.cache(), Some(3));
8628
8629        a.set(5); // d recomputes with the swapped a-only fn
8630        assert_eq!(d.cache(), Some(5));
8631    }
8632
8633    #[test]
8634    fn rewire_unsubscribe_dep_to_zero_deps_is_inert() {
8635        // SD-3: unsubscribe_dep to zero deps → degenerate fn-no-deps, cache preserved, no auto-fire.
8636        let a = Node::<i32>::state(7);
8637        let runs = Rc::new(Cell::new(0usize));
8638        let d = {
8639            let r = runs.clone();
8640            Node::<i32>::derived(vec![a.erased()], move |ctx| {
8641                r.set(r.get() + 1);
8642                ctx.emit(*ctx.data::<i32>(0).unwrap());
8643            })
8644        };
8645        let (_log, sink) = recorder();
8646        let _u = d.subscribe(sink);
8647        assert_eq!(d.cache(), Some(7));
8648        runs.set(0);
8649
8650        let r2 = runs.clone();
8651        d.unsubscribe_dep(a.erased(), move |ctx| {
8652            r2.set(r2.get() + 1);
8653            ctx.emit(-1i32);
8654        });
8655        assert_eq!(d.cache(), Some(7)); // preserved
8656        assert_eq!(runs.get(), 0); // inert — does not fire
8657
8658        a.set(8); // a is no longer a dep
8659        assert_eq!(d.cache(), Some(7));
8660        assert_eq!(runs.get(), 0);
8661    }
8662
8663    #[test]
8664    fn rewire_replace_deps_reorders_kept_deps() {
8665        // Option-C / DepRecord-ref dispatch: reorder kept deps without losing state; a kept
8666        // dep's callback reroutes to its new index via the shared idx-box (O(1)).
8667        fn combine(ctx: &Ctx) {
8668            ctx.emit(*ctx.data::<i32>(0).unwrap() * 100 + *ctx.data::<i32>(1).unwrap());
8669        }
8670        let a = Node::<i32>::state(10);
8671        let b = Node::<i32>::state(20);
8672        let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], combine);
8673        let (_log, sink) = recorder();
8674        let _u = d.subscribe(sink);
8675        assert_eq!(d.cache(), Some(1020)); // dep0=a=10, dep1=b=20
8676
8677        d.replace_deps(vec![b.erased(), a.erased()], combine); // reorder; kept state preserved
8678        a.set(11); // a is now dep1; reroutes correctly
8679        assert_eq!(d.cache(), Some(2011)); // dep0=b=20, dep1=a=11 → 20*100+11
8680    }
8681
8682    #[test]
8683    fn rewire_same_order_deps_swaps_fn_without_rebuilding_dep_state() {
8684        // DR-8/B54: same-ordered deps are a fn swap only. Option-C state and
8685        // subscriber transport stay untouched; the next dep wave uses the new fn.
8686        let a = Node::<i32>::state(1);
8687        let b = Node::<i32>::state(2);
8688        let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], |ctx| {
8689            ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap())
8690        });
8691        let _u = d.subscribe(|_| {});
8692        assert_eq!(d.cache(), Some(3));
8693        assert_eq!(a.core.subscriber_count(), 1);
8694        assert_eq!(b.core.subscriber_count(), 1);
8695
8696        d.core.with_inner_edges_mut(|_, e| {
8697            e.state.prev[0] = Some(Rc::new(11i32));
8698            e.state.prev[1] = Some(Rc::new(22i32));
8699        });
8700
8701        d.replace_deps(vec![a.erased(), b.erased()], |ctx| {
8702            ctx.emit(*ctx.data::<i32>(0).unwrap() * *ctx.data::<i32>(1).unwrap())
8703        });
8704
8705        assert_eq!(a.core.subscriber_count(), 1);
8706        assert_eq!(b.core.subscriber_count(), 1);
8707        d.core.with_inner_edges(|_, e| {
8708            assert_eq!(
8709                e.state.prev[0]
8710                    .as_ref()
8711                    .and_then(|v| v.downcast_ref::<i32>())
8712                    .copied(),
8713                Some(11),
8714            );
8715            assert_eq!(
8716                e.state.prev[1]
8717                    .as_ref()
8718                    .and_then(|v| v.downcast_ref::<i32>())
8719                    .copied(),
8720                Some(22),
8721            );
8722        });
8723
8724        a.set(3);
8725        assert_eq!(d.cache(), Some(66)); // new fn: dep0=3, dep1=22
8726    }
8727
8728    #[test]
8729    fn rewire_same_order_fn_swap_rejects_reentrant_rewire_from_old_fn_drop() {
8730        struct ReenterOnDrop {
8731            target: Rc<RefCell<Option<Node<i32>>>>,
8732            dep: Core,
8733            saw_guard: Rc<Cell<bool>>,
8734        }
8735
8736        impl Drop for ReenterOnDrop {
8737            fn drop(&mut self) {
8738                let Some(target) = self.target.borrow().as_ref().cloned() else {
8739                    return;
8740                };
8741                self.saw_guard
8742                    .set(target.core.with_inner_edges(|_, e| e.wave.in_dep_mutation));
8743                target.replace_deps(vec![self.dep.clone()], |_| {});
8744            }
8745        }
8746
8747        let a = Node::<i32>::state(1);
8748        let holder: Rc<RefCell<Option<Node<i32>>>> = Rc::new(RefCell::new(None));
8749        let saw_guard = Rc::new(Cell::new(false));
8750        let drop_probe = ReenterOnDrop {
8751            target: holder.clone(),
8752            dep: a.erased(),
8753            saw_guard: saw_guard.clone(),
8754        };
8755        let d: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
8756            let _keep = &drop_probe;
8757            ctx.emit(*ctx.data::<i32>(0).unwrap());
8758        });
8759        *holder.borrow_mut() = Some(d.clone());
8760        let _u = d.subscribe(|_| {});
8761
8762        let _ = catch_unwind(AssertUnwindSafe(|| {
8763            d.replace_deps(vec![a.erased()], |ctx| {
8764                ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
8765            });
8766        }));
8767
8768        assert!(saw_guard.get(), "old fn Drop should see the rewire guard");
8769        assert!(
8770            !d.core.with_inner_edges(|_, e| e.wave.in_dep_mutation),
8771            "DepMutationGuard must clear the fast-path mutation guard after panic"
8772        );
8773        *holder.borrow_mut() = None;
8774    }
8775
8776    #[test]
8777    fn rewire_precompute_keeps_first_duplicate_old_dep_slot() {
8778        // Public construction may still contain duplicate old deps while rewire
8779        // dedups the new dep list. The precomputed index map must preserve the
8780        // old `.position()` behavior: keep the first old slot's DepRecord/idx-box,
8781        // and drain the duplicate old edge.
8782        let a = Node::<i32>::state(1);
8783        let d: Node<i32> = Node::derived(vec![a.erased(), a.erased()], |ctx| {
8784            ctx.emit(*ctx.data::<i32>(0).unwrap())
8785        });
8786        let _u = d.subscribe(|_| {});
8787        assert_eq!(
8788            a.core.subscriber_count(),
8789            2,
8790            "duplicate old deps install two internal edges before rewire"
8791        );
8792
8793        d.core.with_inner_edges_mut(|_, e| {
8794            e.state.prev[0] = Some(Rc::new(11i32));
8795            e.state.prev[1] = Some(Rc::new(22i32));
8796            e.state.has_data[0] = true;
8797            e.state.has_data[1] = true;
8798        });
8799
8800        d.replace_deps(vec![a.erased()], |ctx| {
8801            ctx.emit(*ctx.data::<i32>(0).unwrap())
8802        });
8803
8804        d.core.with_inner_edges(|n, e| {
8805            assert_eq!(n.deps.len(), 1);
8806            assert_eq!(e.state.prev.len(), 1);
8807            assert_eq!(
8808                e.state.prev[0]
8809                    .as_ref()
8810                    .and_then(|v| v.downcast_ref::<i32>())
8811                    .copied(),
8812                Some(11),
8813                "rewire should keep the first old duplicate dep slot"
8814            );
8815            assert_eq!(
8816                e.idx_boxes[0].get(),
8817                0,
8818                "kept first duplicate edge should reroute to new index 0"
8819            );
8820        });
8821        assert_eq!(
8822            a.core.subscriber_count(),
8823            1,
8824            "the duplicate old edge is drained during rewire"
8825        );
8826    }
8827
8828    #[test]
8829    fn rewire_replace_deps_all_replaced_fast_path() {
8830        // DR-8/B54 no-kept fast path: same-length rewire where every old dep is dropped
8831        // and replaced by new deps should re-subscribe to the fresh edge set, drop the old
8832        // callback transport, and run once on the fresh push-on-subscribe wave.
8833        let a = Node::<i32>::state(1);
8834        let b = Node::<i32>::state(2);
8835        let c = Node::<i32>::state(10);
8836        let e = Node::<i32>::state(20);
8837        let runs = Rc::new(Cell::new(0usize));
8838        let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], {
8839            let runs = runs.clone();
8840            move |ctx| {
8841                runs.set(runs.get() + 1);
8842                ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
8843            }
8844        });
8845        let _u = d.subscribe(|_| {});
8846        assert_eq!(d.cache(), Some(3));
8847        runs.set(0);
8848
8849        let runs2 = runs.clone();
8850        d.replace_deps(vec![c.erased(), e.erased()], move |ctx| {
8851            runs2.set(runs2.get() + 1);
8852            ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap() + 100);
8853        });
8854        assert_eq!(
8855            runs.get(),
8856            1,
8857            "one recompute after full no-kept replacement"
8858        );
8859        assert_eq!(d.cache(), Some(130));
8860
8861        assert_eq!(
8862            a.core.subscriber_count(),
8863            0,
8864            "old dep A edge is fully drained"
8865        );
8866        assert_eq!(
8867            b.core.subscriber_count(),
8868            0,
8869            "old dep B edge is fully drained"
8870        );
8871        assert_eq!(c.core.subscriber_count(), 1, "new dep C edge is active");
8872        assert_eq!(e.core.subscriber_count(), 1, "new dep E edge is active");
8873
8874        // Old deps no longer drive cache.
8875        a.set(99);
8876        b.set(99);
8877        assert_eq!(runs.get(), 1);
8878        assert_eq!(d.cache(), Some(130));
8879
8880        // New deps continue to drive and preserve atomic settle.
8881        c.set(11);
8882        assert_eq!(runs.get(), 2);
8883        assert_eq!(d.cache(), Some(131));
8884    }
8885
8886    #[test]
8887    fn rewire_no_kept_fast_path_preserves_remaining_unsubs_on_removed_cleanup_panic() {
8888        // QA regression for the DR-8/B54 no-kept fast path: a panicking removed-dep
8889        // deactivation hook must not drop later old-edge Unsub handles without running
8890        // them. The broader half-applied-rewire panic class is tracked by B16; this
8891        // test pins the new fast path to the slow path's cleanup ownership behavior.
8892        let b_deactivated = Rc::new(Cell::new(false));
8893        let a = Node::<i32>::producer(|ctx| {
8894            ctx.on_deactivation(|| panic!("removed cleanup boom"));
8895            ctx.emit(1i32);
8896        });
8897        let b = {
8898            let b_deactivated = b_deactivated.clone();
8899            Node::<i32>::producer(move |ctx| {
8900                let flag = b_deactivated.clone();
8901                ctx.on_deactivation(move || flag.set(true));
8902                ctx.emit(2i32);
8903            })
8904        };
8905        let c = Node::<i32>::state(10);
8906        let e = Node::<i32>::state(20);
8907
8908        {
8909            let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], |ctx| {
8910                ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
8911            });
8912            let _u = d.subscribe(|_| {});
8913            assert_eq!(d.cache(), Some(3));
8914            assert_eq!(b.core.subscriber_count(), 1);
8915
8916            d.replace_deps(vec![c.erased(), e.erased()], |ctx| {
8917                ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
8918            });
8919            assert_eq!(
8920                b.core.subscriber_count(),
8921                1,
8922                "later old edge remains owned by the rewiring node after the first cleanup panic"
8923            );
8924        }
8925
8926        assert!(
8927            b_deactivated.get(),
8928            "dropping the rewiring node must still run the remaining old-edge unsubscribe"
8929        );
8930        assert_eq!(
8931            b.core.subscriber_count(),
8932            0,
8933            "remaining old edge is not leaked after owner drop"
8934        );
8935    }
8936
8937    #[test]
8938    fn rewire_atomic_multi_add_settles_once() {
8939        // P2: adding ≥2 cached deps in ONE replace_deps settles ATOMICALLY — the fn fires once,
8940        // never on a partial view (an added dep still SENTINEL at invocation = the bug).
8941        let a = Node::<i32>::state(1);
8942        let b = Node::<i32>::state(10);
8943        let c = Node::<i32>::state(100);
8944        let runs = Rc::new(Cell::new(0usize));
8945        let saw_partial = Rc::new(Cell::new(false));
8946        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
8947            ctx.emit(*ctx.data::<i32>(0).unwrap())
8948        });
8949        let (_log, sink) = recorder();
8950        let _u = d.subscribe(sink);
8951        runs.set(0);
8952
8953        let r = runs.clone();
8954        let sp = saw_partial.clone();
8955        d.replace_deps(vec![a.erased(), b.erased(), c.erased()], move |ctx| {
8956            r.set(r.get() + 1);
8957            if ctx.data::<i32>(1).is_none() || ctx.data::<i32>(2).is_none() {
8958                sp.set(true);
8959            }
8960            ctx.emit(
8961                *ctx.data::<i32>(0).unwrap()
8962                    + *ctx.data::<i32>(1).unwrap()
8963                    + *ctx.data::<i32>(2).unwrap(),
8964            );
8965        });
8966        assert_eq!(
8967            runs.get(),
8968            1,
8969            "ONE atomic settle, not one fire per added dep"
8970        );
8971        assert!(
8972            !saw_partial.get(),
8973            "never fired with an added dep still SENTINEL"
8974        );
8975        assert_eq!(d.cache(), Some(111)); // 1 + 10 + 100
8976    }
8977
8978    #[test]
8979    fn rewire_settle_emits_dirty_before_data() {
8980        // D1 / R-dirty-before-data: a rewire-triggered settle is a wave — DIRTY precedes DATA.
8981        let a = Node::<i32>::state(1);
8982        let b = Node::<i32>::state(100);
8983        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
8984            ctx.emit(*ctx.data::<i32>(0).unwrap())
8985        });
8986        let (log, sink) = recorder();
8987        let _u = d.subscribe(sink);
8988        log.borrow_mut().clear(); // isolate the rewire wave
8989
8990        d.subscribe_dep(b.erased(), |ctx| {
8991            ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap())
8992        });
8993        assert_eq!(*log.borrow(), vec!["DIRTY", "DATA"]); // glitch-free two-phase
8994        assert_eq!(d.cache(), Some(101));
8995    }
8996
8997    #[test]
8998    #[should_panic(expected = "self-dependency")]
8999    fn rewire_rejects_self_dep() {
9000        let a = Node::<i32>::state(1);
9001        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
9002            ctx.emit(*ctx.data::<i32>(0).unwrap())
9003        });
9004        let _u = d.subscribe(|_| {});
9005        d.replace_deps(vec![d.erased()], |_| {}); // self-dep → panic
9006    }
9007
9008    #[test]
9009    #[should_panic(expected = "cycle")]
9010    fn rewire_rejects_cycle() {
9011        let a = Node::<i32>::state(1);
9012        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
9013            ctx.emit(*ctx.data::<i32>(0).unwrap())
9014        });
9015        let e: Node<i32> = Node::derived(vec![d.erased()], |ctx| {
9016            ctx.emit(*ctx.data::<i32>(0).unwrap())
9017        });
9018        let _u = e.subscribe(|_| {}); // e depends on d (→ a)
9019        d.subscribe_dep(e.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
9020        // would close d→e→d
9021    }
9022
9023    #[test]
9024    #[should_panic(expected = "terminal dep")]
9025    fn rewire_rejects_adding_terminal_dep() {
9026        let term: Node<i32> = Node::producer(|ctx| ctx.down(vec![Message::Complete]));
9027        let _ut = term.subscribe(|_| {}); // activate → producer completes
9028        assert_eq!(term.status(), Status::Completed);
9029        let a = Node::<i32>::state(1);
9030        let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
9031            ctx.emit(*ctx.data::<i32>(0).unwrap())
9032        });
9033        let _u = d.subscribe(|_| {});
9034        d.subscribe_dep(term.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
9035        // terminal dep → panic
9036    }
9037
9038    #[test]
9039    fn rewire_midfn_becomes_error_not_panic() {
9040        // A fn mutating its OWN deps mid-wave is the D37 feedback cycle. The Rust substrate
9041        // bundles the D30 catch at the wave-owner, so this surfaces as [[ERROR,e]] on the node
9042        // (R-reentrancy), NOT a propagated panic (the externally-called rejects DO panic).
9043        let a = Node::<i32>::state(1);
9044        let x = Node::<i32>::state(9);
9045        let slot: Rc<RefCell<Option<Node<i32>>>> = Rc::new(RefCell::new(None));
9046        let slot2 = slot.clone();
9047        let d: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
9048            if let Some(dh) = slot2.borrow().as_ref() {
9049                // mid-fn self-rewire — illegal (R-rewire / D37)
9050                dh.subscribe_dep(x.erased(), |c| c.emit(*c.data::<i32>(0).unwrap()));
9051            }
9052            ctx.emit(*ctx.data::<i32>(0).unwrap());
9053        });
9054        *slot.borrow_mut() = Some(d.clone());
9055        let (log, sink) = recorder();
9056        let _u = d.subscribe(sink); // activate → fn → mid-fn subscribe_dep → reject → wave-owner → ERROR
9057        assert_eq!(d.status(), Status::Errored);
9058        assert!(
9059            log.borrow().iter().any(|k| k == "ERROR"),
9060            "mid-fn rewire surfaces as ERROR (got {:?})",
9061            *log.borrow()
9062        );
9063    }
9064
9065    #[test]
9066    fn rewire_fnswap_frees_old_fn_handle() {
9067        // B32 (rewire fn-swap GC): a rewire re-registers the fn → a new handle and
9068        // unregisters the OLD handle, freeing the rewired-away closure + its captures.
9069        struct DropFlag(Rc<Cell<bool>>);
9070        impl Drop for DropFlag {
9071            fn drop(&mut self) {
9072                self.0.set(true);
9073            }
9074        }
9075        let old_fn_dropped = Rc::new(Cell::new(false));
9076        let a = Node::<i32>::state(1);
9077        let guard = DropFlag(old_fn_dropped.clone());
9078        let d: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
9079            let _hold = &guard; // the OLD fn captures the guard
9080            ctx.emit(*ctx.data::<i32>(0).unwrap());
9081        });
9082        let _u = d.subscribe(|_| {});
9083        assert!(!old_fn_dropped.get(), "old fn is live before the swap");
9084
9085        d.replace_deps(vec![a.erased()], |ctx| {
9086            ctx.emit(*ctx.data::<i32>(0).unwrap())
9087        }); // fn swap
9088        assert!(
9089            old_fn_dropped.get(),
9090            "the rewired-away fn (and its capture) is freed on swap (B32)"
9091        );
9092    }
9093
9094    #[test]
9095    fn rewire_panic_in_added_dep_activation_does_not_wedge_node() {
9096        // QA-F1 regression: adding a dep whose ACTIVATION fn panics. The wave-owner catch
9097        // blames the added producer (its fn ran) → ERROR lands on IT. The partially
9098        // registered edge is rolled back so `boom` does not retain an orphaned subscriber.
9099        // Without the DepMutationGuard, `d` would be left in_dep_mutation=true forever
9100        // → every future maybe_run defers (never recomputes) + every future rewire is
9101        // rejected as reentrant. With the guard `d` recovers cleanly.
9102        //
9103        // `d` ABSORBS a dep error (errorWhenDepsError:false) so this test isolates the QA-F1
9104        // GUARD concern (in_dep_mutation cleared on the unwind, observed via the recompute
9105        // below) from the orthogonal R-deps-terminal auto-error-cascade (C-15): under the
9106        // default errorWhenDepsError:true, boom's activation-panic→ERROR would correctly
9107        // cascade and TERMINATE d, making "d recovers" un-observable. The guard fires on the
9108        // unwind identically either way; absorbing just keeps d alive to prove it.
9109        let a = Node::<i32>::state(1);
9110        let d: Node<i32> = Node::derived_opts(
9111            vec![a.erased()],
9112            NodeOpts {
9113                error_when_deps_error: false,
9114                ..NodeOpts::default()
9115            },
9116            |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
9117        );
9118        let (_log, sink) = recorder();
9119        let _u = d.subscribe(sink);
9120        assert_eq!(d.cache(), Some(1));
9121
9122        // boom: a producer whose activation fn panics — added as a dep of d.
9123        let boom = Node::<i32>::producer(|_ctx| panic!("boom on activation"));
9124        d.subscribe_dep(boom.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
9125
9126        assert_eq!(
9127            boom.core.subscriber_count(),
9128            0,
9129            "failed added-dep activation rolls back the partially registered subscriber"
9130        );
9131
9132        // d is NOT terminal; the QA-F1 guard cleared in_dep_mutation on the unwind so
9133        // d is not wedged.
9134        assert_ne!(
9135            d.status(),
9136            Status::Errored,
9137            "d absorbs the dep error (errorWhenDepsError:false) and survives the rewire panic"
9138        );
9139
9140        // NOT WEDGED: d still recomputes from its live dep a (in_dep_mutation was cleared by
9141        // the guard on the unwind). Without the guard this stays Some(1) (deferred forever).
9142        a.set(5);
9143        assert_eq!(
9144            d.cache(),
9145            Some(5),
9146            "d recovers and recomputes — a stuck in_dep_mutation would have deferred this"
9147        );
9148
9149        // And a follow-up rewire is NOT rejected as reentrant (the flag is clear).
9150        d.replace_deps(vec![a.erased()], |ctx| {
9151            ctx.emit(*ctx.data::<i32>(0).unwrap() * 2)
9152        });
9153        a.set(3);
9154        assert_eq!(
9155            d.cache(),
9156            Some(6),
9157            "a later rewire works (not 'reentrant'-rejected)"
9158        );
9159    }
9160
9161    #[test]
9162    fn d109_default_v0_advances_only_on_data() {
9163        let s = Node::<i32>::state(1);
9164        assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 0 }));
9165
9166        s.down(vec![Message::Resolved]);
9167        assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 0 }));
9168
9169        s.set(2);
9170        assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 1 }));
9171
9172        s.down(vec![Message::Data(Rc::new(3)), Message::Data(Rc::new(4))]);
9173        assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 3 }));
9174
9175        s.down(vec![Message::Invalidate]);
9176        s.down(vec![Message::Complete]);
9177        assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 3 }));
9178    }
9179
9180    #[test]
9181    fn d112_v1_hash_receives_strict_canonical_json_bytes() {
9182        let calls = Rc::new(RefCell::new(Vec::<String>::new()));
9183        let calls_for_hash = calls.clone();
9184        let hash = Rc::new(move |bytes: &[u8]| {
9185            let text = std::str::from_utf8(bytes)
9186                .expect("strict canonical JSON bytes are UTF-8")
9187                .to_owned();
9188            calls_for_hash.borrow_mut().push(text.clone());
9189            format!("h:{text}")
9190        });
9191        let node = Node::<serde_json::Value>::derived_opts(
9192            vec![],
9193            NodeOpts {
9194                versioning: Some(NodeVersioningPolicy::Level1 { hash: Some(hash) }),
9195                ..NodeOpts::default()
9196            },
9197            |ctx| ctx.emit(json!({ "b": 2, "a": 1 })),
9198        );
9199        let _u = node.subscribe(|_| {});
9200
9201        assert_eq!(
9202            calls.borrow().as_slice(),
9203            &[
9204                "{\"@graphrefly/node-version\":\"v1-absent\"}".to_owned(),
9205                "{\"a\":1,\"b\":2}".to_owned(),
9206            ]
9207        );
9208        assert_eq!(
9209            node.version(),
9210            Some(NodeVersion::V1 {
9211                counter: 1,
9212                cid: "h:{\"a\":1,\"b\":2}".to_owned(),
9213                prev: Some("h:{\"@graphrefly/node-version\":\"v1-absent\"}".to_owned()),
9214            })
9215        );
9216    }
9217
9218    #[test]
9219    fn d112_v1_rejects_invalid_data_before_cache_or_version_mutation() {
9220        let node = Node::<serde_json::Value>::derived_opts(
9221            vec![],
9222            NodeOpts {
9223                versioning: Some(NodeVersioningPolicy::Level1 { hash: None }),
9224                ..NodeOpts::default()
9225            },
9226            |_ctx| {},
9227        );
9228        let before = node.version();
9229        node.down(vec![Message::Data(Rc::new(f64::NAN))]);
9230
9231        assert_eq!(node.version(), before);
9232        assert_eq!(node.cache(), None);
9233        assert_eq!(node.status(), Status::Errored);
9234    }
9235
9236    #[test]
9237    fn d112_v1_hash_runs_outside_graphcore_borrow() {
9238        let graph = crate::graph::graph();
9239        let probe = graph.state_opts(1i32, crate::graph::GraphNodeOpts::named("probe"));
9240        let probe_core = probe.erased();
9241        let calls = Rc::new(Cell::new(0usize));
9242        let calls_for_hash = calls.clone();
9243        let hash = Rc::new(move |bytes: &[u8]| {
9244            let _ = probe_core.version();
9245            calls_for_hash.set(calls_for_hash.get() + 1);
9246            format!(
9247                "h:{}",
9248                std::str::from_utf8(bytes).expect("strict canonical JSON bytes are UTF-8")
9249            )
9250        });
9251        let node = graph.state_opts(
9252            json!(1),
9253            crate::graph::GraphNodeOpts {
9254                name: Some("versioned".to_owned()),
9255                node: NodeOpts {
9256                    versioning: Some(NodeVersioningPolicy::Level1 { hash: Some(hash) }),
9257                    ..NodeOpts::default()
9258                },
9259                ..crate::graph::GraphNodeOpts::default()
9260            },
9261        );
9262
9263        node.set(json!(2));
9264
9265        assert_eq!(calls.get(), 2);
9266        assert_eq!(
9267            node.version(),
9268            Some(NodeVersion::V1 {
9269                counter: 1,
9270                cid: "h:2".to_owned(),
9271                prev: Some("h:1".to_owned()),
9272            })
9273        );
9274    }
9275}