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