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