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)]
145pub struct SttpNode {
146    pub raw: String,
147    pub session_id: String,
148    pub tier: String,
149    pub timestamp: DateTime<Utc>,
150    pub compression_depth: i32,
151    pub parent_node_id: Option<String>,
152    pub sync_key: String,
153    pub updated_at: DateTime<Utc>,
154    pub source_metadata: Option<ConnectorMetadata>,
155    pub context_summary: Option<String>,
156    pub embedding: Option<Vec<f32>>,
157    pub embedding_model: Option<String>,
158    pub embedding_dimensions: Option<usize>,
159    pub embedded_at: Option<DateTime<Utc>>,
160    pub user_avec: AvecState,
161    pub model_avec: AvecState,
162    pub compression_avec: Option<AvecState>,
163    pub rho: f32,
164    pub kappa: f32,
165    pub psi: f32,
166}
167
168#[derive(Debug, Clone, PartialEq, SerdeSerialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct ConnectorMetadata {
171    pub connector_id: String,
172    pub source_kind: String,
173    pub upstream_id: String,
174    pub revision: Option<String>,
175    pub observed_at_utc: DateTime<Utc>,
176    pub extra: Option<Value>,
177}
178
179impl SttpNode {
180    pub fn canonical_sync_key(&self) -> String {
181        #[derive(Serialize)]
182        struct SyncFingerprint<'a> {
183            session_id: &'a str,
184            tier: &'a str,
185            timestamp: String,
186            compression_depth: i32,
187            parent_node_id: &'a Option<String>,
188            raw: &'a str,
189            user_avec: AvecState,
190            model_avec: AvecState,
191            compression_avec: Option<AvecState>,
192            rho: f32,
193            kappa: f32,
194            psi: f32,
195        }
196
197        let fingerprint = SyncFingerprint {
198            session_id: &self.session_id,
199            tier: &self.tier,
200            timestamp: self.timestamp.to_rfc3339(),
201            compression_depth: self.compression_depth,
202            parent_node_id: &self.parent_node_id,
203            raw: &self.raw,
204            user_avec: self.user_avec,
205            model_avec: self.model_avec,
206            compression_avec: self.compression_avec,
207            rho: self.rho,
208            kappa: self.kappa,
209            psi: self.psi,
210        };
211
212        let encoded = serde_json::to_vec(&fingerprint).unwrap_or_default();
213        let mut hasher = Sha256::new();
214        hasher.update(encoded);
215        let digest = hasher.finalize();
216
217        digest.iter().map(|byte| format!("{byte:02x}")).collect()
218    }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum NodeUpsertStatus {
223    Created,
224    Updated,
225    Duplicate,
226    Skipped,
227}
228
229#[derive(Debug, Clone)]
230pub struct NodeUpsertResult {
231    pub node_id: String,
232    pub sync_key: String,
233    pub status: NodeUpsertStatus,
234    pub updated_at: DateTime<Utc>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct SyncCursor {
239    pub updated_at: DateTime<Utc>,
240    pub sync_key: String,
241}
242
243#[derive(Debug, Clone, Default)]
244pub struct ChangeQueryResult {
245    pub nodes: Vec<SttpNode>,
246    pub next_cursor: Option<SyncCursor>,
247    pub has_more: bool,
248}
249
250#[derive(Debug, Clone)]
251pub struct SyncCheckpoint {
252    pub session_id: String,
253    pub connector_id: String,
254    pub cursor: Option<SyncCursor>,
255    pub updated_at: DateTime<Utc>,
256    pub metadata: Option<ConnectorMetadata>,
257}
258
259#[derive(Debug, Clone)]
260pub struct SyncPullRequest {
261    pub session_id: String,
262    pub connector_id: String,
263    pub page_size: usize,
264    pub max_batches: Option<usize>,
265}
266
267#[derive(Debug, Clone, Default)]
268pub struct SyncPullResult {
269    pub fetched: usize,
270    pub created: usize,
271    pub updated: usize,
272    pub duplicate: usize,
273    pub skipped: usize,
274    pub filtered: usize,
275    pub batches: usize,
276    pub has_more: bool,
277    pub last_cursor: Option<SyncCursor>,
278    pub checkpoint: Option<SyncCheckpoint>,
279}
280
281#[derive(Debug, Clone)]
282pub struct CalibrationResult {
283    pub previous_avec: AvecState,
284    pub delta: f32,
285    pub drift_classification: DriftClassification,
286    pub trigger: String,
287    pub trigger_history: Vec<String>,
288    pub is_first_calibration: bool,
289}
290
291#[derive(Debug, Clone)]
292pub struct NodeQuery {
293    pub limit: usize,
294    pub session_id: Option<String>,
295    pub from_utc: Option<DateTime<Utc>>,
296    pub to_utc: Option<DateTime<Utc>>,
297    pub tiers: Option<Vec<String>>,
298}
299
300impl Default for NodeQuery {
301    fn default() -> Self {
302        Self {
303            limit: 500,
304            session_id: None,
305            from_utc: None,
306            to_utc: None,
307            tiers: None,
308        }
309    }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq)]
313pub struct NumericRange {
314    pub min: f32,
315    pub max: f32,
316    pub average: f32,
317}
318
319impl Default for NumericRange {
320    fn default() -> Self {
321        Self {
322            min: 0.0,
323            max: 0.0,
324            average: 0.0,
325        }
326    }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq)]
330pub struct PsiRange {
331    pub min: f32,
332    pub max: f32,
333    pub average: f32,
334}
335
336impl Default for PsiRange {
337    fn default() -> Self {
338        Self {
339            min: 0.0,
340            max: 0.0,
341            average: 0.0,
342        }
343    }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
347pub struct ConfidenceBandSummary {
348    pub low: usize,
349    pub medium: usize,
350    pub high: usize,
351}
352
353#[derive(Debug, Clone, Default)]
354pub struct RetrieveResult {
355    pub nodes: Vec<SttpNode>,
356    pub retrieved: usize,
357    pub psi_range: PsiRange,
358}
359
360#[derive(Debug, Clone, Default)]
361pub struct ListNodesResult {
362    pub nodes: Vec<SttpNode>,
363    pub retrieved: usize,
364}
365
366#[derive(Debug, Clone, Default)]
367pub struct ScopeRekeyResult {
368    pub source_tenant_id: String,
369    pub source_session_id: String,
370    pub target_tenant_id: String,
371    pub target_session_id: String,
372    pub temporal_nodes: usize,
373    pub calibrations: usize,
374    pub target_temporal_nodes: usize,
375    pub target_calibrations: usize,
376    pub applied: bool,
377    pub conflict: bool,
378    pub message: Option<String>,
379}
380
381#[derive(Debug, Clone, Default)]
382pub struct BatchRekeyResult {
383    pub dry_run: bool,
384    pub requested_node_ids: usize,
385    pub resolved_node_ids: usize,
386    pub missing_node_ids: Vec<String>,
387    pub scopes: Vec<ScopeRekeyResult>,
388    pub temporal_nodes_updated: usize,
389    pub calibrations_updated: usize,
390}
391
392#[derive(Debug, Clone, Default)]
393pub struct StoreResult {
394    pub node_id: String,
395    pub psi: f32,
396    pub valid: bool,
397    pub validation_error: Option<String>,
398}
399
400#[derive(Debug, Clone)]
401pub struct ParseResult {
402    pub success: bool,
403    pub node: Option<SttpNode>,
404    pub error: Option<String>,
405    pub profile: ParseProfile,
406    pub strict_valid: bool,
407    pub diagnostics: Vec<ParseDiagnostic>,
408    pub canonical_ast: Option<CanonicalAst>,
409}
410
411impl ParseResult {
412    pub fn ok(node: SttpNode) -> Self {
413        Self {
414            success: true,
415            node: Some(node),
416            error: None,
417            profile: ParseProfile::Tolerant,
418            strict_valid: true,
419            diagnostics: Vec::new(),
420            canonical_ast: None,
421        }
422    }
423
424    pub fn ok_with_metadata(
425        node: SttpNode,
426        profile: ParseProfile,
427        strict_valid: bool,
428        diagnostics: Vec<ParseDiagnostic>,
429        canonical_ast: Option<CanonicalAst>,
430    ) -> Self {
431        Self {
432            success: true,
433            node: Some(node),
434            error: None,
435            profile,
436            strict_valid,
437            diagnostics,
438            canonical_ast,
439        }
440    }
441
442    pub fn fail(error: impl Into<String>) -> Self {
443        Self {
444            success: false,
445            node: None,
446            error: Some(error.into()),
447            profile: ParseProfile::Tolerant,
448            strict_valid: false,
449            diagnostics: Vec::new(),
450            canonical_ast: None,
451        }
452    }
453
454    pub fn fail_with_metadata(
455        error: impl Into<String>,
456        profile: ParseProfile,
457        diagnostics: Vec<ParseDiagnostic>,
458        canonical_ast: Option<CanonicalAst>,
459    ) -> Self {
460        Self {
461            success: false,
462            node: None,
463            error: Some(error.into()),
464            profile,
465            strict_valid: false,
466            diagnostics,
467            canonical_ast,
468        }
469    }
470}
471
472#[derive(Debug, Default,Clone, Copy, PartialEq, Eq)]
473pub enum ParseProfile {
474    Strict,
475    StrictTypedIr,
476    #[default]
477    Tolerant,
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum ParseDiagnosticSeverity {
482    Fatal,
483    Error,
484    Warning,
485    Info,
486}
487
488#[derive(Debug, Clone)]
489pub struct ParseDiagnostic {
490    pub code: String,
491    pub message: String,
492    pub severity: ParseDiagnosticSeverity,
493    pub strict_impact: bool,
494    pub span: Option<ParseSpan>,
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
498pub struct ParseSpan {
499    pub start: usize,
500    pub end: usize,
501    pub line: usize,
502    pub column: usize,
503}
504
505#[derive(Debug, Clone)]
506pub struct CanonicalAstLayer {
507    pub source: String,
508    pub span: ParseSpan,
509}
510
511#[derive(Debug, Clone)]
512pub struct CanonicalAst {
513    pub provenance: Option<CanonicalAstLayer>,
514    pub envelope: Option<CanonicalAstLayer>,
515    pub content: Option<CanonicalAstLayer>,
516    pub metrics: Option<CanonicalAstLayer>,
517    pub strict_spine: bool,
518    pub profile: ParseProfile,
519}
520
521#[derive(Debug, Clone)]
522pub struct ValidationResult {
523    pub is_valid: bool,
524    pub error: Option<String>,
525    pub reason: ValidationFailureReason,
526}
527
528impl ValidationResult {
529    pub fn ok() -> Self {
530        Self {
531            is_valid: true,
532            error: None,
533            reason: ValidationFailureReason::None,
534        }
535    }
536
537    pub fn fail(error: impl Into<String>, reason: ValidationFailureReason) -> Self {
538        Self {
539            is_valid: false,
540            error: Some(error.into()),
541            reason,
542        }
543    }
544}
545
546#[derive(Debug, Clone)]
547pub struct MoodCatalogResult {
548    pub presets: Vec<MoodPreset>,
549    pub apply_guide: String,
550    pub swap_preview: Option<MoodSwapPreview>,
551}
552
553#[derive(Debug, Clone)]
554pub struct MoodPreset {
555    pub name: String,
556    pub description: String,
557    pub avec: AvecState,
558}
559
560#[derive(Debug, Clone)]
561pub struct MoodSwapPreview {
562    pub target_mood: String,
563    pub blend: f32,
564    pub current: AvecState,
565    pub target: AvecState,
566    pub blended: AvecState,
567}
568
569#[derive(Debug, Clone)]
570pub struct MonthlyRollupRequest {
571    pub session_id: String,
572    pub start_utc: DateTime<Utc>,
573    pub end_utc: DateTime<Utc>,
574    pub source_session_id: Option<String>,
575    pub parent_node_id: Option<String>,
576    pub limit: usize,
577    pub persist: bool,
578}
579
580impl MonthlyRollupRequest {
581    pub fn new(
582        session_id: impl Into<String>,
583        start_utc: DateTime<Utc>,
584        end_utc: DateTime<Utc>,
585    ) -> Self {
586        Self {
587            session_id: session_id.into(),
588            start_utc,
589            end_utc,
590            source_session_id: None,
591            parent_node_id: None,
592            limit: 5000,
593            persist: true,
594        }
595    }
596}
597
598#[derive(Debug, Clone)]
599pub struct MonthlyRollupResult {
600    pub success: bool,
601    pub node_id: String,
602    pub raw_node: String,
603    pub error: Option<String>,
604    pub source_nodes: usize,
605    pub parent_reference: Option<String>,
606    pub user_average: AvecState,
607    pub model_average: AvecState,
608    pub compression_average: AvecState,
609    pub rho_range: NumericRange,
610    pub kappa_range: NumericRange,
611    pub psi_range: NumericRange,
612    pub rho_bands: ConfidenceBandSummary,
613    pub kappa_bands: ConfidenceBandSummary,
614}
615
616impl Default for MonthlyRollupResult {
617    fn default() -> Self {
618        Self {
619            success: false,
620            node_id: String::new(),
621            raw_node: String::new(),
622            error: None,
623            source_nodes: 0,
624            parent_reference: None,
625            user_average: AvecState::zero(),
626            model_average: AvecState::zero(),
627            compression_average: AvecState::zero(),
628            rho_range: NumericRange::default(),
629            kappa_range: NumericRange::default(),
630            psi_range: NumericRange::default(),
631            rho_bands: ConfidenceBandSummary::default(),
632            kappa_bands: ConfidenceBandSummary::default(),
633        }
634    }
635}