cellstate_pipeline/
ast.rs

1//! Abstract Syntax Tree types
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6// ============================================================================
7// AST TYPES (Task 4.1)
8// ============================================================================
9
10/// The root AST node for a CELLSTATE configuration.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct CellstateAst {
13    pub version: String,
14    pub definitions: Vec<Definition>,
15}
16
17/// A top-level definition in pack config.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub enum Definition {
20    Adapter(AdapterDef),
21    Memory(MemoryDef),
22    Policy(PolicyDef),
23    Injection(InjectionDef),
24    // Battle Intel Feature 3: Evolution mode
25    Evolution(EvolutionDef),
26    // Battle Intel Feature 4: Summarization policies
27    SummarizationPolicy(SummarizationPolicyDef),
28    // Pack-first architecture: New definitions
29    Trajectory(TrajectoryDef),
30    Agent(AgentDef),
31    Cache(CacheDef),
32    Provider(ProviderDef),
33    // Intent Engineering: machine-readable organizational purpose
34    Intent(IntentDef),
35}
36
37/// Adapter definition for storage backends.
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct AdapterDef {
40    pub name: String,
41    pub adapter_type: AdapterType,
42    pub connection: String,
43    pub options: Vec<(String, String)>,
44}
45
46/// Supported adapter types.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48pub enum AdapterType {
49    Postgres,
50    Redis,
51    Memory,
52}
53
54impl fmt::Display for AdapterType {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            AdapterType::Postgres => f.write_str("postgres"),
58            AdapterType::Redis => f.write_str("redis"),
59            AdapterType::Memory => f.write_str("memory"),
60        }
61    }
62}
63
64/// Memory definition for memory types.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct MemoryDef {
67    pub name: String,
68    pub memory_type: MemoryType,
69    pub schema: Vec<FieldDef>,
70    pub retention: Retention,
71    pub lifecycle: Lifecycle,
72    pub parent: Option<String>,
73    pub indexes: Vec<IndexDef>,
74    pub inject_on: Vec<Trigger>,
75    pub artifacts: Vec<String>,
76    /// Pack-first: Memory modifiers (embeddable, summarizable, lockable)
77    pub modifiers: Vec<ModifierDef>,
78}
79
80/// Memory type categories.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub enum MemoryType {
83    Ephemeral,
84    Working,
85    Episodic,
86    Semantic,
87    Procedural,
88    Meta,
89}
90
91impl fmt::Display for MemoryType {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            MemoryType::Ephemeral => f.write_str("ephemeral"),
95            MemoryType::Working => f.write_str("working"),
96            MemoryType::Episodic => f.write_str("episodic"),
97            MemoryType::Semantic => f.write_str("semantic"),
98            MemoryType::Procedural => f.write_str("procedural"),
99            MemoryType::Meta => f.write_str("meta"),
100        }
101    }
102}
103
104/// Field definition in a schema.
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct FieldDef {
107    pub name: String,
108    pub field_type: FieldType,
109    pub nullable: bool,
110    pub default: Option<String>,
111    /// Optional security configuration for PII fields.
112    pub security: Option<FieldSecurity>,
113}
114
115/// Field types supported in schemas.
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub enum FieldType {
118    Uuid,
119    Text,
120    Int,
121    Float,
122    Bool,
123    Timestamp,
124    Json,
125    Embedding(Option<usize>),
126    Enum(Vec<String>),
127    Array(Box<FieldType>),
128}
129
130impl fmt::Display for FieldType {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        match self {
133            FieldType::Uuid => f.write_str("uuid"),
134            FieldType::Text => f.write_str("text"),
135            FieldType::Int => f.write_str("int"),
136            FieldType::Float => f.write_str("float"),
137            FieldType::Bool => f.write_str("bool"),
138            FieldType::Timestamp => f.write_str("timestamp"),
139            FieldType::Json => f.write_str("json"),
140            FieldType::Embedding(Some(d)) => write!(f, "embedding({})", d),
141            FieldType::Embedding(None) => f.write_str("embedding"),
142            FieldType::Enum(variants) => write!(f, "enum({})", variants.join(", ")),
143            FieldType::Array(inner) => write!(f, "array({})", inner),
144        }
145    }
146}
147
148/// Retention policy for memory entries.
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub enum Retention {
151    Persistent,
152    Session,
153    Scope,
154    Duration(String),
155    Max(usize),
156}
157
158impl fmt::Display for Retention {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            Retention::Persistent => f.write_str("persistent"),
162            Retention::Session => f.write_str("session"),
163            Retention::Scope => f.write_str("scope"),
164            Retention::Duration(d) => write!(f, "duration({})", d),
165            Retention::Max(n) => write!(f, "max({})", n),
166        }
167    }
168}
169
170/// Lifecycle management for memory entries.
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub enum Lifecycle {
173    Explicit,
174    AutoClose(Trigger),
175}
176
177impl fmt::Display for Lifecycle {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Lifecycle::Explicit => f.write_str("explicit"),
181            Lifecycle::AutoClose(trigger) => write!(f, "auto_close({})", trigger),
182        }
183    }
184}
185
186/// Trigger events for policies and lifecycle.
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
188pub enum Trigger {
189    TaskStart,
190    TaskEnd,
191    ScopeClose,
192    TurnEnd,
193    Manual,
194    Schedule(String),
195}
196
197impl fmt::Display for Trigger {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            Trigger::TaskStart => f.write_str("task_start"),
201            Trigger::TaskEnd => f.write_str("task_end"),
202            Trigger::ScopeClose => f.write_str("scope_close"),
203            Trigger::TurnEnd => f.write_str("turn_end"),
204            Trigger::Manual => f.write_str("manual"),
205            Trigger::Schedule(s) => write!(f, "schedule:{}", s),
206        }
207    }
208}
209
210/// Index definition for memory fields.
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct IndexDef {
213    pub field: String,
214    pub index_type: IndexType,
215    pub options: Vec<(String, String)>,
216}
217
218/// Supported index types.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220pub enum IndexType {
221    Btree,
222    Hash,
223    Gin,
224    Hnsw,
225    Ivfflat,
226}
227
228impl fmt::Display for IndexType {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            IndexType::Btree => f.write_str("btree"),
232            IndexType::Hash => f.write_str("hash"),
233            IndexType::Gin => f.write_str("gin"),
234            IndexType::Hnsw => f.write_str("hnsw"),
235            IndexType::Ivfflat => f.write_str("ivfflat"),
236        }
237    }
238}
239
240/// Policy definition with trigger-action rules.
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242pub struct PolicyDef {
243    pub name: String,
244    pub rules: Vec<PolicyRule>,
245}
246
247/// A single policy rule.
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub struct PolicyRule {
250    pub trigger: Trigger,
251    pub actions: Vec<Action>,
252}
253
254/// Actions that can be triggered by policies.
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256pub enum Action {
257    Summarize(String),
258    ExtractArtifacts(String),
259    Checkpoint(String),
260    Prune {
261        target: String,
262        criteria: FilterExpr,
263    },
264    Notify(String),
265    Inject {
266        target: String,
267        mode: InjectionMode,
268    },
269    // Battle Intel Feature 4: Auto-summarization action
270    AutoSummarize {
271        source_level: AbstractionLevelParsed,
272        target_level: AbstractionLevelParsed,
273        create_edges: bool,
274    },
275}
276
277/// Injection definition for context assembly.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct InjectionDef {
280    pub source: String,
281    pub target: String,
282    pub mode: InjectionMode,
283    pub priority: i32,
284    pub max_tokens: Option<i32>,
285    pub filter: Option<FilterExpr>,
286}
287
288/// Injection modes for context assembly.
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290pub enum InjectionMode {
291    Full,
292    Summary,
293    TopK(usize),
294    Relevant(f32),
295}
296
297impl fmt::Display for InjectionMode {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        match self {
300            InjectionMode::Full => f.write_str("full"),
301            InjectionMode::Summary => f.write_str("summary"),
302            InjectionMode::TopK(k) => write!(f, "topk:{}", k),
303            InjectionMode::Relevant(threshold) => write!(f, "relevant:{}", threshold),
304        }
305    }
306}
307
308/// Filter expression for queries.
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
310pub enum FilterExpr {
311    Comparison {
312        field: String,
313        op: CompareOp,
314        value: FilterValue,
315    },
316    And(Vec<FilterExpr>),
317    Or(Vec<FilterExpr>),
318    Not(Box<FilterExpr>),
319}
320
321/// Comparison operators for filters.
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
323pub enum CompareOp {
324    Eq,
325    Ne,
326    Gt,
327    Lt,
328    Ge,
329    Le,
330    Contains,
331    Regex,
332    In,
333}
334
335/// Values that can be used in filter expressions.
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub enum FilterValue {
338    String(String),
339    Number(f64),
340    Bool(bool),
341    Null,
342    CurrentTrajectory,
343    CurrentScope,
344    Now,
345    Array(Vec<FilterValue>),
346}
347
348// ============================================================================
349// BATTLE INTEL FEATURE 2: ABSTRACTION LEVELS
350// ============================================================================
351
352/// Abstraction level for pack config (mirrors cellstate_core::AbstractionLevel).
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
354pub enum AbstractionLevelParsed {
355    Raw,       // L0: Direct observation
356    Summary,   // L1: Synthesized from L0s
357    Principle, // L2: High-level abstraction
358}
359
360// ============================================================================
361// BATTLE INTEL FEATURE 3: EVOLUTION MODE (MemEvolve-inspired)
362// ============================================================================
363
364/// Evolution definition for pack config benchmarking.
365///
366/// pack syntax:
367/// ```text
368/// evolution "memory_optimization" {
369///     baseline: "current_prod"
370///     candidates: ["hybrid_search", "aggressive_summarize"]
371///     benchmark_queries: 100
372///     metrics: ["retrieval_accuracy", "token_efficiency"]
373/// }
374/// ```
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376pub struct EvolutionDef {
377    pub name: String,
378    /// Snapshot name to compare against
379    pub baseline: String,
380    /// Candidate config names to test
381    pub candidates: Vec<String>,
382    /// Number of queries to benchmark
383    pub benchmark_queries: i32,
384    /// Metrics to track
385    pub metrics: Vec<String>,
386}
387
388// ============================================================================
389// BATTLE INTEL FEATURE 4: SUMMARIZATION POLICIES
390// ============================================================================
391
392/// Summarization trigger types (mirrors cellstate_core::SummarizationTrigger).
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub enum SummarizationTriggerParsed {
395    /// Trigger when token usage reaches threshold percent
396    DosageThreshold { percent: u8 },
397    /// Trigger when scope closes
398    ScopeClose,
399    /// Trigger every N turns
400    TurnCount { count: i32 },
401    /// Trigger every N artifacts
402    ArtifactCount { count: i32 },
403    /// Manual trigger only
404    Manual,
405}
406
407/// Summarization policy definition.
408///
409/// pack syntax:
410/// ```text
411/// summarization_policy "auto_abstract" {
412///     triggers: [dosage_reached(80), scope_close]
413///     source_level: raw
414///     target_level: summary
415///     max_sources: 10
416///     create_edges: true
417/// }
418/// ```
419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
420pub struct SummarizationPolicyDef {
421    pub name: String,
422    pub triggers: Vec<SummarizationTriggerParsed>,
423    pub source_level: AbstractionLevelParsed,
424    pub target_level: AbstractionLevelParsed,
425    pub max_sources: i32,
426    pub create_edges: bool,
427}
428
429// ============================================================================
430// PACK-FIRST ARCHITECTURE: TRAJECTORY DEFINITIONS
431// ============================================================================
432
433/// Trajectory definition for multi-turn interaction templates.
434///
435/// pack syntax:
436/// ```text
437/// trajectory "customer_support" {
438///     description: "Multi-turn customer support interaction"
439///     agent_type: "support_agent"
440///     token_budget: 8000
441///     memory_refs: [artifacts, notes, scopes]
442/// }
443/// ```
444#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
445pub struct TrajectoryDef {
446    pub name: String,
447    pub description: Option<String>,
448    pub agent_type: String,
449    pub token_budget: i32,
450    pub memory_refs: Vec<String>,
451    pub metadata: Option<serde_json::Value>,
452}
453
454// ============================================================================
455// PACK-FIRST ARCHITECTURE: AGENT DEFINITIONS
456// ============================================================================
457
458/// Agent definition for agent types and capabilities.
459///
460/// pack syntax:
461/// ```text
462/// agent "support_agent" {
463///     capabilities: ["classify_issue", "search_kb", "escalate"]
464///     constraints: {
465///         max_concurrent: 5
466///         timeout_ms: 30000
467///     }
468///     permissions: {
469///         read: [artifacts, notes, scopes]
470///         write: [notes, scopes]
471///         lock: [scopes]
472///     }
473/// }
474/// ```
475#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
476pub struct AgentDef {
477    pub name: String,
478    pub capabilities: Vec<String>,
479    pub constraints: AgentConstraints,
480    pub permissions: PermissionMatrix,
481}
482
483/// Agent runtime constraints.
484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
485pub struct AgentConstraints {
486    pub max_concurrent: i32,
487    pub timeout_ms: i64,
488}
489
490impl Default for AgentConstraints {
491    fn default() -> Self {
492        Self {
493            max_concurrent: 1,
494            timeout_ms: 30000,
495        }
496    }
497}
498
499/// Permission matrix for agent access control.
500#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
501pub struct PermissionMatrix {
502    pub read: Vec<String>,
503    pub write: Vec<String>,
504    pub lock: Vec<String>,
505}
506
507// ============================================================================
508// PACK-FIRST ARCHITECTURE: CACHE CONFIGURATION
509// ============================================================================
510
511/// Cache configuration for the Three Dragons architecture.
512///
513/// pack syntax:
514/// ```text
515/// cache {
516///     backend: lmdb
517///     path: "/var/cellstate/cache"
518///     size_mb: 1024
519///     default_freshness: best_effort { max_staleness: 60s }
520/// }
521/// ```
522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
523pub struct CacheDef {
524    pub backend: CacheBackendType,
525    pub path: Option<String>,
526    pub size_mb: i32,
527    pub default_freshness: FreshnessDef,
528    pub max_entries: Option<i32>,
529    pub ttl: Option<String>,
530}
531
532/// Cache backend types.
533#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
534pub enum CacheBackendType {
535    Lmdb,
536    Memory,
537}
538
539impl fmt::Display for CacheBackendType {
540    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541        match self {
542            CacheBackendType::Lmdb => f.write_str("lmdb"),
543            CacheBackendType::Memory => f.write_str("memory"),
544        }
545    }
546}
547
548/// Freshness policy definition.
549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
550pub enum FreshnessDef {
551    /// Best-effort freshness with max staleness tolerance
552    BestEffort { max_staleness: String },
553    /// Strict freshness - always fetch from source
554    Strict,
555}
556
557impl fmt::Display for FreshnessDef {
558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559        match self {
560            FreshnessDef::BestEffort { max_staleness } => {
561                write!(f, "best_effort(max_staleness: {})", max_staleness)
562            }
563            FreshnessDef::Strict => f.write_str("strict"),
564        }
565    }
566}
567
568impl Default for FreshnessDef {
569    fn default() -> Self {
570        FreshnessDef::BestEffort {
571            max_staleness: "60s".to_string(),
572        }
573    }
574}
575
576// ============================================================================
577// PACK-FIRST ARCHITECTURE: PROVIDER DEFINITIONS
578// ============================================================================
579
580/// LLM provider definition for embeddings and summarization.
581///
582/// pack syntax:
583/// ```text
584/// provider "openai" {
585///     type: openai
586///     api_key: env("OPENAI_API_KEY")
587///     model: "text-embedding-3-small"
588/// }
589/// ```
590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
591pub struct ProviderDef {
592    pub name: String,
593    pub provider_type: ProviderType,
594    pub api_key: EnvValue,
595    pub model: String,
596    pub options: Vec<(String, String)>,
597}
598
599/// LLM provider types.
600#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
601pub enum ProviderType {
602    OpenAI,
603    Anthropic,
604    Local,
605    Custom,
606}
607
608impl fmt::Display for ProviderType {
609    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610        match self {
611            ProviderType::OpenAI => f.write_str("openai"),
612            ProviderType::Anthropic => f.write_str("anthropic"),
613            ProviderType::Local => f.write_str("local"),
614            ProviderType::Custom => f.write_str("custom"),
615        }
616    }
617}
618
619/// Environment variable reference or literal value.
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
621pub enum EnvValue {
622    /// Reference to environment variable: env("VAR_NAME")
623    Env(String),
624    /// Literal string value
625    Literal(String),
626}
627
628// ============================================================================
629// INTENT ENGINEERING: RE-EXPORT FROM CELLSTATE_CORE
630// ============================================================================
631
632pub use cellstate_core::intent::{
633    AlignmentSignal, AutonomyLevel, DelegationBoundary, DriftReport, IntentDef, ResolutionRule,
634    SignalScore, SignalTarget,
635};
636
637// ============================================================================
638// PACK-FIRST ARCHITECTURE: MEMORY MODIFIERS
639// ============================================================================
640
641/// Memory modifier types for embeddable, summarizable, lockable.
642///
643/// pack syntax in memory definition:
644/// ```text
645/// memory artifacts {
646///     modifiers: [
647///         embeddable { provider: "openai" },
648///         summarizable { style: brief, on: [scope_close] }
649///     ]
650/// }
651/// ```
652#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
653pub enum ModifierDef {
654    /// Embeddable modifier - enables vector embeddings
655    Embeddable { provider: String },
656    /// Summarizable modifier - enables auto-summarization
657    Summarizable {
658        style: SummaryStyle,
659        on_triggers: Vec<Trigger>,
660    },
661    /// Lockable modifier - enables distributed locking
662    Lockable { mode: LockMode },
663}
664
665impl fmt::Display for ModifierDef {
666    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667        match self {
668            ModifierDef::Embeddable { provider } => {
669                write!(f, "embeddable(provider: {})", provider)
670            }
671            ModifierDef::Summarizable { style, on_triggers } => {
672                let triggers_str = on_triggers
673                    .iter()
674                    .map(|t| t.to_string())
675                    .collect::<Vec<_>>()
676                    .join(", ");
677                write!(f, "summarizable(style: {}, on: [{}])", style, triggers_str)
678            }
679            ModifierDef::Lockable { mode } => write!(f, "lockable(mode: {})", mode),
680        }
681    }
682}
683
684/// Summary style for summarizable modifier.
685#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
686pub enum SummaryStyle {
687    Brief,
688    Detailed,
689}
690
691impl fmt::Display for SummaryStyle {
692    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
693        match self {
694            SummaryStyle::Brief => f.write_str("brief"),
695            SummaryStyle::Detailed => f.write_str("detailed"),
696        }
697    }
698}
699
700/// Lock mode for lockable modifier.
701#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
702pub enum LockMode {
703    Exclusive,
704    Shared,
705}
706
707impl fmt::Display for LockMode {
708    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
709        match self {
710            LockMode::Exclusive => f.write_str("exclusive"),
711            LockMode::Shared => f.write_str("shared"),
712        }
713    }
714}
715
716// ============================================================================
717// PII & SECURITY AST TYPES (Phase 3)
718// ============================================================================
719
720/// PII/sensitivity classification for a field.
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
722pub enum PIIClassification {
723    /// No restrictions
724    #[default]
725    Public,
726    /// Internal use only
727    Internal,
728    /// Limited access
729    Confidential,
730    /// Highly restricted
731    Restricted,
732    /// Secret - encrypted, redacted in logs
733    Secret,
734}
735
736impl PIIClassification {
737    /// Whether this classification requires runtime redaction.
738    pub fn requires_redaction(&self) -> bool {
739        matches!(self, Self::Confidential | Self::Restricted | Self::Secret)
740    }
741
742    /// Whether this classification requires encrypted vault storage.
743    pub fn requires_encryption(&self) -> bool {
744        matches!(self, Self::Secret)
745    }
746}
747
748/// Field security modifiers.
749///
750/// Controls how agents and systems can interact with sensitive fields.
751#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
752pub struct FieldSecurity {
753    /// Sensitivity classification
754    pub classification: PIIClassification,
755    /// Agent can pass but not read content
756    pub opaque: bool,
757    /// Cannot be modified after creation
758    pub immutable: bool,
759    /// All access is logged
760    pub audited: bool,
761    /// Redact in logs and error messages
762    pub redact_in_logs: bool,
763    /// Source from environment variable
764    pub env_source: Option<String>,
765}
766
767impl Default for FieldSecurity {
768    fn default() -> Self {
769        Self {
770            classification: PIIClassification::Public,
771            opaque: false,
772            immutable: false,
773            audited: false,
774            redact_in_logs: false,
775            env_source: None,
776        }
777    }
778}
779
780impl FieldSecurity {
781    /// Create a public field with no restrictions.
782    pub fn public() -> Self {
783        Self::default()
784    }
785
786    /// Create a secret field with full protection.
787    pub fn secret() -> Self {
788        Self {
789            classification: PIIClassification::Secret,
790            opaque: true,
791            immutable: false,
792            audited: true,
793            redact_in_logs: true,
794            env_source: None,
795        }
796    }
797
798    /// Create a sensitive field.
799    pub fn sensitive() -> Self {
800        Self {
801            classification: PIIClassification::Confidential,
802            opaque: false,
803            immutable: false,
804            audited: false,
805            redact_in_logs: true,
806            env_source: None,
807        }
808    }
809
810    /// Mark as opaque (agent can pass but not read).
811    pub fn with_opaque(mut self) -> Self {
812        self.opaque = true;
813        self
814    }
815
816    /// Mark as immutable.
817    pub fn with_immutable(mut self) -> Self {
818        self.immutable = true;
819        self
820    }
821
822    /// Mark as audited.
823    pub fn with_audited(mut self) -> Self {
824        self.audited = true;
825        self
826    }
827
828    /// Set environment source.
829    pub fn with_env(mut self, var_name: impl Into<String>) -> Self {
830        self.env_source = Some(var_name.into());
831        self
832    }
833}
834
835/// Enhanced field definition with security modifiers.
836///
837/// Extends the basic FieldDef with optional security configuration.
838#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
839pub struct SecureFieldDef {
840    /// Field name
841    pub name: String,
842    /// Field type
843    pub field_type: FieldType,
844    /// Security configuration (optional)
845    pub security: Option<FieldSecurity>,
846    /// Default value (if any)
847    pub default: Option<String>,
848}
849
850// ============================================================================
851// PARSE ERROR (Task 4.8)
852// ============================================================================
853
854/// Parse error with line/column information.
855#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
856pub struct ParseError {
857    pub message: String,
858    pub line: usize,
859    pub column: usize,
860}
861
862impl std::fmt::Display for ParseError {
863    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
864        write!(
865            f,
866            "Parse error at line {}, column {}: {}",
867            self.line, self.column, self.message
868        )
869    }
870}
871
872impl std::error::Error for ParseError {}
873
874/// Collector for accumulating multiple parse errors.
875#[derive(Debug, Clone, Default)]
876pub struct ErrorCollector {
877    errors: Vec<ParseError>,
878}
879
880impl ErrorCollector {
881    pub fn new() -> Self {
882        Self { errors: vec![] }
883    }
884
885    pub fn add(&mut self, error: ParseError) {
886        self.errors.push(error);
887    }
888
889    pub fn has_errors(&self) -> bool {
890        !self.errors.is_empty()
891    }
892
893    pub fn into_errors(self) -> Vec<ParseError> {
894        self.errors
895    }
896
897    /// Combine collected parse errors into a single ParseError for compatibility.
898    ///
899    /// If no errors were collected, returns `None`. If exactly one error exists, returns that
900    /// error unchanged. If multiple errors exist, returns a `ParseError` that uses the first
901    /// error's `line` and `column` and a combined `message` that lists all errors with their
902    /// index, line, column, and original message.
903    ///
904    /// # Examples
905    ///
906    /// ```
907    /// use cellstate_pipeline::{ErrorCollector, ParseError};
908    /// let mut coll = ErrorCollector::new();
909    /// coll.add(ParseError { message: "a".into(), line: 1, column: 2 });
910    /// coll.add(ParseError { message: "b".into(), line: 3, column: 4 });
911    /// let combined = coll.into_single_error().unwrap();
912    /// assert!(combined.message.contains("a"));
913    /// assert!(combined.message.contains("b"));
914    /// assert_eq!(combined.line, 1);
915    /// assert_eq!(combined.column, 2);
916    /// ```
917    pub fn into_single_error(self) -> Option<ParseError> {
918        if self.errors.is_empty() {
919            return None;
920        }
921
922        if self.errors.len() == 1 {
923            return Some(
924                self.errors
925                    .into_iter()
926                    .next()
927                    .expect("length verified as 1"),
928            );
929        }
930
931        // Multiple errors: create combined message
932        let first = &self.errors[0];
933        let mut message = format!(
934            "{} (and {} more errors):\n",
935            first.message,
936            self.errors.len() - 1
937        );
938
939        for (i, err) in self.errors.iter().enumerate() {
940            message.push_str(&format!(
941                "  {}. Line {}, col {}: {}\n",
942                i + 1,
943                err.line,
944                err.column,
945                err.message
946            ));
947        }
948
949        Some(ParseError {
950            message,
951            line: first.line,
952            column: first.column,
953        })
954    }
955}
956
957#[cfg(test)]
958mod tests {
959    use super::*;
960
961    // ── PIIClassification ──────────────────────────────────────────────
962
963    #[test]
964    fn pii_public_no_redaction_no_encryption() {
965        assert!(!PIIClassification::Public.requires_redaction());
966        assert!(!PIIClassification::Public.requires_encryption());
967    }
968
969    #[test]
970    fn pii_internal_no_redaction_no_encryption() {
971        assert!(!PIIClassification::Internal.requires_redaction());
972        assert!(!PIIClassification::Internal.requires_encryption());
973    }
974
975    #[test]
976    fn pii_confidential_requires_redaction_not_encryption() {
977        assert!(PIIClassification::Confidential.requires_redaction());
978        assert!(!PIIClassification::Confidential.requires_encryption());
979    }
980
981    #[test]
982    fn pii_restricted_requires_redaction_not_encryption() {
983        assert!(PIIClassification::Restricted.requires_redaction());
984        assert!(!PIIClassification::Restricted.requires_encryption());
985    }
986
987    #[test]
988    fn pii_secret_requires_both() {
989        assert!(PIIClassification::Secret.requires_redaction());
990        assert!(PIIClassification::Secret.requires_encryption());
991    }
992
993    #[test]
994    fn pii_default_is_public() {
995        assert_eq!(PIIClassification::default(), PIIClassification::Public);
996    }
997
998    // ── FieldSecurity builders ─────────────────────────────────────────
999
1000    #[test]
1001    fn field_security_default_is_public() {
1002        let fs = FieldSecurity::default();
1003        assert_eq!(fs.classification, PIIClassification::Public);
1004        assert!(!fs.opaque);
1005        assert!(!fs.immutable);
1006        assert!(!fs.audited);
1007        assert!(!fs.redact_in_logs);
1008        assert!(fs.env_source.is_none());
1009    }
1010
1011    #[test]
1012    fn field_security_public_equals_default() {
1013        assert_eq!(FieldSecurity::public(), FieldSecurity::default());
1014    }
1015
1016    #[test]
1017    fn field_security_secret_has_full_protection() {
1018        let fs = FieldSecurity::secret();
1019        assert_eq!(fs.classification, PIIClassification::Secret);
1020        assert!(fs.opaque);
1021        assert!(fs.audited);
1022        assert!(fs.redact_in_logs);
1023    }
1024
1025    #[test]
1026    fn field_security_sensitive_redacts_logs() {
1027        let fs = FieldSecurity::sensitive();
1028        assert_eq!(fs.classification, PIIClassification::Confidential);
1029        assert!(fs.redact_in_logs);
1030        assert!(!fs.opaque);
1031    }
1032
1033    #[test]
1034    fn field_security_builder_chain() {
1035        let fs = FieldSecurity::public()
1036            .with_opaque()
1037            .with_immutable()
1038            .with_audited()
1039            .with_env("MY_SECRET");
1040        assert!(fs.opaque);
1041        assert!(fs.immutable);
1042        assert!(fs.audited);
1043        assert_eq!(fs.env_source, Some("MY_SECRET".to_string()));
1044    }
1045
1046    // ── FieldSecurity serde roundtrip ──────────────────────────────────
1047
1048    #[test]
1049    fn field_security_serde_roundtrip() {
1050        let fs = FieldSecurity::secret().with_env("API_KEY");
1051        let json = serde_json::to_string(&fs).unwrap();
1052        let d: FieldSecurity = serde_json::from_str(&json).unwrap();
1053        assert_eq!(d, fs);
1054    }
1055
1056    // ── ParseError ─────────────────────────────────────────────────────
1057
1058    #[test]
1059    fn parse_error_display() {
1060        let err = ParseError {
1061            message: "unexpected token".into(),
1062            line: 10,
1063            column: 5,
1064        };
1065        let s = format!("{}", err);
1066        assert!(s.contains("line 10"));
1067        assert!(s.contains("column 5"));
1068        assert!(s.contains("unexpected token"));
1069    }
1070
1071    #[test]
1072    fn parse_error_is_std_error() {
1073        let err = ParseError {
1074            message: "test".into(),
1075            line: 1,
1076            column: 1,
1077        };
1078        let _: &dyn std::error::Error = &err;
1079    }
1080
1081    // ── ErrorCollector ─────────────────────────────────────────────────
1082
1083    #[test]
1084    fn error_collector_empty() {
1085        let c = ErrorCollector::new();
1086        assert!(!c.has_errors());
1087        assert!(c.into_single_error().is_none());
1088    }
1089
1090    #[test]
1091    fn error_collector_single_error() {
1092        let mut c = ErrorCollector::new();
1093        c.add(ParseError {
1094            message: "oops".into(),
1095            line: 1,
1096            column: 2,
1097        });
1098        assert!(c.has_errors());
1099        let single = c.into_single_error().unwrap();
1100        assert_eq!(single.message, "oops");
1101        assert_eq!(single.line, 1);
1102        assert_eq!(single.column, 2);
1103    }
1104
1105    #[test]
1106    fn error_collector_multiple_errors_combined() {
1107        let mut c = ErrorCollector::new();
1108        c.add(ParseError {
1109            message: "first".into(),
1110            line: 1,
1111            column: 1,
1112        });
1113        c.add(ParseError {
1114            message: "second".into(),
1115            line: 5,
1116            column: 3,
1117        });
1118        let combined = c.into_single_error().unwrap();
1119        assert_eq!(combined.line, 1); // uses first error's position
1120        assert_eq!(combined.column, 1);
1121        assert!(combined.message.contains("first"));
1122        assert!(combined.message.contains("second"));
1123        assert!(combined.message.contains("1 more errors"));
1124    }
1125
1126    #[test]
1127    fn error_collector_into_errors() {
1128        let mut c = ErrorCollector::new();
1129        c.add(ParseError {
1130            message: "a".into(),
1131            line: 1,
1132            column: 1,
1133        });
1134        c.add(ParseError {
1135            message: "b".into(),
1136            line: 2,
1137            column: 2,
1138        });
1139        let errors = c.into_errors();
1140        assert_eq!(errors.len(), 2);
1141    }
1142
1143    #[test]
1144    fn error_collector_default_is_empty() {
1145        let c = ErrorCollector::default();
1146        assert!(!c.has_errors());
1147    }
1148
1149    // ── AgentConstraints default ───────────────────────────────────────
1150
1151    #[test]
1152    fn agent_constraints_default() {
1153        let ac = AgentConstraints::default();
1154        assert_eq!(ac.max_concurrent, 1);
1155        assert_eq!(ac.timeout_ms, 30000);
1156    }
1157
1158    // ── FreshnessDef default ───────────────────────────────────────────
1159
1160    #[test]
1161    fn freshness_def_default_is_best_effort() {
1162        let f = FreshnessDef::default();
1163        assert!(matches!(f, FreshnessDef::BestEffort { .. }));
1164    }
1165
1166    // ── Enum serde roundtrips ──────────────────────────────────────────
1167
1168    #[test]
1169    fn adapter_type_serde_roundtrip() {
1170        for variant in [
1171            AdapterType::Postgres,
1172            AdapterType::Redis,
1173            AdapterType::Memory,
1174        ] {
1175            let json = serde_json::to_string(&variant).unwrap();
1176            let d: AdapterType = serde_json::from_str(&json).unwrap();
1177            assert_eq!(d, variant);
1178        }
1179    }
1180
1181    #[test]
1182    fn memory_type_serde_roundtrip() {
1183        for variant in [
1184            MemoryType::Ephemeral,
1185            MemoryType::Working,
1186            MemoryType::Episodic,
1187            MemoryType::Semantic,
1188            MemoryType::Procedural,
1189            MemoryType::Meta,
1190        ] {
1191            let json = serde_json::to_string(&variant).unwrap();
1192            let d: MemoryType = serde_json::from_str(&json).unwrap();
1193            assert_eq!(d, variant);
1194        }
1195    }
1196
1197    #[test]
1198    fn field_type_serde_roundtrip() {
1199        for variant in [
1200            FieldType::Uuid,
1201            FieldType::Text,
1202            FieldType::Int,
1203            FieldType::Float,
1204            FieldType::Bool,
1205            FieldType::Timestamp,
1206            FieldType::Json,
1207            FieldType::Embedding(Some(1536)),
1208            FieldType::Embedding(None),
1209        ] {
1210            let json = serde_json::to_string(&variant).unwrap();
1211            let d: FieldType = serde_json::from_str(&json).unwrap();
1212            assert_eq!(d, variant);
1213        }
1214    }
1215
1216    #[test]
1217    fn retention_serde_roundtrip() {
1218        for variant in [
1219            Retention::Persistent,
1220            Retention::Session,
1221            Retention::Scope,
1222            Retention::Duration("3600s".into()),
1223            Retention::Max(100),
1224        ] {
1225            let json = serde_json::to_string(&variant).unwrap();
1226            let d: Retention = serde_json::from_str(&json).unwrap();
1227            assert_eq!(d, variant);
1228        }
1229    }
1230
1231    #[test]
1232    fn lifecycle_serde_roundtrip() {
1233        for variant in [
1234            Lifecycle::Explicit,
1235            Lifecycle::AutoClose(Trigger::ScopeClose),
1236        ] {
1237            let json = serde_json::to_string(&variant).unwrap();
1238            let d: Lifecycle = serde_json::from_str(&json).unwrap();
1239            assert_eq!(d, variant);
1240        }
1241    }
1242
1243    #[test]
1244    fn trigger_serde_roundtrip() {
1245        for variant in [
1246            Trigger::TaskStart,
1247            Trigger::TaskEnd,
1248            Trigger::ScopeClose,
1249            Trigger::TurnEnd,
1250            Trigger::Manual,
1251            Trigger::Schedule("0 * * * *".into()),
1252        ] {
1253            let json = serde_json::to_string(&variant).unwrap();
1254            let d: Trigger = serde_json::from_str(&json).unwrap();
1255            assert_eq!(d, variant);
1256        }
1257    }
1258
1259    #[test]
1260    fn index_type_serde_roundtrip() {
1261        for variant in [
1262            IndexType::Btree,
1263            IndexType::Hash,
1264            IndexType::Gin,
1265            IndexType::Hnsw,
1266            IndexType::Ivfflat,
1267        ] {
1268            let json = serde_json::to_string(&variant).unwrap();
1269            let d: IndexType = serde_json::from_str(&json).unwrap();
1270            assert_eq!(d, variant);
1271        }
1272    }
1273
1274    #[test]
1275    fn action_serde_roundtrip() {
1276        let actions = vec![
1277            Action::Summarize("scope".into()),
1278            Action::Checkpoint("scope".into()),
1279            Action::ExtractArtifacts("scope".into()),
1280            Action::Notify("admin".into()),
1281            Action::Inject {
1282                target: "memory".into(),
1283                mode: InjectionMode::Full,
1284            },
1285        ];
1286        for action in actions {
1287            let json = serde_json::to_string(&action).unwrap();
1288            let d: Action = serde_json::from_str(&json).unwrap();
1289            assert_eq!(d, action);
1290        }
1291    }
1292
1293    #[test]
1294    fn injection_mode_serde_roundtrip() {
1295        for variant in [
1296            InjectionMode::Full,
1297            InjectionMode::Summary,
1298            InjectionMode::TopK(5),
1299            InjectionMode::Relevant(0.8),
1300        ] {
1301            let json = serde_json::to_string(&variant).unwrap();
1302            let d: InjectionMode = serde_json::from_str(&json).unwrap();
1303            assert_eq!(d, variant);
1304        }
1305    }
1306
1307    #[test]
1308    fn lock_mode_serde_roundtrip() {
1309        for variant in [LockMode::Exclusive, LockMode::Shared] {
1310            let json = serde_json::to_string(&variant).unwrap();
1311            let d: LockMode = serde_json::from_str(&json).unwrap();
1312            assert_eq!(d, variant);
1313        }
1314    }
1315
1316    #[test]
1317    fn summary_style_serde_roundtrip() {
1318        for variant in [SummaryStyle::Brief, SummaryStyle::Detailed] {
1319            let json = serde_json::to_string(&variant).unwrap();
1320            let d: SummaryStyle = serde_json::from_str(&json).unwrap();
1321            assert_eq!(d, variant);
1322        }
1323    }
1324
1325    #[test]
1326    fn compare_op_serde_roundtrip() {
1327        for variant in [
1328            CompareOp::Eq,
1329            CompareOp::Ne,
1330            CompareOp::Gt,
1331            CompareOp::Lt,
1332            CompareOp::Ge,
1333            CompareOp::Le,
1334            CompareOp::Contains,
1335            CompareOp::In,
1336            CompareOp::Regex,
1337        ] {
1338            let json = serde_json::to_string(&variant).unwrap();
1339            let d: CompareOp = serde_json::from_str(&json).unwrap();
1340            assert_eq!(d, variant);
1341        }
1342    }
1343}