1use crate::{
10 ArtifactId, CellstateError, CellstateResult, ConflictResolution, NoteId, RawContent, Scope,
11 ScopeId, Timestamp, ValidationError,
12};
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum PruneStrategy {
24 OldestFirst,
26 LowestRelevance,
28 Hybrid,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum RecoveryFrequency {
36 OnScopeClose,
38 OnMutation,
40 Manual,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct ContextDagConfig {
47 pub max_depth: i32,
49 pub prune_strategy: PruneStrategy,
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct RecoveryConfig {
56 pub enabled: bool,
58 pub frequency: RecoveryFrequency,
60 pub max_checkpoints: i32,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct DosageConfig {
67 pub max_tokens_per_scope: i32,
69 pub max_artifacts_per_scope: i32,
71 pub max_notes_per_trajectory: i32,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct AntiSprawlConfig {
78 pub max_trajectory_depth: i32,
80 pub max_concurrent_scopes: i32,
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct GroundingConfig {
87 pub require_artifact_backing: bool,
89 pub contradiction_threshold: f32,
91 pub conflict_resolution: ConflictResolution,
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct LintingConfig {
98 pub max_artifact_size: usize,
100 pub min_confidence_threshold: f32,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct StalenessConfig {
107 pub stale_hours: i64,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct PCPConfig {
114 pub context_dag: ContextDagConfig,
116 pub recovery: RecoveryConfig,
118 pub dosage: DosageConfig,
120 pub anti_sprawl: AntiSprawlConfig,
122 pub grounding: GroundingConfig,
124 pub linting: LintingConfig,
126 pub staleness: StalenessConfig,
128}
129
130impl PCPConfig {
134 pub fn validate(&self) -> CellstateResult<()> {
136 if self.context_dag.max_depth <= 0 {
137 return Err(CellstateError::Validation(ValidationError::InvalidValue {
138 field: "context_dag.max_depth".to_string(),
139 reason: "must be positive".to_string(),
140 }));
141 }
142
143 if self.recovery.max_checkpoints < 0 {
144 return Err(CellstateError::Validation(ValidationError::InvalidValue {
145 field: "recovery.max_checkpoints".to_string(),
146 reason: "must be non-negative".to_string(),
147 }));
148 }
149
150 if self.dosage.max_tokens_per_scope <= 0 {
151 return Err(CellstateError::Validation(ValidationError::InvalidValue {
152 field: "dosage.max_tokens_per_scope".to_string(),
153 reason: "must be positive".to_string(),
154 }));
155 }
156 if self.dosage.max_artifacts_per_scope <= 0 {
157 return Err(CellstateError::Validation(ValidationError::InvalidValue {
158 field: "dosage.max_artifacts_per_scope".to_string(),
159 reason: "must be positive".to_string(),
160 }));
161 }
162 if self.dosage.max_notes_per_trajectory <= 0 {
163 return Err(CellstateError::Validation(ValidationError::InvalidValue {
164 field: "dosage.max_notes_per_trajectory".to_string(),
165 reason: "must be positive".to_string(),
166 }));
167 }
168
169 if self.anti_sprawl.max_trajectory_depth <= 0 {
170 return Err(CellstateError::Validation(ValidationError::InvalidValue {
171 field: "anti_sprawl.max_trajectory_depth".to_string(),
172 reason: "must be positive".to_string(),
173 }));
174 }
175 if self.anti_sprawl.max_concurrent_scopes <= 0 {
176 return Err(CellstateError::Validation(ValidationError::InvalidValue {
177 field: "anti_sprawl.max_concurrent_scopes".to_string(),
178 reason: "must be positive".to_string(),
179 }));
180 }
181
182 if self.grounding.contradiction_threshold < 0.0
183 || self.grounding.contradiction_threshold > 1.0
184 {
185 return Err(CellstateError::Validation(ValidationError::InvalidValue {
186 field: "grounding.contradiction_threshold".to_string(),
187 reason: "must be between 0.0 and 1.0".to_string(),
188 }));
189 }
190
191 if self.linting.max_artifact_size == 0 {
192 return Err(CellstateError::Validation(ValidationError::InvalidValue {
193 field: "linting.max_artifact_size".to_string(),
194 reason: "must be positive".to_string(),
195 }));
196 }
197 if self.linting.min_confidence_threshold < 0.0
198 || self.linting.min_confidence_threshold > 1.0
199 {
200 return Err(CellstateError::Validation(ValidationError::InvalidValue {
201 field: "linting.min_confidence_threshold".to_string(),
202 reason: "must be between 0.0 and 1.0".to_string(),
203 }));
204 }
205
206 if self.staleness.stale_hours <= 0 {
207 return Err(CellstateError::Validation(ValidationError::InvalidValue {
208 field: "staleness.stale_hours".to_string(),
209 reason: "must be positive".to_string(),
210 }));
211 }
212
213 Ok(())
214 }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum Severity {
225 Warning,
227 Error,
229 Critical,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
235#[serde(rename_all = "snake_case")]
236pub enum IssueType {
237 StaleData,
239 Contradiction,
241 MissingReference,
243 DosageExceeded,
245 CircularDependency,
247}
248
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251pub struct ValidationIssue {
252 pub severity: Severity,
254 pub issue_type: IssueType,
256 pub message: String,
258 pub entity_id: Option<Uuid>,
260}
261
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
264pub struct ValidationResult {
265 pub valid: bool,
267 pub issues: Vec<ValidationIssue>,
269}
270
271impl ValidationResult {
272 pub fn valid() -> Self {
274 Self {
275 valid: true,
276 issues: Vec::new(),
277 }
278 }
279
280 pub fn invalid(issues: Vec<ValidationIssue>) -> Self {
282 Self {
283 valid: false,
284 issues,
285 }
286 }
287
288 pub fn add_issue(&mut self, issue: ValidationIssue) {
290 if issue.severity == Severity::Error || issue.severity == Severity::Critical {
291 self.valid = false;
292 }
293 self.issues.push(issue);
294 }
295
296 pub fn has_critical(&self) -> bool {
298 self.issues.iter().any(|i| i.severity == Severity::Critical)
299 }
300
301 pub fn has_errors(&self) -> bool {
303 self.issues
304 .iter()
305 .any(|i| i.severity == Severity::Error || i.severity == Severity::Critical)
306 }
307
308 pub fn issues_of_type(&self, issue_type: IssueType) -> Vec<&ValidationIssue> {
310 self.issues
311 .iter()
312 .filter(|i| i.issue_type == issue_type)
313 .collect()
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub enum LintIssueType {
325 TooLarge,
327 Duplicate,
329 MissingEmbedding,
331 LowConfidence,
333 SyntaxError,
335 ReservedCharacterLeak,
337}
338
339#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
341pub struct LintIssue {
342 pub issue_type: LintIssueType,
344 pub message: String,
346 pub artifact_id: ArtifactId,
348}
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352pub struct MarkdownSemanticIssue {
353 pub issue_type: LintIssueType,
355 pub line: usize,
357 pub message: String,
359}
360
361#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363pub struct LintResult {
364 pub passed: bool,
366 pub issues: Vec<LintIssue>,
368}
369
370impl LintResult {
371 pub fn passed() -> Self {
373 Self {
374 passed: true,
375 issues: Vec::new(),
376 }
377 }
378
379 pub fn failed(issues: Vec<LintIssue>) -> Self {
381 Self {
382 passed: false,
383 issues,
384 }
385 }
386
387 pub fn add_issue(&mut self, issue: LintIssue) {
389 self.passed = false;
390 self.issues.push(issue);
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub struct CheckpointState {
401 pub context_snapshot: RawContent,
403 pub artifact_ids: Vec<ArtifactId>,
405 pub note_ids: Vec<NoteId>,
407}
408
409#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
411pub struct PCPCheckpoint {
412 pub checkpoint_id: Uuid,
414 pub scope_id: ScopeId,
416 pub state: CheckpointState,
418 pub created_at: Timestamp,
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
424pub struct RecoveryResult {
425 pub success: bool,
427 pub recovered_scope: Option<Scope>,
429 pub errors: Vec<String>,
431}
432
433impl RecoveryResult {
434 pub fn success(scope: Scope) -> Self {
436 Self {
437 success: true,
438 recovered_scope: Some(scope),
439 errors: Vec::new(),
440 }
441 }
442
443 pub fn failure(errors: Vec<String>) -> Self {
445 Self {
446 success: false,
447 recovered_scope: None,
448 errors,
449 }
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
459pub struct Contradiction {
460 pub artifact_a: ArtifactId,
462 pub artifact_b: ArtifactId,
464 pub similarity_score: f32,
466 pub description: String,
468}
469
470#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
472pub struct DosageResult {
473 pub exceeded: bool,
475 pub pruned_artifacts: Vec<ArtifactId>,
477 pub tokens_trimmed: i32,
479 pub warnings: Vec<String>,
481}
482
483impl DosageResult {
484 pub fn within_limits() -> Self {
486 Self {
487 exceeded: false,
488 pruned_artifacts: Vec::new(),
489 tokens_trimmed: 0,
490 warnings: Vec::new(),
491 }
492 }
493
494 pub fn exceeded_limits() -> Self {
496 Self {
497 exceeded: true,
498 pruned_artifacts: Vec::new(),
499 tokens_trimmed: 0,
500 warnings: Vec::new(),
501 }
502 }
503
504 pub fn add_pruned(&mut self, artifact_id: ArtifactId) {
506 self.pruned_artifacts.push(artifact_id);
507 }
508
509 pub fn add_warning(&mut self, warning: String) {
511 self.warnings.push(warning);
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::{ArtifactId, EntityIdType, NoteId, ScopeId};
519 use chrono::Utc;
520
521 fn make_test_pcp_config() -> PCPConfig {
522 PCPConfig {
523 context_dag: ContextDagConfig {
524 max_depth: 10,
525 prune_strategy: PruneStrategy::OldestFirst,
526 },
527 recovery: RecoveryConfig {
528 enabled: true,
529 frequency: RecoveryFrequency::OnScopeClose,
530 max_checkpoints: 5,
531 },
532 dosage: DosageConfig {
533 max_tokens_per_scope: 8000,
534 max_artifacts_per_scope: 100,
535 max_notes_per_trajectory: 500,
536 },
537 anti_sprawl: AntiSprawlConfig {
538 max_trajectory_depth: 5,
539 max_concurrent_scopes: 10,
540 },
541 grounding: GroundingConfig {
542 require_artifact_backing: false,
543 contradiction_threshold: 0.85,
544 conflict_resolution: ConflictResolution::LastWriteWins,
545 },
546 linting: LintingConfig {
547 max_artifact_size: 1024 * 1024,
548 min_confidence_threshold: 0.3,
549 },
550 staleness: StalenessConfig {
551 stale_hours: 24 * 30,
552 },
553 }
554 }
555
556 #[test]
557 fn test_pcp_config_valid() {
558 let config = make_test_pcp_config();
559 assert!(config.validate().is_ok());
560 }
561
562 #[test]
563 fn test_pcp_config_invalid_max_depth() {
564 let mut config = make_test_pcp_config();
565 config.context_dag.max_depth = 0;
566 assert!(config.validate().is_err());
567 }
568
569 #[test]
570 fn test_pcp_config_invalid_threshold() {
571 let mut config = make_test_pcp_config();
572 config.grounding.contradiction_threshold = 1.5;
573 assert!(config.validate().is_err());
574 }
575
576 #[test]
577 fn test_pcp_config_invalid_negative_checkpoints() {
578 let mut config = make_test_pcp_config();
579 config.recovery.max_checkpoints = -1;
580 assert!(config.validate().is_err());
581 }
582
583 #[test]
584 fn test_pcp_config_invalid_zero_tokens() {
585 let mut config = make_test_pcp_config();
586 config.dosage.max_tokens_per_scope = 0;
587 assert!(config.validate().is_err());
588 }
589
590 #[test]
591 fn test_pcp_config_invalid_zero_artifacts() {
592 let mut config = make_test_pcp_config();
593 config.dosage.max_artifacts_per_scope = 0;
594 assert!(config.validate().is_err());
595 }
596
597 #[test]
598 fn test_pcp_config_invalid_zero_notes() {
599 let mut config = make_test_pcp_config();
600 config.dosage.max_notes_per_trajectory = 0;
601 assert!(config.validate().is_err());
602 }
603
604 #[test]
605 fn test_pcp_config_invalid_zero_trajectory_depth() {
606 let mut config = make_test_pcp_config();
607 config.anti_sprawl.max_trajectory_depth = 0;
608 assert!(config.validate().is_err());
609 }
610
611 #[test]
612 fn test_pcp_config_invalid_zero_concurrent_scopes() {
613 let mut config = make_test_pcp_config();
614 config.anti_sprawl.max_concurrent_scopes = 0;
615 assert!(config.validate().is_err());
616 }
617
618 #[test]
619 fn test_pcp_config_invalid_negative_threshold() {
620 let mut config = make_test_pcp_config();
621 config.grounding.contradiction_threshold = -0.1;
622 assert!(config.validate().is_err());
623 }
624
625 #[test]
626 fn test_pcp_config_invalid_zero_artifact_size() {
627 let mut config = make_test_pcp_config();
628 config.linting.max_artifact_size = 0;
629 assert!(config.validate().is_err());
630 }
631
632 #[test]
633 fn test_pcp_config_invalid_confidence_threshold() {
634 let mut config = make_test_pcp_config();
635 config.linting.min_confidence_threshold = 1.5;
636 assert!(config.validate().is_err());
637 }
638
639 #[test]
640 fn test_pcp_config_invalid_zero_stale_hours() {
641 let mut config = make_test_pcp_config();
642 config.staleness.stale_hours = 0;
643 assert!(config.validate().is_err());
644 }
645
646 #[test]
648 fn test_prune_strategy_serde() {
649 for s in [
650 PruneStrategy::OldestFirst,
651 PruneStrategy::LowestRelevance,
652 PruneStrategy::Hybrid,
653 ] {
654 let json = serde_json::to_string(&s).unwrap();
655 let d: PruneStrategy = serde_json::from_str(&json).unwrap();
656 assert_eq!(d, s);
657 }
658 }
659
660 #[test]
661 fn test_recovery_frequency_serde() {
662 for f in [
663 RecoveryFrequency::OnScopeClose,
664 RecoveryFrequency::OnMutation,
665 RecoveryFrequency::Manual,
666 ] {
667 let json = serde_json::to_string(&f).unwrap();
668 let d: RecoveryFrequency = serde_json::from_str(&json).unwrap();
669 assert_eq!(d, f);
670 }
671 }
672
673 #[test]
674 fn test_severity_serde() {
675 for s in [Severity::Warning, Severity::Error, Severity::Critical] {
676 let json = serde_json::to_string(&s).unwrap();
677 let d: Severity = serde_json::from_str(&json).unwrap();
678 assert_eq!(d, s);
679 }
680 }
681
682 #[test]
683 fn test_issue_type_serde() {
684 for t in [
685 IssueType::StaleData,
686 IssueType::Contradiction,
687 IssueType::MissingReference,
688 IssueType::DosageExceeded,
689 IssueType::CircularDependency,
690 ] {
691 let json = serde_json::to_string(&t).unwrap();
692 let d: IssueType = serde_json::from_str(&json).unwrap();
693 assert_eq!(d, t);
694 }
695 }
696
697 #[test]
698 fn test_lint_issue_type_serde() {
699 for t in [
700 LintIssueType::TooLarge,
701 LintIssueType::Duplicate,
702 LintIssueType::MissingEmbedding,
703 LintIssueType::LowConfidence,
704 LintIssueType::SyntaxError,
705 LintIssueType::ReservedCharacterLeak,
706 ] {
707 let json = serde_json::to_string(&t).unwrap();
708 let d: LintIssueType = serde_json::from_str(&json).unwrap();
709 assert_eq!(d, t);
710 }
711 }
712
713 #[test]
715 fn test_validation_result_valid() {
716 let result = ValidationResult::valid();
717 assert!(result.valid);
718 assert!(result.issues.is_empty());
719 assert!(!result.has_critical());
720 assert!(!result.has_errors());
721 }
722
723 #[test]
724 fn test_validation_result_invalid() {
725 let result = ValidationResult::invalid(vec![ValidationIssue {
726 severity: Severity::Error,
727 issue_type: IssueType::StaleData,
728 message: "stale".into(),
729 entity_id: None,
730 }]);
731 assert!(!result.valid);
732 assert_eq!(result.issues.len(), 1);
733 assert!(result.has_errors());
734 }
735
736 #[test]
737 fn test_validation_result_add_warning_stays_valid() {
738 let mut result = ValidationResult::valid();
739 result.add_issue(ValidationIssue {
740 severity: Severity::Warning,
741 issue_type: IssueType::StaleData,
742 message: "warning".into(),
743 entity_id: None,
744 });
745 assert!(result.valid);
746 assert_eq!(result.issues.len(), 1);
747 }
748
749 #[test]
750 fn test_validation_result_add_error_invalidates() {
751 let mut result = ValidationResult::valid();
752 result.add_issue(ValidationIssue {
753 severity: Severity::Error,
754 issue_type: IssueType::Contradiction,
755 message: "error".into(),
756 entity_id: None,
757 });
758 assert!(!result.valid);
759 }
760
761 #[test]
762 fn test_validation_result_add_critical_invalidates() {
763 let mut result = ValidationResult::valid();
764 result.add_issue(ValidationIssue {
765 severity: Severity::Critical,
766 issue_type: IssueType::CircularDependency,
767 message: "critical".into(),
768 entity_id: Some(Uuid::now_v7()),
769 });
770 assert!(!result.valid);
771 assert!(result.has_critical());
772 }
773
774 #[test]
775 fn test_validation_result_issues_of_type() {
776 let mut result = ValidationResult::valid();
777 result.add_issue(ValidationIssue {
778 severity: Severity::Warning,
779 issue_type: IssueType::StaleData,
780 message: "stale1".into(),
781 entity_id: None,
782 });
783 result.add_issue(ValidationIssue {
784 severity: Severity::Warning,
785 issue_type: IssueType::MissingReference,
786 message: "missing".into(),
787 entity_id: None,
788 });
789 result.add_issue(ValidationIssue {
790 severity: Severity::Warning,
791 issue_type: IssueType::StaleData,
792 message: "stale2".into(),
793 entity_id: None,
794 });
795 let stale = result.issues_of_type(IssueType::StaleData);
796 assert_eq!(stale.len(), 2);
797 }
798
799 #[test]
801 fn test_lint_result_passed() {
802 let result = LintResult::passed();
803 assert!(result.passed);
804 assert!(result.issues.is_empty());
805 }
806
807 #[test]
808 fn test_lint_result_failed() {
809 let result = LintResult::failed(vec![LintIssue {
810 issue_type: LintIssueType::TooLarge,
811 message: "too big".into(),
812 artifact_id: ArtifactId::now_v7(),
813 }]);
814 assert!(!result.passed);
815 assert_eq!(result.issues.len(), 1);
816 }
817
818 #[test]
819 fn test_lint_result_add_issue() {
820 let mut result = LintResult::passed();
821 assert!(result.passed);
822 result.add_issue(LintIssue {
823 issue_type: LintIssueType::LowConfidence,
824 message: "low".into(),
825 artifact_id: ArtifactId::now_v7(),
826 });
827 assert!(!result.passed);
828 assert_eq!(result.issues.len(), 1);
829 }
830
831 #[test]
833 fn test_recovery_result_success() {
834 let scope = crate::Scope {
835 scope_id: ScopeId::now_v7(),
836 trajectory_id: crate::TrajectoryId::now_v7(),
837 parent_scope_id: None,
838 name: "Test Scope".to_string(),
839 purpose: Some("Testing".to_string()),
840 is_active: true,
841 created_at: Utc::now(),
842 closed_at: None,
843 checkpoint: None,
844 token_budget: 8000,
845 tokens_used: 0,
846 metadata: None,
847 };
848 let result = RecoveryResult::success(scope.clone());
849 assert!(result.success);
850 assert_eq!(result.recovered_scope.unwrap().scope_id, scope.scope_id);
851 assert!(result.errors.is_empty());
852 }
853
854 #[test]
855 fn test_recovery_result_failure() {
856 let result = RecoveryResult::failure(vec!["disk full".into()]);
857 assert!(!result.success);
858 assert!(result.recovered_scope.is_none());
859 assert_eq!(result.errors.len(), 1);
860 }
861
862 #[test]
864 fn test_dosage_result_within_limits() {
865 let result = DosageResult::within_limits();
866 assert!(!result.exceeded);
867 assert!(result.pruned_artifacts.is_empty());
868 assert!(result.warnings.is_empty());
869 }
870
871 #[test]
872 fn test_dosage_result_exceeded() {
873 let mut result = DosageResult::exceeded_limits();
874 assert!(result.exceeded);
875 let id = ArtifactId::now_v7();
876 result.add_pruned(id);
877 result.add_warning("over budget".into());
878 assert_eq!(result.pruned_artifacts.len(), 1);
879 assert_eq!(result.warnings.len(), 1);
880 }
881
882 #[test]
884 fn test_contradiction_serde() {
885 let c = Contradiction {
886 artifact_a: ArtifactId::now_v7(),
887 artifact_b: ArtifactId::now_v7(),
888 similarity_score: 0.95,
889 description: "Contradictory".into(),
890 };
891 let json = serde_json::to_string(&c).unwrap();
892 let d: Contradiction = serde_json::from_str(&json).unwrap();
893 assert_eq!(d.similarity_score, 0.95);
894 }
895
896 #[test]
898 fn test_checkpoint_state_serde() {
899 let state = CheckpointState {
900 context_snapshot: b"snapshot".to_vec(),
901 artifact_ids: vec![ArtifactId::now_v7()],
902 note_ids: vec![NoteId::now_v7()],
903 };
904 let json = serde_json::to_string(&state).unwrap();
905 let d: CheckpointState = serde_json::from_str(&json).unwrap();
906 assert_eq!(d.artifact_ids.len(), 1);
907 assert_eq!(d.note_ids.len(), 1);
908 }
909}