Skip to main content

aidens_contracts/
view_runtime.rs

1//! Runtime view, widening, projection, and degradation disclosure DTOs.
2//!
3//! These records disclose local views without becoming a shadow memory/runtime truth system.
4
5use 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}