Skip to main content

locus_core_rs/domain/
models.rs

1use chrono::{DateTime, Utc};
2use serde::Serialize;
3use serde::{Deserialize, Serialize as SerdeSerialize};
4use serde_json::Value;
5use sha2::{Digest, Sha256};
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum DriftClassification {
10    Intentional,
11    Uncontrolled,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ValidationFailureReason {
16    None,
17    ParseFailure,
18    CoherenceFailure,
19    MissingLayer,
20    InvalidTier,
21    NestingDepth,
22    SchemaViolation,
23}
24
25impl fmt::Display for ValidationFailureReason {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            ValidationFailureReason::None => write!(f, "None"),
29            ValidationFailureReason::ParseFailure => write!(f, "ParseFailure"),
30            ValidationFailureReason::CoherenceFailure => write!(f, "CoherenceFailure"),
31            ValidationFailureReason::MissingLayer => write!(f, "MissingLayer"),
32            ValidationFailureReason::InvalidTier => write!(f, "InvalidTier"),
33            ValidationFailureReason::NestingDepth => write!(f, "NestingDepth"),
34            ValidationFailureReason::SchemaViolation => write!(f, "SchemaViolation"),
35        }
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
40pub struct AvecState {
41    pub stability: f32,
42    pub friction: f32,
43    pub logic: f32,
44    pub autonomy: f32,
45}
46
47impl AvecState {
48    pub fn psi(self) -> f32 {
49        self.stability + self.friction + self.logic + self.autonomy
50    }
51
52    pub fn drift_from(self, previous: Self) -> f32 {
53        self.psi() - previous.psi()
54    }
55
56    pub fn classify_drift(self, previous: Self) -> DriftClassification {
57        let delta = self.drift_from(previous).abs();
58        if delta > 0.3 {
59            DriftClassification::Uncontrolled
60        } else {
61            DriftClassification::Intentional
62        }
63    }
64
65    pub const fn zero() -> Self {
66        Self {
67            stability: 0.0,
68            friction: 0.0,
69            logic: 0.0,
70            autonomy: 0.0,
71        }
72    }
73
74    pub const fn focused() -> Self {
75        Self {
76            stability: 0.95,
77            friction: 0.10,
78            logic: 0.95,
79            autonomy: 0.90,
80        }
81    }
82
83    pub const fn creative() -> Self {
84        Self {
85            stability: 0.80,
86            friction: 0.15,
87            logic: 0.70,
88            autonomy: 0.95,
89        }
90    }
91
92    pub const fn analytical() -> Self {
93        Self {
94            stability: 0.90,
95            friction: 0.20,
96            logic: 0.98,
97            autonomy: 0.85,
98        }
99    }
100
101    pub const fn exploratory() -> Self {
102        Self {
103            stability: 0.75,
104            friction: 0.30,
105            logic: 0.65,
106            autonomy: 0.90,
107        }
108    }
109
110    pub const fn collaborative() -> Self {
111        Self {
112            stability: 0.85,
113            friction: 0.10,
114            logic: 0.80,
115            autonomy: 0.70,
116        }
117    }
118
119    pub const fn defensive() -> Self {
120        Self {
121            stability: 0.90,
122            friction: 0.40,
123            logic: 0.90,
124            autonomy: 0.60,
125        }
126    }
127
128    pub const fn passive() -> Self {
129        Self {
130            stability: 0.98,
131            friction: 0.05,
132            logic: 0.60,
133            autonomy: 0.40,
134        }
135    }
136}
137
138impl Default for AvecState {
139    fn default() -> Self {
140        Self::zero()
141    }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Hash, SerdeSerialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct SemanticTagNodeRef {
147    pub tenant_id: String,
148    pub session_id: String,
149    pub node_id: String,
150    pub sync_key: String,
151}
152
153#[derive(Debug, Clone, PartialEq, SerdeSerialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct SemanticTagRecord {
156    pub tenant_id: String,
157    pub session_id: String,
158    pub node_id: String,
159    pub sync_key: String,
160    pub tag: String,
161    pub embedding: Option<Vec<f32>>,
162    pub embedding_model: Option<String>,
163    pub embedding_dimensions: Option<usize>,
164    pub embedded_at: Option<DateTime<Utc>>,
165    pub updated_at: DateTime<Utc>,
166}
167
168#[derive(Debug, Clone, Default)]
169pub struct SemanticTagQueryFilter {
170    pub tenant_id: Option<String>,
171    pub session_id: Option<String>,
172    pub tags: Option<Vec<String>>,
173    pub tag_prefix: Option<String>,
174    pub has_embedding: Option<bool>,
175    pub missing_embedding_only: bool,
176    pub limit: usize,
177}
178
179#[derive(Debug, Clone, PartialEq, SerdeSerialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct SemanticLink {
182    pub rel: String,
183    pub target: String,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub confidence: Option<f32>,
186}
187
188#[derive(Debug, Clone)]
189pub struct SttpNode {
190    pub raw: String,
191    pub session_id: String,
192    pub tier: String,
193    pub timestamp: DateTime<Utc>,
194    pub compression_depth: i32,
195    pub parent_node_id: Option<String>,
196    pub sync_key: String,
197    pub updated_at: DateTime<Utc>,
198    pub source_metadata: Option<ConnectorMetadata>,
199    pub context_summary: Option<String>,
200    pub semantic_tags: Option<Vec<String>>,
201    pub semantic_links: Option<Vec<SemanticLink>>,
202    pub embedding: Option<Vec<f32>>,
203    pub embedding_model: Option<String>,
204    pub embedding_dimensions: Option<usize>,
205    pub embedded_at: Option<DateTime<Utc>>,
206    pub user_avec: AvecState,
207    pub model_avec: AvecState,
208    pub compression_avec: Option<AvecState>,
209    pub rho: f32,
210    pub kappa: f32,
211    pub psi: f32,
212}
213
214#[derive(Debug, Clone, PartialEq, SerdeSerialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct ConnectorMetadata {
217    pub connector_id: String,
218    pub source_kind: String,
219    pub upstream_id: String,
220    pub revision: Option<String>,
221    pub observed_at_utc: DateTime<Utc>,
222    pub extra: Option<Value>,
223}
224
225impl SttpNode {
226    pub fn canonical_sync_key(&self) -> String {
227        #[derive(Serialize)]
228        struct SyncFingerprint<'a> {
229            session_id: &'a str,
230            tier: &'a str,
231            timestamp: String,
232            compression_depth: i32,
233            parent_node_id: &'a Option<String>,
234            raw: &'a str,
235            user_avec: AvecState,
236            model_avec: AvecState,
237            compression_avec: Option<AvecState>,
238            rho: f32,
239            kappa: f32,
240            psi: f32,
241        }
242
243        let fingerprint = SyncFingerprint {
244            session_id: &self.session_id,
245            tier: &self.tier,
246            timestamp: self.timestamp.to_rfc3339(),
247            compression_depth: self.compression_depth,
248            parent_node_id: &self.parent_node_id,
249            raw: &self.raw,
250            user_avec: self.user_avec,
251            model_avec: self.model_avec,
252            compression_avec: self.compression_avec,
253            rho: self.rho,
254            kappa: self.kappa,
255            psi: self.psi,
256        };
257
258        let encoded = serde_json::to_vec(&fingerprint).unwrap_or_default();
259        let mut hasher = Sha256::new();
260        hasher.update(encoded);
261        let digest = hasher.finalize();
262
263        digest.iter().map(|byte| format!("{byte:02x}")).collect()
264    }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub enum NodeUpsertStatus {
269    Created,
270    Updated,
271    Duplicate,
272    Skipped,
273}
274
275#[derive(Debug, Clone)]
276pub struct NodeUpsertResult {
277    pub node_id: String,
278    pub sync_key: String,
279    pub status: NodeUpsertStatus,
280    pub updated_at: DateTime<Utc>,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct SyncCursor {
285    pub updated_at: DateTime<Utc>,
286    pub sync_key: String,
287}
288
289#[derive(Debug, Clone, Default)]
290pub struct ChangeQueryResult {
291    pub nodes: Vec<SttpNode>,
292    pub next_cursor: Option<SyncCursor>,
293    pub has_more: bool,
294}
295
296#[derive(Debug, Clone)]
297pub struct SyncCheckpoint {
298    pub session_id: String,
299    pub connector_id: String,
300    pub cursor: Option<SyncCursor>,
301    pub updated_at: DateTime<Utc>,
302    pub metadata: Option<ConnectorMetadata>,
303}
304
305#[derive(Debug, Clone)]
306pub struct SyncPullRequest {
307    pub session_id: String,
308    pub connector_id: String,
309    pub page_size: usize,
310    pub max_batches: Option<usize>,
311}
312
313#[derive(Debug, Clone, Default)]
314pub struct SyncPullResult {
315    pub fetched: usize,
316    pub created: usize,
317    pub updated: usize,
318    pub duplicate: usize,
319    pub skipped: usize,
320    pub filtered: usize,
321    pub batches: usize,
322    pub has_more: bool,
323    pub last_cursor: Option<SyncCursor>,
324    pub checkpoint: Option<SyncCheckpoint>,
325}
326
327#[derive(Debug, Clone)]
328pub struct CalibrationResult {
329    pub previous_avec: AvecState,
330    pub delta: f32,
331    pub drift_classification: DriftClassification,
332    pub trigger: String,
333    pub trigger_history: Vec<String>,
334    pub is_first_calibration: bool,
335}
336
337#[derive(Debug, Clone)]
338pub struct NodeQuery {
339    pub limit: usize,
340    pub session_id: Option<String>,
341    pub from_utc: Option<DateTime<Utc>>,
342    pub to_utc: Option<DateTime<Utc>>,
343    pub tiers: Option<Vec<String>>,
344}
345
346impl Default for NodeQuery {
347    fn default() -> Self {
348        Self {
349            limit: 500,
350            session_id: None,
351            from_utc: None,
352            to_utc: None,
353            tiers: None,
354        }
355    }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq)]
359pub struct NumericRange {
360    pub min: f32,
361    pub max: f32,
362    pub average: f32,
363}
364
365impl Default for NumericRange {
366    fn default() -> Self {
367        Self {
368            min: 0.0,
369            max: 0.0,
370            average: 0.0,
371        }
372    }
373}
374
375#[derive(Debug, Clone, Copy, PartialEq)]
376pub struct PsiRange {
377    pub min: f32,
378    pub max: f32,
379    pub average: f32,
380}
381
382impl Default for PsiRange {
383    fn default() -> Self {
384        Self {
385            min: 0.0,
386            max: 0.0,
387            average: 0.0,
388        }
389    }
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
393pub struct ConfidenceBandSummary {
394    pub low: usize,
395    pub medium: usize,
396    pub high: usize,
397}
398
399#[derive(Debug, Clone, Default)]
400pub struct RetrieveResult {
401    pub nodes: Vec<SttpNode>,
402    pub retrieved: usize,
403    pub psi_range: PsiRange,
404}
405
406#[derive(Debug, Clone, Default)]
407pub struct ListNodesResult {
408    pub nodes: Vec<SttpNode>,
409    pub retrieved: usize,
410}
411
412#[derive(Debug, Clone, Default)]
413pub struct ScopeRekeyResult {
414    pub source_tenant_id: String,
415    pub source_session_id: String,
416    pub target_tenant_id: String,
417    pub target_session_id: String,
418    pub temporal_nodes: usize,
419    pub calibrations: usize,
420    pub target_temporal_nodes: usize,
421    pub target_calibrations: usize,
422    pub applied: bool,
423    pub conflict: bool,
424    pub message: Option<String>,
425}
426
427#[derive(Debug, Clone, Default)]
428pub struct BatchRekeyResult {
429    pub dry_run: bool,
430    pub requested_node_ids: usize,
431    pub resolved_node_ids: usize,
432    pub missing_node_ids: Vec<String>,
433    pub scopes: Vec<ScopeRekeyResult>,
434    pub temporal_nodes_updated: usize,
435    pub calibrations_updated: usize,
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub enum NodeDeleteStatus {
440    Deleted,
441    NotFound,
442    Blocked,
443    Skipped,
444}
445
446#[derive(Debug, Clone, Default)]
447pub struct InboundNodeReferences {
448    pub child_parent_links: Vec<String>,
449    pub incoming_semantic_refs: Vec<String>,
450}
451
452#[derive(Debug, Clone)]
453pub struct NodeDeleteRecord {
454    pub node_id: String,
455    pub sync_key: String,
456    pub status: NodeDeleteStatus,
457    pub reason: Option<String>,
458}
459
460#[derive(Debug, Clone, Default)]
461pub struct NodeDeleteResult {
462    pub dry_run: bool,
463    pub deleted: usize,
464    pub blocked: usize,
465    pub not_found: usize,
466    pub skipped: usize,
467    pub calibrations_deleted: usize,
468    pub checkpoints_deleted: usize,
469    pub records: Vec<NodeDeleteRecord>,
470}
471
472#[derive(Debug, Clone, Default)]
473pub struct NodeDeleteRequest {
474    pub tenant_id: String,
475    pub session_id: String,
476    pub sync_keys: Vec<String>,
477    pub node_ids: Vec<String>,
478    pub dry_run: bool,
479}
480
481#[derive(Debug, Clone, Default)]
482pub struct SessionPurgeRequest {
483    pub tenant_id: String,
484    pub session_id: String,
485    pub tiers: Option<Vec<String>>,
486    pub dry_run: bool,
487    pub include_calibration: bool,
488    pub include_checkpoints: bool,
489}
490
491#[derive(Debug, Clone, Default)]
492pub struct StoreResult {
493    pub node_id: String,
494    pub psi: f32,
495    pub valid: bool,
496    pub validation_error: Option<String>,
497}
498
499#[derive(Debug, Clone)]
500pub struct ParseResult {
501    pub success: bool,
502    pub node: Option<SttpNode>,
503    pub error: Option<String>,
504    pub profile: ParseProfile,
505    pub strict_valid: bool,
506    pub diagnostics: Vec<ParseDiagnostic>,
507    pub canonical_ast: Option<CanonicalAst>,
508}
509
510impl ParseResult {
511    pub fn ok(node: SttpNode) -> Self {
512        Self {
513            success: true,
514            node: Some(node),
515            error: None,
516            profile: ParseProfile::Tolerant,
517            strict_valid: true,
518            diagnostics: Vec::new(),
519            canonical_ast: None,
520        }
521    }
522
523    pub fn ok_with_metadata(
524        node: SttpNode,
525        profile: ParseProfile,
526        strict_valid: bool,
527        diagnostics: Vec<ParseDiagnostic>,
528        canonical_ast: Option<CanonicalAst>,
529    ) -> Self {
530        Self {
531            success: true,
532            node: Some(node),
533            error: None,
534            profile,
535            strict_valid,
536            diagnostics,
537            canonical_ast,
538        }
539    }
540
541    pub fn fail(error: impl Into<String>) -> Self {
542        Self {
543            success: false,
544            node: None,
545            error: Some(error.into()),
546            profile: ParseProfile::Tolerant,
547            strict_valid: false,
548            diagnostics: Vec::new(),
549            canonical_ast: None,
550        }
551    }
552
553    pub fn fail_with_metadata(
554        error: impl Into<String>,
555        profile: ParseProfile,
556        diagnostics: Vec<ParseDiagnostic>,
557        canonical_ast: Option<CanonicalAst>,
558    ) -> Self {
559        Self {
560            success: false,
561            node: None,
562            error: Some(error.into()),
563            profile,
564            strict_valid: false,
565            diagnostics,
566            canonical_ast,
567        }
568    }
569}
570
571#[derive(Debug, Default,Clone, Copy, PartialEq, Eq)]
572pub enum ParseProfile {
573    Strict,
574    StrictTypedIr,
575    #[default]
576    Tolerant,
577}
578
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum ParseDiagnosticSeverity {
581    Fatal,
582    Error,
583    Warning,
584    Info,
585}
586
587#[derive(Debug, Clone)]
588pub struct ParseDiagnostic {
589    pub code: String,
590    pub message: String,
591    pub severity: ParseDiagnosticSeverity,
592    pub strict_impact: bool,
593    pub span: Option<ParseSpan>,
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
597pub struct ParseSpan {
598    pub start: usize,
599    pub end: usize,
600    pub line: usize,
601    pub column: usize,
602}
603
604#[derive(Debug, Clone)]
605pub struct CanonicalAstLayer {
606    pub source: String,
607    pub span: ParseSpan,
608}
609
610#[derive(Debug, Clone)]
611pub struct CanonicalAst {
612    pub provenance: Option<CanonicalAstLayer>,
613    pub envelope: Option<CanonicalAstLayer>,
614    pub content: Option<CanonicalAstLayer>,
615    pub metrics: Option<CanonicalAstLayer>,
616    pub strict_spine: bool,
617    pub profile: ParseProfile,
618}
619
620#[derive(Debug, Clone)]
621pub struct ValidationResult {
622    pub is_valid: bool,
623    pub error: Option<String>,
624    pub reason: ValidationFailureReason,
625}
626
627impl ValidationResult {
628    pub fn ok() -> Self {
629        Self {
630            is_valid: true,
631            error: None,
632            reason: ValidationFailureReason::None,
633        }
634    }
635
636    pub fn fail(error: impl Into<String>, reason: ValidationFailureReason) -> Self {
637        Self {
638            is_valid: false,
639            error: Some(error.into()),
640            reason,
641        }
642    }
643}
644
645#[derive(Debug, Clone)]
646pub struct MoodCatalogResult {
647    pub presets: Vec<MoodPreset>,
648    pub apply_guide: String,
649    pub swap_preview: Option<MoodSwapPreview>,
650}
651
652#[derive(Debug, Clone)]
653pub struct MoodPreset {
654    pub name: String,
655    pub description: String,
656    pub avec: AvecState,
657}
658
659#[derive(Debug, Clone)]
660pub struct MoodSwapPreview {
661    pub target_mood: String,
662    pub blend: f32,
663    pub current: AvecState,
664    pub target: AvecState,
665    pub blended: AvecState,
666}
667
668#[derive(Debug, Clone)]
669pub struct MonthlyRollupRequest {
670    pub session_id: String,
671    pub start_utc: DateTime<Utc>,
672    pub end_utc: DateTime<Utc>,
673    pub source_session_id: Option<String>,
674    pub parent_node_id: Option<String>,
675    pub limit: usize,
676    pub persist: bool,
677}
678
679impl MonthlyRollupRequest {
680    pub fn new(
681        session_id: impl Into<String>,
682        start_utc: DateTime<Utc>,
683        end_utc: DateTime<Utc>,
684    ) -> Self {
685        Self {
686            session_id: session_id.into(),
687            start_utc,
688            end_utc,
689            source_session_id: None,
690            parent_node_id: None,
691            limit: 5000,
692            persist: true,
693        }
694    }
695}
696
697#[derive(Debug, Clone)]
698pub struct MonthlyRollupResult {
699    pub success: bool,
700    pub node_id: String,
701    pub raw_node: String,
702    pub error: Option<String>,
703    pub source_nodes: usize,
704    pub parent_reference: Option<String>,
705    pub user_average: AvecState,
706    pub model_average: AvecState,
707    pub compression_average: AvecState,
708    pub rho_range: NumericRange,
709    pub kappa_range: NumericRange,
710    pub psi_range: NumericRange,
711    pub rho_bands: ConfidenceBandSummary,
712    pub kappa_bands: ConfidenceBandSummary,
713}
714
715impl Default for MonthlyRollupResult {
716    fn default() -> Self {
717        Self {
718            success: false,
719            node_id: String::new(),
720            raw_node: String::new(),
721            error: None,
722            source_nodes: 0,
723            parent_reference: None,
724            user_average: AvecState::zero(),
725            model_average: AvecState::zero(),
726            compression_average: AvecState::zero(),
727            rho_range: NumericRange::default(),
728            kappa_range: NumericRange::default(),
729            psi_range: NumericRange::default(),
730            rho_bands: ConfidenceBandSummary::default(),
731            kappa_bands: ConfidenceBandSummary::default(),
732        }
733    }
734}