Skip to main content

CausalGraph

Struct CausalGraph 

Source
pub struct CausalGraph { /* private fields */ }
Expand description

A concurrent directed acyclic graph for causal reasoning.

Internally backed by DashMap for lock-free concurrent reads and fine-grained write locking. Edge lists are stored in both forward (outgoing) and reverse (incoming) adjacency maps for efficient bidirectional traversal.

Implementations§

Source§

impl CausalGraph

Source

pub fn new() -> Self

Create an empty causal graph.

Source

pub fn set_chain_manager(&mut self, cm: Arc<ChainManager>)

Set the chain manager for ExoChain event logging.

Source

pub fn set_governance_engine(&mut self, ge: Arc<GovernanceEngine>)

Set the governance engine for gating destructive operations.

Source

pub fn add_node(&self, label: String, metadata: Value) -> NodeId

Add a node with an auto-assigned ID.

Source

pub fn get_node(&self, id: NodeId) -> Option<CausalNode>

Retrieve a clone of the node with the given ID.

Source

pub fn set_node_state(&self, id: NodeId, state: &str) -> bool

Mirror a causal-model lifecycle state onto a node’s metadata.state (ADR-062 P0.2).

The L2 session view (SessionView) owns the authoritative per-turn NodeState; this writes the same tag (state.as_str()) into the durable causal node so the two substrates stay in sync. Returns false if the node is unknown.

§Chain event (ADR-067 P1-graph / WEFT-629)

When a chain manager is attached (exochain feature) and the state changes, emits EVENT_KIND_CAUSAL_NODE_STATE with {node_id, from, to} so fold-replay can reconstruct lifecycle history. No-op transitions (same state) do not append.

Source

pub fn merge_node_metadata( &self, id: NodeId, patch: &Map<String, Value>, ) -> bool

Merge the keys of patch into a node’s metadata object, in place, under the node’s existing per-entry lock (Phase B enrichment path).

This is the single mutable-metadata seam on the causal graph. Async classification enrichment (design §D4) calls it to overwrite the classification blob with an llm-tier refinement after the turn has already committed. Protected identity/lifecycle keys (Self::PROTECTED_METADATA_KEYS) are never clobbered: an attempt to patch one is ignored and logged at WARN, not treated as an error.

The merge is deterministic and holds a single lock for the whole patch (the same get_mut entry lock set_node_state takes), so it serialises cleanly against a concurrent set_node_state: each write lands whole, neither can observe a half-applied patch, and both survive. Under ADR-067 P1-graph replay this makes each patch one observable state-at-T mutation a future snapshot/journal can capture atomically — a “classification refined” event, not a torn write.

Returns false if the node is unknown. A patch that is entirely protected keys still returns true (the node exists); it simply applies nothing.

Source

pub fn remove_node(&self, id: NodeId) -> Option<CausalNode>

Remove a node and all edges incident to it.

Returns the removed node, or None if the ID was not found.

Create an edge from source to target.

Returns false if either endpoint does not exist.

Remove all edges between source and target (in that direction).

Returns the number of edges removed.

Source

pub fn component_count_fast(&self) -> u64

O(1) undirected connected-component count (WEFT-510).

Maintained incrementally on add_node/link; rebuilt after removals. Prefer this over connected_components().len() in hot paths such as EML coherence feature extraction.

Source

pub fn get_forward_edges(&self, id: NodeId) -> Vec<CausalEdge>

Return all edges originating from id.

Source

pub fn get_reverse_edges(&self, id: NodeId) -> Vec<CausalEdge>

Return all edges targeting id.

Source

pub fn get_edges_by_type( &self, id: NodeId, edge_type: &CausalEdgeType, ) -> Vec<CausalEdge>

Return forward edges from id that match edge_type.

Source

pub fn node_count(&self) -> u64

Number of nodes currently in the graph.

Source

pub fn edge_count(&self) -> u64

Number of edges currently in the graph.

Source

pub fn clear(&self) -> Result<(), KernelError>

Remove all nodes and edges (used during calibration cleanup).

When the exochain feature is enabled and a governance engine is configured, this operation is gated: it will return [KernelError::GovernanceDenied] if the governance check fails.

Source

pub fn traverse_forward(&self, start: NodeId, depth: usize) -> Vec<NodeId> ⓘ

BFS traversal forward from start up to depth hops.

Returns all discovered node IDs (excluding start).

Source

pub fn traverse_reverse(&self, start: NodeId, depth: usize) -> Vec<NodeId> ⓘ

BFS traversal backward (following reverse edges) from start up to depth hops.

Returns all discovered node IDs (excluding start).

Source

pub fn find_path( &self, from: NodeId, to: NodeId, max_depth: usize, ) -> Option<Vec<NodeId>>

Find the shortest path from from to to using BFS, limited to max_depth hops.

Returns the node sequence including both endpoints, or None if no path exists within the depth limit.

Source

pub fn trace_causal_chain( &self, from: NodeId, to: NodeId, max_depth: usize, ) -> Option<CausalChain>

Trace a typed causal chain from from to to.

Uses BFS over forward edges (like [find_path]) but records the edge type at each step and builds a human-readable explanation string (e.g., “A –Causes–> B –Enables–> C –EvidenceFor–> D”).

Returns None if no path exists within max_depth hops, or if either endpoint is missing from the graph.

Source

pub fn node_ids(&self) -> Vec<NodeId> ⓘ

List all node IDs currently in the graph.

Source

pub fn nodes_for_conv(&self, conv_id: &str) -> Vec<CausalNode>

All nodes whose metadata.conv_id equals conv_id, cloned in full.

The global graph is unbounded (nodes are never removed for the daemon’s lifetime), so any conversation-scoped projection — the conversation.graph RPC in particular — MUST filter at the source rather than ship the whole graph. Each returned CausalNode carries its metadata intact, including the state tag written by Self::set_node_state, so a caller can read node lifecycle state without a second lookup.

Source

pub fn edges_for_conv(&self, conv_id: &str) -> Vec<CausalEdge>

All forward edges internal to a conversation — both endpoints carry metadata.conv_id == conv_id.

A conversation’s lineage (Follows edges) is intra-conversation by construction, so requiring both endpoints in the conversation drops any stray cross-conversation causal edge rather than dangling it against a node the scoped projection did not emit.

Source

pub fn degree(&self, id: NodeId) -> usize

Degree of a node (in + out edges, treating graph as undirected).

Source

pub fn in_degree(&self, id: NodeId) -> usize

In-degree (number of incoming edges).

Source

pub fn out_degree(&self, id: NodeId) -> usize

Out-degree (number of outgoing edges).

Source

pub fn connected_components(&self) -> Vec<Vec<NodeId>>

Find connected components treating the graph as undirected.

Returns a vec of components, each component being a vec of node IDs. Components are sorted largest-first.

Source

pub fn detect_communities(&self, max_iterations: usize) -> Vec<Vec<NodeId>>

Detect communities using label propagation on the undirected graph.

Each node starts with its own label. In each iteration, every node adopts the most frequent label among its neighbors (weighted by edge weight). Converges when no labels change, or after max_iterations.

Returns a map from community label (a NodeId) to the set of node IDs in that community.

Source

pub fn spectral_analysis(&self, max_iterations: usize) -> SpectralResult

Compute the algebraic connectivity (lambda_2) of the graph.

Lambda_2 is the second-smallest eigenvalue of the graph Laplacian.

  • lambda_2 = 0 means the graph is disconnected.
  • Higher values indicate stronger connectivity.

Uses sparse Lanczos iteration at O(km) where m = number of edges and k = max_iterations. For typical ECC graphs with average degree ~10, this is ~200x faster than the dense O(kn^2) approach.

Returns (lambda_2, fiedler_vector) where the Fiedler vector can be used for spectral partitioning (sign of each component indicates which partition the node belongs to).

Source

pub fn spectral_analysis_rff( &self, num_features: usize, max_iter: usize, ) -> SpectralResult

Approximate spectral analysis using Random Fourier Features.

O(m) per feature vector – 3-6x faster than Lanczos on large graphs (>10K nodes) with approximately 5% accuracy loss. Uses random projections to estimate the graph Laplacian’s eigenvalues without building an explicit Krylov subspace.

§Algorithm
  1. Generate num_features random Gaussian vectors.
  2. For each random vector z, compute L*z (one sparse mat-vec, O(m)).
  3. Build a num_features x num_features covariance matrix C where C[i][j] = z_i^T * L * z_j.
  4. Eigendecompose C (small dense matrix) to get approximate eigenvalues of L.
  5. Return the second-smallest eigenvalue as lambda_2.
§Arguments
  • num_features: Number of random feature vectors (higher = more accurate, but slower). 64-128 is typical.
  • max_iter: Maximum Jacobi iterations for the small dense eigensolve (passed through to tridiag_eigen-style solver). Typically 200.
Source

pub fn spectral_analysis_auto(&self) -> (SpectralMethod, SpectralResult)

Automatic path selection for algebraic connectivity (KG-004 / WEFT-361).

Dispatches between Lanczos and RFF using select_spectral_method. When the policy prefers SpectralMethod::Eml (very large graphs), this still runs RFF for an exact-path Fiedler result — EML is an O(1) feature model in crate::eml_coherence and is not applied here. Callers that want pure EML should check select_spectral_method and invoke EmlCoherenceModel themselves.

Returns (chosen_method_executed, result) where chosen_method_executed is always Lanczos or Rff (never Eml).

§Defaults
  • Lanczos: max_iterations = min(50, n-1)
  • RFF: num_features = 64, Jacobi max_iter = 50 (matches DEMOCRITUS)
Source

pub fn spectral_analysis_with_method( &self, method: SpectralMethod, ) -> SpectralResult

Run a specific spectral method (Lanczos or RFF).

SpectralMethod::Eml is not executable here (needs EmlCoherenceModel); it falls back to RFF so callers can pass the policy enum without a second branch.

Source

pub fn spectral_partition(&self) -> (Vec<NodeId>, Vec<NodeId>)

Partition the graph into two halves using the Fiedler vector.

Nodes with positive Fiedler vector components go to partition A, negative to partition B. This is spectral bisection — the minimum-cut balanced partition.

Source

pub fn compute_coupling( &self, change_events: &[ChangeEvent], ) -> Vec<CouplingPair>

Compute co-modification coupling between nodes based on temporal co-occurrence patterns.

Given a list of “change events” (each event is a set of node IDs that changed together, plus a timestamp), computes a coupling score for every pair of nodes that have been modified together.

The coupling score for nodes (A, B) is: coupling = co_changes(A,B) / max(changes(A), changes(B))

Returns pairs sorted by coupling score descending.

Source

pub fn predict_changes( &self, change_events: &[ChangeEvent], window_size: usize, baseline_factor: f64, ) -> Vec<ChangePrediction>

Detect burst patterns in change events and predict which nodes are likely to change next.

A “burst” is a period where a node has significantly more changes than its baseline rate. Nodes currently in a burst, or recently co-modified with nodes in a burst, are predicted to change next.

window_size is the number of recent events to consider for the burst window. baseline_factor is the multiplier above which a node’s activity is considered a burst (e.g., 2.0 = 2x baseline).

Returns nodes sorted by prediction confidence (descending).

Source§

impl CausalGraph

Source

pub fn save_to_writer<W: Write>(&self, writer: W) -> Result<(), Error>

Serialize the entire graph to a JSON writer.

Source

pub fn load_from_reader<R: Read>(reader: R) -> Result<Self, Error>

Deserialize a graph from a JSON reader.

Source

pub fn save_to_file(&self, path: &Path) -> Result<(), Error>

Save the graph to a file path.

Source

pub fn load_from_file(path: &Path) -> Result<Self, Error>

Load a graph from a file path.

Source§

impl CausalGraph

Source

pub fn rank_candidates( &self, candidates: &[(u64, u64, f32)], fiedler: &[f64], lambda2: f64, ) -> Vec<EvidenceRanking>

Rank candidate edges by predicted coherence impact.

Uses the Fiedler vector from a recent spectral analysis to predict how each candidate edge would affect algebraic connectivity, without actually modifying the graph.

§Arguments
  • candidates: Slice of (source_node_id, target_node_id, weight).
  • fiedler: Fiedler vector (indexed by node position, not node ID).
  • lambda2: Current algebraic connectivity.
§Returns

Rankings sorted by predicted delta descending (biggest impact first).

Source§

impl CausalGraph

Source

pub fn coherence_fast(&self, model: &EmlCoherenceModel) -> CoherencePrediction

O(1) approximate coherence from EML model.

Returns a full CoherencePrediction with lambda_2, Fiedler norm, and uncertainty. Falls back to density-based estimate if model not trained.

Trait Implementations§

Source§

impl Debug for CausalGraph

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for CausalGraph

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Pointee for T

Source§

type Pointer = u32

Source§

fn debug( pointer: <T as Pointee>::Pointer, f: &mut Formatter<'_>, ) -> Result<(), Error>

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more