Skip to main content

hyperstack_interpreter/
ast.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use hyperstack_idl::snapshot::*;
7
8/// Current AST version for SerializableStreamSpec and SerializableStackSpec
9///
10/// ⚠️ IMPORTANT: This constant is duplicated in hyperstack-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/// hyperstack-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
426#[derive(Debug, Clone)]
427pub struct TypedStreamSpec<S> {
428    pub state_name: String,
429    pub identity: IdentitySpec,
430    pub handlers: Vec<TypedHandlerSpec<S>>,
431    pub sections: Vec<EntitySection>, // NEW: Complete structural information
432    pub field_mappings: BTreeMap<String, FieldTypeInfo>, // NEW: All field type info by target path
433    pub resolver_hooks: Vec<ResolverHook>, // NEW: Resolver hooks for PDA key resolution
434    pub instruction_hooks: Vec<InstructionHook>, // NEW: Instruction hooks for PDA registration
435    pub resolver_specs: Vec<ResolverSpec>,
436    pub computed_fields: Vec<String>, // List of computed field paths
437    _phantom: PhantomData<S>,
438}
439
440impl<S> TypedStreamSpec<S> {
441    pub fn new(
442        state_name: String,
443        identity: IdentitySpec,
444        handlers: Vec<TypedHandlerSpec<S>>,
445    ) -> Self {
446        TypedStreamSpec {
447            state_name,
448            identity,
449            handlers,
450            sections: Vec::new(),
451            field_mappings: BTreeMap::new(),
452            resolver_hooks: Vec::new(),
453            instruction_hooks: Vec::new(),
454            resolver_specs: Vec::new(),
455            computed_fields: Vec::new(),
456            _phantom: PhantomData,
457        }
458    }
459
460    /// Enhanced constructor with type information
461    pub fn with_type_info(
462        state_name: String,
463        identity: IdentitySpec,
464        handlers: Vec<TypedHandlerSpec<S>>,
465        sections: Vec<EntitySection>,
466        field_mappings: BTreeMap<String, FieldTypeInfo>,
467    ) -> Self {
468        TypedStreamSpec {
469            state_name,
470            identity,
471            handlers,
472            sections,
473            field_mappings,
474            resolver_hooks: Vec::new(),
475            instruction_hooks: Vec::new(),
476            resolver_specs: Vec::new(),
477            computed_fields: Vec::new(),
478            _phantom: PhantomData,
479        }
480    }
481
482    pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
483        self.resolver_specs = resolver_specs;
484        self
485    }
486
487    /// Get type information for a specific field path
488    pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
489        self.field_mappings.get(path)
490    }
491
492    /// Get all fields for a specific section
493    pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
494        self.sections
495            .iter()
496            .find(|s| s.name == section_name)
497            .map(|s| &s.fields)
498    }
499
500    /// Get all section names
501    pub fn get_section_names(&self) -> Vec<&String> {
502        self.sections.iter().map(|s| &s.name).collect()
503    }
504
505    /// Convert to serializable format
506    pub fn to_serializable(&self) -> SerializableStreamSpec {
507        let mut spec = SerializableStreamSpec {
508            ast_version: CURRENT_AST_VERSION.to_string(),
509            state_name: self.state_name.clone(),
510            program_id: None,
511            idl: None,
512            identity: self.identity.clone(),
513            handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
514            sections: self.sections.clone(),
515            field_mappings: self.field_mappings.clone(),
516            resolver_hooks: self.resolver_hooks.clone(),
517            instruction_hooks: self.instruction_hooks.clone(),
518            resolver_specs: self.resolver_specs.clone(),
519            computed_fields: self.computed_fields.clone(),
520            computed_field_specs: Vec::new(),
521            content_hash: None,
522            views: Vec::new(),
523        };
524        spec.content_hash = Some(spec.compute_content_hash());
525        spec
526    }
527
528    /// Create from serializable format
529    pub fn from_serializable(spec: SerializableStreamSpec) -> Self {
530        TypedStreamSpec {
531            state_name: spec.state_name,
532            identity: spec.identity,
533            handlers: spec
534                .handlers
535                .into_iter()
536                .map(|h| TypedHandlerSpec::from_serializable(h))
537                .collect(),
538            sections: spec.sections,
539            field_mappings: spec.field_mappings,
540            resolver_hooks: spec.resolver_hooks,
541            instruction_hooks: spec.instruction_hooks,
542            resolver_specs: spec.resolver_specs,
543            computed_fields: spec.computed_fields,
544            _phantom: PhantomData,
545        }
546    }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct IdentitySpec {
551    pub primary_keys: Vec<String>,
552    pub lookup_indexes: Vec<LookupIndexSpec>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct LookupIndexSpec {
557    pub field_name: String,
558    pub temporal_field: Option<String>,
559}
560
561// ============================================================================
562// Level 1: Declarative Hook Extensions
563// ============================================================================
564
565/// Declarative resolver hook specification
566#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct ResolverHook {
568    /// Account type this resolver applies to (e.g., "BondingCurveState")
569    pub account_type: String,
570
571    /// Resolution strategy
572    pub strategy: ResolverStrategy,
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub enum ResolverStrategy {
577    /// Look up PDA in reverse lookup table, queue if not found
578    PdaReverseLookup {
579        lookup_name: String,
580        /// Instruction discriminators to queue until (8 bytes each)
581        queue_discriminators: Vec<Vec<u8>>,
582    },
583
584    /// Extract primary key directly from account data (future)
585    DirectField { field_path: FieldPath },
586}
587
588/// Declarative instruction hook specification
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct InstructionHook {
591    /// Instruction type this hook applies to (e.g., "CreateIxState")
592    pub instruction_type: String,
593
594    /// Actions to perform when this instruction is processed
595    pub actions: Vec<HookAction>,
596
597    /// Lookup strategy for finding the entity
598    pub lookup_by: Option<FieldPath>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub enum HookAction {
603    /// Register a PDA mapping for reverse lookup
604    RegisterPdaMapping {
605        pda_field: FieldPath,
606        seed_field: FieldPath,
607        lookup_name: String,
608    },
609
610    /// Set a field value (for #[track_from])
611    SetField {
612        target_field: String,
613        source: MappingSource,
614        condition: Option<ConditionExpr>,
615    },
616
617    /// Increment a field value (for conditional aggregations)
618    IncrementField {
619        target_field: String,
620        increment_by: i64,
621        condition: Option<ConditionExpr>,
622    },
623}
624
625/// Simple condition expression (Level 1 - basic comparisons only)
626#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct ConditionExpr {
628    /// Expression as string (will be parsed and validated)
629    pub expression: String,
630
631    /// Parsed representation (for validation and execution)
632    pub parsed: Option<ParsedCondition>,
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub enum ParsedCondition {
637    /// Binary comparison: field op value
638    Comparison {
639        field: FieldPath,
640        op: ComparisonOp,
641        value: serde_json::Value,
642    },
643
644    /// Logical AND/OR
645    Logical {
646        op: LogicalOp,
647        conditions: Vec<ParsedCondition>,
648    },
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
652pub enum ComparisonOp {
653    Equal,
654    NotEqual,
655    GreaterThan,
656    GreaterThanOrEqual,
657    LessThan,
658    LessThanOrEqual,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub enum LogicalOp {
663    And,
664    Or,
665}
666
667/// Serializable version of HandlerSpec without phantom types
668#[derive(Debug, Clone, Serialize, Deserialize)]
669pub struct SerializableHandlerSpec {
670    pub source: SourceSpec,
671    pub key_resolution: KeyResolutionStrategy,
672    pub mappings: Vec<SerializableFieldMapping>,
673    pub conditions: Vec<Condition>,
674    pub emit: bool,
675}
676
677#[derive(Debug, Clone)]
678pub struct TypedHandlerSpec<S> {
679    pub source: SourceSpec,
680    pub key_resolution: KeyResolutionStrategy,
681    pub mappings: Vec<TypedFieldMapping<S>>,
682    pub conditions: Vec<Condition>,
683    pub emit: bool,
684    _phantom: PhantomData<S>,
685}
686
687impl<S> TypedHandlerSpec<S> {
688    pub fn new(
689        source: SourceSpec,
690        key_resolution: KeyResolutionStrategy,
691        mappings: Vec<TypedFieldMapping<S>>,
692        emit: bool,
693    ) -> Self {
694        TypedHandlerSpec {
695            source,
696            key_resolution,
697            mappings,
698            conditions: vec![],
699            emit,
700            _phantom: PhantomData,
701        }
702    }
703
704    /// Convert to serializable format
705    pub fn to_serializable(&self) -> SerializableHandlerSpec {
706        SerializableHandlerSpec {
707            source: self.source.clone(),
708            key_resolution: self.key_resolution.clone(),
709            mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
710            conditions: self.conditions.clone(),
711            emit: self.emit,
712        }
713    }
714
715    /// Create from serializable format
716    pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
717        TypedHandlerSpec {
718            source: spec.source,
719            key_resolution: spec.key_resolution,
720            mappings: spec
721                .mappings
722                .into_iter()
723                .map(|m| TypedFieldMapping::from_serializable(m))
724                .collect(),
725            conditions: spec.conditions,
726            emit: spec.emit,
727            _phantom: PhantomData,
728        }
729    }
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
733pub enum KeyResolutionStrategy {
734    Embedded {
735        primary_field: FieldPath,
736    },
737    Lookup {
738        primary_field: FieldPath,
739    },
740    Computed {
741        primary_field: FieldPath,
742        compute_partition: ComputeFunction,
743    },
744    TemporalLookup {
745        lookup_field: FieldPath,
746        timestamp_field: FieldPath,
747        index_name: String,
748    },
749}
750
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub enum SourceSpec {
753    Source {
754        program_id: Option<String>,
755        discriminator: Option<Vec<u8>>,
756        type_name: String,
757        #[serde(default, skip_serializing_if = "Option::is_none")]
758        serialization: Option<IdlSerializationSnapshot>,
759        /// True when this handler listens to an account-state event (not an
760        /// instruction or custom event).  Set at code-generation time from
761        /// the structural source kind so the compiler does not need to rely
762        /// on naming-convention heuristics.
763        #[serde(default)]
764        is_account: bool,
765    },
766}
767
768/// Serializable version of FieldMapping without phantom types
769#[derive(Debug, Clone, Serialize, Deserialize)]
770pub struct SerializableFieldMapping {
771    pub target_path: String,
772    pub source: MappingSource,
773    pub transform: Option<Transformation>,
774    pub population: PopulationStrategy,
775    #[serde(default, skip_serializing_if = "Option::is_none")]
776    pub condition: Option<ConditionExpr>,
777    #[serde(default, skip_serializing_if = "Option::is_none")]
778    pub when: Option<String>,
779    #[serde(default, skip_serializing_if = "Option::is_none")]
780    pub stop: Option<String>,
781    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
782    pub emit: bool,
783}
784
785fn default_emit() -> bool {
786    true
787}
788
789fn default_instruction_discriminant_size() -> usize {
790    8
791}
792
793fn is_true(value: &bool) -> bool {
794    *value
795}
796
797#[derive(Debug, Clone)]
798pub struct TypedFieldMapping<S> {
799    pub target_path: String,
800    pub source: MappingSource,
801    pub transform: Option<Transformation>,
802    pub population: PopulationStrategy,
803    pub condition: Option<ConditionExpr>,
804    pub when: Option<String>,
805    pub stop: Option<String>,
806    pub emit: bool,
807    _phantom: PhantomData<S>,
808}
809
810impl<S> TypedFieldMapping<S> {
811    pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
812        TypedFieldMapping {
813            target_path,
814            source,
815            transform: None,
816            population,
817            condition: None,
818            when: None,
819            stop: None,
820            emit: true,
821            _phantom: PhantomData,
822        }
823    }
824
825    pub fn with_transform(mut self, transform: Transformation) -> Self {
826        self.transform = Some(transform);
827        self
828    }
829
830    pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
831        self.condition = Some(condition);
832        self
833    }
834
835    pub fn with_when(mut self, when: String) -> Self {
836        self.when = Some(when);
837        self
838    }
839
840    pub fn with_stop(mut self, stop: String) -> Self {
841        self.stop = Some(stop);
842        self
843    }
844
845    pub fn with_emit(mut self, emit: bool) -> Self {
846        self.emit = emit;
847        self
848    }
849
850    /// Convert to serializable format
851    pub fn to_serializable(&self) -> SerializableFieldMapping {
852        SerializableFieldMapping {
853            target_path: self.target_path.clone(),
854            source: self.source.clone(),
855            transform: self.transform.clone(),
856            population: self.population.clone(),
857            condition: self.condition.clone(),
858            when: self.when.clone(),
859            stop: self.stop.clone(),
860            emit: self.emit,
861        }
862    }
863
864    /// Create from serializable format
865    pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
866        TypedFieldMapping {
867            target_path: mapping.target_path,
868            source: mapping.source,
869            transform: mapping.transform,
870            population: mapping.population,
871            condition: mapping.condition,
872            when: mapping.when,
873            stop: mapping.stop,
874            emit: mapping.emit,
875            _phantom: PhantomData,
876        }
877    }
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize)]
881pub enum MappingSource {
882    FromSource {
883        path: FieldPath,
884        default: Option<Value>,
885        transform: Option<Transformation>,
886    },
887    Constant(Value),
888    Computed {
889        inputs: Vec<FieldPath>,
890        function: ComputeFunction,
891    },
892    FromState {
893        path: String,
894    },
895    AsEvent {
896        fields: Vec<Box<MappingSource>>,
897    },
898    WholeSource,
899    /// Similar to WholeSource but with field-level transformations
900    /// Used by #[capture] macro to apply transforms to specific fields in an account
901    AsCapture {
902        field_transforms: BTreeMap<String, Transformation>,
903    },
904    /// From instruction context (timestamp, slot, signature)
905    /// Used by #[track_from] with special fields like __timestamp
906    FromContext {
907        field: String,
908    },
909}
910
911impl MappingSource {
912    pub fn with_transform(self, transform: Transformation) -> Self {
913        match self {
914            MappingSource::FromSource {
915                path,
916                default,
917                transform: _,
918            } => MappingSource::FromSource {
919                path,
920                default,
921                transform: Some(transform),
922            },
923            other => other,
924        }
925    }
926}
927
928#[derive(Debug, Clone, Serialize, Deserialize)]
929pub enum ComputeFunction {
930    Sum,
931    Concat,
932    Format(String),
933    Custom(String),
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
937pub struct Condition {
938    pub field: FieldPath,
939    pub operator: ConditionOp,
940    pub value: Value,
941}
942
943#[derive(Debug, Clone, Serialize, Deserialize)]
944pub enum ConditionOp {
945    Equals,
946    NotEquals,
947    GreaterThan,
948    LessThan,
949    Contains,
950    Exists,
951}
952
953/// Language-agnostic type information for fields
954#[derive(Debug, Clone, Serialize, Deserialize)]
955pub struct FieldTypeInfo {
956    pub field_name: String,
957    pub rust_type_name: String, // Full Rust type: "Option<i64>", "Vec<Value>", etc.
958    pub base_type: BaseType,    // Fundamental type classification
959    pub is_optional: bool,      // true for Option<T>
960    pub is_array: bool,         // true for Vec<T>
961    pub inner_type: Option<String>, // For Option<T> or Vec<T>, store the inner type
962    pub source_path: Option<String>, // Path to source field if this is mapped
963    /// Resolved type information for complex types (instructions, accounts, custom types)
964    #[serde(default)]
965    pub resolved_type: Option<ResolvedStructType>,
966    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
967    pub emit: bool,
968}
969
970/// Resolved structure type with field information from IDL
971#[derive(Debug, Clone, Serialize, Deserialize)]
972pub struct ResolvedStructType {
973    pub type_name: String,
974    pub fields: Vec<ResolvedField>,
975    pub is_instruction: bool,
976    pub is_account: bool,
977    pub is_event: bool,
978    /// If true, this is an enum type and enum_variants should be used instead of fields
979    #[serde(default)]
980    pub is_enum: bool,
981    /// For enum types, list of variant names
982    #[serde(default)]
983    pub enum_variants: Vec<String>,
984}
985
986/// A resolved field within a complex type
987#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct ResolvedField {
989    pub field_name: String,
990    pub field_type: String,
991    pub base_type: BaseType,
992    pub is_optional: bool,
993    pub is_array: bool,
994}
995
996/// Language-agnostic base type classification
997#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
998pub enum BaseType {
999    // Numeric types
1000    Integer, // i8, i16, i32, i64, u8, u16, u32, u64, usize, isize
1001    Float,   // f32, f64
1002    // Text types
1003    String, // String, &str
1004    // Boolean
1005    Boolean, // bool
1006    // Complex types
1007    Object, // Custom structs, HashMap, etc.
1008    Array,  // Vec<T>, arrays
1009    Binary, // Bytes, binary data
1010    // Special types
1011    Timestamp, // Detected from field names ending in _at, _time, etc.
1012    Pubkey,    // Solana public key (Base58 encoded)
1013    Any,       // serde_json::Value, unknown types
1014}
1015
1016/// Represents a logical section/group of fields in the entity
1017#[derive(Debug, Clone, Serialize, Deserialize)]
1018pub struct EntitySection {
1019    pub name: String,
1020    pub fields: Vec<FieldTypeInfo>,
1021    pub is_nested_struct: bool,
1022    pub parent_field: Option<String>, // If this section comes from a nested struct field
1023}
1024
1025impl FieldTypeInfo {
1026    pub fn new(field_name: String, rust_type_name: String) -> Self {
1027        let (base_type, is_optional, is_array, inner_type) =
1028            Self::analyze_rust_type(&rust_type_name);
1029
1030        FieldTypeInfo {
1031            field_name: field_name.clone(),
1032            rust_type_name,
1033            base_type: Self::infer_semantic_type(&field_name, base_type),
1034            is_optional,
1035            is_array,
1036            inner_type,
1037            source_path: None,
1038            resolved_type: None,
1039            emit: true,
1040        }
1041    }
1042
1043    pub fn with_source_path(mut self, source_path: String) -> Self {
1044        self.source_path = Some(source_path);
1045        self
1046    }
1047
1048    /// Analyze a Rust type string and extract structural information
1049    fn analyze_rust_type(rust_type: &str) -> (BaseType, bool, bool, Option<String>) {
1050        let type_str = rust_type.trim();
1051
1052        // Handle Option<T>
1053        if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1054            let (inner_base_type, _, inner_is_array, inner_inner_type) =
1055                Self::analyze_rust_type(&inner);
1056            return (
1057                inner_base_type,
1058                true,
1059                inner_is_array,
1060                inner_inner_type.or(Some(inner)),
1061            );
1062        }
1063
1064        // Handle Vec<T>
1065        if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1066            let (_inner_base_type, inner_is_optional, _, inner_inner_type) =
1067                Self::analyze_rust_type(&inner);
1068            return (
1069                BaseType::Array,
1070                inner_is_optional,
1071                true,
1072                inner_inner_type.or(Some(inner)),
1073            );
1074        }
1075
1076        // Handle primitive types
1077        let base_type = match type_str {
1078            "i8" | "i16" | "i32" | "i64" | "isize" | "u8" | "u16" | "u32" | "u64" | "usize" => {
1079                BaseType::Integer
1080            }
1081            "f32" | "f64" => BaseType::Float,
1082            "bool" => BaseType::Boolean,
1083            "String" | "&str" | "str" => BaseType::String,
1084            "Value" | "serde_json::Value" => BaseType::Any,
1085            "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1086            _ => {
1087                // Check for binary types
1088                if type_str.contains("Bytes") || type_str.contains("bytes") {
1089                    BaseType::Binary
1090                } else if type_str.contains("Pubkey") {
1091                    BaseType::Pubkey
1092                } else {
1093                    BaseType::Object
1094                }
1095            }
1096        };
1097
1098        (base_type, false, false, None)
1099    }
1100
1101    /// Extract inner type from generic like "Option<T>" -> "T"
1102    fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1103        let pattern = format!("{}<", generic_name);
1104        if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1105            let start = pattern.len();
1106            let end = type_str.len() - 1;
1107            if end > start {
1108                return Some(type_str[start..end].trim().to_string());
1109            }
1110        }
1111        None
1112    }
1113
1114    /// Infer semantic type based on field name patterns
1115    fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1116        let lower_name = field_name.to_lowercase();
1117
1118        // If already classified as integer, check if it should be timestamp
1119        if base_type == BaseType::Integer
1120            && (lower_name.ends_with("_at")
1121                || lower_name.ends_with("_time")
1122                || lower_name.contains("timestamp")
1123                || lower_name.contains("created")
1124                || lower_name.contains("settled")
1125                || lower_name.contains("activated"))
1126        {
1127            return BaseType::Timestamp;
1128        }
1129
1130        base_type
1131    }
1132}
1133
1134pub trait FieldAccessor<S> {
1135    fn path(&self) -> String;
1136}
1137
1138// ============================================================================
1139// SerializableStreamSpec Implementation
1140// ============================================================================
1141
1142impl SerializableStreamSpec {
1143    /// Compute deterministic content hash (SHA256 of canonical JSON).
1144    ///
1145    /// The hash is computed over the entire spec except the content_hash field itself,
1146    /// ensuring the same AST always produces the same hash regardless of when it was
1147    /// generated or by whom.
1148    pub fn compute_content_hash(&self) -> String {
1149        use sha2::{Digest, Sha256};
1150
1151        // Clone and clear the hash field for computation
1152        let mut spec_for_hash = self.clone();
1153        spec_for_hash.content_hash = None;
1154
1155        // Serialize to JSON (serde_json produces consistent output for the same struct)
1156        let json =
1157            serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1158
1159        // Compute SHA256 hash
1160        let mut hasher = Sha256::new();
1161        hasher.update(json.as_bytes());
1162        let result = hasher.finalize();
1163
1164        // Return hex-encoded hash
1165        hex::encode(result)
1166    }
1167
1168    /// Verify that the content_hash matches the computed hash.
1169    /// Returns true if hash is valid or not set.
1170    pub fn verify_content_hash(&self) -> bool {
1171        match &self.content_hash {
1172            Some(hash) => {
1173                let computed = self.compute_content_hash();
1174                hash == &computed
1175            }
1176            None => true, // No hash to verify
1177        }
1178    }
1179
1180    /// Set the content_hash field to the computed hash.
1181    pub fn with_content_hash(mut self) -> Self {
1182        self.content_hash = Some(self.compute_content_hash());
1183        self
1184    }
1185}
1186
1187// ============================================================================
1188// PDA and Instruction Types — For SDK code generation
1189// ============================================================================
1190
1191/// PDA (Program-Derived Address) definition for the stack-level registry.
1192/// PDAs defined here can be referenced by instructions via `pdaRef`.
1193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1194pub struct PdaDefinition {
1195    /// Human-readable name (e.g., "miner", "bondingCurve")
1196    pub name: String,
1197
1198    /// Seeds for PDA derivation, in order
1199    pub seeds: Vec<PdaSeedDef>,
1200
1201    /// Program ID that owns this PDA.
1202    /// If None, uses the stack's primary programId.
1203    #[serde(default, skip_serializing_if = "Option::is_none")]
1204    pub program_id: Option<String>,
1205}
1206
1207/// Single seed in a PDA derivation.
1208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1209#[serde(tag = "type", rename_all = "camelCase")]
1210pub enum PdaSeedDef {
1211    /// Static string seed: "miner" → "miner".as_bytes()
1212    Literal { value: String },
1213
1214    /// Static byte array (for non-UTF8 seeds)
1215    Bytes { value: Vec<u8> },
1216
1217    /// Reference to an instruction argument: arg("roundId") → args.roundId as bytes
1218    ArgRef {
1219        arg_name: String,
1220        /// Optional type hint for serialization (e.g., "u64", "pubkey")
1221        #[serde(default, skip_serializing_if = "Option::is_none")]
1222        arg_type: Option<String>,
1223    },
1224
1225    /// Reference to another account in the instruction: account("mint") → accounts.mint pubkey
1226    AccountRef { account_name: String },
1227}
1228
1229/// How an instruction account's address is determined.
1230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1231#[serde(tag = "category", rename_all = "camelCase")]
1232pub enum AccountResolution {
1233    /// Must sign the transaction (uses wallet.publicKey)
1234    Signer,
1235
1236    /// Fixed known address (e.g., System Program, Token Program)
1237    Known { address: String },
1238
1239    /// Reference to a PDA in the stack's pdas registry
1240    PdaRef { pda_name: String },
1241
1242    /// Inline PDA definition (for one-off PDAs not in the registry)
1243    PdaInline {
1244        seeds: Vec<PdaSeedDef>,
1245        #[serde(default, skip_serializing_if = "Option::is_none")]
1246        program_id: Option<String>,
1247    },
1248
1249    /// User must provide at call time via options.accounts
1250    UserProvided,
1251}
1252
1253/// Account metadata for an instruction.
1254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1255pub struct InstructionAccountDef {
1256    /// Account name (e.g., "user", "mint", "bondingCurve")
1257    pub name: String,
1258
1259    /// Whether this account must sign the transaction
1260    #[serde(default)]
1261    pub is_signer: bool,
1262
1263    /// Whether this account is writable
1264    #[serde(default)]
1265    pub is_writable: bool,
1266
1267    /// How this account's address is resolved
1268    pub resolution: AccountResolution,
1269
1270    /// Whether this account can be omitted (optional accounts)
1271    #[serde(default)]
1272    pub is_optional: bool,
1273
1274    /// Documentation from IDL
1275    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1276    pub docs: Vec<String>,
1277}
1278
1279/// Argument definition for an instruction.
1280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1281pub struct InstructionArgDef {
1282    /// Argument name
1283    pub name: String,
1284
1285    /// Type from IDL (e.g., "u64", "bool", "pubkey", "Option<u64>")
1286    #[serde(rename = "type")]
1287    pub arg_type: String,
1288
1289    /// Documentation from IDL
1290    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1291    pub docs: Vec<String>,
1292}
1293
1294/// Full instruction definition in the AST.
1295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1296pub struct InstructionDef {
1297    /// Instruction name (e.g., "buy", "sell", "automate")
1298    pub name: String,
1299
1300    /// Discriminator bytes (8 bytes for Anchor, 1 byte for Steel)
1301    pub discriminator: Vec<u8>,
1302
1303    /// Size of discriminator in bytes (for buffer allocation)
1304    #[serde(default = "default_instruction_discriminant_size")]
1305    pub discriminator_size: usize,
1306
1307    /// Accounts required by this instruction, in order
1308    pub accounts: Vec<InstructionAccountDef>,
1309
1310    /// Arguments for this instruction, in order
1311    pub args: Vec<InstructionArgDef>,
1312
1313    /// Error definitions specific to this instruction
1314    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1315    pub errors: Vec<IdlErrorSnapshot>,
1316
1317    /// Program ID for this instruction (usually same as stack's programId)
1318    #[serde(default, skip_serializing_if = "Option::is_none")]
1319    pub program_id: Option<String>,
1320
1321    /// Documentation from IDL
1322    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1323    pub docs: Vec<String>,
1324}
1325
1326// ============================================================================
1327// Stack Spec — Unified multi-entity AST format
1328// ============================================================================
1329
1330/// A unified stack specification containing all entities.
1331/// Written to `.hyperstack/{StackName}.stack.json`.
1332#[derive(Debug, Clone, Serialize, Deserialize)]
1333pub struct SerializableStackSpec {
1334    /// AST schema version for backward compatibility
1335    /// Uses semver format (e.g., "0.0.1")
1336    #[serde(default = "default_ast_version")]
1337    pub ast_version: String,
1338    /// Stack name (PascalCase, derived from module ident)
1339    pub stack_name: String,
1340    /// Program IDs (one per IDL, in order)
1341    #[serde(default)]
1342    pub program_ids: Vec<String>,
1343    /// IDL snapshots (one per program)
1344    #[serde(default)]
1345    pub idls: Vec<IdlSnapshot>,
1346    /// All entity specifications in this stack
1347    pub entities: Vec<SerializableStreamSpec>,
1348    /// PDA registry - defines all PDAs for the stack, grouped by program name
1349    /// Outer key is program name (e.g., "ore", "entropy"), inner key is PDA name
1350    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1351    pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1352    /// Instruction definitions for SDK code generation
1353    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1354    pub instructions: Vec<InstructionDef>,
1355    /// Deterministic content hash of the entire stack
1356    #[serde(default, skip_serializing_if = "Option::is_none")]
1357    pub content_hash: Option<String>,
1358}
1359
1360impl SerializableStackSpec {
1361    /// Compute deterministic content hash (SHA256 of canonical JSON).
1362    pub fn compute_content_hash(&self) -> String {
1363        use sha2::{Digest, Sha256};
1364        let mut spec_for_hash = self.clone();
1365        spec_for_hash.content_hash = None;
1366        let json = serde_json::to_string(&spec_for_hash)
1367            .expect("Failed to serialize stack spec for hashing");
1368        let mut hasher = Sha256::new();
1369        hasher.update(json.as_bytes());
1370        hex::encode(hasher.finalize())
1371    }
1372
1373    pub fn with_content_hash(mut self) -> Self {
1374        self.content_hash = Some(self.compute_content_hash());
1375        self
1376    }
1377}
1378
1379// ============================================================================
1380// View Pipeline Types - Composable View Definitions
1381// ============================================================================
1382
1383/// Sort order for view transforms
1384#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1385#[serde(rename_all = "lowercase")]
1386pub enum SortOrder {
1387    #[default]
1388    Asc,
1389    Desc,
1390}
1391
1392/// Comparison operators for predicates
1393#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1394pub enum CompareOp {
1395    Eq,
1396    Ne,
1397    Gt,
1398    Gte,
1399    Lt,
1400    Lte,
1401}
1402
1403/// Value in a predicate comparison
1404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1405pub enum PredicateValue {
1406    /// Literal JSON value
1407    Literal(serde_json::Value),
1408    /// Dynamic runtime value (e.g., "now()" for current timestamp)
1409    Dynamic(String),
1410    /// Reference to another field
1411    Field(FieldPath),
1412}
1413
1414/// Predicate for filtering entities
1415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1416pub enum Predicate {
1417    /// Field comparison: field op value
1418    Compare {
1419        field: FieldPath,
1420        op: CompareOp,
1421        value: PredicateValue,
1422    },
1423    /// Logical AND of predicates
1424    And(Vec<Predicate>),
1425    /// Logical OR of predicates
1426    Or(Vec<Predicate>),
1427    /// Negation
1428    Not(Box<Predicate>),
1429    /// Field exists (is not null)
1430    Exists { field: FieldPath },
1431}
1432
1433/// Transform operation in a view pipeline
1434#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1435pub enum ViewTransform {
1436    /// Filter entities matching a predicate
1437    Filter { predicate: Predicate },
1438
1439    /// Sort entities by a field
1440    Sort {
1441        key: FieldPath,
1442        #[serde(default)]
1443        order: SortOrder,
1444    },
1445
1446    /// Take first N entities (after sort)
1447    Take { count: usize },
1448
1449    /// Skip first N entities
1450    Skip { count: usize },
1451
1452    /// Take only the first entity (after sort) - produces Single output
1453    First,
1454
1455    /// Take only the last entity (after sort) - produces Single output
1456    Last,
1457
1458    /// Get entity with maximum value for field - produces Single output
1459    MaxBy { key: FieldPath },
1460
1461    /// Get entity with minimum value for field - produces Single output
1462    MinBy { key: FieldPath },
1463}
1464
1465/// Source for a view definition
1466#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1467pub enum ViewSource {
1468    /// Derive directly from entity mutations
1469    Entity { name: String },
1470    /// Derive from another view's output
1471    View { id: String },
1472}
1473
1474/// Output mode for a view
1475#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1476pub enum ViewOutput {
1477    /// Multiple entities (list-like semantics)
1478    #[default]
1479    Collection,
1480    /// Single entity (state-like semantics)
1481    Single,
1482    /// Keyed lookup by a specific field
1483    Keyed { key_field: FieldPath },
1484}
1485
1486/// Definition of a view in the pipeline
1487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1488pub struct ViewDef {
1489    /// Unique view identifier (e.g., "OreRound/latest")
1490    pub id: String,
1491
1492    /// Source this view derives from
1493    pub source: ViewSource,
1494
1495    /// Pipeline of transforms to apply (in order)
1496    #[serde(default)]
1497    pub pipeline: Vec<ViewTransform>,
1498
1499    /// Output mode for this view
1500    #[serde(default)]
1501    pub output: ViewOutput,
1502}
1503
1504impl ViewDef {
1505    /// Create a new list view for an entity
1506    pub fn list(entity_name: &str) -> Self {
1507        ViewDef {
1508            id: format!("{}/list", entity_name),
1509            source: ViewSource::Entity {
1510                name: entity_name.to_string(),
1511            },
1512            pipeline: vec![],
1513            output: ViewOutput::Collection,
1514        }
1515    }
1516
1517    /// Create a new state view for an entity
1518    pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1519        ViewDef {
1520            id: format!("{}/state", entity_name),
1521            source: ViewSource::Entity {
1522                name: entity_name.to_string(),
1523            },
1524            pipeline: vec![],
1525            output: ViewOutput::Keyed {
1526                key_field: FieldPath::new(key_field),
1527            },
1528        }
1529    }
1530
1531    /// Check if this view produces a single entity
1532    pub fn is_single(&self) -> bool {
1533        matches!(self.output, ViewOutput::Single)
1534    }
1535
1536    /// Check if any transform in the pipeline produces a single result
1537    pub fn has_single_transform(&self) -> bool {
1538        self.pipeline.iter().any(|t| {
1539            matches!(
1540                t,
1541                ViewTransform::First
1542                    | ViewTransform::Last
1543                    | ViewTransform::MaxBy { .. }
1544                    | ViewTransform::MinBy { .. }
1545            )
1546        })
1547    }
1548}
1549
1550#[macro_export]
1551macro_rules! define_accessor {
1552    ($name:ident, $state:ty, $path:expr) => {
1553        pub struct $name;
1554
1555        impl $crate::ast::FieldAccessor<$state> for $name {
1556            fn path(&self) -> String {
1557                $path.to_string()
1558            }
1559        }
1560    };
1561}