Skip to main content

arete_interpreter/
ast.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use arete_idl::snapshot::*;
7
8/// Current AST version for SerializableStreamSpec and SerializableStackSpec
9///
10/// ⚠️ IMPORTANT: This constant is duplicated in arete-macros/src/ast/types.rs due to
11/// circular dependency between proc-macro crates and their output crates.
12/// When bumping this version, you MUST also update the constant in the
13/// arete-macros crate. A test in versioned.rs verifies they stay in sync.
14pub const CURRENT_AST_VERSION: &str = "0.0.1";
15
16fn default_ast_version() -> String {
17    CURRENT_AST_VERSION.to_string()
18}
19
20pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
21    match ty {
22        IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
23        IdlTypeSnapshot::Array(arr) => {
24            if arr.array.len() == 2 {
25                match (&arr.array[0], &arr.array[1]) {
26                    (IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
27                        format!("[{}; {}]", map_simple_idl_type(t), size)
28                    }
29                    (
30                        IdlArrayElementSnapshot::Type(nested),
31                        IdlArrayElementSnapshot::Size(size),
32                    ) => {
33                        format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
34                    }
35                    _ => "Vec<u8>".to_string(),
36                }
37            } else {
38                "Vec<u8>".to_string()
39            }
40        }
41        IdlTypeSnapshot::Option(opt) => {
42            format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
43        }
44        IdlTypeSnapshot::Vec(vec) => {
45            format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
46        }
47        IdlTypeSnapshot::HashMap(map) => {
48            let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
49            let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
50            format!("std::collections::HashMap<{}, {}>", key_type, val_type)
51        }
52        IdlTypeSnapshot::Defined(def) => match &def.defined {
53            IdlDefinedInnerSnapshot::Named { name } => name.clone(),
54            IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
55        },
56    }
57}
58
59fn map_simple_idl_type(idl_type: &str) -> String {
60    match idl_type {
61        "u8" => "u8".to_string(),
62        "u16" => "u16".to_string(),
63        "u32" => "u32".to_string(),
64        "u64" => "u64".to_string(),
65        "u128" => "u128".to_string(),
66        "i8" => "i8".to_string(),
67        "i16" => "i16".to_string(),
68        "i32" => "i32".to_string(),
69        "i64" => "i64".to_string(),
70        "i128" => "i128".to_string(),
71        "bool" => "bool".to_string(),
72        "string" => "String".to_string(),
73        "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
74        "bytes" => "Vec<u8>".to_string(),
75        _ => idl_type.to_string(),
76    }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
80pub struct FieldPath {
81    pub segments: Vec<String>,
82    pub offsets: Option<Vec<usize>>,
83}
84
85impl FieldPath {
86    pub fn new(segments: &[&str]) -> Self {
87        FieldPath {
88            segments: segments.iter().map(|s| s.to_string()).collect(),
89            offsets: None,
90        }
91    }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95pub enum Transformation {
96    HexEncode,
97    HexDecode,
98    Base58Encode,
99    Base58Decode,
100    ToString,
101    ToNumber,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum PopulationStrategy {
106    SetOnce,
107    LastWrite,
108    Append,
109    Merge,
110    Max,
111    /// Sum numeric values (accumulator pattern for aggregations)
112    Sum,
113    /// Count occurrences (increments by 1 for each update)
114    Count,
115    /// Track minimum value
116    Min,
117    /// Track unique values and store the count
118    /// Internally maintains a HashSet, exposes only the count
119    UniqueCount,
120}
121
122// ============================================================================
123// Computed Field Expression AST
124// ============================================================================
125
126/// Specification for a computed/derived field
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ComputedFieldSpec {
129    /// Target field path (e.g., "trading.total_volume")
130    pub target_path: String,
131    /// Expression AST
132    pub expression: ComputedExpr,
133    /// Result type (e.g., "Option<u64>", "Option<f64>")
134    pub result_type: String,
135}
136
137// ============================================================================
138// Resolver Specifications
139// ============================================================================
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
142#[serde(rename_all = "lowercase")]
143pub enum ResolverType {
144    Token,
145    Url(UrlResolverConfig),
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
149#[serde(rename_all = "lowercase")]
150pub enum HttpMethod {
151    #[default]
152    Get,
153    Post,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
157pub enum UrlTemplatePart {
158    Literal(String),
159    FieldRef(String),
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
163pub enum UrlSource {
164    FieldPath(String),
165    Template(Vec<UrlTemplatePart>),
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
169pub struct UrlResolverConfig {
170    pub url_source: UrlSource,
171    #[serde(default)]
172    pub method: HttpMethod,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub extract_path: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178pub struct ResolverExtractSpec {
179    pub target_path: String,
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub source_path: Option<String>,
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub transform: Option<Transformation>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
187pub enum ResolveStrategy {
188    #[default]
189    SetOnce,
190    LastWrite,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
194pub struct ResolverCondition {
195    pub field_path: String,
196    pub op: ComparisonOp,
197    pub value: Value,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ResolverSpec {
202    pub resolver: ResolverType,
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub input_path: Option<String>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub input_value: Option<Value>,
207    #[serde(default)]
208    pub strategy: ResolveStrategy,
209    pub extracts: Vec<ResolverExtractSpec>,
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub condition: Option<ResolverCondition>,
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub schedule_at: Option<String>,
214}
215
216/// AST for computed field expressions
217/// Supports a subset of Rust expressions needed for computed fields:
218/// - Field references (possibly from other sections)
219/// - Unwrap with defaults
220/// - Basic arithmetic and comparisons
221/// - Type casts
222/// - Method calls
223/// - Let bindings and conditionals
224/// - Byte array operations
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub enum ComputedExpr {
227    // Existing variants
228    /// Reference to a field: "field_name" or "section.field_name"
229    FieldRef {
230        path: String,
231    },
232
233    /// Unwrap with default: expr.unwrap_or(default)
234    UnwrapOr {
235        expr: Box<ComputedExpr>,
236        default: serde_json::Value,
237    },
238
239    /// Binary operation: left op right
240    Binary {
241        op: BinaryOp,
242        left: Box<ComputedExpr>,
243        right: Box<ComputedExpr>,
244    },
245
246    /// Type cast: expr as type
247    Cast {
248        expr: Box<ComputedExpr>,
249        to_type: String,
250    },
251
252    /// Method call: expr.method(args)
253    MethodCall {
254        expr: Box<ComputedExpr>,
255        method: String,
256        args: Vec<ComputedExpr>,
257    },
258
259    /// Computation provided by a resolver
260    ResolverComputed {
261        resolver: String,
262        method: String,
263        args: Vec<ComputedExpr>,
264    },
265
266    /// Literal value: numbers, booleans, strings
267    Literal {
268        value: serde_json::Value,
269    },
270
271    /// Parenthesized expression for grouping
272    Paren {
273        expr: Box<ComputedExpr>,
274    },
275
276    // Variable reference (for let bindings)
277    Var {
278        name: String,
279    },
280
281    // Let binding: let name = value; body
282    Let {
283        name: String,
284        value: Box<ComputedExpr>,
285        body: Box<ComputedExpr>,
286    },
287
288    // Conditional: if condition { then_branch } else { else_branch }
289    If {
290        condition: Box<ComputedExpr>,
291        then_branch: Box<ComputedExpr>,
292        else_branch: Box<ComputedExpr>,
293    },
294
295    // Option constructors
296    None,
297    Some {
298        value: Box<ComputedExpr>,
299    },
300
301    // Byte/array operations
302    Slice {
303        expr: Box<ComputedExpr>,
304        start: usize,
305        end: usize,
306    },
307    Index {
308        expr: Box<ComputedExpr>,
309        index: usize,
310    },
311
312    // Byte conversion functions
313    U64FromLeBytes {
314        bytes: Box<ComputedExpr>,
315    },
316    U64FromBeBytes {
317        bytes: Box<ComputedExpr>,
318    },
319
320    // Byte array literals: [0u8; 32] or [1, 2, 3]
321    ByteArray {
322        bytes: Vec<u8>,
323    },
324
325    // Closure for map operations: |x| body
326    Closure {
327        param: String,
328        body: Box<ComputedExpr>,
329    },
330
331    // Unary operations
332    Unary {
333        op: UnaryOp,
334        expr: Box<ComputedExpr>,
335    },
336
337    // JSON array to bytes conversion (for working with captured byte arrays)
338    JsonToBytes {
339        expr: Box<ComputedExpr>,
340    },
341
342    // Context access - slot and timestamp from the update that triggered evaluation
343    /// Access the slot number from the current update context
344    ContextSlot,
345    /// Access the unix timestamp from the current update context
346    ContextTimestamp,
347
348    /// Keccak256 hash function for computing Ethereum-compatible hashes
349    /// Takes a byte array expression and returns the 32-byte hash as a Vec<u8>
350    Keccak256 {
351        expr: Box<ComputedExpr>,
352    },
353}
354
355/// Binary operators for computed expressions
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub enum BinaryOp {
358    // Arithmetic
359    Add,
360    Sub,
361    Mul,
362    Div,
363    Mod,
364    // Comparison
365    Gt,
366    Lt,
367    Gte,
368    Lte,
369    Eq,
370    Ne,
371    // Logical
372    And,
373    Or,
374    // Bitwise
375    Xor,
376    BitAnd,
377    BitOr,
378    Shl,
379    Shr,
380}
381
382/// Unary operators for computed expressions
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub enum UnaryOp {
385    Not,
386    ReverseBits,
387}
388
389/// Serializable version of StreamSpec without phantom types
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct SerializableStreamSpec {
392    /// AST schema version for backward compatibility
393    /// Uses semver format (e.g., "0.0.1")
394    #[serde(default = "default_ast_version")]
395    pub ast_version: String,
396    pub state_name: String,
397    /// Program ID (Solana address) - extracted from IDL
398    #[serde(default)]
399    pub program_id: Option<String>,
400    /// Embedded IDL for AST-only compilation
401    #[serde(default)]
402    pub idl: Option<IdlSnapshot>,
403    pub identity: IdentitySpec,
404    pub handlers: Vec<SerializableHandlerSpec>,
405    pub sections: Vec<EntitySection>,
406    pub field_mappings: BTreeMap<String, FieldTypeInfo>,
407    pub resolver_hooks: Vec<ResolverHook>,
408    pub instruction_hooks: Vec<InstructionHook>,
409    #[serde(default)]
410    pub resolver_specs: Vec<ResolverSpec>,
411    /// Computed field paths (legacy, for backward compatibility)
412    #[serde(default)]
413    pub computed_fields: Vec<String>,
414    /// Computed field specifications with full expression AST
415    #[serde(default)]
416    pub computed_field_specs: Vec<ComputedFieldSpec>,
417    /// Deterministic content hash (SHA256 of canonical JSON, excluding this field)
418    /// Used for deduplication and version tracking
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub content_hash: Option<String>,
421    /// View definitions for derived/projected views
422    #[serde(default)]
423    pub views: Vec<ViewDef>,
424}
425
426impl SerializableStreamSpec {
427    /// Normalize legacy event names emitted by older AST generators.
428    pub fn normalize_event_names(&mut self) {
429        for handler in &mut self.handlers {
430            handler.normalize_event_names();
431        }
432
433        for hook in &mut self.instruction_hooks {
434            hook.instruction_type =
435                crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
436        }
437    }
438}
439
440#[derive(Debug, Clone)]
441pub struct TypedStreamSpec<S> {
442    pub state_name: String,
443    pub identity: IdentitySpec,
444    pub handlers: Vec<TypedHandlerSpec<S>>,
445    pub sections: Vec<EntitySection>, // NEW: Complete structural information
446    pub field_mappings: BTreeMap<String, FieldTypeInfo>, // NEW: All field type info by target path
447    pub resolver_hooks: Vec<ResolverHook>, // NEW: Resolver hooks for PDA key resolution
448    pub instruction_hooks: Vec<InstructionHook>, // NEW: Instruction hooks for PDA registration
449    pub resolver_specs: Vec<ResolverSpec>,
450    pub computed_fields: Vec<String>, // List of computed field paths
451    _phantom: PhantomData<S>,
452}
453
454impl<S> TypedStreamSpec<S> {
455    pub fn new(
456        state_name: String,
457        identity: IdentitySpec,
458        handlers: Vec<TypedHandlerSpec<S>>,
459    ) -> Self {
460        TypedStreamSpec {
461            state_name,
462            identity,
463            handlers,
464            sections: Vec::new(),
465            field_mappings: BTreeMap::new(),
466            resolver_hooks: Vec::new(),
467            instruction_hooks: Vec::new(),
468            resolver_specs: Vec::new(),
469            computed_fields: Vec::new(),
470            _phantom: PhantomData,
471        }
472    }
473
474    /// Enhanced constructor with type information
475    pub fn with_type_info(
476        state_name: String,
477        identity: IdentitySpec,
478        handlers: Vec<TypedHandlerSpec<S>>,
479        sections: Vec<EntitySection>,
480        field_mappings: BTreeMap<String, FieldTypeInfo>,
481    ) -> Self {
482        TypedStreamSpec {
483            state_name,
484            identity,
485            handlers,
486            sections,
487            field_mappings,
488            resolver_hooks: Vec::new(),
489            instruction_hooks: Vec::new(),
490            resolver_specs: Vec::new(),
491            computed_fields: Vec::new(),
492            _phantom: PhantomData,
493        }
494    }
495
496    pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
497        self.resolver_specs = resolver_specs;
498        self
499    }
500
501    /// Get type information for a specific field path
502    pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
503        self.field_mappings.get(path)
504    }
505
506    /// Get all fields for a specific section
507    pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
508        self.sections
509            .iter()
510            .find(|s| s.name == section_name)
511            .map(|s| &s.fields)
512    }
513
514    /// Get all section names
515    pub fn get_section_names(&self) -> Vec<&String> {
516        self.sections.iter().map(|s| &s.name).collect()
517    }
518
519    /// Convert to serializable format
520    pub fn to_serializable(&self) -> SerializableStreamSpec {
521        let mut spec = SerializableStreamSpec {
522            ast_version: CURRENT_AST_VERSION.to_string(),
523            state_name: self.state_name.clone(),
524            program_id: None,
525            idl: None,
526            identity: self.identity.clone(),
527            handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
528            sections: self.sections.clone(),
529            field_mappings: self.field_mappings.clone(),
530            resolver_hooks: self.resolver_hooks.clone(),
531            instruction_hooks: self.instruction_hooks.clone(),
532            resolver_specs: self.resolver_specs.clone(),
533            computed_fields: self.computed_fields.clone(),
534            computed_field_specs: Vec::new(),
535            content_hash: None,
536            views: Vec::new(),
537        };
538        spec.content_hash = Some(spec.compute_content_hash());
539        spec
540    }
541
542    /// Create from serializable format
543    pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
544        spec.normalize_event_names();
545        TypedStreamSpec {
546            state_name: spec.state_name,
547            identity: spec.identity,
548            handlers: spec
549                .handlers
550                .into_iter()
551                .map(|h| TypedHandlerSpec::from_serializable(h))
552                .collect(),
553            sections: spec.sections,
554            field_mappings: spec.field_mappings,
555            resolver_hooks: spec.resolver_hooks,
556            instruction_hooks: spec.instruction_hooks,
557            resolver_specs: spec.resolver_specs,
558            computed_fields: spec.computed_fields,
559            _phantom: PhantomData,
560        }
561    }
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct IdentitySpec {
566    pub primary_keys: Vec<String>,
567    pub lookup_indexes: Vec<LookupIndexSpec>,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct LookupIndexSpec {
572    pub field_name: String,
573    pub temporal_field: Option<String>,
574}
575
576// ============================================================================
577// Level 1: Declarative Hook Extensions
578// ============================================================================
579
580/// Declarative resolver hook specification
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct ResolverHook {
583    /// Account type this resolver applies to (e.g., "BondingCurveState")
584    pub account_type: String,
585
586    /// Resolution strategy
587    pub strategy: ResolverStrategy,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub enum ResolverStrategy {
592    /// Look up PDA in reverse lookup table, queue if not found
593    PdaReverseLookup {
594        lookup_name: String,
595        /// Instruction discriminators to queue until (8 bytes each)
596        queue_discriminators: Vec<Vec<u8>>,
597    },
598
599    /// Extract primary key directly from account data (future)
600    DirectField { field_path: FieldPath },
601}
602
603/// Declarative instruction hook specification
604#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct InstructionHook {
606    /// Instruction type this hook applies to (e.g., "CreateIxState")
607    pub instruction_type: String,
608
609    /// Actions to perform when this instruction is processed
610    pub actions: Vec<HookAction>,
611
612    /// Lookup strategy for finding the entity
613    pub lookup_by: Option<FieldPath>,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
617pub enum HookAction {
618    /// Register a PDA mapping for reverse lookup
619    RegisterPdaMapping {
620        pda_field: FieldPath,
621        seed_field: FieldPath,
622        lookup_name: String,
623    },
624
625    /// Set a field value (for #[track_from])
626    SetField {
627        target_field: String,
628        source: MappingSource,
629        condition: Option<ConditionExpr>,
630    },
631
632    /// Increment a field value (for conditional aggregations)
633    IncrementField {
634        target_field: String,
635        increment_by: i64,
636        condition: Option<ConditionExpr>,
637    },
638}
639
640/// Simple condition expression (Level 1 - basic comparisons only)
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct ConditionExpr {
643    /// Expression as string (will be parsed and validated)
644    pub expression: String,
645
646    /// Parsed representation (for validation and execution)
647    pub parsed: Option<ParsedCondition>,
648}
649
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub enum ParsedCondition {
652    /// Binary comparison: field op value
653    Comparison {
654        field: FieldPath,
655        op: ComparisonOp,
656        value: serde_json::Value,
657    },
658
659    /// Logical AND/OR
660    Logical {
661        op: LogicalOp,
662        conditions: Vec<ParsedCondition>,
663    },
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
667pub enum ComparisonOp {
668    Equal,
669    NotEqual,
670    GreaterThan,
671    GreaterThanOrEqual,
672    LessThan,
673    LessThanOrEqual,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
677pub enum LogicalOp {
678    And,
679    Or,
680}
681
682/// Serializable version of HandlerSpec without phantom types
683#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct SerializableHandlerSpec {
685    pub source: SourceSpec,
686    pub key_resolution: KeyResolutionStrategy,
687    pub mappings: Vec<SerializableFieldMapping>,
688    pub conditions: Vec<Condition>,
689    pub emit: bool,
690}
691
692impl SerializableHandlerSpec {
693    pub fn normalize_event_names(&mut self) {
694        let SourceSpec::Source { type_name, .. } = &mut self.source;
695        *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
696
697        for mapping in &mut self.mappings {
698            if let Some(when) = &mut mapping.when {
699                *when = crate::event_type_helpers::canonicalize_event_type_name(when);
700            }
701            if let Some(stop) = &mut mapping.stop {
702                *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
703            }
704        }
705    }
706}
707
708#[derive(Debug, Clone)]
709pub struct TypedHandlerSpec<S> {
710    pub source: SourceSpec,
711    pub key_resolution: KeyResolutionStrategy,
712    pub mappings: Vec<TypedFieldMapping<S>>,
713    pub conditions: Vec<Condition>,
714    pub emit: bool,
715    _phantom: PhantomData<S>,
716}
717
718impl<S> TypedHandlerSpec<S> {
719    pub fn new(
720        source: SourceSpec,
721        key_resolution: KeyResolutionStrategy,
722        mappings: Vec<TypedFieldMapping<S>>,
723        emit: bool,
724    ) -> Self {
725        TypedHandlerSpec {
726            source,
727            key_resolution,
728            mappings,
729            conditions: vec![],
730            emit,
731            _phantom: PhantomData,
732        }
733    }
734
735    /// Convert to serializable format
736    pub fn to_serializable(&self) -> SerializableHandlerSpec {
737        SerializableHandlerSpec {
738            source: self.source.clone(),
739            key_resolution: self.key_resolution.clone(),
740            mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
741            conditions: self.conditions.clone(),
742            emit: self.emit,
743        }
744    }
745
746    /// Create from serializable format
747    pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
748        TypedHandlerSpec {
749            source: spec.source,
750            key_resolution: spec.key_resolution,
751            mappings: spec
752                .mappings
753                .into_iter()
754                .map(|m| TypedFieldMapping::from_serializable(m))
755                .collect(),
756            conditions: spec.conditions,
757            emit: spec.emit,
758            _phantom: PhantomData,
759        }
760    }
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize)]
764pub enum KeyResolutionStrategy {
765    Embedded {
766        primary_field: FieldPath,
767    },
768    Lookup {
769        primary_field: FieldPath,
770    },
771    Computed {
772        primary_field: FieldPath,
773        compute_partition: ComputeFunction,
774    },
775    TemporalLookup {
776        lookup_field: FieldPath,
777        timestamp_field: FieldPath,
778        index_name: String,
779    },
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize)]
783pub enum SourceSpec {
784    Source {
785        program_id: Option<String>,
786        discriminator: Option<Vec<u8>>,
787        type_name: String,
788        #[serde(default, skip_serializing_if = "Option::is_none")]
789        serialization: Option<IdlSerializationSnapshot>,
790        /// True when this handler listens to an account-state event (not an
791        /// instruction or custom event).  Set at code-generation time from
792        /// the structural source kind so the compiler does not need to rely
793        /// on naming-convention heuristics.
794        #[serde(default)]
795        is_account: bool,
796    },
797}
798
799/// Serializable version of FieldMapping without phantom types
800#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct SerializableFieldMapping {
802    pub target_path: String,
803    pub source: MappingSource,
804    pub transform: Option<Transformation>,
805    pub population: PopulationStrategy,
806    #[serde(default, skip_serializing_if = "Option::is_none")]
807    pub condition: Option<ConditionExpr>,
808    #[serde(default, skip_serializing_if = "Option::is_none")]
809    pub when: Option<String>,
810    #[serde(default, skip_serializing_if = "Option::is_none")]
811    pub stop: Option<String>,
812    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
813    pub emit: bool,
814}
815
816fn default_emit() -> bool {
817    true
818}
819
820fn default_instruction_discriminant_size() -> usize {
821    8
822}
823
824fn is_true(value: &bool) -> bool {
825    *value
826}
827
828#[derive(Debug, Clone)]
829pub struct TypedFieldMapping<S> {
830    pub target_path: String,
831    pub source: MappingSource,
832    pub transform: Option<Transformation>,
833    pub population: PopulationStrategy,
834    pub condition: Option<ConditionExpr>,
835    pub when: Option<String>,
836    pub stop: Option<String>,
837    pub emit: bool,
838    _phantom: PhantomData<S>,
839}
840
841impl<S> TypedFieldMapping<S> {
842    pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
843        TypedFieldMapping {
844            target_path,
845            source,
846            transform: None,
847            population,
848            condition: None,
849            when: None,
850            stop: None,
851            emit: true,
852            _phantom: PhantomData,
853        }
854    }
855
856    pub fn with_transform(mut self, transform: Transformation) -> Self {
857        self.transform = Some(transform);
858        self
859    }
860
861    pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
862        self.condition = Some(condition);
863        self
864    }
865
866    pub fn with_when(mut self, when: String) -> Self {
867        self.when = Some(when);
868        self
869    }
870
871    pub fn with_stop(mut self, stop: String) -> Self {
872        self.stop = Some(stop);
873        self
874    }
875
876    pub fn with_emit(mut self, emit: bool) -> Self {
877        self.emit = emit;
878        self
879    }
880
881    /// Convert to serializable format
882    pub fn to_serializable(&self) -> SerializableFieldMapping {
883        SerializableFieldMapping {
884            target_path: self.target_path.clone(),
885            source: self.source.clone(),
886            transform: self.transform.clone(),
887            population: self.population.clone(),
888            condition: self.condition.clone(),
889            when: self.when.clone(),
890            stop: self.stop.clone(),
891            emit: self.emit,
892        }
893    }
894
895    /// Create from serializable format
896    pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
897        TypedFieldMapping {
898            target_path: mapping.target_path,
899            source: mapping.source,
900            transform: mapping.transform,
901            population: mapping.population,
902            condition: mapping.condition,
903            when: mapping.when,
904            stop: mapping.stop,
905            emit: mapping.emit,
906            _phantom: PhantomData,
907        }
908    }
909}
910
911#[derive(Debug, Clone, Serialize, Deserialize)]
912pub enum MappingSource {
913    FromSource {
914        path: FieldPath,
915        default: Option<Value>,
916        transform: Option<Transformation>,
917    },
918    Constant(Value),
919    Computed {
920        inputs: Vec<FieldPath>,
921        function: ComputeFunction,
922    },
923    FromState {
924        path: String,
925    },
926    AsEvent {
927        fields: Vec<Box<MappingSource>>,
928    },
929    WholeSource,
930    /// Similar to WholeSource but with field-level transformations
931    /// Used by #[capture] macro to apply transforms to specific fields in an account
932    AsCapture {
933        field_transforms: BTreeMap<String, Transformation>,
934    },
935    /// From instruction context (timestamp, slot, signature)
936    /// Used by #[track_from] with special fields like __timestamp
937    FromContext {
938        field: String,
939    },
940}
941
942impl MappingSource {
943    pub fn with_transform(self, transform: Transformation) -> Self {
944        match self {
945            MappingSource::FromSource {
946                path,
947                default,
948                transform: _,
949            } => MappingSource::FromSource {
950                path,
951                default,
952                transform: Some(transform),
953            },
954            other => other,
955        }
956    }
957}
958
959#[derive(Debug, Clone, Serialize, Deserialize)]
960pub enum ComputeFunction {
961    Sum,
962    Concat,
963    Format(String),
964    Custom(String),
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize)]
968pub struct Condition {
969    pub field: FieldPath,
970    pub operator: ConditionOp,
971    pub value: Value,
972}
973
974#[derive(Debug, Clone, Serialize, Deserialize)]
975pub enum ConditionOp {
976    Equals,
977    NotEquals,
978    GreaterThan,
979    LessThan,
980    Contains,
981    Exists,
982}
983
984/// Language-agnostic type information for fields
985#[derive(Debug, Clone, Serialize, Deserialize)]
986pub struct FieldTypeInfo {
987    pub field_name: String,
988    pub rust_type_name: String, // Full Rust type: "Option<i64>", "Vec<Value>", etc.
989    pub base_type: BaseType,    // Fundamental type classification
990    pub is_optional: bool,      // true for Option<T>
991    pub is_array: bool,         // true for Vec<T>
992    pub inner_type: Option<String>, // For Option<T> or Vec<T>, store the inner type
993    pub source_path: Option<String>, // Path to source field if this is mapped
994    /// Resolved type information for complex types (instructions, accounts, custom types)
995    #[serde(default)]
996    pub resolved_type: Option<ResolvedStructType>,
997    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
998    pub emit: bool,
999}
1000
1001/// Resolved structure type with field information from IDL
1002#[derive(Debug, Clone, Serialize, Deserialize)]
1003pub struct ResolvedStructType {
1004    pub type_name: String,
1005    pub fields: Vec<ResolvedField>,
1006    pub is_instruction: bool,
1007    pub is_account: bool,
1008    pub is_event: bool,
1009    /// If true, this is an enum type and enum_variants should be used instead of fields
1010    #[serde(default)]
1011    pub is_enum: bool,
1012    /// For enum types, list of variant names
1013    #[serde(default)]
1014    pub enum_variants: Vec<String>,
1015}
1016
1017/// A resolved field within a complex type
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1019pub struct ResolvedField {
1020    pub field_name: String,
1021    pub field_type: String,
1022    pub base_type: BaseType,
1023    pub is_optional: bool,
1024    pub is_array: bool,
1025}
1026
1027/// Language-agnostic base type classification
1028#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1029pub enum BaseType {
1030    // Numeric types
1031    Integer, // i8, i16, i32, i64, u8, u16, u32, u64, usize, isize
1032    Float,   // f32, f64
1033    // Text types
1034    String, // String, &str
1035    // Boolean
1036    Boolean, // bool
1037    // Complex types
1038    Object, // Custom structs, HashMap, etc.
1039    Array,  // Vec<T>, arrays
1040    Binary, // Bytes, binary data
1041    // Special types
1042    Timestamp, // Detected from field names ending in _at, _time, etc.
1043    Pubkey,    // Solana public key (Base58 encoded)
1044    Any,       // serde_json::Value, unknown types
1045}
1046
1047/// Represents a logical section/group of fields in the entity
1048#[derive(Debug, Clone, Serialize, Deserialize)]
1049pub struct EntitySection {
1050    pub name: String,
1051    pub fields: Vec<FieldTypeInfo>,
1052    pub is_nested_struct: bool,
1053    pub parent_field: Option<String>, // If this section comes from a nested struct field
1054}
1055
1056impl FieldTypeInfo {
1057    pub fn new(field_name: String, rust_type_name: String) -> Self {
1058        let (base_type, is_optional, is_array, inner_type) =
1059            Self::analyze_rust_type(&rust_type_name);
1060
1061        FieldTypeInfo {
1062            field_name: field_name.clone(),
1063            rust_type_name,
1064            base_type: Self::infer_semantic_type(&field_name, base_type),
1065            is_optional,
1066            is_array,
1067            inner_type,
1068            source_path: None,
1069            resolved_type: None,
1070            emit: true,
1071        }
1072    }
1073
1074    pub fn with_source_path(mut self, source_path: String) -> Self {
1075        self.source_path = Some(source_path);
1076        self
1077    }
1078
1079    /// Analyze a Rust type string and extract structural information
1080    fn analyze_rust_type(rust_type: &str) -> (BaseType, bool, bool, Option<String>) {
1081        let type_str = rust_type.trim();
1082
1083        // Handle Option<T>
1084        if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1085            let (inner_base_type, _, inner_is_array, inner_inner_type) =
1086                Self::analyze_rust_type(&inner);
1087            return (
1088                inner_base_type,
1089                true,
1090                inner_is_array,
1091                inner_inner_type.or(Some(inner)),
1092            );
1093        }
1094
1095        // Handle Vec<T>
1096        if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1097            let (_inner_base_type, inner_is_optional, _, inner_inner_type) =
1098                Self::analyze_rust_type(&inner);
1099            return (
1100                BaseType::Array,
1101                inner_is_optional,
1102                true,
1103                inner_inner_type.or(Some(inner)),
1104            );
1105        }
1106
1107        // Handle primitive types
1108        let base_type = match type_str {
1109            "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
1110                BaseType::Integer
1111            }
1112            "f32" | "f64" => BaseType::Float,
1113            "bool" => BaseType::Boolean,
1114            "String" | "&str" | "str" => BaseType::String,
1115            "Value" | "serde_json::Value" => BaseType::Any,
1116            "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1117            _ => {
1118                // Check for binary types
1119                if type_str.contains("Bytes") || type_str.contains("bytes") {
1120                    BaseType::Binary
1121                } else if type_str.contains("Pubkey") {
1122                    BaseType::Pubkey
1123                } else {
1124                    BaseType::Object
1125                }
1126            }
1127        };
1128
1129        (base_type, false, false, None)
1130    }
1131
1132    /// Extract inner type from generic like "Option<T>" -> "T"
1133    fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1134        let pattern = format!("{}<", generic_name);
1135        if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1136            let start = pattern.len();
1137            let end = type_str.len() - 1;
1138            if end > start {
1139                return Some(type_str[start..end].trim().to_string());
1140            }
1141        }
1142        None
1143    }
1144
1145    /// Infer semantic type based on field name patterns
1146    fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1147        let lower_name = field_name.to_lowercase();
1148
1149        // If already classified as integer, check if it should be timestamp
1150        if base_type == BaseType::Integer
1151            && (lower_name.ends_with("_at")
1152                || lower_name.ends_with("_time")
1153                || lower_name.contains("timestamp")
1154                || lower_name.contains("created")
1155                || lower_name.contains("settled")
1156                || lower_name.contains("activated"))
1157        {
1158            return BaseType::Timestamp;
1159        }
1160
1161        base_type
1162    }
1163}
1164
1165pub trait FieldAccessor<S> {
1166    fn path(&self) -> String;
1167}
1168
1169// ============================================================================
1170// SerializableStreamSpec Implementation
1171// ============================================================================
1172
1173impl SerializableStreamSpec {
1174    /// Compute deterministic content hash (SHA256 of canonical JSON).
1175    ///
1176    /// The hash is computed over the entire spec except the content_hash field itself,
1177    /// ensuring the same AST always produces the same hash regardless of when it was
1178    /// generated or by whom.
1179    pub fn compute_content_hash(&self) -> String {
1180        use sha2::{Digest, Sha256};
1181
1182        // Clone and clear the hash field for computation
1183        let mut spec_for_hash = self.clone();
1184        spec_for_hash.content_hash = None;
1185
1186        // Serialize to JSON (serde_json produces consistent output for the same struct)
1187        let json =
1188            serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1189
1190        // Compute SHA256 hash
1191        let mut hasher = Sha256::new();
1192        hasher.update(json.as_bytes());
1193        let result = hasher.finalize();
1194
1195        // Return hex-encoded hash
1196        hex::encode(result)
1197    }
1198
1199    /// Verify that the content_hash matches the computed hash.
1200    /// Returns true if hash is valid or not set.
1201    pub fn verify_content_hash(&self) -> bool {
1202        match &self.content_hash {
1203            Some(hash) => {
1204                let computed = self.compute_content_hash();
1205                hash == &computed
1206            }
1207            None => true, // No hash to verify
1208        }
1209    }
1210
1211    /// Set the content_hash field to the computed hash.
1212    pub fn with_content_hash(mut self) -> Self {
1213        self.content_hash = Some(self.compute_content_hash());
1214        self
1215    }
1216}
1217
1218// ============================================================================
1219// PDA and Instruction Types — For SDK code generation
1220// ============================================================================
1221
1222/// PDA (Program-Derived Address) definition for the stack-level registry.
1223/// PDAs defined here can be referenced by instructions via `pdaRef`.
1224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1225pub struct PdaDefinition {
1226    /// Human-readable name (e.g., "miner", "bondingCurve")
1227    pub name: String,
1228
1229    /// Seeds for PDA derivation, in order
1230    pub seeds: Vec<PdaSeedDef>,
1231
1232    /// Program ID that owns this PDA.
1233    /// If None, uses the stack's primary programId.
1234    #[serde(default, skip_serializing_if = "Option::is_none")]
1235    pub program_id: Option<String>,
1236}
1237
1238/// Single seed in a PDA derivation.
1239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1240#[serde(tag = "type", rename_all = "camelCase")]
1241pub enum PdaSeedDef {
1242    /// Static string seed: "miner" → "miner".as_bytes()
1243    Literal { value: String },
1244
1245    /// Static byte array (for non-UTF8 seeds)
1246    Bytes { value: Vec<u8> },
1247
1248    /// Reference to an instruction argument: arg("roundId") → args.roundId as bytes
1249    ArgRef {
1250        arg_name: String,
1251        /// Optional type hint for serialization (e.g., "u64", "pubkey")
1252        #[serde(default, skip_serializing_if = "Option::is_none")]
1253        arg_type: Option<String>,
1254    },
1255
1256    /// Reference to another account in the instruction: account("mint") → accounts.mint pubkey
1257    AccountRef { account_name: String },
1258}
1259
1260/// How an instruction account's address is determined.
1261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1262#[serde(tag = "category", rename_all = "camelCase")]
1263pub enum AccountResolution {
1264    /// Must sign the transaction (uses wallet.publicKey)
1265    Signer,
1266
1267    /// Fixed known address (e.g., System Program, Token Program)
1268    Known { address: String },
1269
1270    /// Reference to a PDA in the stack's pdas registry
1271    PdaRef { pda_name: String },
1272
1273    /// Inline PDA definition (for one-off PDAs not in the registry)
1274    PdaInline {
1275        seeds: Vec<PdaSeedDef>,
1276        #[serde(default, skip_serializing_if = "Option::is_none")]
1277        program_id: Option<String>,
1278    },
1279
1280    /// User must provide at call time via options.accounts
1281    UserProvided,
1282}
1283
1284/// Account metadata for an instruction.
1285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1286pub struct InstructionAccountDef {
1287    /// Account name (e.g., "user", "mint", "bondingCurve")
1288    pub name: String,
1289
1290    /// Whether this account must sign the transaction
1291    #[serde(default)]
1292    pub is_signer: bool,
1293
1294    /// Whether this account is writable
1295    #[serde(default)]
1296    pub is_writable: bool,
1297
1298    /// How this account's address is resolved
1299    pub resolution: AccountResolution,
1300
1301    /// Whether this account can be omitted (optional accounts)
1302    #[serde(default)]
1303    pub is_optional: bool,
1304
1305    /// Documentation from IDL
1306    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1307    pub docs: Vec<String>,
1308}
1309
1310/// Argument definition for an instruction.
1311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1312pub struct InstructionArgDef {
1313    /// Argument name
1314    pub name: String,
1315
1316    /// Type from IDL (e.g., "u64", "bool", "pubkey", "Option<u64>")
1317    #[serde(rename = "type")]
1318    pub arg_type: String,
1319
1320    /// Documentation from IDL
1321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1322    pub docs: Vec<String>,
1323}
1324
1325/// Full instruction definition in the AST.
1326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1327pub struct InstructionDef {
1328    /// Instruction name (e.g., "buy", "sell", "automate")
1329    pub name: String,
1330
1331    /// Discriminator bytes (8 bytes for Anchor, 1 byte for Steel)
1332    pub discriminator: Vec<u8>,
1333
1334    /// Size of discriminator in bytes (for buffer allocation)
1335    #[serde(default = "default_instruction_discriminant_size")]
1336    pub discriminator_size: usize,
1337
1338    /// Accounts required by this instruction, in order
1339    pub accounts: Vec<InstructionAccountDef>,
1340
1341    /// Arguments for this instruction, in order
1342    pub args: Vec<InstructionArgDef>,
1343
1344    /// Error definitions specific to this instruction
1345    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1346    pub errors: Vec<IdlErrorSnapshot>,
1347
1348    /// Program ID for this instruction (usually same as stack's programId)
1349    #[serde(default, skip_serializing_if = "Option::is_none")]
1350    pub program_id: Option<String>,
1351
1352    /// Documentation from IDL
1353    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1354    pub docs: Vec<String>,
1355}
1356
1357// ============================================================================
1358// Stack Spec — Unified multi-entity AST format
1359// ============================================================================
1360
1361/// A unified stack specification containing all entities.
1362/// Written to `.arete/{StackName}.stack.json`.
1363#[derive(Debug, Clone, Serialize, Deserialize)]
1364pub struct SerializableStackSpec {
1365    /// AST schema version for backward compatibility
1366    /// Uses semver format (e.g., "0.0.1")
1367    #[serde(default = "default_ast_version")]
1368    pub ast_version: String,
1369    /// Stack name (PascalCase, derived from module ident)
1370    pub stack_name: String,
1371    /// Program IDs (one per IDL, in order)
1372    #[serde(default)]
1373    pub program_ids: Vec<String>,
1374    /// IDL snapshots (one per program)
1375    #[serde(default)]
1376    pub idls: Vec<IdlSnapshot>,
1377    /// All entity specifications in this stack
1378    pub entities: Vec<SerializableStreamSpec>,
1379    /// PDA registry - defines all PDAs for the stack, grouped by program name
1380    /// Outer key is program name (e.g., "ore", "entropy"), inner key is PDA name
1381    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1382    pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1383    /// Instruction definitions for SDK code generation
1384    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1385    pub instructions: Vec<InstructionDef>,
1386    /// Deterministic content hash of the entire stack
1387    #[serde(default, skip_serializing_if = "Option::is_none")]
1388    pub content_hash: Option<String>,
1389}
1390
1391impl SerializableStackSpec {
1392    /// Normalize legacy event names emitted by older AST generators.
1393    pub fn normalize_event_names(&mut self) {
1394        for entity in &mut self.entities {
1395            entity.normalize_event_names();
1396        }
1397    }
1398
1399    /// Compute deterministic content hash (SHA256 of canonical JSON).
1400    pub fn compute_content_hash(&self) -> String {
1401        use sha2::{Digest, Sha256};
1402        let mut spec_for_hash = self.clone();
1403        spec_for_hash.content_hash = None;
1404        let json = serde_json::to_string(&spec_for_hash)
1405            .expect("Failed to serialize stack spec for hashing");
1406        let mut hasher = Sha256::new();
1407        hasher.update(json.as_bytes());
1408        hex::encode(hasher.finalize())
1409    }
1410
1411    pub fn with_content_hash(mut self) -> Self {
1412        self.content_hash = Some(self.compute_content_hash());
1413        self
1414    }
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419    use super::{
1420        HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1421        SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1422        SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1423    };
1424    use serde_json::Value;
1425
1426    fn legacy_stream_spec() -> SerializableStreamSpec {
1427        SerializableStreamSpec {
1428            ast_version: CURRENT_AST_VERSION.to_string(),
1429            state_name: "PumpfunToken".to_string(),
1430            program_id: None,
1431            idl: None,
1432            identity: IdentitySpec {
1433                primary_keys: vec!["id.mint".to_string()],
1434                lookup_indexes: vec![],
1435            },
1436            handlers: vec![SerializableHandlerSpec {
1437                source: SourceSpec::Source {
1438                    program_id: None,
1439                    discriminator: None,
1440                    type_name: "pump::buyIxState".to_string(),
1441                    serialization: None,
1442                    is_account: false,
1443                },
1444                key_resolution: super::KeyResolutionStrategy::Embedded {
1445                    primary_field: super::FieldPath::new(&["accounts", "mint"]),
1446                },
1447                mappings: vec![SerializableFieldMapping {
1448                    target_path: "info.last_buy_at".to_string(),
1449                    source: MappingSource::Constant(Value::Null),
1450                    transform: None,
1451                    population: PopulationStrategy::SetOnce,
1452                    condition: None,
1453                    when: Some("pump::sellIxState".to_string()),
1454                    stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1455                    emit: true,
1456                }],
1457                conditions: vec![],
1458                emit: true,
1459            }],
1460            sections: vec![],
1461            field_mappings: Default::default(),
1462            resolver_hooks: vec![],
1463            instruction_hooks: vec![InstructionHook {
1464                instruction_type: "pump::buyIxState".to_string(),
1465                actions: vec![HookAction::SetField {
1466                    target_field: "info.last_buy_at".to_string(),
1467                    source: MappingSource::Constant(Value::Null),
1468                    condition: None,
1469                }],
1470                lookup_by: None,
1471            }],
1472            resolver_specs: vec![],
1473            computed_fields: vec![],
1474            computed_field_specs: vec![],
1475            content_hash: None,
1476            views: vec![],
1477        }
1478    }
1479
1480    #[test]
1481    fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1482        let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1483
1484        let handler = &typed.handlers[0];
1485        let SourceSpec::Source { type_name, .. } = &handler.source;
1486        assert_eq!(type_name, "pump::BuyIxState");
1487        assert_eq!(
1488            handler.mappings[0].when.as_deref(),
1489            Some("pump::SellIxState")
1490        );
1491        assert_eq!(
1492            handler.mappings[0].stop.as_deref(),
1493            Some("pump::BuyExactSolInIxState")
1494        );
1495        assert_eq!(
1496            typed.instruction_hooks[0].instruction_type,
1497            "pump::BuyIxState"
1498        );
1499    }
1500
1501    #[test]
1502    fn stack_spec_normalize_event_names_updates_all_entities() {
1503        let mut stack = SerializableStackSpec {
1504            ast_version: CURRENT_AST_VERSION.to_string(),
1505            stack_name: "PumpStack".to_string(),
1506            program_ids: vec![],
1507            idls: vec![],
1508            entities: vec![legacy_stream_spec()],
1509            pdas: Default::default(),
1510            instructions: vec![],
1511            content_hash: None,
1512        };
1513
1514        stack.normalize_event_names();
1515
1516        let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1517        assert_eq!(type_name, "pump::BuyIxState");
1518        assert_eq!(
1519            stack.entities[0].instruction_hooks[0].instruction_type,
1520            "pump::BuyIxState"
1521        );
1522    }
1523}
1524
1525// ============================================================================
1526// View Pipeline Types - Composable View Definitions
1527// ============================================================================
1528
1529/// Sort order for view transforms
1530#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1531#[serde(rename_all = "lowercase")]
1532pub enum SortOrder {
1533    #[default]
1534    Asc,
1535    Desc,
1536}
1537
1538/// Comparison operators for predicates
1539#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1540pub enum CompareOp {
1541    Eq,
1542    Ne,
1543    Gt,
1544    Gte,
1545    Lt,
1546    Lte,
1547}
1548
1549/// Value in a predicate comparison
1550#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1551pub enum PredicateValue {
1552    /// Literal JSON value
1553    Literal(serde_json::Value),
1554    /// Dynamic runtime value (e.g., "now()" for current timestamp)
1555    Dynamic(String),
1556    /// Reference to another field
1557    Field(FieldPath),
1558}
1559
1560/// Predicate for filtering entities
1561#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1562pub enum Predicate {
1563    /// Field comparison: field op value
1564    Compare {
1565        field: FieldPath,
1566        op: CompareOp,
1567        value: PredicateValue,
1568    },
1569    /// Logical AND of predicates
1570    And(Vec<Predicate>),
1571    /// Logical OR of predicates
1572    Or(Vec<Predicate>),
1573    /// Negation
1574    Not(Box<Predicate>),
1575    /// Field exists (is not null)
1576    Exists { field: FieldPath },
1577}
1578
1579/// Transform operation in a view pipeline
1580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1581pub enum ViewTransform {
1582    /// Filter entities matching a predicate
1583    Filter { predicate: Predicate },
1584
1585    /// Sort entities by a field
1586    Sort {
1587        key: FieldPath,
1588        #[serde(default)]
1589        order: SortOrder,
1590    },
1591
1592    /// Take first N entities (after sort)
1593    Take { count: usize },
1594
1595    /// Skip first N entities
1596    Skip { count: usize },
1597
1598    /// Take only the first entity (after sort) - produces Single output
1599    First,
1600
1601    /// Take only the last entity (after sort) - produces Single output
1602    Last,
1603
1604    /// Get entity with maximum value for field - produces Single output
1605    MaxBy { key: FieldPath },
1606
1607    /// Get entity with minimum value for field - produces Single output
1608    MinBy { key: FieldPath },
1609}
1610
1611/// Source for a view definition
1612#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1613pub enum ViewSource {
1614    /// Derive directly from entity mutations
1615    Entity { name: String },
1616    /// Derive from another view's output
1617    View { id: String },
1618}
1619
1620/// Output mode for a view
1621#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1622pub enum ViewOutput {
1623    /// Multiple entities (list-like semantics)
1624    #[default]
1625    Collection,
1626    /// Single entity (state-like semantics)
1627    Single,
1628    /// Keyed lookup by a specific field
1629    Keyed { key_field: FieldPath },
1630}
1631
1632/// Definition of a view in the pipeline
1633#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1634pub struct ViewDef {
1635    /// Unique view identifier (e.g., "OreRound/latest")
1636    pub id: String,
1637
1638    /// Source this view derives from
1639    pub source: ViewSource,
1640
1641    /// Pipeline of transforms to apply (in order)
1642    #[serde(default)]
1643    pub pipeline: Vec<ViewTransform>,
1644
1645    /// Output mode for this view
1646    #[serde(default)]
1647    pub output: ViewOutput,
1648}
1649
1650impl ViewDef {
1651    /// Create a new list view for an entity
1652    pub fn list(entity_name: &str) -> Self {
1653        ViewDef {
1654            id: format!("{}/list", entity_name),
1655            source: ViewSource::Entity {
1656                name: entity_name.to_string(),
1657            },
1658            pipeline: vec![],
1659            output: ViewOutput::Collection,
1660        }
1661    }
1662
1663    /// Create a new state view for an entity
1664    pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1665        ViewDef {
1666            id: format!("{}/state", entity_name),
1667            source: ViewSource::Entity {
1668                name: entity_name.to_string(),
1669            },
1670            pipeline: vec![],
1671            output: ViewOutput::Keyed {
1672                key_field: FieldPath::new(key_field),
1673            },
1674        }
1675    }
1676
1677    /// Check if this view produces a single entity
1678    pub fn is_single(&self) -> bool {
1679        matches!(self.output, ViewOutput::Single)
1680    }
1681
1682    /// Check if any transform in the pipeline produces a single result
1683    pub fn has_single_transform(&self) -> bool {
1684        self.pipeline.iter().any(|t| {
1685            matches!(
1686                t,
1687                ViewTransform::First
1688                    | ViewTransform::Last
1689                    | ViewTransform::MaxBy { .. }
1690                    | ViewTransform::MinBy { .. }
1691            )
1692        })
1693    }
1694}
1695
1696#[macro_export]
1697macro_rules! define_accessor {
1698    ($name:ident, $state:ty, $path:expr) => {
1699        pub struct $name;
1700
1701        impl $crate::ast::FieldAccessor<$state> for $name {
1702            fn path(&self) -> String {
1703                $path.to_string()
1704            }
1705        }
1706    };
1707}