Skip to main content

serializer/llm/
types.rs

1//! Core data types for LLM and Human format serialization
2//!
3//! This module defines the value types for the DX **LLM format** (text, token-efficient).
4//! The LLM format is optimized for minimal token usage in LLM context windows,
5//! achieving 73%+ token savings compared to JSON.
6//!
7//! # Format Comparison
8//!
9//! DX provides two serialization formats:
10//!
11//! | Format | Type | Use Case | Performance |
12//! |--------|------|----------|-------------|
13//! | **Machine** | [`DxValue`](crate::DxValue) | Binary, zero-copy, runtime | RKYV format |
14//! | **LLM** | [`DxLlmValue`] | Text, token-efficient, LLM context | 73%+ token savings |
15//!
16//! # When to Use This Module
17//!
18//! Use [`DxLlmValue`] and the LLM format when:
19//! - You're preparing data for LLM context windows
20//! - Token efficiency is critical (API costs, context limits)
21//! - You need human-readable output
22//! - You're converting to/from JSON, YAML, or TOML
23//!
24//! Use [`DxValue`](crate::DxValue) when:
25//! - You need binary serialization with RKYV
26//! - You're working with binary data or network protocols
27//! - You want zero-copy deserialization
28//!
29//! # Document Structure
30//!
31//! The LLM format uses a document-based structure:
32//!
33//! - [`DxDocument`]: Top-level container with context, refs, and sections
34//! - [`DxSection`]: A data section with schema and rows (tabular data)
35//! - [`DxLlmValue`]: Individual values within sections
36//!
37//! # Thread Safety
38//!
39//! All types in this module implement `Send + Sync` and can be safely shared
40//! between threads:
41//!
42//! - [`DxDocument`]: The main document type, safe for concurrent read access
43//! - [`DxSection`]: Section data, safe for concurrent read access
44//! - [`DxLlmValue`]: Value enum, all variants are thread-safe
45//!
46//! For mutation, clone the document or use appropriate synchronization primitives
47//! like `Arc<Mutex<DxDocument>>` or `Arc<RwLock<DxDocument>>`.
48
49use indexmap::IndexMap;
50use std::fmt;
51
52/// Top-level document container for the DX LLM format.
53///
54/// A `DxDocument` represents a complete DX document in the LLM format,
55/// containing context configuration, reference definitions, and data sections.
56///
57/// # Structure
58///
59/// ```text
60/// #c context_key=value          // Context section
61/// #: ref_key=ref_value          // Reference definitions
62/// #d(id,name)[                  // Data section 'd' with schema
63///   1,Alice
64///   2,Bob
65/// ]
66/// ```
67///
68/// # Examples
69///
70/// ## Creating a Document
71///
72/// ```rust
73/// use serializer::llm::{DxDocument, DxSection, DxLlmValue};
74///
75/// let mut doc = DxDocument::new();
76///
77/// // Add context
78/// doc.context.insert("version".to_string(), DxLlmValue::Str("1.0".to_string()));
79///
80/// // Add a reference
81/// doc.refs.insert("company".to_string(), "Acme Corp".to_string());
82///
83/// // Add a data section
84/// let mut section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
85/// section.add_row(vec![DxLlmValue::Num(1.0), DxLlmValue::Str("Alice".to_string())]).unwrap();
86/// doc.sections.insert('d', section);
87/// ```
88///
89/// # Thread Safety
90///
91/// `DxDocument` implements `Send + Sync` and can be safely shared between threads
92/// for concurrent read access. For mutation, use appropriate synchronization.
93///
94/// # See Also
95///
96/// - [`DxSection`] - Data section with schema and rows
97/// - [`DxLlmValue`] - Value type for section data
98#[derive(Debug, Clone, PartialEq)]
99pub struct DxDocument {
100    /// Context/config section (`#c`).
101    ///
102    /// Contains configuration key-value pairs that apply to the entire document.
103    pub context: IndexMap<String, DxLlmValue>,
104    /// Reference definitions (`#:`).
105    ///
106    /// Defines reusable string values that can be referenced with `^key` syntax.
107    /// This enables deduplication of repeated values for token efficiency.
108    pub refs: IndexMap<String, String>,
109    /// Data sections (`#<letter>`).
110    ///
111    /// Each section is identified by a single character (e.g., 'd' for `#d`).
112    /// Sections contain tabular data with a schema and rows.
113    pub sections: IndexMap<char, DxSection>,
114    /// Section names mapping (char ID -> full name like "dependencies")
115    ///
116    /// Maps single-character section IDs to their full names for human-readable output.
117    pub section_names: IndexMap<char, String>,
118    /// Entry order tracking
119    ///
120    /// Tracks the order of all entries (context keys and section IDs) as they appear
121    /// in the original document. Each entry is either a context key or a section ID.
122    pub entry_order: Vec<EntryRef>,
123}
124
125/// Reference to an entry in the document (context or section)
126#[derive(Debug, Clone, PartialEq)]
127pub enum EntryRef {
128    /// Reference to a context entry by key
129    Context(String),
130    /// Reference to a section by its char ID
131    Section(char),
132}
133
134impl DxDocument {
135    /// Create a new empty document
136    #[must_use]
137    pub fn new() -> Self {
138        Self {
139            context: IndexMap::new(),
140            refs: IndexMap::new(),
141            sections: IndexMap::new(),
142            section_names: IndexMap::new(),
143            entry_order: Vec::new(),
144        }
145    }
146
147    /// Check if the document is empty
148    #[must_use]
149    pub fn is_empty(&self) -> bool {
150        self.context.is_empty() && self.refs.is_empty() && self.sections.is_empty()
151    }
152
153    /// Get a value by canonical LLM path.
154    ///
155    /// This supports both root keys (`project`) and object paths
156    /// (`project.name`) where the root value is an inline object such as
157    /// `project(name=dx-devtools version=0.1.0)`.
158    #[must_use]
159    pub fn get_path(&self, path: &str) -> Option<&DxLlmValue> {
160        if let Some(value) = self.context.get(path) {
161            return Some(value);
162        }
163
164        let (root, nested_path) = path.split_once('.')?;
165        let mut current = self.context.get(root)?;
166        for segment in nested_path.split('.') {
167            match current {
168                DxLlmValue::Obj(fields) => {
169                    current = fields.get(segment)?;
170                }
171                _ => return None,
172            }
173        }
174        Some(current)
175    }
176
177    /// Find a wrapped dataframe/table section by its full section name.
178    #[must_use]
179    pub fn section_by_name(&self, name: &str) -> Option<&DxSection> {
180        self.section_names
181            .iter()
182            .find_map(|(section_id, section_name)| {
183                (section_name == name)
184                    .then(|| self.sections.get(section_id))
185                    .flatten()
186            })
187    }
188}
189
190impl Default for DxDocument {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196/// A data section with schema-defined columns and rows.
197///
198/// `DxSection` represents tabular data in the DX LLM format. Each section
199/// has a schema (column names) and rows of [`DxLlmValue`] data.
200///
201/// # LLM Format Syntax
202///
203/// ```text
204/// #d(id,name,active)[    // Section 'd' with schema: id, name, active
205///   1,Alice,+            // Row 1
206///   2,Bob,-              // Row 2
207/// ]
208/// ```
209///
210/// # Examples
211///
212/// ## Creating a Section
213///
214/// ```rust
215/// use serializer::llm::{DxSection, DxLlmValue};
216///
217/// let mut section = DxSection::new(vec![
218///     "id".to_string(),
219///     "name".to_string(),
220///     "active".to_string(),
221/// ]);
222///
223/// // Add rows (must match schema length)
224/// section.add_row(vec![
225///     DxLlmValue::Num(1.0),
226///     DxLlmValue::Str("Alice".to_string()),
227///     DxLlmValue::Bool(true),
228/// ]).unwrap();
229///
230/// assert_eq!(section.row_count(), 1);
231/// assert_eq!(section.column_count(), 3);
232/// ```
233///
234/// ## Error Handling
235///
236/// ```rust
237/// use serializer::llm::{DxSection, DxLlmValue};
238///
239/// let mut section = DxSection::new(vec!["a".to_string(), "b".to_string()]);
240///
241/// // Wrong number of columns returns an error
242/// let result = section.add_row(vec![DxLlmValue::Num(1.0)]);
243/// assert!(result.is_err());
244/// ```
245///
246/// # Thread Safety
247///
248/// `DxSection` implements `Send + Sync` and can be safely shared between threads.
249///
250/// # See Also
251///
252/// - [`DxDocument`] - Parent container for sections
253/// - [`DxLlmValue`] - Value type for row data
254#[derive(Debug, Clone, PartialEq)]
255pub struct DxSection {
256    /// Column names from the schema.
257    ///
258    /// Defines the structure of each row. All rows must have exactly
259    /// `schema.len()` values.
260    pub schema: Vec<String>,
261    /// Row data.
262    ///
263    /// Each row is a vector of [`DxLlmValue`] with length matching the schema.
264    pub rows: Vec<Vec<DxLlmValue>>,
265}
266
267impl DxSection {
268    /// Create a new section with the given schema
269    #[must_use]
270    pub fn new(schema: Vec<String>) -> Self {
271        Self {
272            schema,
273            rows: Vec::new(),
274        }
275    }
276
277    /// Add a row to the section
278    ///
279    /// # Errors
280    ///
281    /// Returns an error if the row length doesn't match the schema length.
282    pub fn add_row(&mut self, row: Vec<DxLlmValue>) -> Result<(), String> {
283        if row.len() != self.schema.len() {
284            return Err(format!(
285                "Row length {} doesn't match schema length {}",
286                row.len(),
287                self.schema.len()
288            ));
289        }
290        self.rows.push(row);
291        Ok(())
292    }
293
294    /// Get the number of rows
295    #[must_use]
296    pub fn row_count(&self) -> usize {
297        self.rows.len()
298    }
299
300    /// Get the zero-based column index by schema name.
301    #[must_use]
302    pub fn column_index(&self, name: &str) -> Option<usize> {
303        self.schema.iter().position(|column| column == name)
304    }
305
306    /// Return all values from a named column.
307    #[must_use]
308    pub fn column_values(&self, name: &str) -> Option<Vec<&DxLlmValue>> {
309        let column_index = self.column_index(name)?;
310        Some(
311            self.rows
312                .iter()
313                .filter_map(|row| row.get(column_index))
314                .collect(),
315        )
316    }
317
318    /// Find a value by matching one column and returning another.
319    #[must_use]
320    pub fn value_by_key(
321        &self,
322        key_column: &str,
323        key: &str,
324        value_column: &str,
325    ) -> Option<&DxLlmValue> {
326        let key_index = self.column_index(key_column)?;
327        let value_index = self.column_index(value_column)?;
328        self.rows.iter().find_map(|row| {
329            let row_key = row.get(key_index)?.to_string();
330            (row_key == key).then(|| row.get(value_index)).flatten()
331        })
332    }
333
334    /// Get the number of columns
335    #[must_use]
336    pub fn column_count(&self) -> usize {
337        self.schema.len()
338    }
339}
340
341/// Value type for the DX **LLM format** (text, token-efficient).
342///
343/// `DxLlmValue` represents all possible values in the DX LLM format, optimized
344/// for minimal token usage in LLM context windows. This format achieves 73%+
345/// token savings compared to JSON.
346///
347/// # Relationship to DxValue
348///
349/// DX provides two value types for different use cases:
350///
351/// - **[`DxValue`](crate::DxValue)**: For the binary machine format. Use when you need
352///   maximum performance, zero-copy parsing, or runtime data structures.
353///
354/// - **`DxLlmValue`** (this type): For the text LLM format. Use when preparing
355///   data for LLM context windows or when token efficiency matters.
356///
357/// ## Key Differences
358///
359/// | Aspect | `DxValue` | `DxLlmValue` |
360/// |--------|-----------|--------------|
361/// | Format | Binary | Text (Dx Serializer) |
362/// | Numbers | Separate `Int`/`Float` | Single `Num` |
363/// | Structured data | `Object`, `Table` | `Obj` variant or `DxSection` |
364/// | References | `Ref(usize)` | `Ref(String)` |
365/// | Optimization | Speed | Token count |
366///
367/// The type differences reflect the different use cases:
368/// - LLMs don't distinguish integer vs float, so `Num` is sufficient
369/// - LLM format uses string-based references for readability
370/// - Inline objects use `Obj` variant, tabular data uses `DxSection`
371///
372/// # When to Use DxLlmValue
373///
374/// Choose `DxLlmValue` when:
375/// - **Token efficiency matters**: 73%+ savings vs JSON reduces API costs
376/// - **LLM context limits**: Fit more data in limited context windows
377/// - **Human readability**: Text format is easier to inspect and debug
378/// - **Format conversion**: Converting to/from JSON, YAML, TOML
379///
380/// # Examples
381///
382/// ## Creating Values
383///
384/// ```rust
385/// use serializer::llm::DxLlmValue;
386/// use std::collections::HashMap;
387///
388/// // Primitive values
389/// let null = DxLlmValue::Null;
390/// let boolean = DxLlmValue::Bool(true);
391/// let number = DxLlmValue::Num(42.0);
392/// let string = DxLlmValue::Str("hello".to_string());
393///
394/// // Object value
395/// let mut fields = HashMap::new();
396/// fields.insert("host".to_string(), DxLlmValue::Str("localhost".to_string()));
397/// fields.insert("port".to_string(), DxLlmValue::Num(8080.0));
398/// let obj = DxLlmValue::Obj(fields);
399/// ```
400///
401/// ## Using From Traits
402///
403/// ```rust
404/// use serializer::llm::DxLlmValue;
405///
406/// // Convenient conversions
407/// let from_str: DxLlmValue = "hello".into();
408/// let from_string: DxLlmValue = String::from("world").into();
409/// let from_int: DxLlmValue = 42i64.into();
410/// let from_float: DxLlmValue = 3.14f64.into();
411/// let from_bool: DxLlmValue = true.into();
412/// ```
413///
414/// ## Working with Arrays
415///
416/// ```rust
417/// use serializer::llm::DxLlmValue;
418///
419/// let arr = DxLlmValue::Arr(vec![
420///     DxLlmValue::Num(1.0),
421///     DxLlmValue::Num(2.0),
422///     DxLlmValue::Num(3.0),
423/// ]);
424///
425/// if let DxLlmValue::Arr(values) = &arr {
426///     assert_eq!(values.len(), 3);
427/// }
428/// ```
429///
430/// ## Type Inspection
431///
432/// ```rust
433/// use serializer::llm::DxLlmValue;
434///
435/// let value = DxLlmValue::Num(42.0);
436/// assert_eq!(value.type_name(), "number");
437/// assert_eq!(value.as_num(), Some(42.0));
438/// assert!(value.as_str().is_none());
439/// ```
440///
441/// ## Display Formatting
442///
443/// ```rust
444/// use serializer::llm::DxLlmValue;
445///
446/// // Values format nicely for display
447/// assert_eq!(format!("{}", DxLlmValue::Num(42.0)), "42");
448/// assert_eq!(format!("{}", DxLlmValue::Bool(true)), "true");
449/// assert_eq!(format!("{}", DxLlmValue::Null), "null");
450/// assert_eq!(format!("{}", DxLlmValue::Ref("A".to_string())), "^A");
451/// ```
452///
453/// # Thread Safety
454///
455/// `DxLlmValue` implements `Send + Sync` and can be safely shared between threads.
456/// This is verified at compile time via static assertions.
457///
458/// # See Also
459///
460/// - [`DxValue`](crate::DxValue) - Value type for the binary machine format
461/// - [`DxDocument`] - Top-level document container for LLM format
462/// - [`DxSection`] - Section with schema and rows for tabular data
463#[derive(Debug, Clone, PartialEq)]
464pub enum DxLlmValue {
465    /// String value.
466    ///
467    /// In LLM format, strings are represented without quotes when possible,
468    /// contributing to token efficiency.
469    Str(String),
470    /// Numeric value (integer or float).
471    ///
472    /// Unlike [`DxValue`](crate::DxValue) which has separate `Int` and `Float`
473    /// variants, `DxLlmValue` uses a single `Num` variant because LLMs don't
474    /// distinguish between integer and floating-point numbers.
475    ///
476    /// Integers are stored as `f64` but display without decimal points when
477    /// the fractional part is zero (e.g., `42` not `42.0`).
478    Num(f64),
479    /// Boolean value.
480    ///
481    /// In LLM format: `true` or `false`
482    /// In Human format: `true` or `false`
483    Bool(bool),
484    /// Null value.
485    ///
486    /// In LLM format: `null`
487    /// In Human format: `null`
488    Null,
489    /// Array value.
490    ///
491    /// In LLM format: `*a,b,c` (inline array syntax)
492    Arr(Vec<DxLlmValue>),
493    /// Object value with key-value pairs.
494    ///
495    /// In LLM format: `name[key=value,key2=value2]` (inline object syntax)
496    ///
497    /// Objects are stored as an IndexMap for efficient key lookup while
498    /// maintaining insertion order and type safety. This is preferred over encoding objects
499    /// as strings, which would lose type information.
500    Obj(IndexMap<String, DxLlmValue>),
501    /// Reference pointer to a defined ref.
502    ///
503    /// In LLM format: `^key` references a value defined in the `#:` refs section.
504    /// This enables deduplication of repeated values, further reducing token count.
505    ///
506    /// Unlike [`DxValue::Ref`](crate::DxValue::Ref) which uses numeric indices,
507    /// `DxLlmValue::Ref` uses string keys for human readability.
508    Ref(String),
509}
510
511impl DxLlmValue {
512    /// Check if this value is null
513    #[must_use]
514    pub fn is_null(&self) -> bool {
515        matches!(self, DxLlmValue::Null)
516    }
517
518    /// Get the type name for error messages
519    #[must_use]
520    pub fn type_name(&self) -> &'static str {
521        match self {
522            DxLlmValue::Str(_) => "string",
523            DxLlmValue::Num(_) => "number",
524            DxLlmValue::Bool(_) => "bool",
525            DxLlmValue::Null => "null",
526            DxLlmValue::Arr(_) => "array",
527            DxLlmValue::Obj(_) => "object",
528            DxLlmValue::Ref(_) => "ref",
529        }
530    }
531
532    /// Try to get as string
533    #[must_use]
534    pub fn as_str(&self) -> Option<&str> {
535        match self {
536            DxLlmValue::Str(s) => Some(s),
537            _ => None,
538        }
539    }
540
541    /// Try to get as number
542    #[must_use]
543    pub fn as_num(&self) -> Option<f64> {
544        match self {
545            DxLlmValue::Num(n) => Some(*n),
546            _ => None,
547        }
548    }
549
550    /// Try to get as bool
551    #[must_use]
552    pub fn as_bool(&self) -> Option<bool> {
553        match self {
554            DxLlmValue::Bool(b) => Some(*b),
555            _ => None,
556        }
557    }
558
559    /// Try to get as array
560    #[must_use]
561    pub fn as_arr(&self) -> Option<&Vec<DxLlmValue>> {
562        match self {
563            DxLlmValue::Arr(arr) => Some(arr),
564            _ => None,
565        }
566    }
567
568    /// Try to get as object
569    #[must_use]
570    pub fn as_obj(&self) -> Option<&IndexMap<String, DxLlmValue>> {
571        match self {
572            DxLlmValue::Obj(obj) => Some(obj),
573            _ => None,
574        }
575    }
576
577    /// Try to get as reference key
578    #[must_use]
579    pub fn as_ref(&self) -> Option<&str> {
580        match self {
581            DxLlmValue::Ref(key) => Some(key),
582            _ => None,
583        }
584    }
585}
586
587impl fmt::Display for DxLlmValue {
588    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
589        match self {
590            DxLlmValue::Str(s) => write!(f, "{}", s),
591            DxLlmValue::Num(n) => {
592                if n.fract() == 0.0 {
593                    write!(f, "{}", *n as i64)
594                } else {
595                    write!(f, "{}", n)
596                }
597            }
598            DxLlmValue::Bool(b) => write!(f, "{}", if *b { "true" } else { "false" }),
599            DxLlmValue::Null => write!(f, "null"),
600            DxLlmValue::Arr(arr) => {
601                write!(f, "[")?;
602                for (i, v) in arr.iter().enumerate() {
603                    if i > 0 {
604                        write!(f, ", ")?;
605                    }
606                    write!(f, "{}", v)?;
607                }
608                write!(f, "]")
609            }
610            DxLlmValue::Obj(obj) => {
611                write!(f, "[")?;
612                for (i, (k, v)) in obj.iter().enumerate() {
613                    if i > 0 {
614                        write!(f, ",")?;
615                    }
616                    write!(f, "{}={}", k, v)?;
617                }
618                write!(f, "]")
619            }
620            DxLlmValue::Ref(key) => write!(f, "^{}", key),
621        }
622    }
623}
624
625impl From<&str> for DxLlmValue {
626    fn from(s: &str) -> Self {
627        DxLlmValue::Str(s.to_string())
628    }
629}
630
631impl From<String> for DxLlmValue {
632    fn from(s: String) -> Self {
633        DxLlmValue::Str(s)
634    }
635}
636
637impl From<f64> for DxLlmValue {
638    fn from(n: f64) -> Self {
639        DxLlmValue::Num(n)
640    }
641}
642
643impl From<i64> for DxLlmValue {
644    fn from(n: i64) -> Self {
645        DxLlmValue::Num(n as f64)
646    }
647}
648
649impl From<bool> for DxLlmValue {
650    fn from(b: bool) -> Self {
651        DxLlmValue::Bool(b)
652    }
653}
654
655// =============================================================================
656// Thread Safety Compile-Time Assertions
657// =============================================================================
658
659// These static assertions verify at compile time that our types are thread-safe.
660// If any of these types stop implementing Send or Sync, compilation will fail.
661
662/// Compile-time assertion that a type implements Send
663const fn _assert_send<T: Send>() {}
664
665/// Compile-time assertion that a type implements Sync
666const fn _assert_sync<T: Sync>() {}
667
668// Verify DxDocument is Send + Sync
669const _: () = _assert_send::<DxDocument>();
670const _: () = _assert_sync::<DxDocument>();
671
672// Verify DxSection is Send + Sync
673const _: () = _assert_send::<DxSection>();
674const _: () = _assert_sync::<DxSection>();
675
676// Verify DxLlmValue is Send + Sync
677const _: () = _assert_send::<DxLlmValue>();
678const _: () = _assert_sync::<DxLlmValue>();
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683
684    #[test]
685    fn test_dx_document_new() {
686        let doc = DxDocument::new();
687        assert!(doc.is_empty());
688        assert!(doc.context.is_empty());
689        assert!(doc.refs.is_empty());
690        assert!(doc.sections.is_empty());
691    }
692
693    #[test]
694    fn test_dx_section_add_row() {
695        let mut section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
696
697        // Valid row
698        let result = section.add_row(vec![
699            DxLlmValue::Num(1.0),
700            DxLlmValue::Str("Test".to_string()),
701        ]);
702        assert!(result.is_ok());
703        assert_eq!(section.row_count(), 1);
704
705        // Invalid row (wrong length)
706        let result = section.add_row(vec![DxLlmValue::Num(2.0)]);
707        assert!(result.is_err());
708    }
709
710    #[test]
711    fn test_dx_document_canonical_path_and_named_section_access() {
712        let mut doc = DxDocument::new();
713        let mut project = IndexMap::new();
714        project.insert(
715            "name".to_string(),
716            DxLlmValue::Str("dx-devtools".to_string()),
717        );
718        project.insert("kind".to_string(), DxLlmValue::Str("www-app".to_string()));
719        doc.context
720            .insert("project".to_string(), DxLlmValue::Obj(project));
721
722        let mut tools = DxSection::new(vec![
723            "name".to_string(),
724            "command".to_string(),
725            "enabled".to_string(),
726            "output".to_string(),
727        ]);
728        tools
729            .add_row(vec![
730                DxLlmValue::Str("style".to_string()),
731                DxLlmValue::Str("dx style build".to_string()),
732                DxLlmValue::Bool(true),
733                DxLlmValue::Str("styles/app.generated.css".to_string()),
734            ])
735            .unwrap();
736        doc.sections.insert('t', tools);
737        doc.section_names.insert('t', "tools".to_string());
738
739        assert_eq!(
740            doc.get_path("project.name").unwrap().as_str(),
741            Some("dx-devtools")
742        );
743        let tools = doc.section_by_name("tools").unwrap();
744        assert_eq!(tools.column_index("command"), Some(1));
745        assert_eq!(
746            tools
747                .value_by_key("name", "style", "output")
748                .unwrap()
749                .as_str(),
750            Some("styles/app.generated.css")
751        );
752    }
753
754    #[test]
755    fn test_dx_llm_value_type_name() {
756        assert_eq!(DxLlmValue::Str("test".to_string()).type_name(), "string");
757        assert_eq!(DxLlmValue::Num(42.0).type_name(), "number");
758        assert_eq!(DxLlmValue::Bool(true).type_name(), "bool");
759        assert_eq!(DxLlmValue::Null.type_name(), "null");
760        assert_eq!(DxLlmValue::Arr(vec![]).type_name(), "array");
761        assert_eq!(DxLlmValue::Obj(IndexMap::new()).type_name(), "object");
762        assert_eq!(DxLlmValue::Ref("key".to_string()).type_name(), "ref");
763    }
764
765    #[test]
766    #[allow(clippy::approx_constant)] // Using 3.14 intentionally for test data
767    fn test_dx_llm_value_display() {
768        assert_eq!(format!("{}", DxLlmValue::Str("hello".to_string())), "hello");
769        assert_eq!(format!("{}", DxLlmValue::Num(42.0)), "42");
770        assert_eq!(format!("{}", DxLlmValue::Num(3.14)), "3.14");
771        assert_eq!(format!("{}", DxLlmValue::Bool(true)), "true");
772        assert_eq!(format!("{}", DxLlmValue::Bool(false)), "false");
773        assert_eq!(format!("{}", DxLlmValue::Null), "null");
774        assert_eq!(format!("{}", DxLlmValue::Ref("A".to_string())), "^A");
775    }
776
777    #[test]
778    fn test_dx_llm_value_obj() {
779        let mut fields = IndexMap::new();
780        fields.insert("host".to_string(), DxLlmValue::Str("localhost".to_string()));
781        fields.insert("port".to_string(), DxLlmValue::Num(8080.0));
782        let obj = DxLlmValue::Obj(fields);
783
784        assert_eq!(obj.type_name(), "object");
785        assert!(obj.as_obj().is_some());
786        let obj_map = obj.as_obj().unwrap();
787        assert_eq!(obj_map.len(), 2);
788        assert_eq!(obj_map.get("host").unwrap().as_str(), Some("localhost"));
789        assert_eq!(obj_map.get("port").unwrap().as_num(), Some(8080.0));
790    }
791
792    // Note: Send+Sync assertions are now compile-time checks at module level,
793    // not runtime tests. See the const assertions above the test module.
794}