Skip to main content

ryo_analysis/query/
specflow_v2.rs

1//! SpecFlowGraph V2 - Data-Oriented Design implementation
2//!
3//! High-performance domain semantics tracking using:
4//! - SoA (Structure of Arrays) for cache efficiency
5//! - Inverted indices for O(1) group/spec lookups
6//! - Partial String-free design (SpecAlias is String-free, Group keeps name)
7//!
8//! # Architecture
9//!
10//! ```text
11//! SpecFlowGraphV2
12//! ├── Data Storage (SoA)
13//! │   ├── groups: Vec<GroupData>           // String required (domain labels)
14//! │   ├── spec_aliases: Vec<SpecAliasData> // String-free!
15//! │   ├── constraints: Vec<ConstraintData>
16//! │   └── intents: Vec<IntentData>
17//! │
18//! ├── Edge Relations (Side Tables)
19//! │   ├── spec_to_group: Vec<u32>          // BelongsTo (1:1)
20//! │   ├── spec_dependencies: HashMap       // DependsOn (N:N)
21//! │   └── spec_to_constraints: HashMap     // ConstrainedBy (1:N)
22//! │
23//! └── Lookup Tables (Inverted Index)
24//!     ├── symbol_to_spec: HashMap<SymbolId, u32>
25//!     └── group_to_specs: Vec<SmallVec<[u32; 8]>>
26//! ```
27
28use crate::symbol::SymbolId;
29use serde::{Deserialize, Serialize};
30use smallvec::SmallVec;
31use std::collections::HashMap;
32
33// Re-use shared types
34pub use super::specflow_common::{ConstraintKind, IntentKind, SpecSource};
35
36// ============================================================================
37// Node ID Types
38// ============================================================================
39
40/// Unified node ID with kind encoded in top 2 bits.
41#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
42#[repr(transparent)]
43pub struct SpecNodeId(u32);
44
45impl SpecNodeId {
46    const KIND_SHIFT: u32 = 30;
47    const KIND_MASK: u32 = 0xC000_0000;
48    const INDEX_MASK: u32 = 0x3FFF_FFFF;
49
50    const KIND_GROUP: u32 = 0;
51    const KIND_SPEC: u32 = 1;
52    const KIND_CONSTRAINT: u32 = 2;
53    const KIND_INTENT: u32 = 3;
54
55    /// Create a group node ID.
56    #[inline]
57    pub const fn group(idx: u32) -> Self {
58        Self((Self::KIND_GROUP << Self::KIND_SHIFT) | idx)
59    }
60
61    /// Create a spec alias node ID.
62    #[inline]
63    pub const fn spec(idx: u32) -> Self {
64        Self((Self::KIND_SPEC << Self::KIND_SHIFT) | idx)
65    }
66
67    /// Create a constraint node ID.
68    #[inline]
69    pub const fn constraint(idx: u32) -> Self {
70        Self((Self::KIND_CONSTRAINT << Self::KIND_SHIFT) | idx)
71    }
72
73    /// Create an intent node ID.
74    #[inline]
75    pub const fn intent(idx: u32) -> Self {
76        Self((Self::KIND_INTENT << Self::KIND_SHIFT) | idx)
77    }
78
79    /// Get the node kind.
80    #[inline]
81    pub const fn kind(self) -> SpecNodeKind {
82        match (self.0 & Self::KIND_MASK) >> Self::KIND_SHIFT {
83            Self::KIND_GROUP => SpecNodeKind::Group,
84            Self::KIND_SPEC => SpecNodeKind::SpecAlias,
85            Self::KIND_CONSTRAINT => SpecNodeKind::Constraint,
86            Self::KIND_INTENT => SpecNodeKind::Intent,
87            _ => unreachable!(),
88        }
89    }
90
91    /// Get the raw index within the kind's array.
92    #[inline]
93    pub const fn index(self) -> u32 {
94        self.0 & Self::INDEX_MASK
95    }
96
97    /// Get raw u32 value.
98    #[inline]
99    pub const fn as_u32(self) -> u32 {
100        self.0
101    }
102}
103
104/// Node kind discriminant.
105#[derive(Copy, Clone, Eq, PartialEq, Debug)]
106pub enum SpecNodeKind {
107    /// Semantic-group node (`@spec:group(...)`).
108    Group,
109    /// Spec alias node introduced via a type alias or `@spec:` directive.
110    SpecAlias,
111    /// Constraint node (range / pattern / invariant / depends-on / custom).
112    Constraint,
113    /// Intent node carrying design/performance/security/business intent.
114    Intent,
115}
116
117// ============================================================================
118// Data Structures (SoA Components)
119// ============================================================================
120
121/// Group node data.
122/// String is required for domain semantics labels.
123#[derive(Clone, Debug, Serialize, Deserialize)]
124pub struct GroupData {
125    /// Group name (e.g., "ConfigGroup")
126    pub name: String,
127    /// Optional description
128    pub description: Option<String>,
129}
130
131/// SpecAlias node data with pre-resolved names for DoD.
132#[derive(Clone, Debug, Serialize, Deserialize)]
133pub struct SpecAliasData {
134    /// SymbolId of the type alias
135    pub alias_id: SymbolId,
136    /// Pre-resolved alias name (for DoD - no registry lookup needed)
137    pub alias_name: String,
138    /// Resolved wrapped type SymbolId
139    pub wrapped_type_id: Option<SymbolId>,
140    /// Pre-resolved wrapped type name (for DoD - no registry lookup needed)
141    pub wrapped_type_name: Option<String>,
142    /// Index into groups array
143    pub group_idx: u32,
144    /// Source of this spec
145    pub source: SpecSource,
146}
147
148/// Constraint node data.
149/// String is required for domain constraint expressions.
150#[derive(Clone, Debug, Serialize, Deserialize)]
151pub struct ConstraintData {
152    /// Constraint kind (contains String for patterns/invariants)
153    pub kind: ConstraintKind,
154}
155
156/// Intent node data.
157/// String is required for human-readable descriptions.
158#[derive(Clone, Debug, Serialize, Deserialize)]
159pub struct IntentData {
160    /// Intent description
161    pub description: String,
162    /// Intent kind
163    pub kind: IntentKind,
164}
165
166// ============================================================================
167// Lookup Table
168// ============================================================================
169
170/// Inverted indices for O(1) lookups.
171#[derive(Clone, Debug, Default, Serialize, Deserialize)]
172pub struct SpecLookupTable {
173    /// SymbolId → SpecAlias index (for specs with resolved alias_id)
174    pub symbol_to_spec: HashMap<SymbolId, u32>,
175
176    /// Group name → Group index
177    pub name_to_group: HashMap<String, u32>,
178
179    /// Group index → SpecAlias indices (inverted BelongsTo)
180    pub group_to_specs: Vec<SmallVec<[u32; 8]>>,
181}
182
183// ============================================================================
184// SpecFlowGraphV2 - Main Structure
185// ============================================================================
186
187/// SpecFlowGraph V2 - SoA + Side Tables design.
188///
189/// NOTE: Deserialize is NOT derived because SpecFlowGraphV2 contains
190/// HashMap<SymbolId, ...>. SymbolId is process-specific.
191/// Serialize is kept for debugging/inspection.
192#[derive(Clone, Debug, Default, Serialize)]
193pub struct SpecFlowGraphV2 {
194    // =========================================================================
195    // Data Storage (SoA)
196    // =========================================================================
197    /// Group nodes
198    groups: Vec<GroupData>,
199
200    /// SpecAlias nodes (String-free!)
201    spec_aliases: Vec<SpecAliasData>,
202
203    /// Constraint nodes
204    constraints: Vec<ConstraintData>,
205
206    /// Intent nodes
207    intents: Vec<IntentData>,
208
209    // =========================================================================
210    // Edge Relations (Side Tables)
211    // =========================================================================
212    /// SpecAlias → SpecAlias dependencies (DependsOn)
213    spec_dependencies: HashMap<u32, SmallVec<[u32; 2]>>,
214
215    /// SpecAlias → Constraint indices (ConstrainedBy)
216    spec_to_constraints: HashMap<u32, SmallVec<[u32; 2]>>,
217
218    /// SpecAlias → Intent indices (HasIntent)
219    spec_to_intents: HashMap<u32, SmallVec<[u32; 2]>>,
220
221    /// SpecAlias → SpecAlias related (RelatedTo, bidirectional)
222    spec_related: HashMap<u32, SmallVec<[u32; 2]>>,
223
224    // =========================================================================
225    // Lookup Tables
226    // =========================================================================
227    lookup: SpecLookupTable,
228}
229
230impl SpecFlowGraphV2 {
231    /// Create a new empty graph.
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    // =========================================================================
237    // Node Addition
238    // =========================================================================
239
240    /// Add a group, returning its ID. Reuses existing if name matches.
241    pub fn add_group(
242        &mut self,
243        name: impl Into<String>,
244        description: Option<String>,
245    ) -> SpecNodeId {
246        let name = name.into();
247
248        // Check if exists
249        if let Some(&idx) = self.lookup.name_to_group.get(&name) {
250            // Update description if provided
251            if description.is_some() {
252                self.groups[idx as usize].description = description;
253            }
254            return SpecNodeId::group(idx);
255        }
256
257        // Add new
258        let idx = self.groups.len() as u32;
259        self.groups.push(GroupData {
260            name: name.clone(),
261            description,
262        });
263        self.lookup.name_to_group.insert(name, idx);
264        self.lookup.group_to_specs.push(SmallVec::new());
265
266        SpecNodeId::group(idx)
267    }
268
269    /// Add a spec alias.
270    pub fn add_spec_alias(
271        &mut self,
272        alias_id: SymbolId,
273        alias_name: String,
274        group_name: &str,
275        wrapped_type_id: Option<SymbolId>,
276        wrapped_type_name: Option<String>,
277        source: SpecSource,
278    ) -> SpecNodeId {
279        // Ensure group exists
280        let group_node = self.add_group(group_name, None);
281        let group_idx = group_node.index();
282
283        // Add spec
284        let spec_idx = self.spec_aliases.len() as u32;
285        self.spec_aliases.push(SpecAliasData {
286            alias_id,
287            alias_name,
288            wrapped_type_id,
289            wrapped_type_name,
290            group_idx,
291            source,
292        });
293
294        // Update lookup tables
295        self.lookup.symbol_to_spec.insert(alias_id, spec_idx);
296        self.lookup.group_to_specs[group_idx as usize].push(spec_idx);
297
298        SpecNodeId::spec(spec_idx)
299    }
300
301    /// Add a constraint.
302    pub fn add_constraint(&mut self, kind: ConstraintKind) -> SpecNodeId {
303        let idx = self.constraints.len() as u32;
304        self.constraints.push(ConstraintData { kind });
305        SpecNodeId::constraint(idx)
306    }
307
308    /// Add an intent.
309    pub fn add_intent(&mut self, description: impl Into<String>, kind: IntentKind) -> SpecNodeId {
310        let idx = self.intents.len() as u32;
311        self.intents.push(IntentData {
312            description: description.into(),
313            kind,
314        });
315        SpecNodeId::intent(idx)
316    }
317
318    // =========================================================================
319    // Edge Addition
320    // =========================================================================
321
322    /// Add dependency: from depends on to.
323    pub fn add_dependency(&mut self, from: SpecNodeId, to: SpecNodeId) {
324        debug_assert!(from.kind() == SpecNodeKind::SpecAlias);
325        debug_assert!(to.kind() == SpecNodeKind::SpecAlias);
326        self.spec_dependencies
327            .entry(from.index())
328            .or_default()
329            .push(to.index());
330    }
331
332    /// Add related edge (bidirectional).
333    pub fn add_related(&mut self, a: SpecNodeId, b: SpecNodeId) {
334        debug_assert!(a.kind() == SpecNodeKind::SpecAlias);
335        debug_assert!(b.kind() == SpecNodeKind::SpecAlias);
336        self.spec_related
337            .entry(a.index())
338            .or_default()
339            .push(b.index());
340        self.spec_related
341            .entry(b.index())
342            .or_default()
343            .push(a.index());
344    }
345
346    /// Link constraint to spec.
347    pub fn link_constraint(&mut self, spec: SpecNodeId, constraint: SpecNodeId) {
348        debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
349        debug_assert!(constraint.kind() == SpecNodeKind::Constraint);
350        self.spec_to_constraints
351            .entry(spec.index())
352            .or_default()
353            .push(constraint.index());
354    }
355
356    /// Link intent to spec.
357    pub fn link_intent(&mut self, spec: SpecNodeId, intent: SpecNodeId) {
358        debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
359        debug_assert!(intent.kind() == SpecNodeKind::Intent);
360        self.spec_to_intents
361            .entry(spec.index())
362            .or_default()
363            .push(intent.index());
364    }
365
366    // =========================================================================
367    // Query Methods
368    // =========================================================================
369
370    /// Get group by name. O(1)
371    pub fn group_by_name(&self, name: &str) -> Option<SpecNodeId> {
372        self.lookup
373            .name_to_group
374            .get(name)
375            .map(|&idx| SpecNodeId::group(idx))
376    }
377
378    /// Get spec by SymbolId. O(1)
379    pub fn spec_by_symbol(&self, id: SymbolId) -> Option<SpecNodeId> {
380        self.lookup
381            .symbol_to_spec
382            .get(&id)
383            .map(|&idx| SpecNodeId::spec(idx))
384    }
385
386    /// Get all specs in a group. O(1)
387    pub fn specs_in_group(&self, group: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
388        debug_assert!(group.kind() == SpecNodeKind::Group);
389        self.lookup
390            .group_to_specs
391            .get(group.index() as usize)
392            .into_iter()
393            .flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
394    }
395
396    /// Get dependencies of a spec. O(1)
397    pub fn dependencies(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
398        debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
399        self.spec_dependencies
400            .get(&spec.index())
401            .into_iter()
402            .flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
403    }
404
405    /// Get constraints of a spec. O(1)
406    pub fn constraints(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
407        debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
408        self.spec_to_constraints
409            .get(&spec.index())
410            .into_iter()
411            .flat_map(|v| v.iter().map(|&idx| SpecNodeId::constraint(idx)))
412    }
413
414    // =========================================================================
415    // Data Access
416    // =========================================================================
417
418    /// Get group data.
419    pub fn get_group(&self, id: SpecNodeId) -> Option<&GroupData> {
420        debug_assert!(id.kind() == SpecNodeKind::Group);
421        self.groups.get(id.index() as usize)
422    }
423
424    /// Get spec alias data.
425    pub fn get_spec_alias(&self, id: SpecNodeId) -> Option<&SpecAliasData> {
426        debug_assert!(id.kind() == SpecNodeKind::SpecAlias);
427        self.spec_aliases.get(id.index() as usize)
428    }
429
430    /// Get constraint data.
431    pub fn get_constraint(&self, id: SpecNodeId) -> Option<&ConstraintData> {
432        debug_assert!(id.kind() == SpecNodeKind::Constraint);
433        self.constraints.get(id.index() as usize)
434    }
435
436    /// Get intent data.
437    pub fn get_intent(&self, id: SpecNodeId) -> Option<&IntentData> {
438        debug_assert!(id.kind() == SpecNodeKind::Intent);
439        self.intents.get(id.index() as usize)
440    }
441
442    // =========================================================================
443    // Iteration
444    // =========================================================================
445
446    /// Iterate all groups.
447    pub fn all_groups(&self) -> impl Iterator<Item = (SpecNodeId, &GroupData)> {
448        self.groups
449            .iter()
450            .enumerate()
451            .map(|(i, data)| (SpecNodeId::group(i as u32), data))
452    }
453
454    /// Iterate all spec aliases.
455    pub fn all_spec_aliases(&self) -> impl Iterator<Item = (SpecNodeId, &SpecAliasData)> {
456        self.spec_aliases
457            .iter()
458            .enumerate()
459            .map(|(i, data)| (SpecNodeId::spec(i as u32), data))
460    }
461
462    // =========================================================================
463    // Statistics
464    // =========================================================================
465
466    /// Number of groups.
467    pub fn group_count(&self) -> usize {
468        self.groups.len()
469    }
470
471    /// Number of spec aliases.
472    pub fn spec_count(&self) -> usize {
473        self.spec_aliases.len()
474    }
475
476    /// Number of constraints.
477    pub fn constraint_count(&self) -> usize {
478        self.constraints.len()
479    }
480
481    /// Total node count.
482    pub fn node_count(&self) -> usize {
483        self.groups.len() + self.spec_aliases.len() + self.constraints.len() + self.intents.len()
484    }
485
486    /// Check if empty.
487    pub fn is_empty(&self) -> bool {
488        self.node_count() == 0
489    }
490
491    // =========================================================================
492    // CLI Compatibility Methods
493    // =========================================================================
494
495    /// Get all group names. O(N groups)
496    pub fn group_names(&self) -> impl Iterator<Item = &str> {
497        self.groups.iter().map(|g| g.name.as_str())
498    }
499
500    /// Get specs in a group by name. O(1)
501    pub fn specs_in_group_by_name(&self, name: &str) -> impl Iterator<Item = SpecNodeId> + '_ {
502        self.lookup
503            .name_to_group
504            .get(name)
505            .and_then(|&idx| self.lookup.group_to_specs.get(idx as usize))
506            .into_iter()
507            .flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
508    }
509
510    /// Get dependents of a spec (reverse dependencies). O(N specs)
511    pub fn dependents(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
512        debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
513        let target_idx = spec.index();
514        self.spec_dependencies
515            .iter()
516            .filter(move |(_, deps)| deps.contains(&target_idx))
517            .map(|(&from_idx, _)| SpecNodeId::spec(from_idx))
518    }
519
520    /// Total edge count.
521    pub fn edge_count(&self) -> usize {
522        let dep_edges: usize = self.spec_dependencies.values().map(|v| v.len()).sum();
523        let constraint_edges: usize = self.spec_to_constraints.values().map(|v| v.len()).sum();
524        let intent_edges: usize = self.spec_to_intents.values().map(|v| v.len()).sum();
525        let related_edges: usize = self.spec_related.values().map(|v| v.len()).sum();
526        let belongs_to_edges = self.spec_aliases.len(); // Each spec belongs to exactly one group
527
528        dep_edges + constraint_edges + intent_edges + related_edges + belongs_to_edges
529    }
530
531    /// Get related specs (bidirectional). O(1)
532    pub fn related(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
533        debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
534        self.spec_related
535            .get(&spec.index())
536            .into_iter()
537            .flat_map(|v| v.iter().map(|&idx| SpecNodeId::spec(idx)))
538    }
539
540    /// Get intents of a spec. O(1)
541    pub fn intents(&self, spec: SpecNodeId) -> impl Iterator<Item = SpecNodeId> + '_ {
542        debug_assert!(spec.kind() == SpecNodeKind::SpecAlias);
543        self.spec_to_intents
544            .get(&spec.index())
545            .into_iter()
546            .flat_map(|v| v.iter().map(|&idx| SpecNodeId::intent(idx)))
547    }
548
549    // =========================================================================
550    // Name Resolution (DoD - pre-resolved, no registry needed)
551    // =========================================================================
552
553    /// Get spec alias name (pre-resolved during build).
554    pub fn spec_name(&self, id: SpecNodeId) -> Option<&str> {
555        self.get_spec_alias(id).map(|data| data.alias_name.as_str())
556    }
557
558    /// Get wrapped type name (pre-resolved during build).
559    pub fn wrapped_type_name(&self, id: SpecNodeId) -> Option<&str> {
560        self.get_spec_alias(id)
561            .and_then(|data| data.wrapped_type_name.as_deref())
562    }
563
564    /// Get group name for a spec alias.
565    pub fn spec_group_name(&self, id: SpecNodeId) -> Option<&str> {
566        self.get_spec_alias(id)
567            .and_then(|data| self.groups.get(data.group_idx as usize))
568            .map(|g| g.name.as_str())
569    }
570
571    /// Find spec by name (DoD - uses pre-resolved names). O(N specs)
572    pub fn spec_by_name(&self, name: &str) -> Option<SpecNodeId> {
573        for (id, data) in self.all_spec_aliases() {
574            if data.alias_name == name {
575                return Some(id);
576            }
577        }
578        None
579    }
580}
581
582// ============================================================================
583// SpecFlowBuilderV2 - Build from TypeAliasRegistry
584// ============================================================================
585
586use super::type_alias_registry::TypeAliasRegistry;
587use crate::symbol::SymbolRegistry;
588
589/// Builds SpecFlowGraphV2 from TypeAliasRegistry.
590///
591/// # Example
592///
593/// ```ignore
594/// let builder = SpecFlowBuilderV2::new(&alias_registry, &symbol_registry);
595/// let spec_graph = builder.build();
596/// ```
597pub struct SpecFlowBuilderV2<'a> {
598    alias_registry: &'a TypeAliasRegistry,
599    symbol_registry: &'a SymbolRegistry,
600}
601
602impl<'a> SpecFlowBuilderV2<'a> {
603    /// Create a new builder.
604    pub fn new(alias_registry: &'a TypeAliasRegistry, symbol_registry: &'a SymbolRegistry) -> Self {
605        Self {
606            alias_registry,
607            symbol_registry,
608        }
609    }
610
611    /// Build SpecFlowGraphV2 from TypeAliasRegistry.
612    pub fn build(self) -> SpecFlowGraphV2 {
613        let mut graph = SpecFlowGraphV2::new();
614
615        // Extract Spec<G, T> patterns from TypeAliasRegistry
616        for spec_info in self.alias_registry.spec_aliases() {
617            // Pre-resolve names during build (DoD)
618            let alias_name = self
619                .symbol_registry
620                .resolve(spec_info.entry.alias_id)
621                .map(|path| path.name().to_string())
622                .unwrap_or_default();
623
624            let wrapped_type_name = spec_info.entry.resolved.and_then(|wrapped_id| {
625                self.symbol_registry
626                    .resolve(wrapped_id)
627                    .map(|path| path.to_string())
628            });
629
630            graph.add_spec_alias(
631                spec_info.entry.alias_id,
632                alias_name,
633                &spec_info.group_name,
634                spec_info.entry.resolved,
635                wrapped_type_name,
636                SpecSource::TypeAlias,
637            );
638        }
639
640        graph
641    }
642}
643
644// ============================================================================
645// Tests
646// ============================================================================
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use crate::symbol::{SymbolPath, SymbolRegistry};
652    use crate::SymbolKind;
653
654    fn create_test_symbol(registry: &mut SymbolRegistry, name: &str) -> SymbolId {
655        registry
656            .register(SymbolPath::parse(name).unwrap(), SymbolKind::TypeAlias)
657            .unwrap()
658    }
659
660    #[test]
661    fn test_empty_graph() {
662        let graph = SpecFlowGraphV2::new();
663        assert!(graph.is_empty());
664        assert_eq!(graph.node_count(), 0);
665    }
666
667    #[test]
668    fn test_add_group() {
669        let mut graph = SpecFlowGraphV2::new();
670        let g1 = graph.add_group("ConfigGroup", Some("Config types".to_string()));
671        let g2 = graph.add_group("ConfigGroup", None); // Should return same
672
673        assert_eq!(g1, g2);
674        assert_eq!(graph.group_count(), 1);
675
676        let data = graph.get_group(g1).unwrap();
677        assert_eq!(data.name, "ConfigGroup");
678        assert_eq!(data.description, Some("Config types".to_string()));
679    }
680
681    #[test]
682    fn test_add_spec_alias() {
683        let mut symbol_registry = SymbolRegistry::new();
684        let alias_id = create_test_symbol(&mut symbol_registry, "test::DbConfig");
685        let wrapped_id = create_test_symbol(&mut symbol_registry, "test::DatabaseConfig");
686
687        let mut graph = SpecFlowGraphV2::new();
688        let spec = graph.add_spec_alias(
689            alias_id,
690            "DbConfig".to_string(),
691            "ConfigGroup",
692            Some(wrapped_id),
693            Some("test::DatabaseConfig".to_string()),
694            SpecSource::TypeAlias,
695        );
696
697        assert_eq!(graph.spec_count(), 1);
698        assert_eq!(graph.group_count(), 1); // Auto-created
699
700        // Check spec data
701        let data = graph.get_spec_alias(spec).unwrap();
702        assert_eq!(data.alias_id, alias_id);
703        assert_eq!(data.wrapped_type_id, Some(wrapped_id));
704
705        // Check lookup
706        assert_eq!(graph.spec_by_symbol(alias_id), Some(spec));
707
708        // Check group membership
709        let group = graph.group_by_name("ConfigGroup").unwrap();
710        let specs: Vec<_> = graph.specs_in_group(group).collect();
711        assert_eq!(specs.len(), 1);
712        assert_eq!(specs[0], spec);
713    }
714
715    #[test]
716    fn test_dependencies() {
717        let mut symbol_registry = SymbolRegistry::new();
718        let db_id = create_test_symbol(&mut symbol_registry, "test::DbConfig");
719        let cache_id = create_test_symbol(&mut symbol_registry, "test::CacheConfig");
720
721        let mut graph = SpecFlowGraphV2::new();
722        let db_spec = graph.add_spec_alias(
723            db_id,
724            "DbConfig".to_string(),
725            "ConfigGroup",
726            None,
727            None,
728            SpecSource::TypeAlias,
729        );
730        let cache_spec = graph.add_spec_alias(
731            cache_id,
732            "CacheConfig".to_string(),
733            "ConfigGroup",
734            None,
735            None,
736            SpecSource::TypeAlias,
737        );
738
739        // CacheConfig depends on DbConfig
740        graph.add_dependency(cache_spec, db_spec);
741
742        let deps: Vec<_> = graph.dependencies(cache_spec).collect();
743        assert_eq!(deps.len(), 1);
744        assert_eq!(deps[0], db_spec);
745    }
746
747    #[test]
748    fn test_constraints() {
749        let mut symbol_registry = SymbolRegistry::new();
750        let id = create_test_symbol(&mut symbol_registry, "test::UserId");
751
752        let mut graph = SpecFlowGraphV2::new();
753        let spec = graph.add_spec_alias(
754            id,
755            "UserId".to_string(),
756            "DomainGroup",
757            None,
758            None,
759            SpecSource::TypeAlias,
760        );
761        let constraint = graph.add_constraint(ConstraintKind::Range {
762            min: Some(1),
763            max: None,
764        });
765
766        graph.link_constraint(spec, constraint);
767
768        let constraints: Vec<_> = graph.constraints(spec).collect();
769        assert_eq!(constraints.len(), 1);
770
771        let data = graph.get_constraint(constraints[0]).unwrap();
772        assert!(matches!(
773            data.kind,
774            ConstraintKind::Range {
775                min: Some(1),
776                max: None
777            }
778        ));
779    }
780}