Skip to main content

dejadb_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 dejadb_cal::store_types::GrainTypeDiversityConfig;
8use dejadb_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}
98
99impl FormatPolicy {
100    pub fn new(format: OutputFormat) -> Self {
101        Self {
102            format,
103            metadata: MetadataLevel::Minimal,
104            ordering: Ordering::ByRelevance,
105            sections: SectionConfig::default(),
106            token_budget: None,
107            grain_overrides: HashMap::new(),
108            query_text: None,
109            grain_type_diversity: Some(GrainTypeDiversityConfig::default()),
110        }
111    }
112
113    pub fn metadata(mut self, level: MetadataLevel) -> Self {
114        self.metadata = level;
115        self
116    }
117
118    pub fn ordering(mut self, ordering: Ordering) -> Self {
119        self.ordering = ordering;
120        self
121    }
122
123    pub fn token_budget(mut self, budget: usize) -> Self {
124        self.token_budget = Some(budget);
125        self
126    }
127
128    pub fn group_by_type(mut self) -> Self {
129        self.sections.group_by_type = true;
130        self
131    }
132
133    pub fn grain_override(mut self, gt: GrainType, ovr: GrainTypeOverride) -> Self {
134        self.grain_overrides.insert(gt, ovr);
135        self
136    }
137
138    /// Set the original query text for recency detection in KU rendering.
139    pub fn query_text(mut self, text: impl Into<String>) -> Self {
140        self.query_text = Some(text.into());
141        self
142    }
143
144    /// Set grain type diversity configuration.
145    pub fn grain_type_diversity(mut self, config: GrainTypeDiversityConfig) -> Self {
146        self.grain_type_diversity = Some(config);
147        self
148    }
149
150    /// Disable grain type diversity floor (pure priority allocation).
151    pub fn no_grain_type_diversity(mut self) -> Self {
152        self.grain_type_diversity = None;
153        self
154    }
155}
156
157impl Default for FormatPolicy {
158    fn default() -> Self {
159        Self::new(OutputFormat::PlainText)
160            .metadata(MetadataLevel::Minimal)
161            .ordering(Ordering::ByRelevance)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_default_policy() {
171        let p = FormatPolicy::default();
172        assert_eq!(p.format, OutputFormat::PlainText);
173        assert_eq!(p.metadata, MetadataLevel::Minimal);
174        assert_eq!(p.ordering, Ordering::ByRelevance);
175        assert!(p.token_budget.is_none());
176        assert!(!p.sections.group_by_type);
177    }
178
179    #[test]
180    fn test_builder_chain() {
181        let p = FormatPolicy::new(OutputFormat::Sml)
182            .metadata(MetadataLevel::Full)
183            .ordering(Ordering::Chronological)
184            .token_budget(4096)
185            .group_by_type()
186            .grain_override(
187                GrainType::Consent,
188                GrainTypeOverride {
189                    include: true,
190                    max_count: Some(5),
191                },
192            );
193
194        assert_eq!(p.format, OutputFormat::Sml);
195        assert_eq!(p.metadata, MetadataLevel::Full);
196        assert_eq!(p.ordering, Ordering::Chronological);
197        assert_eq!(p.token_budget, Some(4096));
198        assert!(p.sections.group_by_type);
199        assert_eq!(
200            p.grain_overrides
201                .get(&GrainType::Consent)
202                .unwrap()
203                .max_count,
204            Some(5)
205        );
206    }
207}