Skip to main content

remem/context_bundle/
domain.rs

1//! Versioned DTOs for the Context Bundle v1 contract (GH-932).
2//!
3//! Every top-level DTO carries `schema_version` so later revisions can
4//! break the shape explicitly. Serialization is serde JSON with snake_case
5//! enum values; `tests/schema.rs` pins the exact structure.
6
7use serde::{Deserialize, Serialize};
8
9/// Version of the request/plan/bundle/audit JSON shapes.
10pub const CONTEXT_BUNDLE_SCHEMA_VERSION: u32 = 1;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum AgentRole {
15    Coder,
16    Reviewer,
17    Planner,
18    Researcher,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum RiskClass {
24    Low,
25    Medium,
26    High,
27}
28
29/// Bounded degradation contract: enrichment/vector/rerank incompatibility
30/// may degrade to `CanonicalOnly`; only canonical schema or scope-safety
31/// failures produce `Blocked`.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum DegradedMode {
35    Full,
36    CanonicalOnly,
37    Blocked,
38}
39
40/// Where an item's text came from. Generated or graph-derived projections
41/// explain "why it was found" and must never masquerade as canonical
42/// memory.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum SourceKind {
46    Canonical,
47    Generated,
48    GraphDerived,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ItemValidity {
54    Current,
55    Stale,
56    Superseded,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum TrustClass {
62    Trusted,
63    Standard,
64    Quarantined,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum ContextIntent {
70    SessionStart,
71    // GH-934 retrieval-router intents. The router compiles a
72    // `RetrievalPlan` for these; `SessionStart` stays the bundle-planner
73    // intent and is not routable (see `crate::retrieval_router`).
74    ResumeWork,
75    ExplainDecision,
76    DebugFailure,
77    ApplyPreference,
78    ReviewChange,
79    ExploreHistory,
80}
81
82/// The retrieval channels the v1 planner knows about; they mirror the
83/// SessionStart sections.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum ChannelKind {
87    Preferences,
88    Lessons,
89    Core,
90    Workstreams,
91    MemoryIndex,
92    Sessions,
93}
94
95impl ChannelKind {
96    /// Deterministic execution/budget order; mirrors the SessionStart
97    /// render order (lessons before core before index before sessions).
98    pub const ORDERED: [ChannelKind; 6] = [
99        ChannelKind::Preferences,
100        ChannelKind::Lessons,
101        ChannelKind::Core,
102        ChannelKind::Workstreams,
103        ChannelKind::MemoryIndex,
104        ChannelKind::Sessions,
105    ];
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct ProjectRef {
110    pub key: String,
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub struct ContextRequest {
115    pub schema_version: u32,
116    pub task: String,
117    pub project: ProjectRef,
118    pub branch: Option<String>,
119    pub worktree: Option<String>,
120    pub role: AgentRole,
121    pub as_of_epoch: i64,
122    pub token_budget: u32,
123    pub risk: RiskClass,
124    pub include_superseded: bool,
125}
126
127/// One candidate or selected context item. `stable_key` follows the
128/// SessionStart identity convention (`memory:<id>`, `session_summary:<id>`).
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct ContextItem {
131    pub stable_key: String,
132    pub channel: ChannelKind,
133    pub title: String,
134    pub text: String,
135    pub source_kind: SourceKind,
136    pub canonical_ref: Option<String>,
137    pub projection_ref: Option<String>,
138    pub evidence_refs: Vec<String>,
139    pub validity: ItemValidity,
140    pub trust: TrustClass,
141    pub project: Option<String>,
142    pub branch: Option<String>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct PlannedChannel {
147    pub channel: ChannelKind,
148    pub item_limit: u32,
149    /// Whether the SessionStart relevance selector governs this channel.
150    pub relevance_governed: bool,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct ContextFilters {
155    pub project: String,
156    pub branch: Option<String>,
157    pub include_superseded: bool,
158    pub as_of_epoch: i64,
159}
160
161/// Per-section token budgets plus the total request budget.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub struct SectionBudgets {
164    pub total_tokens: u32,
165    pub preferences: u32,
166    pub lessons: u32,
167    pub core: u32,
168    pub workstreams: u32,
169    pub memory_index: u32,
170    pub sessions: u32,
171}
172
173impl SectionBudgets {
174    pub fn for_channel(&self, channel: ChannelKind) -> u32 {
175        match channel {
176            ChannelKind::Preferences => self.preferences,
177            ChannelKind::Lessons => self.lessons,
178            ChannelKind::Core => self.core,
179            ChannelKind::Workstreams => self.workstreams,
180            ChannelKind::MemoryIndex => self.memory_index,
181            ChannelKind::Sessions => self.sessions,
182        }
183    }
184}
185
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct AuditEntry {
189    pub stable_key: String,
190    pub channel: ChannelKind,
191    pub source_kind: SourceKind,
192    pub validity: ItemValidity,
193    pub selected: bool,
194    /// Machine-readable snake_case reason; see `policy` reason constants
195    /// and the SessionStart relevance drop reasons.
196    pub reason: String,
197    pub relevance_score: Option<f64>,
198    pub token_estimate: u32,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202#[serde(deny_unknown_fields)]
203pub struct ContextAudit {
204    pub schema_version: u32,
205    pub policy_version: String,
206    pub relevance_policy_version: String,
207    pub plan_hash: String,
208    pub degraded_mode: DegradedMode,
209    pub candidates_considered: u32,
210    pub selected_count: u32,
211    pub dropped_count: u32,
212    pub token_estimate: u32,
213    pub token_budget: u32,
214    pub truncation_reason: Option<String>,
215    pub entries: Vec<AuditEntry>,
216    /// G3: Core-channel mapping vs CurrentTruth selected claims, recorded
217    /// before activation rewrites the live `current_truth` section.
218    #[serde(default, skip_serializing_if = "Vec::is_empty")]
219    pub shadow_comparison: Vec<CurrentTruthShadowDiff>,
220}
221
222/// Inclusion/exclusion diff between today's Core channel and CurrentTruth.
223/// Not a user-facing section: audit-only, no memory text.
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(deny_unknown_fields)]
226pub struct CurrentTruthShadowDiff {
227    pub stable_key: String,
228    /// `core_only`, `projection_only`, or `abstained`.
229    pub verdict: String,
230    pub projection_ref: Option<String>,
231    pub claim_refs: Vec<String>,
232    pub reason: String,
233}
234
235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236pub struct ContextBundle {
237    pub schema_version: u32,
238    pub plan_hash: String,
239    pub degraded_mode: DegradedMode,
240    pub preferences: Vec<ContextItem>,
241    pub failure_lessons: Vec<ContextItem>,
242    pub current_truth: Vec<ContextItem>,
243    pub workstreams: Vec<ContextItem>,
244    pub memory_index: Vec<ContextItem>,
245    pub recent_sessions: Vec<ContextItem>,
246    pub audit: ContextAudit,
247}
248
249impl ContextBundle {
250    pub fn section_mut(&mut self, channel: ChannelKind) -> &mut Vec<ContextItem> {
251        match channel {
252            ChannelKind::Preferences => &mut self.preferences,
253            ChannelKind::Lessons => &mut self.failure_lessons,
254            ChannelKind::Core => &mut self.current_truth,
255            ChannelKind::Workstreams => &mut self.workstreams,
256            ChannelKind::MemoryIndex => &mut self.memory_index,
257            ChannelKind::Sessions => &mut self.recent_sessions,
258        }
259    }
260}