Skip to main content

graphrefly_graph/
graph.rs

1//! `Graph` container — a **Core-free** namespace + mount tree (D246).
2//!
3//! D246 β-simplification: the actor-model [`Core`] is move-only and
4//! single-owner; the embedder owns it (via
5//! [`graphrefly_core::OwnedCore`]). `Graph` carries **no `Core` and no
6//! `&Core`** — it is purely the named namespace + mount tree. There is
7//! **one** `Graph` type (no `SubgraphRef`/`GraphOps`/`NamespaceHandle`,
8//! no `'g` lifetime): a subgraph is just another `Graph` handle into a
9//! child node of the tree (a cheap `Arc` clone). Every Core-touching op
10//! takes an explicit `&Core` first argument (D246 rule 2 — the owner
11//! always has it; one arg, trivially-correct ownership). Pure-namespace
12//! ops (`node`, `name_of`, `try_resolve`, …) take no `Core`.
13//!
14//! `Graph` is `Clone` (an `Rc` bump) and intentionally `!Send + !Sync`
15//! (D246/S2c single-owner: `Rc<RefCell<GraphInner>>`). It lives on, and
16//! is touched only by, the one thread that owns the `Core` — captured
17//! into owner-side `Sink`/`MailboxOp::Defer` closures (also `!Send`
18//! post-D248). It replaces the old `NamespaceHandle`.
19//!
20//! `mount` / `describe` / `observe` / `snapshot` live in sibling
21//! modules and follow the same `&Core`-explicit convention.
22
23use std::cell::RefCell;
24use std::rc::{Rc, Weak};
25use std::sync::Arc;
26
27use graphrefly_core::{
28    Core, EqualsMode, FnId, HandleId, LockId, NodeId, PauseError, ResumeReport, SetDepsError, Sink,
29    SubscriptionId, TopologySubscriptionId,
30};
31use indexmap::IndexMap;
32
33use crate::debug::DebugBindingBoundary;
34use crate::describe::{
35    describe_of, describe_reactive_in, DescribeSink, GraphDescribeOutput, ReactiveDescribeHandle,
36};
37use crate::mount::{GraphRemoveAudit, MountError};
38use crate::observe::{GraphObserveAll, GraphObserveAllReactive, GraphObserveOne};
39
40/// Namespace path separator (canonical spec R3.5.1).
41pub(crate) const PATH_SEP: &str = "::";
42
43/// Errors from [`Graph::remove`].
44#[derive(Debug, thiserror::Error)]
45pub enum RemoveError {
46    #[error("Graph::remove: name `{0}` not found (neither a node nor a mounted subgraph)")]
47    NotFound(String),
48    #[error("Graph::remove: graph has been destroyed")]
49    Destroyed,
50}
51
52/// Signal kind for [`Graph::signal`] (canonical R3.7.1).
53#[derive(Debug, Clone, Copy)]
54pub enum SignalKind {
55    /// Wipe caches (with meta filtering per R3.7.2).
56    Invalidate,
57    /// Pause every named node with the given lock.
58    Pause(LockId),
59    /// Resume every named node with the given lock.
60    Resume(LockId),
61    /// Mark every named node as terminal with `COMPLETE`.
62    Complete,
63    /// Mark every named node as terminal with `ERROR` carrying the handle.
64    Error(HandleId),
65}
66
67/// Path resolution errors returned by [`Graph::try_resolve_checked`].
68#[derive(Debug, thiserror::Error)]
69pub enum PathError {
70    /// Path is empty.
71    #[error("Path is empty")]
72    Empty,
73    /// A `..` segment was used but the graph has no parent.
74    #[error("Path segment `..` used on root graph (no parent)")]
75    NoParent,
76    /// A segment named a child graph that doesn't exist.
77    #[error("Path segment `{0}` does not match any child graph")]
78    ChildNotFound(String),
79    /// The graph has been destroyed.
80    #[error("Graph has been destroyed")]
81    Destroyed,
82}
83
84/// Errors from name registration.
85#[derive(Debug, thiserror::Error)]
86pub enum NameError {
87    #[error("Graph::add: name `{0}` already registered in this graph")]
88    Collision(String),
89    #[error("Graph: name `{0}` may not contain the `::` path separator")]
90    InvalidName(String),
91    #[error("Graph: name `{0}` uses the reserved `_anon_` prefix (collides with anonymous node describe format)")]
92    ReservedPrefix(String),
93    #[error("Graph: graph has been destroyed; further registration refused")]
94    Destroyed,
95}
96
97/// Callback type for graph-level namespace change notifications, used
98/// by reactive describe and reactive `observe_all`.
99///
100/// D246 (rule 2/6): namespace changes (`add`/`remove`/`mount`/`unmount`/
101/// `destroy`) are **owner-invoked** — the caller holds `&Core` — so the
102/// sink receives that `&Core` and may re-snapshot / re-subscribe
103/// synchronously owner-side. (The in-wave `DepsChanged`/`NodeTornDown`
104/// re-entry path is the one that defers via `MailboxOp::Defer` — see
105/// `describe.rs`/`observe.rs`.)
106pub(crate) type NamespaceChangeSink = Arc<dyn Fn(&Core)>;
107
108/// Inner namespace + mount-tree state for one graph level. D246: holds
109/// **no `Core`** — purely the named namespace, the mounted children,
110/// and the namespace-change sinks. D246/S2c/D247: single-owner ⇒
111/// `Rc<RefCell<GraphInner>>` (the prior `Arc<Mutex<…>>` was
112/// shared-Core-era legacy); the shareable-handle + parent-`Weak` cycle
113/// stays, single-threaded.
114pub struct GraphInner {
115    pub(crate) name: String,
116    /// Local namespace: name → `NodeId`. Insertion order is load-bearing
117    /// for `describe()` stability.
118    pub(crate) names: IndexMap<String, NodeId>,
119    /// Reverse lookup.
120    pub(crate) names_inverse: IndexMap<NodeId, String>,
121    /// Mounted child subgraphs — each is just another level's inner
122    /// state (no `Core`; the single owned `Core` is the embedder's, and
123    /// is threaded as `&Core` through every Core-touching tree-walk).
124    pub(crate) children: IndexMap<String, Rc<RefCell<GraphInner>>>,
125    /// Parent inner-state pointer (for `ancestors()`). Weak to break
126    /// the strong cycle.
127    pub(crate) parent: Option<Weak<RefCell<GraphInner>>>,
128    /// True after `destroy()` completes — subsequent mutations refuse.
129    pub(crate) destroyed: bool,
130    /// Namespace-change sinks — fired from `add()`, `remove()`, etc.
131    /// after the inner lock is dropped. Keyed by subscription id.
132    pub(crate) namespace_sinks: IndexMap<u64, NamespaceChangeSink>,
133    pub(crate) next_ns_sink_id: u64,
134}
135
136/// Graph container — the one Core-free namespace + mount-tree handle
137/// (canonical §3, D246).
138///
139/// `Clone` is a cheap `Rc` bump; a subgraph (from `mount*`/`ancestors`)
140/// is just a `Graph` into a child level. Intentionally `!Send + !Sync`
141/// (D246/S2c single-owner): no `Core`, no borrow — captured into
142/// owner-side sinks on the one thread that owns the `Core`. Pass the
143/// embedder's `&Core` (e.g. `owned.core()`) into every Core-touching
144/// method.
145#[derive(Clone)]
146pub struct Graph {
147    pub(crate) inner: Rc<RefCell<GraphInner>>,
148}
149
150impl std::fmt::Debug for Graph {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        let inner = self.inner.borrow_mut();
153        f.debug_struct("Graph")
154            .field("name", &inner.name)
155            .field("node_count", &inner.names.len())
156            .field("subgraph_count", &inner.children.len())
157            .field("destroyed", &inner.destroyed)
158            .finish_non_exhaustive()
159    }
160}
161
162// `state`/`derived`/`dynamic` use `.expect()` on invariant-unreachable
163// `register_*` error paths (caller-validated) — same documented stance
164// the pre-D246 `GraphOps` trait carried via this exact allow.
165#[allow(clippy::missing_panics_doc, clippy::must_use_candidate)]
166impl Graph {
167    /// Construct a named, empty root graph. D246: the graph is
168    /// Core-free — the embedder owns the `Core` (see
169    /// [`graphrefly_core::OwnedCore`]) and passes `&Core` into
170    /// Core-touching ops.
171    #[must_use]
172    pub fn new(name: impl Into<String>) -> Self {
173        Self::with_parent(name.into(), None)
174    }
175
176    pub(crate) fn with_parent(name: String, parent: Option<Weak<RefCell<GraphInner>>>) -> Self {
177        Self {
178            inner: Rc::new(RefCell::new(GraphInner {
179                name,
180                names: IndexMap::new(),
181                names_inverse: IndexMap::new(),
182                children: IndexMap::new(),
183                parent,
184                destroyed: false,
185                namespace_sinks: IndexMap::new(),
186                next_ns_sink_id: 0,
187            })),
188        }
189    }
190
191    /// Wrap an existing inner level as a `Graph` handle (mount/ancestors).
192    pub(crate) fn from_inner(inner: Rc<RefCell<GraphInner>>) -> Self {
193        Self { inner }
194    }
195
196    /// This level's namespace handle (the `Graph` ↔ tree-walk seam).
197    #[inline]
198    pub(crate) fn inner_arc(&self) -> &Rc<RefCell<GraphInner>> {
199        &self.inner
200    }
201
202    /// Whether `name` is a legal local node/subgraph name.
203    #[must_use]
204    pub fn is_valid_name(name: &str) -> bool {
205        !name.contains(PATH_SEP) && !name.starts_with("_anon_")
206    }
207
208    /// The graph's name as set at construction (or via `mount`).
209    #[must_use]
210    pub fn name(&self) -> String {
211        self.inner.borrow_mut().name.clone()
212    }
213
214    // --- namespace-change sinks (used by observe / describe / mount) ---
215
216    /// Subscribe to namespace changes (add, remove, mount, unmount,
217    /// destroy). The sink fires AFTER the inner lock is dropped, with
218    /// the owner's `&Core`. Returns a subscription id.
219    pub fn subscribe_namespace_change(&self, sink: NamespaceChangeSink) -> u64 {
220        register_ns_sink(&self.inner, sink)
221    }
222
223    /// Unsubscribe a namespace-change sink by id (owner-invoked,
224    /// D246 rule 3 — no RAII below the binding).
225    pub fn unsubscribe_namespace_change(&self, id: u64) {
226        unregister_ns_sink(&self.inner, id);
227    }
228
229    // --------------------- describe / observe (§3.6) -------------------
230
231    /// Snapshot the graph's topology + lifecycle state (JSON form).
232    /// `value` fields are raw u64 handles.
233    #[must_use]
234    pub fn describe(&self, core: &Core) -> GraphDescribeOutput {
235        describe_of(core, &self.inner, None)
236    }
237
238    /// [`Self::describe`] with each `value` rendered via `debug`.
239    #[must_use]
240    pub fn describe_with_debug(
241        &self,
242        core: &Core,
243        debug: &dyn DebugBindingBoundary,
244    ) -> GraphDescribeOutput {
245        describe_of(core, &self.inner, Some(debug))
246    }
247
248    /// Subscribe to live topology snapshots (canonical §3.6.1
249    /// `reactive: true`). Push-on-subscribe + re-fires on namespace
250    /// changes and `set_deps`. D246 rule 3: returns an id-bearing
251    /// handle with an explicit owner-invoked
252    /// [`ReactiveDescribeHandle::detach`] — no RAII `Drop`.
253    #[must_use = "dropping the handle without calling .detach(&core) leaks the topology sub"]
254    pub fn describe_reactive(&self, core: &Core, sink: &DescribeSink) -> ReactiveDescribeHandle {
255        describe_reactive_in(core, &self.inner, sink)
256    }
257
258    /// Tap a single node's downstream message stream. Pure-namespace
259    /// resolution; pass `&Core` to the returned handle's `subscribe`.
260    #[must_use = "GraphObserveOne is only useful via .subscribe(...) — dropping it silently no-ops"]
261    pub fn observe(&self, path: &str) -> GraphObserveOne {
262        let id = self.node(path);
263        GraphObserveOne::new(self.clone(), id)
264    }
265
266    /// Tap every named node in this graph (snapshot at `subscribe()`).
267    #[must_use = "GraphObserveAll is only useful via .subscribe(...) — dropping it silently no-ops"]
268    pub fn observe_all(&self) -> GraphObserveAll {
269        GraphObserveAll::new(self.clone())
270    }
271
272    /// Tap every named node AND auto-subscribe late-added nodes.
273    #[must_use = "GraphObserveAllReactive is only useful via .subscribe(...) — dropping it silently no-ops"]
274    pub fn observe_all_reactive(&self) -> GraphObserveAllReactive {
275        GraphObserveAllReactive::new(self.clone())
276    }
277
278    /// Fire all namespace-change sinks owner-side (D246 rule 2 —
279    /// caller holds `&Core`).
280    pub fn fire_namespace_change(&self, core: &Core) {
281        fire_ns(core, &self.inner);
282    }
283
284    // ------------------------- namespace (§3.5) -------------------------
285
286    /// Register an existing `node_id` under `name` in this graph's
287    /// namespace.
288    ///
289    /// # Errors
290    /// See [`NameError`].
291    pub fn add(
292        &self,
293        core: &Core,
294        node_id: NodeId,
295        name: impl Into<String>,
296    ) -> Result<NodeId, NameError> {
297        let name = name.into();
298        validate_name(&name)?;
299        {
300            let mut inner = self.inner.borrow_mut();
301            if inner.destroyed {
302                return Err(NameError::Destroyed);
303            }
304            if inner.names.contains_key(&name) {
305                return Err(NameError::Collision(name));
306            }
307            inner.names.insert(name.clone(), node_id);
308            inner.names_inverse.insert(node_id, name);
309        }
310        self.fire_namespace_change(core);
311        Ok(node_id)
312    }
313
314    /// Resolve a path to a `NodeId`. Panics if missing.
315    #[must_use]
316    pub fn node(&self, path: &str) -> NodeId {
317        self.try_resolve(path)
318            .unwrap_or_else(|| panic!("Graph::node: no node at path `{path}`"))
319    }
320
321    /// Non-panicking [`Self::node`].
322    #[must_use]
323    pub fn try_resolve(&self, path: &str) -> Option<NodeId> {
324        self.try_resolve_checked(path).ok().flatten()
325    }
326
327    /// Path resolution with typed errors (pure-namespace; never touches
328    /// `Core`).
329    ///
330    /// # Errors
331    /// See [`PathError`].
332    pub fn try_resolve_checked(&self, path: &str) -> Result<Option<NodeId>, PathError> {
333        resolve_checked(&self.inner, path)
334    }
335
336    /// Reverse lookup: the local name for a `node_id`.
337    #[must_use]
338    pub fn name_of(&self, node_id: NodeId) -> Option<String> {
339        self.inner.borrow_mut().names_inverse.get(&node_id).cloned()
340    }
341
342    /// Number of named nodes in this graph.
343    #[must_use]
344    pub fn node_count(&self) -> usize {
345        self.inner.borrow_mut().names.len()
346    }
347
348    /// Snapshot of local node names in insertion order.
349    #[must_use]
350    pub fn node_names(&self) -> Vec<String> {
351        self.inner.borrow_mut().names.keys().cloned().collect()
352    }
353
354    /// Snapshot of mounted child names in insertion order.
355    #[must_use]
356    pub fn child_names(&self) -> Vec<String> {
357        self.inner.borrow_mut().children.keys().cloned().collect()
358    }
359
360    /// Returns `true` after [`Self::destroy`] has been called.
361    #[must_use]
362    pub fn is_destroyed(&self) -> bool {
363        self.inner.borrow_mut().destroyed
364    }
365
366    // ---------------------- sugar constructors (§3.9) -------------------
367
368    /// Register a state node under `name`.
369    ///
370    /// # Errors
371    /// See [`NameError`].
372    pub fn state(
373        &self,
374        core: &Core,
375        name: impl Into<String>,
376        initial: Option<HandleId>,
377    ) -> Result<NodeId, NameError> {
378        let id = core
379            .register_state(initial.unwrap_or(graphrefly_core::NO_HANDLE), false)
380            .expect("invariant: register_state has no error variants reachable for caller-controlled inputs");
381        self.add(core, id, name)
382    }
383
384    /// Register a static-derived node (fn fires on every dep change).
385    ///
386    /// # Errors
387    /// See [`NameError`].
388    pub fn derived(
389        &self,
390        core: &Core,
391        name: impl Into<String>,
392        deps: &[NodeId],
393        fn_id: FnId,
394        equals: EqualsMode,
395    ) -> Result<NodeId, NameError> {
396        let id = core
397            .register_derived(deps, fn_id, equals, false)
398            .expect("invariant: caller has validated dep ids before calling register_derived");
399        self.add(core, id, name)
400    }
401
402    /// Register a dynamic-derived node (fn declares read dep indices).
403    ///
404    /// # Errors
405    /// See [`NameError`].
406    pub fn dynamic(
407        &self,
408        core: &Core,
409        name: impl Into<String>,
410        deps: &[NodeId],
411        fn_id: FnId,
412        equals: EqualsMode,
413    ) -> Result<NodeId, NameError> {
414        let id = core
415            .register_dynamic(deps, fn_id, equals, false)
416            .expect("invariant: caller has validated dep ids before calling register_dynamic");
417        self.add(core, id, name)
418    }
419
420    // -------------------- named-sugar wrappers (§3.2.1) -----------------
421
422    /// Emit a value on a named state node.
423    pub fn set(&self, core: &Core, name: &str, handle: HandleId) {
424        let id = self.node(name);
425        core.emit(id, handle);
426    }
427
428    /// Read the cached value of a named node.
429    #[must_use]
430    pub fn get(&self, core: &Core, name: &str) -> HandleId {
431        let id = self.node(name);
432        core.cache_of(id)
433    }
434
435    /// Clear the cache of a named node and cascade `[INVALIDATE]`.
436    pub fn invalidate_by_name(&self, core: &Core, name: &str) {
437        let id = self.node(name);
438        core.invalidate(id);
439    }
440
441    /// Mark a named node terminal with COMPLETE.
442    pub fn complete_by_name(&self, core: &Core, name: &str) {
443        let id = self.node(name);
444        core.complete(id);
445    }
446
447    /// Mark a named node terminal with ERROR.
448    pub fn error_by_name(&self, core: &Core, name: &str, error_handle: HandleId) {
449        let id = self.node(name);
450        core.error(id, error_handle);
451    }
452
453    // --------------------------- remove (§3.2.3) ------------------------
454
455    /// Remove a named node OR mounted subgraph (R3.2.3 / R3.7.3
456    /// ordering — namespace cleared AFTER the TEARDOWN cascade).
457    ///
458    /// # Errors
459    /// See [`RemoveError`].
460    pub fn remove(&self, core: &Core, name: &str) -> Result<GraphRemoveAudit, RemoveError> {
461        {
462            let inner = self.inner.borrow_mut();
463            if inner.destroyed {
464                return Err(RemoveError::Destroyed);
465            }
466            if inner.children.contains_key(name) {
467                drop(inner);
468                return self.unmount(core, name).map_err(|e| match e {
469                    MountError::Destroyed => RemoveError::Destroyed,
470                    _ => RemoveError::NotFound(name.to_owned()),
471                });
472            }
473        }
474        let node_id = {
475            let inner = self.inner.borrow_mut();
476            if inner.destroyed {
477                return Err(RemoveError::Destroyed);
478            }
479            *inner
480                .names
481                .get(name)
482                .ok_or_else(|| RemoveError::NotFound(name.to_owned()))?
483        };
484        core.teardown(node_id);
485        {
486            let mut inner = self.inner.borrow_mut();
487            inner.names.shift_remove(name);
488            inner.names_inverse.shift_remove(&node_id);
489        }
490        self.fire_namespace_change(core);
491        Ok(GraphRemoveAudit {
492            node_count: 1,
493            mount_count: 0,
494        })
495    }
496
497    // --------------------------- edges (§3.3.1) -------------------------
498
499    /// Derive `[from, to]` edge name pairs. `recursive` qualifies names
500    /// across the mount tree with `::`.
501    #[must_use]
502    pub fn edges(&self, core: &Core, recursive: bool) -> Vec<(String, String)> {
503        let names_map = collect_qualified_names_in(&self.inner, "", recursive);
504        edges_in(core, &self.inner, "", recursive, &names_map)
505    }
506
507    // ------------------- lifecycle pass-throughs (§3.7) -----------------
508
509    /// Subscribe a sink. Returns a [`SubscriptionId`]; pass it back to
510    /// [`Self::unsubscribe`] (owner-invoked, synchronous — D246 rule 3;
511    /// no RAII below the binding).
512    #[must_use = "the SubscriptionId must be kept to later unsubscribe; dropping it leaks the sink"]
513    pub fn subscribe(&self, core: &Core, node_id: NodeId, sink: Sink) -> SubscriptionId {
514        core.subscribe(node_id, sink)
515    }
516
517    /// Detach a sink previously registered via [`Self::subscribe`].
518    pub fn unsubscribe(&self, core: &Core, node_id: NodeId, sub_id: SubscriptionId) {
519        core.unsubscribe(node_id, sub_id);
520    }
521
522    /// Detach a Core topology subscription by id.
523    pub fn unsubscribe_topology(&self, core: &Core, id: TopologySubscriptionId) {
524        core.unsubscribe_topology(id);
525    }
526
527    /// Emit a value on a state node.
528    pub fn emit(&self, core: &Core, node_id: NodeId, new_handle: HandleId) {
529        core.emit(node_id, new_handle);
530    }
531
532    /// Read a node's current cache.
533    #[must_use]
534    pub fn cache_of(&self, core: &Core, node_id: NodeId) -> HandleId {
535        core.cache_of(node_id)
536    }
537
538    /// Whether the node's fn has fired at least once.
539    #[must_use]
540    pub fn has_fired_once(&self, core: &Core, node_id: NodeId) -> bool {
541        core.has_fired_once(node_id)
542    }
543
544    /// Mark the node terminal with COMPLETE.
545    pub fn complete(&self, core: &Core, node_id: NodeId) {
546        core.complete(node_id);
547    }
548
549    /// Mark the node terminal with ERROR.
550    pub fn error(&self, core: &Core, node_id: NodeId, error_handle: HandleId) {
551        core.error(node_id, error_handle);
552    }
553
554    /// Tear the node down (R2.6.4).
555    pub fn teardown(&self, core: &Core, node_id: NodeId) {
556        core.teardown(node_id);
557    }
558
559    /// Clear the node's cache and cascade `[INVALIDATE]`.
560    pub fn invalidate(&self, core: &Core, node_id: NodeId) {
561        core.invalidate(node_id);
562    }
563
564    /// Acquire a pause lock.
565    ///
566    /// # Errors
567    /// See [`PauseError`].
568    pub fn pause(&self, core: &Core, node_id: NodeId, lock_id: LockId) -> Result<(), PauseError> {
569        core.pause(node_id, lock_id)
570    }
571
572    /// Release a pause lock.
573    ///
574    /// # Errors
575    /// See [`PauseError`].
576    pub fn resume(
577        &self,
578        core: &Core,
579        node_id: NodeId,
580        lock_id: LockId,
581    ) -> Result<Option<ResumeReport>, PauseError> {
582        core.resume(node_id, lock_id)
583    }
584
585    /// Allocate a fresh `LockId`.
586    #[must_use]
587    pub fn alloc_lock_id(&self, core: &Core) -> LockId {
588        core.alloc_lock_id()
589    }
590
591    /// Atomically rewire a node's deps.
592    ///
593    /// # Errors
594    /// See [`SetDepsError`].
595    ///
596    /// # Hazards
597    ///
598    /// Re-entrant `set_deps` from inside the firing node's own fn
599    /// corrupts Dynamic `tracked` indices (D1 in
600    /// `~/src/graphrefly-rs/docs/porting-deferred.md`). Acceptable v1:
601    /// most callers are external orchestrators, not the firing node.
602    pub fn set_deps(
603        &self,
604        core: &Core,
605        n: NodeId,
606        new_deps: &[NodeId],
607    ) -> Result<(), SetDepsError> {
608        core.set_deps(n, new_deps)
609    }
610
611    /// Mark the node as resubscribable (R2.2.7).
612    pub fn set_resubscribable(&self, core: &Core, node_id: NodeId, resubscribable: bool) {
613        core.set_resubscribable(node_id, resubscribable);
614    }
615
616    /// Attach `companion` as a meta companion of `parent` (R1.3.9.d).
617    pub fn add_meta_companion(&self, core: &Core, parent: NodeId, companion: NodeId) {
618        core.add_meta_companion(parent, companion);
619    }
620
621    /// Coalesce multiple emissions into a single wave.
622    pub fn batch<F: FnOnce()>(&self, core: &Core, f: F) {
623        core.batch(f);
624    }
625
626    // --------------------- graph-level lifecycle (§3.7) -----------------
627
628    /// General broadcast (canonical R3.7.1).
629    pub fn signal(&self, core: &Core, kind: SignalKind) {
630        match kind {
631            SignalKind::Invalidate => self.signal_invalidate(core),
632            SignalKind::Pause(lock_id) => {
633                for id in collect_signal_ids_with_meta_filter(core, &self.inner) {
634                    let _ = core.pause(id, lock_id);
635                }
636            }
637            SignalKind::Resume(lock_id) => {
638                for id in collect_signal_ids_with_meta_filter(core, &self.inner) {
639                    let _ = core.resume(id, lock_id);
640                }
641            }
642            SignalKind::Complete => {
643                for id in collect_signal_ids_with_meta_filter(core, &self.inner) {
644                    core.complete(id);
645                }
646            }
647            SignalKind::Error(h) => {
648                let ids = collect_signal_ids_with_meta_filter(core, &self.inner);
649                // F1 /qa fix: each core.error() releases one caller share.
650                // Pre-retain (N-1) so the handle survives all N releases.
651                for _ in 1..ids.len() {
652                    core.binding_ptr().retain_handle(h);
653                }
654                for id in ids {
655                    core.error(id, h);
656                }
657            }
658        }
659    }
660
661    /// Broadcast `[INVALIDATE]` across this graph + mount tree
662    /// (meta-companion filtered per R3.7.2; Graph locks dropped before
663    /// any `Core::invalidate`). Idempotent on a destroyed graph.
664    pub fn signal_invalidate(&self, core: &Core) {
665        let to_invalidate = collect_signal_invalidate_ids(core, &self.inner);
666        for id in to_invalidate {
667            core.invalidate(id);
668        }
669    }
670
671    /// Tear down every named node + recursively into mounted children,
672    /// then clear namespace + mount-tree state. R3.7.3 ordering
673    /// (children-first, then own teardown, then clear, then fire
674    /// ns-change) preserved verbatim.
675    pub fn destroy(&self, core: &Core) {
676        destroy_subtree(core, &self.inner);
677    }
678
679    // --------------------------- mount (§3.4) ---------------------------
680
681    /// Embed an existing `child` subgraph under `name`. Fires ns-change
682    /// sinks owner-side (P3) — hence `&Core` (D246 rule 2).
683    ///
684    /// # Errors
685    /// See [`MountError`].
686    pub fn mount(
687        &self,
688        core: &Core,
689        name: impl Into<String>,
690        child: &Graph,
691    ) -> Result<Graph, MountError> {
692        crate::mount::mount(core, &self.inner, name.into(), child)
693    }
694
695    /// Create an empty subgraph (shares the embedder's one `Core`).
696    ///
697    /// # Errors
698    /// See [`MountError`].
699    pub fn mount_new(&self, core: &Core, name: impl Into<String>) -> Result<Graph, MountError> {
700        crate::mount::mount_new(core, &self.inner, name.into())
701    }
702
703    /// Builder pattern: create an empty subgraph, run `builder`, return it.
704    ///
705    /// # Errors
706    /// See [`MountError`].
707    pub fn mount_with<F: FnOnce(&Graph)>(
708        &self,
709        core: &Core,
710        name: impl Into<String>,
711        builder: F,
712    ) -> Result<Graph, MountError> {
713        let child = self.mount_new(core, name)?;
714        builder(&child);
715        Ok(child)
716    }
717
718    /// Detach a previously-mounted subgraph (TEARDOWN cascade).
719    ///
720    /// # Errors
721    /// See [`MountError`].
722    pub fn unmount(&self, core: &Core, name: &str) -> Result<GraphRemoveAudit, MountError> {
723        crate::mount::unmount(core, &self.inner, name)
724    }
725
726    /// Parent chain (root last). `include_self = true` prepends this
727    /// graph.
728    #[must_use]
729    pub fn ancestors(&self, include_self: bool) -> Vec<Graph> {
730        crate::mount::ancestors(&self.inner, include_self)
731    }
732}
733
734// =====================================================================
735// D246/D237: free fns over (&Core, &Rc<RefCell<GraphInner>>)
736// =====================================================================
737
738fn validate_name(name: &str) -> Result<(), NameError> {
739    if name.contains(PATH_SEP) {
740        Err(NameError::InvalidName(name.to_owned()))
741    } else if name.starts_with("_anon_") {
742        Err(NameError::ReservedPrefix(name.to_owned()))
743    } else {
744        Ok(())
745    }
746}
747
748/// Register a namespace-change sink on one graph level. Returns its id.
749pub(crate) fn register_ns_sink(
750    inner_arc: &Rc<RefCell<GraphInner>>,
751    sink: NamespaceChangeSink,
752) -> u64 {
753    let mut inner = inner_arc.borrow_mut();
754    let id = inner.next_ns_sink_id;
755    inner.next_ns_sink_id += 1;
756    inner.namespace_sinks.insert(id, sink);
757    id
758}
759
760/// Remove a namespace-change sink by id (inner-only; no `Core`).
761pub(crate) fn unregister_ns_sink(inner_arc: &Rc<RefCell<GraphInner>>, id: u64) {
762    inner_arc.borrow_mut().namespace_sinks.shift_remove(&id);
763}
764
765/// Pure-namespace path resolution (R3.5.1/R3.5.2). Never touches `Core`.
766pub(crate) fn resolve_checked(
767    inner_arc: &Rc<RefCell<GraphInner>>,
768    path: &str,
769) -> Result<Option<NodeId>, PathError> {
770    if path.is_empty() {
771        return Err(PathError::Empty);
772    }
773    let inner = inner_arc.borrow_mut();
774    if inner.destroyed {
775        return Err(PathError::Destroyed);
776    }
777    let segments: Vec<&str> = path.split(PATH_SEP).collect();
778    let first = segments[0];
779    if first == ".." {
780        let parent_weak = inner.parent.as_ref().ok_or(PathError::NoParent)?;
781        let parent_inner = parent_weak.upgrade().ok_or(PathError::NoParent)?;
782        drop(inner);
783        if segments.len() == 1 {
784            return Ok(None);
785        }
786        let rest = segments[1..].join(PATH_SEP);
787        resolve_checked(&parent_inner, &rest)
788    } else if segments.len() > 1 {
789        let child = inner
790            .children
791            .get(first)
792            .cloned()
793            .ok_or_else(|| PathError::ChildNotFound(first.to_string()))?;
794        drop(inner);
795        let rest = segments[1..].join(PATH_SEP);
796        resolve_checked(&child, &rest)
797    } else {
798        Ok(inner.names.get(first).copied())
799    }
800}
801
802/// Fire one graph level's namespace-change sinks with the owner's
803/// `&Core` (D246 rule 2 owner-side). Sinks run with no Graph lock held.
804pub(crate) fn fire_ns(core: &Core, inner_arc: &Rc<RefCell<GraphInner>>) {
805    let sinks: Vec<NamespaceChangeSink> = {
806        let inner = inner_arc.borrow_mut();
807        inner.namespace_sinks.values().cloned().collect()
808    };
809    for sink in sinks {
810        sink(core);
811    }
812}
813
814/// `destroy()` over `Rc<RefCell<GraphInner>>` + root `&Core`. Ordering
815/// preserved verbatim (R3.7.3).
816pub(crate) fn destroy_subtree(core: &Core, inner_arc: &Rc<RefCell<GraphInner>>) {
817    let (own_ids, child_clones) = {
818        let mut inner = inner_arc.borrow_mut();
819        if inner.destroyed {
820            return; // Idempotent.
821        }
822        inner.destroyed = true;
823        let own = inner.names.values().copied().collect::<Vec<_>>();
824        let kids = inner.children.values().cloned().collect::<Vec<_>>();
825        (own, kids)
826    };
827    for child in &child_clones {
828        destroy_subtree(core, child);
829    }
830    for id in own_ids {
831        core.teardown(id);
832    }
833    {
834        let mut inner = inner_arc.borrow_mut();
835        inner.names.clear();
836        inner.names_inverse.clear();
837        inner.children.clear();
838    }
839    // Fire the final namespace change (reactive describe/observe see the
840    // emptied graph) BEFORE dropping the sinks — so observers get the
841    // destroy notification, then the sinks are released.
842    fire_ns(core, inner_arc);
843    // QA-A1: clear the namespace-change sinks on destroy. Without this a
844    // reactive describe/observe ns-sink (registered via
845    // `register_ns_sink`, NOT an `OwnedCore`-tracked Core sub) would
846    // outlive `destroy()` for the whole `GraphInner` lifetime — the
847    // documented `graph.destroy(core)` teardown fallback must actually
848    // collect it. (The handle's Core *topology* sub is still owner-
849    // detach-only — see the corrected `#[must_use]` text.)
850    inner_arc.borrow_mut().namespace_sinks.clear();
851}
852
853/// Tree-wide gather for `signal_pause`/`resume`/`complete`/`error`
854/// (meta-companion filtered per D4). `Rc<RefCell<GraphInner>>` worklist
855/// + the single root `&Core`.
856fn collect_signal_ids_with_meta_filter(core: &Core, root: &Rc<RefCell<GraphInner>>) -> Vec<NodeId> {
857    let mut out: Vec<NodeId> = Vec::new();
858    let mut worklist: Vec<Rc<RefCell<GraphInner>>> = vec![root.clone()];
859    while let Some(inner_arc) = worklist.pop() {
860        let (own_ids, meta_set, child_clones) = {
861            let inner = inner_arc.borrow_mut();
862            if inner.destroyed {
863                continue;
864            }
865            let mut meta_set: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
866            for &parent_id in inner.names.values() {
867                for child_id in core.meta_companions_of(parent_id) {
868                    meta_set.insert(child_id);
869                }
870            }
871            (
872                inner.names.values().copied().collect::<Vec<_>>(),
873                meta_set,
874                inner.children.values().cloned().collect::<Vec<_>>(),
875            )
876        };
877        for id in own_ids {
878            if meta_set.contains(&id) {
879                continue;
880            }
881            out.push(id);
882        }
883        worklist.extend(child_clones);
884    }
885    out
886}
887
888/// Iterative gather for `signal_invalidate` (DFS, meta-filtered).
889fn collect_signal_invalidate_ids(core: &Core, root: &Rc<RefCell<GraphInner>>) -> Vec<NodeId> {
890    let mut out: Vec<NodeId> = Vec::new();
891    let mut worklist: Vec<Rc<RefCell<GraphInner>>> = vec![root.clone()];
892    while let Some(inner_arc) = worklist.pop() {
893        let (own_ids, meta_set, child_clones) = {
894            let inner = inner_arc.borrow_mut();
895            if inner.destroyed {
896                continue;
897            }
898            let mut meta_set: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
899            for &parent_id in inner.names.values() {
900                for child_id in core.meta_companions_of(parent_id) {
901                    meta_set.insert(child_id);
902                }
903            }
904            (
905                inner.names.values().copied().collect::<Vec<_>>(),
906                meta_set,
907                inner.children.values().cloned().collect::<Vec<_>>(),
908            )
909        };
910        for id in own_ids {
911            if meta_set.contains(&id) {
912                continue;
913            }
914            out.push(id);
915        }
916        worklist.extend(child_clones);
917    }
918    out
919}
920
921/// Build an `id → qualified-name` map across this graph + (if
922/// `recursive`) its mount tree. Pure-namespace (no `Core`).
923pub(crate) fn collect_qualified_names_in(
924    inner_arc: &Rc<RefCell<GraphInner>>,
925    prefix: &str,
926    recursive: bool,
927) -> IndexMap<NodeId, String> {
928    let inner = inner_arc.borrow_mut();
929    let mut map: IndexMap<NodeId, String> = inner
930        .names
931        .iter()
932        .map(|(n, id)| (*id, format!("{prefix}{n}")))
933        .collect();
934    let children: Vec<(String, Rc<RefCell<GraphInner>>)> = if recursive {
935        inner
936            .children
937            .iter()
938            .map(|(n, g)| (n.clone(), g.clone()))
939            .collect()
940    } else {
941        Vec::new()
942    };
943    drop(inner);
944    for (child_name, child_inner) in children {
945        let child_prefix = format!("{prefix}{child_name}::");
946        let child_map = collect_qualified_names_in(&child_inner, &child_prefix, true);
947        for (id, name) in child_map {
948            map.entry(id).or_insert(name);
949        }
950    }
951    map
952}
953
954/// Derive `[from, to]` edge name pairs. Needs the root `&Core`.
955pub(crate) fn edges_in(
956    core: &Core,
957    inner_arc: &Rc<RefCell<GraphInner>>,
958    prefix: &str,
959    recursive: bool,
960    names_map: &IndexMap<NodeId, String>,
961) -> Vec<(String, String)> {
962    let inner = inner_arc.borrow_mut();
963    let qualified: Vec<(String, NodeId)> = inner
964        .names
965        .iter()
966        .map(|(n, id)| (format!("{prefix}{n}"), *id))
967        .collect();
968    let children: Vec<(String, Rc<RefCell<GraphInner>>)> = if recursive {
969        inner
970            .children
971            .iter()
972            .map(|(n, g)| (n.clone(), g.clone()))
973            .collect()
974    } else {
975        Vec::new()
976    };
977    drop(inner);
978    let mut result: Vec<(String, String)> = Vec::new();
979    for (to_name, id) in &qualified {
980        let dep_ids = core.deps_of(*id);
981        for dep_id in dep_ids {
982            let from_name = names_map
983                .get(&dep_id)
984                .cloned()
985                .unwrap_or_else(|| format!("{prefix}_anon_{}", dep_id.raw()));
986            result.push((from_name, to_name.clone()));
987        }
988    }
989    for (child_name, child_inner) in children {
990        let child_prefix = format!("{prefix}{child_name}::");
991        result.extend(edges_in(core, &child_inner, &child_prefix, true, names_map));
992    }
993    result
994}
995
996// D247/D248: the QA-A4 `Graph: Send + Sync + 'static` assertion is
997// **deleted**. Under D246/S2c single-owner the namespace tree is
998// `Rc<RefCell<GraphInner>>` (the `Arc<Mutex<>>` + `Send+Sync` was
999// shared-Core-era legacy), so `Graph` is intentionally `!Send + !Sync`
1000// — it lives on, and is touched only by, the one thread that owns the
1001// `Core`.