1use super::*;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
8#[serde(rename_all = "kebab-case")]
9pub enum RuntimeViewModeV1 {
10 Semantic,
11 Temporal,
12 Entity,
13 Causal,
14 Control,
15 Execution,
16 CombinedWithDisclosure,
17}
18
19impl fmt::Display for RuntimeViewModeV1 {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 f.write_str(match self {
22 Self::Semantic => "semantic",
23 Self::Temporal => "temporal",
24 Self::Entity => "entity",
25 Self::Causal => "causal",
26 Self::Control => "control",
27 Self::Execution => "execution",
28 Self::CombinedWithDisclosure => "combined-with-disclosure",
29 })
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
34#[serde(rename_all = "kebab-case")]
35pub enum RuntimeTimeScopeModeV1 {
36 Timeless,
37 AsOf,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
41pub struct RuntimeTimeScopeV1 {
42 pub mode: RuntimeTimeScopeModeV1,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub valid_at: Option<DateTime<Utc>>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub recorded_at: Option<DateTime<Utc>>,
47}
48
49impl RuntimeTimeScopeV1 {
50 pub fn timeless() -> Self {
51 Self {
52 mode: RuntimeTimeScopeModeV1::Timeless,
53 valid_at: None,
54 recorded_at: None,
55 }
56 }
57
58 pub fn as_of(valid_at: DateTime<Utc>, recorded_at: DateTime<Utc>) -> Self {
59 Self {
60 mode: RuntimeTimeScopeModeV1::AsOf,
61 valid_at: Some(valid_at),
62 recorded_at: Some(recorded_at),
63 }
64 }
65
66 pub fn is_time_scoped(&self) -> bool {
67 self.mode == RuntimeTimeScopeModeV1::AsOf
68 && self.valid_at.is_some()
69 && self.recorded_at.is_some()
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
74#[serde(rename_all = "kebab-case")]
75pub enum IdentityExpansionPolicyV1 {
76 ExactOnly,
77 AliasExpansionAllowed,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
81#[serde(rename_all = "kebab-case")]
82pub enum QueryWideningKindV1 {
83 AliasExpansion,
84 TimeWindowExpansion,
85 ScopeExpansion,
86 FallbackIndex,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
90pub struct RetrievalPolicyV1 {
91 pub policy_id: ArtifactId,
92 pub kind: ArtifactKindV1,
93 pub view_mode: RuntimeViewModeV1,
94 pub time_scope: RuntimeTimeScopeV1,
95 pub identity_expansion: IdentityExpansionPolicyV1,
96 pub allow_query_widening: bool,
97 pub allow_timeless_fallback: bool,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub scopes: Vec<String>,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub reason_codes: Vec<String>,
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
104 pub created_at: DateTime<Utc>,
105}
106
107impl RetrievalPolicyV1 {
108 pub fn time_scoped(
109 view_mode: RuntimeViewModeV1,
110 valid_at: DateTime<Utc>,
111 recorded_at: DateTime<Utc>,
112 ) -> Self {
113 Self {
114 policy_id: display_only_unstable_id("retrieval-policy"),
115 kind: ArtifactKindV1::RetrievalPolicy,
116 view_mode,
117 time_scope: RuntimeTimeScopeV1::as_of(valid_at, recorded_at),
118 identity_expansion: IdentityExpansionPolicyV1::ExactOnly,
119 allow_query_widening: false,
120 allow_timeless_fallback: false,
121 scopes: Vec::new(),
122 reason_codes: vec!["time-scoped-query-no-silent-timeless-fallback".into()],
123 canonical_backpointers: canonical_owner_backpointer(
124 "knowledge-runtime",
125 "QueryTrace",
126 "canonical-runtime-query-owner",
127 ),
128 created_at: Utc::now(),
129 }
130 }
131
132 pub fn timeless(view_mode: RuntimeViewModeV1) -> Self {
133 Self {
134 policy_id: display_only_unstable_id("retrieval-policy"),
135 kind: ArtifactKindV1::RetrievalPolicy,
136 view_mode,
137 time_scope: RuntimeTimeScopeV1::timeless(),
138 identity_expansion: IdentityExpansionPolicyV1::ExactOnly,
139 allow_query_widening: false,
140 allow_timeless_fallback: false,
141 scopes: Vec::new(),
142 reason_codes: vec!["timeless-query-explicit".into()],
143 canonical_backpointers: canonical_owner_backpointer(
144 "knowledge-runtime",
145 "QueryTrace",
146 "canonical-runtime-query-owner",
147 ),
148 created_at: Utc::now(),
149 }
150 }
151
152 pub fn with_alias_expansion(mut self) -> Self {
153 self.identity_expansion = IdentityExpansionPolicyV1::AliasExpansionAllowed;
154 self.allow_query_widening = true;
155 self.reason_codes
156 .push("alias-expansion-requires-receipt".into());
157 self.reason_codes.sort();
158 self.reason_codes.dedup();
159 self
160 }
161
162 pub fn with_timeless_fallback(mut self) -> Self {
163 self.allow_timeless_fallback = true;
164 self.reason_codes
165 .push("timeless-fallback-requires-degradation-event".into());
166 self.reason_codes.sort();
167 self.reason_codes.dedup();
168 self
169 }
170
171 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
172 self.scopes.push(scope.into());
173 self.scopes.sort();
174 self.scopes.dedup();
175 self
176 }
177
178 pub fn forbids_silent_timeless_fallback(&self) -> bool {
179 self.time_scope.is_time_scoped()
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
184pub struct RuntimeViewRequestV1 {
185 pub request_id: ArtifactId,
186 pub kind: ArtifactKindV1,
187 pub view_mode: RuntimeViewModeV1,
188 pub query: String,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub subject: Option<String>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub predicate: Option<String>,
193 #[serde(default, skip_serializing_if = "Vec::is_empty")]
194 pub aliases: Vec<String>,
195 pub retrieval_policy: RetrievalPolicyV1,
196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
197 pub reason_codes: Vec<String>,
198 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
200 pub requested_at: DateTime<Utc>,
201}
202
203impl RuntimeViewRequestV1 {
204 pub fn new(query: impl Into<String>, retrieval_policy: RetrievalPolicyV1) -> Self {
205 let view_mode = retrieval_policy.view_mode.clone();
206 Self {
207 request_id: display_only_unstable_id("runtime-view-request"),
208 kind: ArtifactKindV1::RuntimeViewRequest,
209 view_mode,
210 query: query.into(),
211 subject: None,
212 predicate: None,
213 aliases: Vec::new(),
214 retrieval_policy,
215 reason_codes: vec!["runtime-view-requested".into()],
216 canonical_backpointers: canonical_owner_backpointer(
217 "knowledge-runtime",
218 "QueryTrace",
219 "canonical-runtime-view-owner",
220 ),
221 requested_at: Utc::now(),
222 }
223 }
224
225 pub fn subject(mut self, subject: impl Into<String>) -> Self {
226 self.subject = Some(subject.into());
227 self
228 }
229
230 pub fn predicate(mut self, predicate: impl Into<String>) -> Self {
231 self.predicate = Some(predicate.into());
232 self
233 }
234
235 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
236 self.aliases.push(alias.into());
237 self.aliases.sort();
238 self.aliases.dedup();
239 self
240 }
241
242 pub fn is_time_scoped(&self) -> bool {
243 self.retrieval_policy.time_scope.is_time_scoped()
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
248pub struct QueryWideningReportV1 {
249 pub receipt_id: ArtifactId,
250 pub kind: ArtifactKindV1,
251 pub request_id: ArtifactId,
252 pub policy_id: ArtifactId,
253 pub widening_kind: QueryWideningKindV1,
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 pub original_terms: Vec<String>,
256 #[serde(default, skip_serializing_if = "Vec::is_empty")]
257 pub widened_terms: Vec<String>,
258 pub allowed_by_policy: bool,
259 #[serde(default, skip_serializing_if = "Vec::is_empty")]
260 pub reason_codes: Vec<String>,
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
262 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
263 pub recorded_at: DateTime<Utc>,
264}
265
266impl QueryWideningReportV1 {
267 pub fn alias_expansion(
268 request: &RuntimeViewRequestV1,
269 original_terms: Vec<String>,
270 widened_terms: Vec<String>,
271 ) -> Self {
272 Self {
273 receipt_id: display_only_unstable_id("query-widening"),
274 kind: ArtifactKindV1::QueryWidening,
275 request_id: request.request_id.clone(),
276 policy_id: request.retrieval_policy.policy_id.clone(),
277 widening_kind: QueryWideningKindV1::AliasExpansion,
278 original_terms,
279 widened_terms,
280 allowed_by_policy: request.retrieval_policy.allow_query_widening
281 && request.retrieval_policy.identity_expansion
282 == IdentityExpansionPolicyV1::AliasExpansionAllowed,
283 reason_codes: vec!["alias-expansion-disclosed".into()],
284 canonical_backpointers: canonical_owner_backpointer(
285 "knowledge-runtime",
286 "WideningDisclosure",
287 "canonical-widening-disclosure-owner",
288 ),
289 recorded_at: Utc::now(),
290 }
291 }
292
293 pub fn is_alias_expansion(&self) -> bool {
294 self.widening_kind == QueryWideningKindV1::AliasExpansion
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
299pub struct DegradationEventV1 {
300 pub event_id: ArtifactId,
301 pub kind: ArtifactKindV1,
302 pub request_id: ArtifactId,
303 pub view_mode: RuntimeViewModeV1,
304 pub degraded: bool,
305 pub fallback_attempted: bool,
306 pub fallback_allowed: bool,
307 pub reason_code: String,
308 #[serde(default, skip_serializing_if = "Vec::is_empty")]
309 pub affected_claim_ids: Vec<ArtifactId>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub canonical_degradation_record_id: Option<StackDegradationRecordId>,
312 #[serde(default, skip_serializing_if = "Vec::is_empty")]
313 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
314 pub recorded_at: DateTime<Utc>,
315}
316
317impl DegradationEventV1 {
318 pub fn new(
319 request: &RuntimeViewRequestV1,
320 reason_code: impl Into<String>,
321 fallback_attempted: bool,
322 fallback_allowed: bool,
323 affected_claim_ids: Vec<ArtifactId>,
324 ) -> Self {
325 Self {
326 event_id: display_only_unstable_id("degradation-event"),
327 kind: ArtifactKindV1::DegradationEvent,
328 request_id: request.request_id.clone(),
329 view_mode: request.view_mode.clone(),
330 degraded: true,
331 fallback_attempted,
332 fallback_allowed,
333 reason_code: reason_code.into(),
334 affected_claim_ids,
335 canonical_degradation_record_id: None,
336 canonical_backpointers: canonical_owner_backpointer(
337 "knowledge-runtime",
338 "WideningDisclosure",
339 "canonical-degradation-disclosure-owner",
340 ),
341 recorded_at: Utc::now(),
342 }
343 }
344
345 pub fn proves_no_silent_timeless_fallback(&self) -> bool {
346 !self.fallback_attempted || !self.fallback_allowed || self.degraded
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
351pub struct ProjectionDigestV1 {
352 pub projection_id: ArtifactId,
353 pub kind: ArtifactKindV1,
354 pub view_mode: RuntimeViewModeV1,
355 pub policy_id: ArtifactId,
356 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 pub source_episode_ids: Vec<ArtifactId>,
358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub source_claim_ids: Vec<ArtifactId>,
360 #[serde(default, skip_serializing_if = "Vec::is_empty")]
361 pub source_evidence_ids: Vec<ArtifactId>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub canonical_projection_id: Option<StackProjectionId>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub canonical_import_batch_id: Option<StackImportBatchId>,
366 pub digest: DisplayDigestV1,
367 pub rebuilt_from_authoritative_memory: bool,
368 #[serde(default, skip_serializing_if = "Vec::is_empty")]
369 pub reason_codes: Vec<String>,
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
371 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
372 pub created_at: DateTime<Utc>,
373}
374
375impl ProjectionDigestV1 {
376 pub fn new(
377 view_mode: RuntimeViewModeV1,
378 policy_id: ArtifactId,
379 source_episode_ids: Vec<ArtifactId>,
380 source_claim_ids: Vec<ArtifactId>,
381 source_evidence_ids: Vec<ArtifactId>,
382 digest_payload: serde_json::Value,
383 ) -> Self {
384 Self {
385 projection_id: display_only_unstable_id("projection-digest"),
386 kind: ArtifactKindV1::ProjectionDigest,
387 view_mode,
388 policy_id,
389 source_episode_ids,
390 source_claim_ids,
391 source_evidence_ids,
392 canonical_projection_id: None,
393 canonical_import_batch_id: None,
394 digest: DisplayDigestV1::for_json_value(&digest_payload),
395 rebuilt_from_authoritative_memory: true,
396 reason_codes: vec!["projection-rebuild-from-authoritative-memory".into()],
397 canonical_backpointers: vec![
398 CanonicalBackpointerV1::owner_type(
399 "semantic-memory",
400 "ImportReceipt",
401 "canonical-memory-import-owner",
402 ),
403 CanonicalBackpointerV1::owner_type(
404 "forge-memory-bridge",
405 "ProjectionImportBatchV3",
406 "canonical-projection-transform-owner",
407 ),
408 ],
409 created_at: Utc::now(),
410 }
411 }
412
413 pub fn equivalent_digest(&self, other: &Self) -> bool {
414 self.digest == other.digest && self.policy_id == other.policy_id
415 }
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
419pub struct ViewDisclosureReportV1 {
420 pub receipt_id: ArtifactId,
421 pub kind: ArtifactKindV1,
422 pub request_id: ArtifactId,
423 pub view_mode: RuntimeViewModeV1,
424 pub policy_id: ArtifactId,
425 pub projection_digest: ProjectionDigestV1,
426 #[serde(default, skip_serializing_if = "Vec::is_empty")]
427 pub matched_claim_ids: Vec<ArtifactId>,
428 #[serde(default, skip_serializing_if = "Vec::is_empty")]
429 pub query_widening_receipt_ids: Vec<ArtifactId>,
430 #[serde(default, skip_serializing_if = "Vec::is_empty")]
431 pub degradation_event_ids: Vec<ArtifactId>,
432 pub authoritative_source: String,
433 pub separates_execution_from_domain_truth: bool,
434 #[serde(default, skip_serializing_if = "Vec::is_empty")]
435 pub reason_codes: Vec<String>,
436 #[serde(default, skip_serializing_if = "Vec::is_empty")]
437 pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
438 pub recorded_at: DateTime<Utc>,
439}
440
441impl ViewDisclosureReportV1 {
442 pub fn new(
443 request: &RuntimeViewRequestV1,
444 projection_digest: ProjectionDigestV1,
445 matched_claim_ids: Vec<ArtifactId>,
446 query_widening_receipt_ids: Vec<ArtifactId>,
447 degradation_event_ids: Vec<ArtifactId>,
448 ) -> Self {
449 Self {
450 receipt_id: display_only_unstable_id("view-disclosure"),
451 kind: ArtifactKindV1::ViewDisclosure,
452 request_id: request.request_id.clone(),
453 view_mode: request.view_mode.clone(),
454 policy_id: request.retrieval_policy.policy_id.clone(),
455 projection_digest,
456 matched_claim_ids,
457 query_widening_receipt_ids,
458 degradation_event_ids,
459 authoritative_source: "append-only-memory-and-receipts".into(),
460 separates_execution_from_domain_truth: true,
461 reason_codes: vec!["view-disclosure-emitted-before-results".into()],
462 canonical_backpointers: canonical_owner_backpointer(
463 "knowledge-runtime",
464 "QueryTrace",
465 "canonical-runtime-disclosure-owner",
466 ),
467 recorded_at: Utc::now(),
468 }
469 }
470
471 pub fn discloses_policy_events(&self) -> bool {
472 self.separates_execution_from_domain_truth
473 && self
474 .query_widening_receipt_ids
475 .iter()
476 .all(|id| !id.0.is_empty())
477 && self.degradation_event_ids.iter().all(|id| !id.0.is_empty())
478 }
479
480 pub fn has_visible_widening_or_degradation(&self) -> bool {
481 !self.query_widening_receipt_ids.is_empty() || !self.degradation_event_ids.is_empty()
482 }
483}