Skip to main content

areev_context/
policy.rs

1//! Format policy types for context rendering.
2//!
3//! `FormatPolicy` controls how recalled grains are rendered into LLM-ready strings:
4//! output format (SML/Markdown/PlainText/JSON), metadata verbosity, ordering,
5//! token budget, and section grouping.
6
7use areev_cal::store_types::GrainTypeDiversityConfig;
8use areev_core::types::GrainType;
9use std::collections::HashMap;
10
11/// Output format for rendered context.
12#[derive(Debug, Clone, PartialEq)]
13pub enum OutputFormat {
14    /// SML tags: `<fact>`, `<event>`, etc. Optimized for Claude.
15    Sml,
16    /// TOON (Token-Oriented Object Notation). Compact, indentation-based format optimized for LLM token efficiency.
17    Toon,
18    /// Markdown with headers and formatting. Good for GPT-4/Gemini.
19    Markdown,
20    /// Plain text with `===` section headers.
21    PlainText,
22    /// Structured JSON. For programmatic consumers and A2A.
23    Json,
24}
25
26/// How much metadata to include per grain.
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum MetadataLevel {
29    /// Content fields only — no hash, no confidence, no timestamps.
30    None,
31    /// Confidence + created_at only.
32    Minimal,
33    /// All common metadata: hash, confidence, tags, source_type, created_at,
34    /// verification_status, namespace, author_did.
35    Full,
36}
37
38/// Ordering strategy for grains in output.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum Ordering {
41    /// By retrieval score (highest first). Default.
42    ByRelevance,
43    /// Oldest first (ascending created_at).
44    Chronological,
45    /// Newest first (descending created_at).
46    ReverseChronological,
47    /// Group by entity (subject field), within each group order by relevance.
48    ByEntity,
49}
50
51/// Section grouping configuration.
52#[derive(Debug, Clone, Default)]
53pub struct SectionConfig {
54    /// Whether to group grains by type with section headers.
55    pub group_by_type: bool,
56    /// Custom section order. When empty, uses default:
57    /// State, Goal, Fact, Tool, Event, Observation, Reasoning,
58    /// Workflow, Consensus, Consent.
59    pub type_order: Vec<GrainType>,
60}
61
62/// Per-grain-type overrides.
63#[derive(Debug, Clone)]
64pub struct GrainTypeOverride {
65    /// Whether to include this grain type. Default: true.
66    pub include: bool,
67    /// Max grains of this type. None = no limit (budget-constrained).
68    pub max_count: Option<usize>,
69}
70
71impl Default for GrainTypeOverride {
72    fn default() -> Self {
73        Self {
74            include: true,
75            max_count: None,
76        }
77    }
78}
79
80/// Complete formatting policy. Constructed via builder pattern.
81#[derive(Debug, Clone)]
82pub struct FormatPolicy {
83    pub format: OutputFormat,
84    pub metadata: MetadataLevel,
85    pub ordering: Ordering,
86    pub sections: SectionConfig,
87    pub token_budget: Option<usize>,
88    pub grain_overrides: HashMap<GrainType, GrainTypeOverride>,
89    /// Original query text. Used by Knowledge Update chain rendering to detect
90    /// recency intent ("currently", "most recently", etc.) for update-only
91    /// suppression mode.
92    pub query_text: Option<String>,
93    /// Grain type diversity floor. When `Some`, the budget allocator reserves
94    /// slots for underrepresented grain types before filling by priority.
95    /// Default: `Some(GrainTypeDiversityConfig::default())`.
96    pub grain_type_diversity: Option<GrainTypeDiversityConfig>,
97    /// Admit grains whose `verification_status` is `retracted`. Off by
98    /// default: a retraction withholds a memory from being treated as true,
99    /// so assembly must not hand one to a model. Turn it on for audit and
100    /// forensic reads, which want the withdrawn record precisely because it
101    /// was withdrawn.
102    pub include_retracted: bool,
103}
104
105impl FormatPolicy {
106    pub fn new(format: OutputFormat) -> Self {
107        Self {
108            format,
109            metadata: MetadataLevel::Minimal,
110            ordering: Ordering::ByRelevance,
111            sections: SectionConfig::default(),
112            token_budget: None,
113            grain_overrides: HashMap::new(),
114            query_text: None,
115            grain_type_diversity: Some(GrainTypeDiversityConfig::default()),
116            include_retracted: false,
117        }
118    }
119
120    pub fn include_retracted(mut self, on: bool) -> Self {
121        self.include_retracted = on;
122        self
123    }
124
125    pub fn metadata(mut self, level: MetadataLevel) -> Self {
126        self.metadata = level;
127        self
128    }
129
130    pub fn ordering(mut self, ordering: Ordering) -> Self {
131        self.ordering = ordering;
132        self
133    }
134
135    pub fn token_budget(mut self, budget: usize) -> Self {
136        self.token_budget = Some(budget);
137        self
138    }
139
140    pub fn group_by_type(mut self) -> Self {
141        self.sections.group_by_type = true;
142        self
143    }
144
145    pub fn grain_override(mut self, gt: GrainType, ovr: GrainTypeOverride) -> Self {
146        self.grain_overrides.insert(gt, ovr);
147        self
148    }
149
150    /// Set the original query text for recency detection in KU rendering.
151    pub fn query_text(mut self, text: impl Into<String>) -> Self {
152        self.query_text = Some(text.into());
153        self
154    }
155
156    /// Set grain type diversity configuration.
157    pub fn grain_type_diversity(mut self, config: GrainTypeDiversityConfig) -> Self {
158        self.grain_type_diversity = Some(config);
159        self
160    }
161
162    /// Disable grain type diversity floor (pure priority allocation).
163    pub fn no_grain_type_diversity(mut self) -> Self {
164        self.grain_type_diversity = None;
165        self
166    }
167}
168
169impl Default for FormatPolicy {
170    fn default() -> Self {
171        Self::new(OutputFormat::PlainText)
172            .metadata(MetadataLevel::Minimal)
173            .ordering(Ordering::ByRelevance)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_default_policy() {
183        let p = FormatPolicy::default();
184        assert_eq!(p.format, OutputFormat::PlainText);
185        assert_eq!(p.metadata, MetadataLevel::Minimal);
186        assert_eq!(p.ordering, Ordering::ByRelevance);
187        assert!(p.token_budget.is_none());
188        assert!(!p.sections.group_by_type);
189    }
190
191    #[test]
192    fn test_builder_chain() {
193        let p = FormatPolicy::new(OutputFormat::Sml)
194            .metadata(MetadataLevel::Full)
195            .ordering(Ordering::Chronological)
196            .token_budget(4096)
197            .group_by_type()
198            .grain_override(
199                GrainType::Consent,
200                GrainTypeOverride {
201                    include: true,
202                    max_count: Some(5),
203                },
204            );
205
206        assert_eq!(p.format, OutputFormat::Sml);
207        assert_eq!(p.metadata, MetadataLevel::Full);
208        assert_eq!(p.ordering, Ordering::Chronological);
209        assert_eq!(p.token_budget, Some(4096));
210        assert!(p.sections.group_by_type);
211        assert_eq!(
212            p.grain_overrides
213                .get(&GrainType::Consent)
214                .unwrap()
215                .max_count,
216            Some(5)
217        );
218    }
219}