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
impl CausalGraph
Sourcepub fn set_chain_manager(&mut self, cm: Arc<ChainManager>)
pub fn set_chain_manager(&mut self, cm: Arc<ChainManager>)
Set the chain manager for ExoChain event logging.
Sourcepub fn set_governance_engine(&mut self, ge: Arc<GovernanceEngine>)
pub fn set_governance_engine(&mut self, ge: Arc<GovernanceEngine>)
Set the governance engine for gating destructive operations.
Sourcepub fn add_node(&self, label: String, metadata: Value) -> NodeId
pub fn add_node(&self, label: String, metadata: Value) -> NodeId
Add a node with an auto-assigned ID.
Sourcepub fn get_node(&self, id: NodeId) -> Option<CausalNode>
pub fn get_node(&self, id: NodeId) -> Option<CausalNode>
Retrieve a clone of the node with the given ID.
Sourcepub fn set_node_state(&self, id: NodeId, state: &str) -> bool
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.
Sourcepub fn merge_node_metadata(
&self,
id: NodeId,
patch: &Map<String, Value>,
) -> bool
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.
Sourcepub fn remove_node(&self, id: NodeId) -> Option<CausalNode>
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.
Sourcepub fn link(
&self,
source: NodeId,
target: NodeId,
edge_type: CausalEdgeType,
weight: f32,
timestamp: u64,
chain_seq: u64,
) -> bool
pub fn link( &self, source: NodeId, target: NodeId, edge_type: CausalEdgeType, weight: f32, timestamp: u64, chain_seq: u64, ) -> bool
Create an edge from source to target.
Returns false if either endpoint does not exist.
Sourcepub fn unlink(&self, source: NodeId, target: NodeId) -> usize
pub fn unlink(&self, source: NodeId, target: NodeId) -> usize
Remove all edges between source and target (in that direction).
Returns the number of edges removed.
Sourcepub fn component_count_fast(&self) -> u64
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.
Sourcepub fn get_forward_edges(&self, id: NodeId) -> Vec<CausalEdge>
pub fn get_forward_edges(&self, id: NodeId) -> Vec<CausalEdge>
Return all edges originating from id.
Sourcepub fn get_reverse_edges(&self, id: NodeId) -> Vec<CausalEdge>
pub fn get_reverse_edges(&self, id: NodeId) -> Vec<CausalEdge>
Return all edges targeting id.
Sourcepub fn get_edges_by_type(
&self,
id: NodeId,
edge_type: &CausalEdgeType,
) -> Vec<CausalEdge>
pub fn get_edges_by_type( &self, id: NodeId, edge_type: &CausalEdgeType, ) -> Vec<CausalEdge>
Return forward edges from id that match edge_type.
Sourcepub fn node_count(&self) -> u64
pub fn node_count(&self) -> u64
Number of nodes currently in the graph.
Sourcepub fn edge_count(&self) -> u64
pub fn edge_count(&self) -> u64
Number of edges currently in the graph.
Sourcepub fn clear(&self) -> Result<(), KernelError>
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.
Sourcepub fn traverse_forward(&self, start: NodeId, depth: usize) -> Vec<NodeId> ⓘ
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).
Sourcepub fn traverse_reverse(&self, start: NodeId, depth: usize) -> Vec<NodeId> ⓘ
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).
Sourcepub fn find_path(
&self,
from: NodeId,
to: NodeId,
max_depth: usize,
) -> Option<Vec<NodeId>>
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.
Sourcepub fn trace_causal_chain(
&self,
from: NodeId,
to: NodeId,
max_depth: usize,
) -> Option<CausalChain>
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.
Sourcepub fn nodes_for_conv(&self, conv_id: &str) -> Vec<CausalNode>
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.
Sourcepub fn edges_for_conv(&self, conv_id: &str) -> Vec<CausalEdge>
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.
Sourcepub fn degree(&self, id: NodeId) -> usize
pub fn degree(&self, id: NodeId) -> usize
Degree of a node (in + out edges, treating graph as undirected).
Sourcepub fn out_degree(&self, id: NodeId) -> usize
pub fn out_degree(&self, id: NodeId) -> usize
Out-degree (number of outgoing edges).
Sourcepub fn connected_components(&self) -> Vec<Vec<NodeId>>
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.
Sourcepub fn detect_communities(&self, max_iterations: usize) -> Vec<Vec<NodeId>>
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.
Sourcepub fn spectral_analysis(&self, max_iterations: usize) -> SpectralResult
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).
Sourcepub fn spectral_analysis_rff(
&self,
num_features: usize,
max_iter: usize,
) -> SpectralResult
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
- Generate
num_featuresrandom Gaussian vectors. - For each random vector z, compute L*z (one sparse mat-vec, O(m)).
- Build a
num_features x num_featurescovariance matrix C where C[i][j] = z_i^T * L * z_j. - Eigendecompose C (small dense matrix) to get approximate eigenvalues of L.
- 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 totridiag_eigen-style solver). Typically 200.
Sourcepub fn spectral_analysis_auto(&self) -> (SpectralMethod, SpectralResult)
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, Jacobimax_iter = 50(matches DEMOCRITUS)
Sourcepub fn spectral_analysis_with_method(
&self,
method: SpectralMethod,
) -> SpectralResult
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.
Sourcepub fn spectral_partition(&self) -> (Vec<NodeId>, Vec<NodeId>)
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.
Sourcepub fn compute_coupling(
&self,
change_events: &[ChangeEvent],
) -> Vec<CouplingPair>
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.
Sourcepub fn predict_changes(
&self,
change_events: &[ChangeEvent],
window_size: usize,
baseline_factor: f64,
) -> Vec<ChangePrediction>
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
impl CausalGraph
Sourcepub fn save_to_writer<W: Write>(&self, writer: W) -> Result<(), Error>
pub fn save_to_writer<W: Write>(&self, writer: W) -> Result<(), Error>
Serialize the entire graph to a JSON writer.
Sourcepub fn load_from_reader<R: Read>(reader: R) -> Result<Self, Error>
pub fn load_from_reader<R: Read>(reader: R) -> Result<Self, Error>
Deserialize a graph from a JSON reader.
Sourcepub fn load_from_file(path: &Path) -> Result<Self, Error>
pub fn load_from_file(path: &Path) -> Result<Self, Error>
Load a graph from a file path.
Source§impl CausalGraph
impl CausalGraph
Sourcepub fn rank_candidates(
&self,
candidates: &[(u64, u64, f32)],
fiedler: &[f64],
lambda2: f64,
) -> Vec<EvidenceRanking>
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
impl CausalGraph
Sourcepub fn coherence_fast(&self, model: &EmlCoherenceModel) -> CoherencePrediction
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
impl Debug for CausalGraph
Auto Trait Implementations§
impl !Freeze for CausalGraph
impl !RefUnwindSafe for CausalGraph
impl Send for CausalGraph
impl Sync for CausalGraph
impl Unpin for CausalGraph
impl UnsafeUnpin for CausalGraph
impl UnwindSafe for CausalGraph
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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