Skip to main content

turnframe_core/
knowledge.rs

1//! Knowledge retrieval contract (spec §19.2).
2//!
3//! Retrieved content is evidence for answers, never authorization for
4//! commands (spec §19.3).
5
6use chrono::NaiveDate;
7use serde::{Deserialize, Serialize};
8
9use crate::case::CaseRef;
10use crate::ids::AccountId;
11use crate::locale::Locale;
12use crate::read::{DataSensitivity, TrustLevel};
13use crate::reduce::SourcePolicy;
14
15/// Citation metadata of a knowledge chunk.
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct Citation {
18    /// Source identifier.
19    pub source_id: String,
20    /// Human label of the source.
21    pub label: String,
22    /// Where to read it.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub uri: Option<String>,
25    /// Locator within the source (section, page, article).
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub locator: Option<String>,
28    /// Source version.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub version: Option<String>,
31}
32
33/// A retrieval request.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct KnowledgeRequest {
36    /// Tenant.
37    pub account_id: AccountId,
38    /// The query.
39    pub query: String,
40    /// Locale of the user.
41    pub locale: Locale,
42    /// Cases the question is about.
43    pub case_refs: Vec<CaseRef>,
44    /// Source requirements.
45    pub source_policy: SourcePolicy,
46    /// Maximum chunks to return, or `None` to let the provider decide.
47    ///
48    /// Unset by default: how much retrieved material belongs in an answer is a
49    /// property of the corpus and the model, and a provider that owns the
50    /// corpus is better placed to bound it than a library that has not seen it.
51    pub max_chunks: Option<usize>,
52    /// Only sources effective on this date.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub as_of: Option<NaiveDate>,
55}
56
57/// One retrieved chunk with provenance (spec §19.2).
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct KnowledgeChunk {
60    /// Chunk identifier.
61    pub chunk_id: String,
62    /// Source identifier.
63    pub source_id: String,
64    /// Source version.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub source_version: Option<String>,
67    /// First day the content is effective.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub effective_from: Option<NaiveDate>,
70    /// Last day the content is effective.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub effective_to: Option<NaiveDate>,
73    /// Permission tags required to read it.
74    #[serde(default)]
75    pub permissions: Vec<String>,
76    /// Citation metadata.
77    pub citation: Citation,
78    /// The text.
79    pub text: String,
80    /// Trust level.
81    pub trust: TrustLevel,
82    /// Sensitivity.
83    pub sensitivity: DataSensitivity,
84}
85
86impl KnowledgeChunk {
87    /// Returns `true` when the chunk is effective on `date`.
88    #[must_use]
89    pub fn is_effective_on(&self, date: NaiveDate) -> bool {
90        self.effective_from.is_none_or(|from| from <= date)
91            && self.effective_to.is_none_or(|to| date <= to)
92    }
93}
94
95/// Retrieval failures.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
97#[serde(tag = "kind", rename_all = "snake_case")]
98#[non_exhaustive]
99pub enum KnowledgeError {
100    /// A source is unavailable.
101    #[error("knowledge source {source_id} unavailable")]
102    Unavailable {
103        /// The source.
104        source_id: String,
105    },
106    /// The actor may not read the requested sources.
107    #[error("knowledge access unauthorized")]
108    Unauthorized,
109    /// Retrieval timed out.
110    #[error("knowledge retrieval timed out")]
111    Timeout,
112    /// The provider returned malformed data.
113    #[error("knowledge provider returned malformed data")]
114    Malformed,
115    /// Provider-specific failure.
116    #[error("knowledge failure {code}")]
117    Other {
118        /// Stable code.
119        code: String,
120    },
121}
122
123/// A knowledge retrieval adapter (spec §19.2).
124#[async_trait::async_trait]
125pub trait KnowledgeProvider: Send + Sync {
126    /// Retrieves chunks for a request. Must enforce the actor's permissions.
127    async fn retrieve(
128        &self,
129        request: KnowledgeRequest,
130    ) -> Result<Vec<KnowledgeChunk>, KnowledgeError>;
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn effective_window() {
139        let chunk = KnowledgeChunk {
140            chunk_id: "c".into(),
141            source_id: "s".into(),
142            source_version: None,
143            effective_from: NaiveDate::from_ymd_opt(2026, 1, 1),
144            effective_to: NaiveDate::from_ymd_opt(2026, 12, 31),
145            permissions: vec![],
146            citation: Citation {
147                source_id: "s".into(),
148                label: "Source".into(),
149                uri: None,
150                locator: None,
151                version: None,
152            },
153            text: "t".into(),
154            trust: TrustLevel::Retrieved,
155            sensitivity: DataSensitivity::Public,
156        };
157        assert!(chunk.is_effective_on(NaiveDate::from_ymd_opt(2026, 6, 1).unwrap()));
158        assert!(!chunk.is_effective_on(NaiveDate::from_ymd_opt(2025, 6, 1).unwrap()));
159    }
160}