Skip to main content

converge_pack/
context.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Context keys and the shared context contract.
5//!
6//! Context is the API. Suggestors don't call each other — they read from and
7//! write to shared context through typed keys.
8
9use serde::{Deserialize, Serialize};
10
11use crate::fact::{Fact, ProposedFact};
12
13/// Typed keys for the shared context namespace.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
15#[cfg_attr(feature = "strum", derive(strum::EnumIter))]
16pub enum ContextKey {
17    /// Initial inputs from the root intent. Set once at initialization.
18    Seeds,
19    /// Proposed ideas and hypotheses from analysis suggestors.
20    Hypotheses,
21    /// Action plans and strategic recommendations.
22    Strategies,
23    /// Limitations, rules, and boundary conditions.
24    Constraints,
25    /// Observations, market data, and signals from the environment.
26    Signals,
27    /// Competitive intelligence and comparisons.
28    Competitors,
29    /// Assessments, ratings, and evaluations of other facts.
30    Evaluations,
31    /// LLM-generated suggestions awaiting validation.
32    Proposals,
33    /// Error and debugging information. Never blocks convergence.
34    Diagnostic,
35    /// Votes cast on topics — payload is `governance::Vote`.
36    Votes,
37    /// Substantive concerns recorded by participants — payload is
38    /// `governance::Disagreement`.
39    Disagreements,
40    /// Deterministic outcomes of evaluating votes against a `ConsensusRule` —
41    /// payload is `governance::ConsensusOutcome`.
42    ConsensusOutcomes,
43}
44
45/// Read-only view of the shared context.
46///
47/// Suggestors receive `&dyn Context` during `accepts()` and `execute()`.
48/// They cannot mutate it directly — mutations happen through `AgentEffect`
49/// after the engine collects all effects and merges them deterministically.
50pub trait Context: Send + Sync {
51    /// Check whether any facts exist under this key.
52    fn has(&self, key: ContextKey) -> bool;
53
54    /// Get all facts under this key.
55    fn get(&self, key: ContextKey) -> &[Fact];
56
57    /// Get all proposed facts (unvalidated).
58    fn get_proposals(&self, key: ContextKey) -> &[ProposedFact] {
59        let _ = key;
60        &[]
61    }
62
63    /// Count of facts under a key.
64    fn count(&self, key: ContextKey) -> usize {
65        self.get(key).len()
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    struct MockContext {
74        facts: std::collections::HashMap<ContextKey, Vec<Fact>>,
75    }
76
77    impl MockContext {
78        fn empty() -> Self {
79            Self {
80                facts: std::collections::HashMap::new(),
81            }
82        }
83    }
84
85    impl Context for MockContext {
86        fn has(&self, key: ContextKey) -> bool {
87            self.facts.get(&key).is_some_and(|v| !v.is_empty())
88        }
89
90        fn get(&self, key: ContextKey) -> &[Fact] {
91            self.facts.get(&key).map_or(&[], Vec::as_slice)
92        }
93    }
94
95    #[test]
96    fn get_proposals_default_returns_empty() {
97        let ctx = MockContext::empty();
98        assert!(ctx.get_proposals(ContextKey::Seeds).is_empty());
99        assert!(ctx.get_proposals(ContextKey::Hypotheses).is_empty());
100    }
101
102    #[test]
103    fn count_default_delegates_to_get() {
104        let ctx = MockContext::empty();
105        assert_eq!(ctx.count(ContextKey::Seeds), 0);
106    }
107
108    #[test]
109    fn has_returns_false_for_empty() {
110        let ctx = MockContext::empty();
111        assert!(!ctx.has(ContextKey::Seeds));
112    }
113
114    #[cfg(feature = "kernel-authority")]
115    #[test]
116    fn count_reflects_facts() {
117        use crate::fact::kernel_authority;
118
119        let mut ctx = MockContext::empty();
120        ctx.facts.insert(
121            ContextKey::Seeds,
122            vec![kernel_authority::new_fact(ContextKey::Seeds, "f1", "a")],
123        );
124        assert_eq!(ctx.count(ContextKey::Seeds), 1);
125        assert!(ctx.has(ContextKey::Seeds));
126        assert!(!ctx.has(ContextKey::Hypotheses));
127    }
128}