Skip to main content

vox_types/
schema.rs

1//! Schema extraction and tracking for vox wire protocol.
2//!
3//! The canonical schema model lives in `vox-schema`. This module re-exports
4//! those shared types and adds vox-specific extraction plus per-connection
5//! send/receive tracking.
6
7pub use vox_schema::*;
8
9use facet::Facet;
10use facet_core::{DeclId, Def, ScalarType, Shape, StructKind, Type, UserType};
11use indexmap::IndexMap;
12use std::collections::{HashMap, HashSet};
13use std::sync::Mutex;
14
15use crate::{MethodId, RequestCall, RequestResponse, is_rx, is_tx};
16
17// ============================================================================
18// Schema extraction
19// ============================================================================
20
21/// Errors that can occur during schema extraction.
22#[derive(Debug)]
23pub enum SchemaExtractError {
24    /// Encountered a type that schema extraction doesn't know how to handle.
25    UnhandledType { type_desc: String },
26
27    /// A pointer type had no type_params to follow.
28    PointerWithoutTypeParams { shape_desc: String },
29
30    /// A temporary ID was not resolved during finalization.
31    UnresolvedTempId { temp_id: CycleSchemaIndex },
32
33    /// A DeclId was expected in the assigned map but wasn't found.
34    MissingAssignment { context: String },
35}
36
37impl std::fmt::Display for SchemaExtractError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::UnhandledType { type_desc } => {
41                write!(f, "schema extraction: unhandled type: {type_desc}")
42            }
43            Self::PointerWithoutTypeParams { shape_desc } => {
44                write!(
45                    f,
46                    "schema extraction: Pointer type without type_params: {shape_desc}"
47                )
48            }
49            Self::UnresolvedTempId { temp_id } => {
50                write!(
51                    f,
52                    "schema extraction: unresolved temp ID {temp_id:?} during finalization"
53                )
54            }
55            Self::MissingAssignment { context } => {
56                write!(f, "schema extraction: missing DeclId assignment: {context}")
57            }
58        }
59    }
60}
61
62/// A value for which a schema can be attached
63pub trait Schematic {
64    fn direction(&self) -> BindingDirection;
65    fn attach_schemas(&mut self, schemas: CborPayload);
66}
67
68impl<'payload> Schematic for RequestCall<'payload> {
69    fn direction(&self) -> BindingDirection {
70        BindingDirection::Args
71    }
72
73    fn attach_schemas(&mut self, schemas: CborPayload) {
74        self.schemas = schemas;
75    }
76}
77
78impl<'payload> Schematic for RequestResponse<'payload> {
79    fn direction(&self) -> BindingDirection {
80        BindingDirection::Response
81    }
82
83    fn attach_schemas(&mut self, schemas: CborPayload) {
84        self.schemas = schemas;
85    }
86}
87
88impl std::error::Error for SchemaExtractError {}
89
90// ============================================================================
91// SchemaSendTracker — outbound dedup, owned by SessionCore (no Arc, no Mutex)
92// ============================================================================
93
94/// Tracks which schemas have been sent on the current connection.
95///
96/// Plain struct — owned by `SessionCore` behind the same Mutex as the
97/// conduit tx. Reset on reconnection.
98// r[impl schema.tracking.sent]
99// r[impl schema.type-id.per-connection]
100pub struct SchemaSendTracker {
101    /// Per-method, per-direction: the CborPayload that was sent. Keyed by
102    /// (method_id, direction). If present, schemas were already sent.
103    sent_bindings: HashSet<(MethodId, BindingDirection)>,
104
105    /// SchemaHashes already sent on this connection.
106    sent_schemas: HashSet<SchemaHash>,
107
108    /// All extracted schemas, kept for the operation store to pull from.
109    registry: SchemaRegistry,
110}
111
112impl SchemaSendTracker {
113    pub fn new() -> Self {
114        SchemaSendTracker {
115            registry: HashMap::new(),
116            sent_bindings: HashSet::new(),
117            sent_schemas: HashSet::new(),
118        }
119    }
120
121    /// Reset connection-scoped state — call on reconnection.
122    /// The registry is preserved (schemas don't change across connections).
123    pub fn reset(&mut self) {
124        self.sent_bindings.clear();
125        self.sent_schemas.clear();
126    }
127
128    /// Borrow the schema registry. Used by the operation store to pull
129    /// schemas it hasn't stored yet.
130    pub fn registry(&self) -> &SchemaRegistry {
131        &self.registry
132    }
133
134    /// Prepare schemas for a method call/response, returning a CBOR payload
135    /// to inline in the request/response. Returns empty payload if schemas
136    /// were already sent for this shape.
137    ///
138    // r[impl schema.tracking.transitive]
139    // r[impl schema.exchange.idempotent]
140    // r[impl schema.principles.once-per-type]
141    // r[impl schema.principles.sender-driven]
142    // r[impl schema.principles.no-roundtrips]
143    pub fn attach_schemas_for_shape_if_needed(
144        &mut self,
145        method_id: MethodId,
146        shape: &'static Shape,
147        schematic: &mut impl Schematic,
148    ) -> Result<CborPayload, SchemaExtractError> {
149        let key = (method_id, schematic.direction());
150
151        // Fast path: already sent for this method+direction.
152        if self.sent_bindings.contains(&key) {
153            let empty = CborPayload::default();
154            schematic.attach_schemas(empty.clone());
155            return Ok(empty);
156        }
157
158        // Slow path: extract, deduplicate, encode.
159        let already_sent = self.sent_schemas.clone();
160
161        // Extraction is intentionally fresh and pure here. Wire dedup happens
162        // on content hashes, not DeclId caching, to avoid aliasing structural
163        // roots like unary tuples across unrelated method shapes.
164        let extracted = extract_schemas(shape)?;
165
166        // Add all schemas to the persistent registry (for the operation store).
167        for schema in &extracted.schemas {
168            self.registry
169                .entry(schema.id)
170                .or_insert_with(|| schema.clone());
171        }
172
173        // Filter to only schemas not already sent on the wire.
174        let unsent: Vec<Schema> = extracted
175            .schemas
176            .into_iter()
177            .filter(|s| !already_sent.contains(&s.id))
178            .collect();
179
180        // Track sent schemas.
181        for s in &unsent {
182            self.sent_schemas.insert(s.id);
183        }
184
185        let schema_payload = SchemaPayload {
186            schemas: unsent,
187            root: extracted.root,
188        };
189        dlog!(
190            "[schema] send binding: method={:?} direction={:?} root={:?} schema_count={}",
191            method_id,
192            schematic.direction(),
193            schema_payload.root,
194            schema_payload.schemas.len()
195        );
196        let cbor = schema_payload.to_cbor();
197        schematic.attach_schemas(cbor.clone());
198        self.sent_bindings.insert(key);
199        Ok(cbor)
200    }
201
202    /// Prepare schemas for sending, sourcing them from a `SchemaSource`.
203    ///
204    /// Used for replay paths where we don't have a live value shape but do
205    /// have the bound root `TypeRef` and a schema source.
206    pub fn prepare_send(
207        &mut self,
208        method_id: MethodId,
209        direction: BindingDirection,
210        root_type: &TypeRef,
211        source: &dyn SchemaSource,
212    ) -> CborPayload {
213        let key = (method_id, direction);
214        if self.sent_bindings.contains(&key) {
215            return CborPayload::default();
216        }
217
218        let already_sent = self.sent_schemas.clone();
219        let mut all_schemas = Vec::new();
220        let mut visited = HashSet::new();
221        let mut queue = Vec::new();
222        root_type.collect_ids(&mut queue);
223
224        while let Some(id) = queue.pop() {
225            if !visited.insert(id) {
226                continue;
227            }
228            if let Some(schema) = source.get_schema(id) {
229                for child_id in schema_child_ids(&schema.kind) {
230                    queue.push(child_id);
231                }
232                all_schemas.push(schema);
233            }
234        }
235
236        for schema in &all_schemas {
237            self.registry
238                .entry(schema.id)
239                .or_insert_with(|| schema.clone());
240        }
241
242        let unsent: Vec<Schema> = all_schemas
243            .into_iter()
244            .filter(|schema| !already_sent.contains(&schema.id))
245            .collect();
246
247        for schema in &unsent {
248            self.sent_schemas.insert(schema.id);
249        }
250
251        let schema_payload = SchemaPayload {
252            schemas: unsent,
253            root: root_type.clone(),
254        };
255        dlog!(
256            "[schema] resend binding: method={:?} direction={:?} root={:?} schema_count={}",
257            method_id,
258            direction,
259            schema_payload.root,
260            schema_payload.schemas.len()
261        );
262        let cbor = schema_payload.to_cbor();
263        self.sent_bindings.insert(key);
264        cbor
265    }
266
267    /// Compatibility shim: schema extraction is now independent from
268    /// connection-scoped send tracking.
269    pub fn extract_schemas(
270        &mut self,
271        shape: &'static Shape,
272    ) -> Result<ExtractedSchemas, SchemaExtractError> {
273        self::extract_schemas(shape)
274    }
275}
276
277impl Default for SchemaSendTracker {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283impl std::fmt::Debug for SchemaSendTracker {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        f.debug_struct("SchemaSendTracker").finish_non_exhaustive()
286    }
287}
288
289// ============================================================================
290// SchemaRecvTracker — inbound storage, shared via Arc
291// ============================================================================
292
293/// Tracks schemas received from the remote peer on the current connection.
294///
295/// Uses interior mutability (Mutex) so it can be shared via `Arc` between the
296/// session recv loop and in-flight handler tasks. Created fresh on each
297/// connection — NOT reused across reconnections.
298// r[impl schema.tracking.received]
299// r[impl schema.type-id.per-connection]
300pub struct SchemaRecvTracker {
301    /// Type schemas received from the remote peer.
302    received: Mutex<HashMap<SchemaHash, Schema>>,
303    /// Args bindings received: method_id → root TypeRef for args.
304    received_args_bindings: Mutex<HashMap<MethodId, TypeRef>>,
305    /// Response bindings received: method_id → root TypeRef for response.
306    received_response_bindings: Mutex<HashMap<MethodId, TypeRef>>,
307}
308
309/// Error returned when recording received schemas detects a protocol violation.
310#[derive(Debug)]
311pub struct DuplicateSchemaError {
312    pub type_id: SchemaHash,
313}
314
315impl std::fmt::Display for DuplicateSchemaError {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        write!(
318            f,
319            "duplicate TypeSchemaId {:?} received on same connection — protocol error",
320            self.type_id
321        )
322    }
323}
324
325impl std::error::Error for DuplicateSchemaError {}
326
327impl SchemaRecvTracker {
328    pub fn new() -> Self {
329        SchemaRecvTracker {
330            received: Mutex::new(HashMap::new()),
331            received_args_bindings: Mutex::new(HashMap::new()),
332            received_response_bindings: Mutex::new(HashMap::new()),
333        }
334    }
335
336    /// Record a parsed schema message from the remote peer.
337    ///
338    /// Returns `Err` if a TypeSchemaId was already received — this is a
339    /// protocol error (the send tracker didn't reset on reconnection).
340    pub fn record_received(
341        &self,
342        method_id: MethodId,
343        direction: BindingDirection,
344        payload: SchemaPayload,
345    ) -> Result<(), DuplicateSchemaError> {
346        {
347            let mut received = self.received.lock().unwrap();
348            for schema in &payload.schemas {
349                dlog!("[schema] record_received: id={:?}", schema.id);
350            }
351            for schema in payload.schemas {
352                if let Some(existing) = received.get(&schema.id) {
353                    dlog!(
354                        "[schema] DUPLICATE: id={:?} existing={:?} new={:?}",
355                        schema.id,
356                        existing,
357                        schema
358                    );
359                    return Err(DuplicateSchemaError { type_id: schema.id });
360                }
361                received.insert(schema.id, schema);
362            }
363        }
364        let map = match direction {
365            BindingDirection::Args => &self.received_args_bindings,
366            BindingDirection::Response => &self.received_response_bindings,
367        };
368        dlog!(
369            "[schema] record binding: method={:?} direction={:?} root={:?}",
370            method_id,
371            direction,
372            payload.root
373        );
374        map.lock().unwrap().insert(method_id, payload.root);
375        Ok(())
376    }
377
378    /// Look up the remote's root TypeRef for a method's args.
379    pub fn get_remote_args_root(&self, method_id: MethodId) -> Option<TypeRef> {
380        self.received_args_bindings
381            .lock()
382            .unwrap()
383            .get(&method_id)
384            .cloned()
385    }
386
387    /// Look up the remote's root TypeRef for a method's response.
388    pub fn get_remote_response_root(&self, method_id: MethodId) -> Option<TypeRef> {
389        self.received_response_bindings
390            .lock()
391            .unwrap()
392            .get(&method_id)
393            .cloned()
394    }
395
396    /// Look up a received schema by type ID.
397    pub fn get_received(&self, type_id: &SchemaHash) -> Option<Schema> {
398        self.received.lock().unwrap().get(type_id).cloned()
399    }
400
401    /// Get a snapshot of the received schema registry for building translation plans.
402    pub fn received_registry(&self) -> SchemaRegistry {
403        self.received.lock().unwrap().clone()
404    }
405}
406
407impl Default for SchemaRecvTracker {
408    fn default() -> Self {
409        Self::new()
410    }
411}
412
413impl SchemaSource for SchemaRecvTracker {
414    fn get_schema(&self, id: SchemaHash) -> Option<Schema> {
415        self.get_received(&id)
416    }
417}
418
419impl std::fmt::Debug for SchemaRecvTracker {
420    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421        f.debug_struct("SchemaRecvTracker").finish_non_exhaustive()
422    }
423}
424
425/// Result of schema extraction: the schemas and the root TypeRef.
426pub struct ExtractedSchemas {
427    /// All schemas in dependency order (dependencies before dependents).
428    pub schemas: Vec<Schema>,
429
430    /// The root TypeRef — may be generic (e.g. `Concrete { id: result_id, args: [i64, MathError] }`).
431    pub root: TypeRef,
432}
433
434/// Extract schemas without a tracker (uses a temporary counter).
435/// Useful for tests and one-off schema extraction.
436pub fn extract_schemas(shape: &'static Shape) -> Result<ExtractedSchemas, SchemaExtractError> {
437    let mut ctx = ExtractCtx {
438        next_id: CycleSchemaIndex::first(),
439        schemas: IndexMap::new(),
440        assigned: HashMap::new(),
441        seen: HashSet::new(),
442    };
443    let root_mixed_ref = ctx.extract(shape)?;
444    let schemas: Vec<MixedSchema> = ctx.schemas.into_values().collect();
445    let (finalized, temp_to_final) = finalize_content_hashes(schemas)?;
446
447    let resolve = |mid: MixedId| -> SchemaHash {
448        match mid {
449            MixedId::Final(tid) => tid,
450            MixedId::Temp(t) => temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0)),
451        }
452    };
453    let root_type_ref = root_mixed_ref.map(resolve);
454
455    Ok(ExtractedSchemas {
456        schemas: finalized,
457        root: root_type_ref,
458    })
459}
460
461/// Replace temporary incrementing IDs with blake3 content hashes.
462///
463/// Schemas must be in dependency order (dependencies before dependents).
464/// For non-recursive types, this is a simple bottom-up pass. For recursive
465/// types, the 4-step algorithm from r[schema.hash.recursive] is used.
466// r[impl schema.type-id.hash]
467// r[impl schema.hash.recursive]
468/// Resolve a MixedId to a TypeSchemaId for hashing purposes.
469fn resolve_mixed(id: MixedId, temp_to_final: &HashMap<CycleSchemaIndex, SchemaHash>) -> SchemaHash {
470    match id {
471        MixedId::Final(tid) => tid,
472        MixedId::Temp(t) => temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0)),
473    }
474}
475
476#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
477enum ExtractKey {
478    Decl(DeclId),
479    AnonymousTupleArity(usize),
480}
481
482/// Convert a Vec<MixedSchema> (from extraction) into Vec<Schema> with
483/// content-hashed TypeSchemaIds.
484///
485/// Schemas must be in dependency order (dependencies before dependents).
486/// For non-recursive types, this is a simple bottom-up pass. For recursive
487/// types, the 4-step algorithm from r[schema.hash.recursive] is used.
488///
489/// Returns the finalized schemas and a mapping from temp IDs to final IDs.
490// r[impl schema.type-id.hash]
491// r[impl schema.hash.recursive]
492fn finalize_content_hashes(
493    schemas: Vec<MixedSchema>,
494) -> Result<(Vec<Schema>, HashMap<CycleSchemaIndex, SchemaHash>), SchemaExtractError> {
495    // Only Temp entries need hashing. Build index of temp IDs.
496    let temp_to_idx: HashMap<CycleSchemaIndex, usize> = schemas
497        .iter()
498        .enumerate()
499        .filter_map(|(i, s)| match s.id {
500            MixedId::Temp(t) => Some((t, i)),
501            MixedId::Final(_) => None,
502        })
503        .collect();
504
505    fn collect_refs(kind: &MixedSchemaKind) -> Vec<MixedId> {
506        let mut refs = Vec::new();
507        kind.for_each_type_ref(&mut |tr: &TypeRef<MixedId>| tr.collect_ids(&mut refs));
508        refs
509    }
510
511    // Detect recursive groups among temp schemas.
512    let n = schemas.len();
513    let mut in_recursive_group: Vec<bool> = vec![false; n];
514
515    for (i, schema) in schemas.iter().enumerate() {
516        if matches!(schema.id, MixedId::Final(_)) {
517            continue; // Already finalized, skip.
518        }
519        for r in collect_refs(&schema.kind) {
520            if let MixedId::Temp(t) = r
521                && let Some(&ref_idx) = temp_to_idx.get(&t)
522                && ref_idx >= i
523            {
524                in_recursive_group[i] = true;
525                in_recursive_group[ref_idx] = true;
526            }
527        }
528    }
529
530    // Map from temp ID -> final content hash.
531    let mut temp_to_final: HashMap<CycleSchemaIndex, SchemaHash> = HashMap::new();
532
533    // Phase 1: Hash non-recursive temp types bottom-up.
534    for (i, schema) in schemas.iter().enumerate() {
535        if in_recursive_group[i] {
536            continue;
537        }
538        if let MixedId::Temp(temp) = schema.id {
539            let final_id = compute_content_hash(&schema.kind, &schema.type_params, &|mid| {
540                resolve_mixed(mid, &temp_to_final)
541            });
542            temp_to_final.insert(temp, final_id);
543        }
544    }
545
546    // Phase 2: Hash recursive groups using the 4-step algorithm.
547    let mut i = 0;
548    while i < n {
549        if !in_recursive_group[i] {
550            i += 1;
551            continue;
552        }
553
554        let group_start = i;
555        while i < n && in_recursive_group[i] {
556            i += 1;
557        }
558        let group_end = i;
559
560        // Collect the temp IDs in this group.
561        let group_temp_ids: HashSet<CycleSchemaIndex> = schemas[group_start..group_end]
562            .iter()
563            .filter_map(|s| match s.id {
564                MixedId::Temp(t) => Some(t),
565                _ => None,
566            })
567            .collect();
568
569        // Step 1: Preliminary hashes — intra-group refs become sentinel (0).
570        let mut prelim_hashes: Vec<SchemaHash> = Vec::new();
571        for schema in &schemas[group_start..group_end] {
572            let prelim =
573                compute_content_hash(&schema.kind, &schema.type_params, &|mid| match mid {
574                    MixedId::Final(tid) => tid,
575                    MixedId::Temp(t) => {
576                        if group_temp_ids.contains(&t) {
577                            SchemaHash(0) // sentinel
578                        } else {
579                            temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0))
580                        }
581                    }
582                });
583            prelim_hashes.push(prelim);
584        }
585
586        // Step 3: Canonical ordering.
587        let mut order: Vec<usize> = (0..prelim_hashes.len()).collect();
588        order.sort_by_key(|&i| prelim_hashes[i].0);
589
590        // Step 4: Final hashes.
591        let mut group_hasher = blake3::Hasher::new();
592        for &idx in &order {
593            group_hasher.update(&prelim_hashes[idx].0.to_le_bytes());
594        }
595        let gh = group_hasher.finalize();
596        let group_hash = u64::from_le_bytes(gh.as_bytes()[0..8].try_into().unwrap());
597
598        for (position, &idx) in order.iter().enumerate() {
599            let mut fh = blake3::Hasher::new();
600            fh.update(&group_hash.to_le_bytes());
601            fh.update(&(position as u64).to_le_bytes());
602            let fo = fh.finalize();
603            let final_hash =
604                SchemaHash(u64::from_le_bytes(fo.as_bytes()[0..8].try_into().unwrap()));
605
606            if let MixedId::Temp(t) = schemas[group_start + idx].id {
607                temp_to_final.insert(t, final_hash);
608            }
609        }
610    }
611
612    // Phase 3: Convert MixedSchema -> Schema by resolving all MixedIds.
613    let resolve = |mid: MixedId| -> Result<SchemaHash, SchemaExtractError> {
614        match mid {
615            MixedId::Final(tid) => Ok(tid),
616            MixedId::Temp(t) => temp_to_final
617                .get(&t)
618                .copied()
619                .ok_or(SchemaExtractError::UnresolvedTempId { temp_id: t }),
620        }
621    };
622
623    let mut resolve_type_ref =
624        |type_ref: TypeRef<MixedId>| -> Result<TypeRef<SchemaHash>, SchemaExtractError> {
625            type_ref.try_map(&resolve)
626        };
627
628    let mut seen_ids = HashSet::new();
629    let finalized: Vec<Schema> = schemas
630        .into_iter()
631        .map(|s| {
632            let type_id = resolve(s.id)?;
633            Ok(Schema {
634                id: type_id,
635                type_params: s.type_params,
636                kind: s.kind.try_map_type_refs(&mut resolve_type_ref)?,
637            })
638        })
639        .collect::<Result<Vec<_>, _>>()?
640        .into_iter()
641        .filter(|s| seen_ids.insert(s.id))
642        .collect();
643
644    Ok((finalized, temp_to_final))
645}
646
647struct ExtractCtx {
648    /// Counter for assigning temp IDs
649    next_id: CycleSchemaIndex,
650    /// Schemas being built in this extraction pass, keyed by extraction identity.
651    /// Insertion order is dependency order.
652    schemas: IndexMap<ExtractKey, MixedSchema>,
653    /// ExtractKey → MixedId for types we've started extracting (may not be
654    /// fully built yet — needed for cycle references).
655    assigned: HashMap<ExtractKey, MixedId>,
656    /// Shapes we've started walking. If we encounter a shape already in
657    /// this set, we're in a cycle.
658    seen: HashSet<&'static Shape>,
659}
660
661impl ExtractCtx {
662    /// Get or assign a MixedId for an extraction key.
663    fn id_for_key(&mut self, key: ExtractKey) -> MixedId {
664        if let Some(&id) = self.assigned.get(&key) {
665            return id;
666        }
667        let id = MixedId::Temp(self.next_id.next());
668        self.assigned.insert(key, id);
669        id
670    }
671
672    /// Emit a schema for an extraction key (if not already emitted in this pass).
673    fn emit_schema(&mut self, key: ExtractKey, schema: MixedSchema) {
674        self.schemas.entry(key).or_insert(schema);
675    }
676
677    fn key_for_shape(&self, shape: &'static Shape) -> ExtractKey {
678        match anonymous_tuple_arity(shape) {
679            Some(arity) => ExtractKey::AnonymousTupleArity(arity),
680            None => ExtractKey::Decl(shape.decl_id),
681        }
682    }
683
684    /// Build a TypeRef for a field/element shape, substituting Var references
685    /// for shapes that match a type parameter.
686    fn type_ref_for_shape(
687        &mut self,
688        shape: &'static Shape,
689        param_map: &[(&'static Shape, TypeParamName)],
690    ) -> Result<TypeRef<MixedId>, SchemaExtractError> {
691        if let Some((_, name)) = param_map
692            .iter()
693            .find(|(param_shape, _)| shape.is_shape(param_shape))
694        {
695            // This shape is a type parameter — emit Var reference.
696            // But we still need to extract the concrete type's schema.
697            self.extract(shape)?;
698            Ok(TypeRef::Var { name: name.clone() })
699        } else {
700            self.extract(shape)
701        }
702    }
703
704    /// Extract a schema for the given shape, returning a TypeRef to it.
705    /// Recursively extracts dependencies first.
706    fn extract(&mut self, shape: &'static Shape) -> Result<TypeRef<MixedId>, SchemaExtractError> {
707        // Channel types: emit a Channel schema with direction and element type.
708        if is_tx(shape) || is_rx(shape) {
709            let direction = if is_tx(shape) {
710                ChannelDirection::Tx
711            } else {
712                ChannelDirection::Rx
713            };
714            if let Some(inner) = shape.type_params.first() {
715                let elem_ref = self.extract(inner.shape)?;
716                let key = self.key_for_shape(shape);
717                let id = self.id_for_key(key);
718                // For channels, the element in the schema body uses Var("T")
719                // since channels are generic over their element type.
720                let type_params = vec![TypeParamName("T".to_string())];
721                self.emit_schema(
722                    key,
723                    MixedSchema {
724                        id,
725                        type_params,
726                        kind: SchemaKind::Channel {
727                            direction,
728                            element: TypeRef::Var {
729                                name: TypeParamName("T".to_string()),
730                            },
731                        },
732                    },
733                );
734                self.seen.insert(shape);
735                return Ok(TypeRef::Concrete {
736                    type_id: id,
737                    args: vec![elem_ref],
738                });
739            }
740        }
741
742        // Transparent wrappers: follow inner.
743        if shape.is_transparent()
744            && let Some(inner) = shape.inner
745        {
746            return self.extract(inner);
747        }
748
749        // Pointer types (Box, Arc, etc.): follow through to pointee.
750        // Must be before id_for_decl to avoid orphaned temp IDs.
751        if let Def::Pointer(ptr_def) = shape.def
752            && let Some(pointee) = ptr_def.pointee
753        {
754            return self.extract(pointee);
755        }
756
757        let key = self.key_for_shape(shape);
758        let id = self.id_for_key(key);
759
760        // r[impl schema.format.recursive]
761        // Cycle detection: if we've already started walking this shape,
762        // return the assigned id without re-entering.
763        if !self.seen.insert(shape) {
764            // Already seen — either fully processed or a cycle.
765            // Extract type args if generic (they may contain new types).
766            let args = self.extract_instantiation_args(shape)?;
767            return Ok(if args.is_empty() {
768                TypeRef::concrete(id)
769            } else {
770                TypeRef::generic(id, args)
771            });
772        }
773
774        // If we've already emitted a schema for this extraction key (in this pass),
775        // we still need to extract type args for this particular instantiation.
776        let already_emitted = self.schemas.contains_key(&key);
777        if already_emitted {
778            let args = self.extract_instantiation_args(shape)?;
779            return Ok(if args.is_empty() {
780                TypeRef::concrete(id)
781            } else {
782                TypeRef::generic(id, args)
783            });
784        }
785
786        // Build a map from shape pointer → type param name for this type.
787        // Used to emit Var references in the schema body.
788        let param_map: Vec<(&'static Shape, TypeParamName)> = shape
789            .type_params
790            .iter()
791            .map(|tp| (tp.shape, TypeParamName(tp.name.to_string())))
792            .collect();
793        let type_param_names: Vec<TypeParamName> = shape
794            .type_params
795            .iter()
796            .map(|tp| TypeParamName(tp.name.to_string()))
797            .collect();
798
799        // r[impl schema.format.primitive]
800        // Scalars
801        if let Some(scalar) = shape.scalar_type() {
802            self.emit_schema(
803                key,
804                MixedSchema {
805                    id,
806                    type_params: vec![],
807                    kind: SchemaKind::Primitive {
808                        primitive_type: scalar_to_primitive(scalar),
809                    },
810                },
811            );
812            return Ok(TypeRef::concrete(id));
813        }
814
815        // r[impl schema.format.container]
816        // Containers
817        match shape.def {
818            Def::List(list_def) => {
819                if let Some(ScalarType::U8) = list_def.t().scalar_type() {
820                    self.emit_schema(
821                        key,
822                        MixedSchema {
823                            id,
824                            type_params: vec![],
825                            kind: SchemaKind::Primitive {
826                                primitive_type: PrimitiveType::Bytes,
827                            },
828                        },
829                    );
830                    return Ok(TypeRef::concrete(id));
831                }
832                let elem_ref = self.type_ref_for_shape(list_def.t(), &param_map)?;
833                let args = self.extract_type_args(shape)?;
834                self.emit_schema(
835                    key,
836                    MixedSchema {
837                        id,
838                        type_params: type_param_names,
839                        kind: SchemaKind::List { element: elem_ref },
840                    },
841                );
842                return Ok(if args.is_empty() {
843                    TypeRef::concrete(id)
844                } else {
845                    TypeRef::generic(id, args)
846                });
847            }
848            Def::Array(array_def) => {
849                let elem_ref = self.type_ref_for_shape(array_def.t(), &param_map)?;
850                let args = self.extract_type_args(shape)?;
851                self.emit_schema(
852                    key,
853                    MixedSchema {
854                        id,
855                        type_params: type_param_names,
856                        kind: SchemaKind::Array {
857                            element: elem_ref,
858                            length: array_def.n as u64,
859                        },
860                    },
861                );
862                return Ok(if args.is_empty() {
863                    TypeRef::concrete(id)
864                } else {
865                    TypeRef::generic(id, args)
866                });
867            }
868            Def::Slice(slice_def) => {
869                if let Some(ScalarType::U8) = slice_def.t().scalar_type() {
870                    self.emit_schema(
871                        key,
872                        MixedSchema {
873                            id,
874                            type_params: vec![],
875                            kind: SchemaKind::Primitive {
876                                primitive_type: PrimitiveType::Bytes,
877                            },
878                        },
879                    );
880                    return Ok(TypeRef::concrete(id));
881                }
882                let elem_ref = self.type_ref_for_shape(slice_def.t(), &param_map)?;
883                let args = self.extract_type_args(shape)?;
884                self.emit_schema(
885                    key,
886                    MixedSchema {
887                        id,
888                        type_params: type_param_names,
889                        kind: SchemaKind::List { element: elem_ref },
890                    },
891                );
892                return Ok(if args.is_empty() {
893                    TypeRef::concrete(id)
894                } else {
895                    TypeRef::generic(id, args)
896                });
897            }
898            Def::Map(map_def) => {
899                let key_ref = self.type_ref_for_shape(map_def.k(), &param_map)?;
900                let val_ref = self.type_ref_for_shape(map_def.v(), &param_map)?;
901                let args = self.extract_type_args(shape)?;
902                self.emit_schema(
903                    key,
904                    MixedSchema {
905                        id,
906                        type_params: type_param_names,
907                        kind: SchemaKind::Map {
908                            key: key_ref,
909                            value: val_ref,
910                        },
911                    },
912                );
913                return Ok(if args.is_empty() {
914                    TypeRef::concrete(id)
915                } else {
916                    TypeRef::generic(id, args)
917                });
918            }
919            Def::Set(set_def) => {
920                let elem_ref = self.type_ref_for_shape(set_def.t(), &param_map)?;
921                let args = self.extract_type_args(shape)?;
922                self.emit_schema(
923                    key,
924                    MixedSchema {
925                        id,
926                        type_params: type_param_names,
927                        kind: SchemaKind::List { element: elem_ref },
928                    },
929                );
930                return Ok(if args.is_empty() {
931                    TypeRef::concrete(id)
932                } else {
933                    TypeRef::generic(id, args)
934                });
935            }
936            Def::Option(opt_def) => {
937                let elem_ref = self.type_ref_for_shape(opt_def.t(), &param_map)?;
938                let args = self.extract_type_args(shape)?;
939                self.emit_schema(
940                    key,
941                    MixedSchema {
942                        id,
943                        type_params: type_param_names,
944                        kind: SchemaKind::Option { element: elem_ref },
945                    },
946                );
947                return Ok(if args.is_empty() {
948                    TypeRef::concrete(id)
949                } else {
950                    TypeRef::generic(id, args)
951                });
952            }
953            Def::Result(result_def) => {
954                let ok_ref = self.type_ref_for_shape(result_def.t(), &param_map)?;
955                let err_ref = self.type_ref_for_shape(result_def.e(), &param_map)?;
956                let args = self.extract_type_args(shape)?;
957                self.emit_schema(
958                    key,
959                    MixedSchema {
960                        id,
961                        type_params: type_param_names,
962                        kind: SchemaKind::Enum {
963                            name: shape.type_identifier.to_string(),
964                            variants: vec![
965                                VariantSchema {
966                                    name: "Ok".to_string(),
967                                    index: 0,
968                                    payload: VariantPayload::Newtype { type_ref: ok_ref },
969                                },
970                                VariantSchema {
971                                    name: "Err".to_string(),
972                                    index: 1,
973                                    payload: VariantPayload::Newtype { type_ref: err_ref },
974                                },
975                            ],
976                        },
977                    },
978                );
979                return Ok(if args.is_empty() {
980                    TypeRef::concrete(id)
981                } else {
982                    TypeRef::generic(id, args)
983                });
984            }
985            _ => {}
986        }
987
988        // User-defined types.
989        let kind = match shape.ty {
990            // r[impl schema.format.struct]
991            // r[impl schema.format.tuple]
992            Type::User(UserType::Struct(struct_type)) => match struct_type.kind {
993                StructKind::Unit => {
994                    let primitive_type = if is_infallible_shape(shape) {
995                        PrimitiveType::Never
996                    } else {
997                        PrimitiveType::Unit
998                    };
999                    SchemaKind::Primitive { primitive_type }
1000                }
1001                StructKind::TupleStruct | StructKind::Tuple => {
1002                    if let Some(arity) = anonymous_tuple_arity(shape) {
1003                        let args = self.extract_instantiation_args(shape)?;
1004                        let type_params = tuple_type_params(arity);
1005                        let elements = type_params
1006                            .iter()
1007                            .cloned()
1008                            .map(|name| TypeRef::Var { name })
1009                            .collect();
1010                        self.emit_schema(
1011                            key,
1012                            MixedSchema {
1013                                id,
1014                                type_params,
1015                                kind: SchemaKind::Tuple { elements },
1016                            },
1017                        );
1018                        return Ok(TypeRef::generic(id, args));
1019                    }
1020                    let mut elements = Vec::with_capacity(struct_type.fields.len());
1021                    for f in struct_type.fields {
1022                        elements.push(self.type_ref_for_shape(f.shape(), &param_map)?);
1023                    }
1024                    SchemaKind::Tuple { elements }
1025                }
1026                StructKind::Struct => {
1027                    let mut fields = Vec::with_capacity(struct_type.fields.len());
1028                    for f in struct_type.fields {
1029                        fields.push(FieldSchema {
1030                            name: f.name.to_string(),
1031                            type_ref: self.type_ref_for_shape(f.shape(), &param_map)?,
1032                            required: f.default.is_none(),
1033                        });
1034                    }
1035                    SchemaKind::Struct {
1036                        name: shape.type_identifier.to_string(),
1037                        fields,
1038                    }
1039                }
1040            },
1041            // r[impl schema.format.enum]
1042            Type::User(UserType::Enum(enum_type)) => {
1043                let mut variants = Vec::with_capacity(enum_type.variants.len());
1044                for (i, v) in enum_type.variants.iter().enumerate() {
1045                    let payload = match v.data.kind {
1046                        StructKind::Unit => VariantPayload::Unit,
1047                        StructKind::TupleStruct | StructKind::Tuple => {
1048                            if v.data.fields.len() == 1 {
1049                                VariantPayload::Newtype {
1050                                    type_ref: self
1051                                        .type_ref_for_shape(v.data.fields[0].shape(), &param_map)?,
1052                                }
1053                            } else {
1054                                let mut types = Vec::with_capacity(v.data.fields.len());
1055                                for f in v.data.fields {
1056                                    types.push(self.type_ref_for_shape(f.shape(), &param_map)?);
1057                                }
1058                                VariantPayload::Tuple { types }
1059                            }
1060                        }
1061                        StructKind::Struct => {
1062                            let mut fields = Vec::with_capacity(v.data.fields.len());
1063                            for f in v.data.fields {
1064                                fields.push(FieldSchema {
1065                                    name: f.name.to_string(),
1066                                    type_ref: self.type_ref_for_shape(f.shape(), &param_map)?,
1067                                    required: true,
1068                                });
1069                            }
1070                            VariantPayload::Struct { fields }
1071                        }
1072                    };
1073                    variants.push(VariantSchema {
1074                        name: v.name.to_string(),
1075                        index: i as u32,
1076                        payload,
1077                    });
1078                }
1079                SchemaKind::Enum {
1080                    name: shape.type_identifier.to_string(),
1081                    variants,
1082                }
1083            }
1084            Type::User(UserType::Opaque) => SchemaKind::Primitive {
1085                primitive_type: PrimitiveType::Payload,
1086            },
1087            other => {
1088                return Err(SchemaExtractError::UnhandledType {
1089                    type_desc: format!("{other:?} for shape {shape} (def={:?})", shape.def),
1090                });
1091            }
1092        };
1093
1094        let args = self.extract_type_args(shape)?;
1095        self.emit_schema(
1096            key,
1097            MixedSchema {
1098                id,
1099                type_params: type_param_names,
1100                kind,
1101            },
1102        );
1103
1104        Ok(if args.is_empty() {
1105            TypeRef::concrete(id)
1106        } else {
1107            TypeRef::generic(id, args)
1108        })
1109    }
1110
1111    /// Extract the concrete type arguments for a generic shape.
1112    /// For `Vec<u32>`, this extracts u32 and returns `[TypeRef::concrete(u32_id)]`.
1113    /// For non-generic types, returns an empty vec.
1114    fn extract_type_args(
1115        &mut self,
1116        shape: &'static Shape,
1117    ) -> Result<Vec<TypeRef<MixedId>>, SchemaExtractError> {
1118        if shape.type_params.is_empty() {
1119            return Ok(vec![]);
1120        }
1121        let mut args = Vec::with_capacity(shape.type_params.len());
1122        for tp in shape.type_params {
1123            args.push(self.extract(tp.shape)?);
1124        }
1125        Ok(args)
1126    }
1127
1128    /// Extract the concrete instantiation arguments for a shape.
1129    ///
1130    /// Most generic shapes get their args from facet `type_params`.
1131    /// Anonymous tuples are synthesized as generic families per arity,
1132    /// so their "type args" come from their element shapes.
1133    fn extract_instantiation_args(
1134        &mut self,
1135        shape: &'static Shape,
1136    ) -> Result<Vec<TypeRef<MixedId>>, SchemaExtractError> {
1137        if anonymous_tuple_arity(shape).is_some()
1138            && let Type::User(UserType::Struct(struct_type)) = shape.ty
1139        {
1140            let mut args = Vec::with_capacity(struct_type.fields.len());
1141            for field in struct_type.fields {
1142                args.push(self.extract(field.shape())?);
1143            }
1144            return Ok(args);
1145        }
1146        self.extract_type_args(shape)
1147    }
1148}
1149
1150fn anonymous_tuple_arity(shape: &'static Shape) -> Option<usize> {
1151    match shape.ty {
1152        Type::User(UserType::Struct(struct_type))
1153            if struct_type.kind == StructKind::Tuple && shape.type_identifier.starts_with('(') =>
1154        {
1155            Some(struct_type.fields.len())
1156        }
1157        _ => None,
1158    }
1159}
1160
1161fn tuple_type_params(arity: usize) -> Vec<TypeParamName> {
1162    (0..arity)
1163        .map(|index| TypeParamName(format!("T{index}")))
1164        .collect()
1165}
1166
1167fn is_infallible_shape(shape: &'static Shape) -> bool {
1168    shape.is_shape(<std::convert::Infallible as Facet<'static>>::SHAPE)
1169}
1170
1171fn scalar_to_primitive(scalar: ScalarType) -> PrimitiveType {
1172    match scalar {
1173        ScalarType::Unit => PrimitiveType::Unit,
1174        ScalarType::Bool => PrimitiveType::Bool,
1175        ScalarType::Char => PrimitiveType::Char,
1176        ScalarType::Str | ScalarType::String | ScalarType::CowStr => PrimitiveType::String,
1177        ScalarType::F32 => PrimitiveType::F32,
1178        ScalarType::F64 => PrimitiveType::F64,
1179        ScalarType::U8 => PrimitiveType::U8,
1180        ScalarType::U16 => PrimitiveType::U16,
1181        ScalarType::U32 => PrimitiveType::U32,
1182        ScalarType::U64 => PrimitiveType::U64,
1183        ScalarType::U128 => PrimitiveType::U128,
1184        ScalarType::USize => PrimitiveType::U64,
1185        ScalarType::I8 => PrimitiveType::I8,
1186        ScalarType::I16 => PrimitiveType::I16,
1187        ScalarType::I32 => PrimitiveType::I32,
1188        ScalarType::I64 => PrimitiveType::I64,
1189        ScalarType::I128 => PrimitiveType::I128,
1190        ScalarType::ISize => PrimitiveType::I64,
1191        ScalarType::ConstTypeId => PrimitiveType::U64,
1192        _ => PrimitiveType::Unit,
1193    }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199    use facet::Facet;
1200
1201    struct TestSchematic {
1202        direction: BindingDirection,
1203        shape: &'static Shape,
1204        attached: CborPayload,
1205    }
1206
1207    impl TestSchematic {
1208        fn new(direction: BindingDirection, shape: &'static Shape) -> Self {
1209            Self {
1210                direction,
1211                shape,
1212                attached: CborPayload::default(),
1213            }
1214        }
1215    }
1216
1217    impl Schematic for TestSchematic {
1218        fn direction(&self) -> BindingDirection {
1219            self.direction
1220        }
1221
1222        fn attach_schemas(&mut self, schemas: CborPayload) {
1223            self.attached = schemas;
1224        }
1225    }
1226
1227    // r[verify schema.type-id]
1228    #[test]
1229    fn type_ids_are_u64_content_hashes() {
1230        let id = SchemaHash(42);
1231        assert_eq!(id.0, 42);
1232        assert_eq!(id, SchemaHash(42));
1233        assert_ne!(id, SchemaHash(43));
1234    }
1235
1236    // r[verify schema.principles.cbor]
1237    // r[verify schema.format.self-contained]
1238    #[test]
1239    fn cbor_round_trip() {
1240        let schema = Schema {
1241            id: SchemaHash(1),
1242            type_params: vec![],
1243            kind: SchemaKind::Primitive {
1244                primitive_type: PrimitiveType::U32,
1245            },
1246        };
1247        let bytes = SchemaPayload {
1248            schemas: vec![schema.clone()],
1249            root: TypeRef::concrete(schema.id),
1250        }
1251        .to_cbor();
1252        let payload = SchemaPayload::from_cbor(&bytes.0).expect("should parse CBOR");
1253        assert_eq!(payload.schemas.len(), 1);
1254        assert_eq!(payload.schemas[0].id, schema.id);
1255        assert_eq!(payload.root, TypeRef::concrete(schema.id));
1256    }
1257
1258    // r[verify schema.format.primitive]
1259    #[test]
1260    fn primitive_u32() {
1261        let schemas = extract_schemas(<u32 as Facet>::SHAPE).unwrap().schemas;
1262        assert_eq!(schemas.len(), 1);
1263        assert!(matches!(
1264            schemas[0].kind,
1265            SchemaKind::Primitive {
1266                primitive_type: PrimitiveType::U32
1267            }
1268        ));
1269    }
1270
1271    #[test]
1272    fn primitive_string() {
1273        let schemas = extract_schemas(<String as Facet>::SHAPE).unwrap().schemas;
1274        assert_eq!(schemas.len(), 1);
1275        assert!(matches!(
1276            schemas[0].kind,
1277            SchemaKind::Primitive {
1278                primitive_type: PrimitiveType::String
1279            }
1280        ));
1281    }
1282
1283    #[test]
1284    fn primitive_bool() {
1285        let schemas = extract_schemas(<bool as Facet>::SHAPE).unwrap().schemas;
1286        assert_eq!(schemas.len(), 1);
1287        assert!(matches!(
1288            schemas[0].kind,
1289            SchemaKind::Primitive {
1290                primitive_type: PrimitiveType::Bool
1291            }
1292        ));
1293    }
1294
1295    // r[verify schema.format.struct]
1296    #[test]
1297    fn simple_struct() {
1298        #[derive(Facet)]
1299        struct Point {
1300            x: f64,
1301            y: f64,
1302        }
1303
1304        let schemas = extract_schemas(Point::SHAPE).unwrap().schemas;
1305        assert!(schemas.len() >= 2);
1306
1307        let point_schema = schemas.last().unwrap();
1308        match &point_schema.kind {
1309            SchemaKind::Struct { name, fields } => {
1310                assert!(
1311                    name.contains("Point"),
1312                    "expected name to contain Point, got {name}"
1313                );
1314                assert_eq!(fields.len(), 2);
1315                assert_eq!(fields[0].name, "x");
1316                assert_eq!(fields[1].name, "y");
1317                assert!(fields[0].required);
1318                assert_eq!(fields[0].type_ref, fields[1].type_ref);
1319            }
1320            other => panic!("expected Struct, got {other:?}"),
1321        }
1322    }
1323
1324    // r[verify schema.format.enum]
1325    #[test]
1326    fn simple_enum() {
1327        #[derive(Facet)]
1328        #[repr(u8)]
1329        enum Color {
1330            Red,
1331            Green,
1332            Blue,
1333        }
1334
1335        let schemas = extract_schemas(Color::SHAPE).unwrap().schemas;
1336        let color_schema = schemas.last().unwrap();
1337        match &color_schema.kind {
1338            SchemaKind::Enum { variants, .. } => {
1339                assert_eq!(variants.len(), 3);
1340                assert_eq!(variants[0].name, "Red");
1341                assert_eq!(variants[1].name, "Green");
1342                assert_eq!(variants[2].name, "Blue");
1343                assert!(matches!(variants[0].payload, VariantPayload::Unit));
1344            }
1345            other => panic!("expected Enum, got {other:?}"),
1346        }
1347    }
1348
1349    // r[verify schema.format.enum]
1350    #[test]
1351    fn enum_with_payloads() {
1352        #[derive(Facet)]
1353        #[repr(u8)]
1354        #[allow(dead_code)]
1355        enum Shape {
1356            Circle(f64),
1357            Rect { w: f64, h: f64 },
1358            Empty,
1359        }
1360
1361        let schemas = extract_schemas(Shape::SHAPE).unwrap().schemas;
1362        let shape_schema = schemas.last().unwrap();
1363        match &shape_schema.kind {
1364            SchemaKind::Enum { variants, .. } => {
1365                assert_eq!(variants.len(), 3);
1366                assert!(matches!(
1367                    variants[0].payload,
1368                    VariantPayload::Newtype { .. }
1369                ));
1370                match &variants[1].payload {
1371                    VariantPayload::Struct { fields } => {
1372                        assert_eq!(fields.len(), 2);
1373                        assert_eq!(fields[0].name, "w");
1374                        assert_eq!(fields[1].name, "h");
1375                    }
1376                    other => panic!("expected Struct variant, got {other:?}"),
1377                }
1378                assert!(matches!(variants[2].payload, VariantPayload::Unit));
1379            }
1380            other => panic!("expected Enum, got {other:?}"),
1381        }
1382    }
1383
1384    // r[verify schema.format.container]
1385    #[test]
1386    fn container_vec() {
1387        let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE).unwrap().schemas;
1388        assert_eq!(schemas.len(), 2);
1389        assert!(matches!(
1390            schemas[0].kind,
1391            SchemaKind::Primitive {
1392                primitive_type: PrimitiveType::U32
1393            }
1394        ));
1395        assert!(matches!(schemas[1].kind, SchemaKind::List { .. }));
1396    }
1397
1398    // r[verify schema.format.container]
1399    #[test]
1400    fn container_option() {
1401        let schemas = extract_schemas(<Option<String> as Facet>::SHAPE)
1402            .unwrap()
1403            .schemas;
1404        assert_eq!(schemas.len(), 2);
1405        assert!(matches!(
1406            schemas[0].kind,
1407            SchemaKind::Primitive {
1408                primitive_type: PrimitiveType::String
1409            }
1410        ));
1411        assert!(matches!(schemas[1].kind, SchemaKind::Option { .. }));
1412    }
1413
1414    // r[verify schema.format.recursive]
1415    #[test]
1416    fn recursive_type_terminates() {
1417        #[derive(Facet)]
1418        struct Node {
1419            value: u32,
1420            next: Option<Box<Node>>,
1421        }
1422
1423        let schemas = extract_schemas(Node::SHAPE).unwrap().schemas;
1424        assert!(schemas.len() >= 2);
1425
1426        let node_schema = schemas.last().unwrap();
1427        assert!(matches!(node_schema.kind, SchemaKind::Struct { .. }));
1428    }
1429
1430    // r[verify schema.format.primitive]
1431    #[test]
1432    fn vec_u8_is_bytes() {
1433        let schemas = extract_schemas(<Vec<u8> as Facet>::SHAPE).unwrap().schemas;
1434        assert_eq!(schemas.len(), 1);
1435        assert!(matches!(
1436            schemas[0].kind,
1437            SchemaKind::Primitive {
1438                primitive_type: PrimitiveType::Bytes
1439            }
1440        ));
1441    }
1442
1443    #[test]
1444    fn slice_u8_is_bytes() {
1445        let schemas = extract_schemas(<&[u8] as Facet>::SHAPE).unwrap().schemas;
1446        assert_eq!(schemas.len(), 1);
1447        assert!(matches!(
1448            schemas[0].kind,
1449            SchemaKind::Primitive {
1450                primitive_type: PrimitiveType::Bytes
1451            }
1452        ));
1453    }
1454
1455    #[test]
1456    fn cbor_payload_is_bytes() {
1457        let schemas = extract_schemas(CborPayload::SHAPE).unwrap().schemas;
1458        assert_eq!(schemas.len(), 1);
1459        assert!(matches!(
1460            schemas[0].kind,
1461            SchemaKind::Primitive {
1462                primitive_type: PrimitiveType::Bytes
1463            }
1464        ));
1465    }
1466
1467    // r[verify zerocopy.framing.value.opaque]
1468    #[test]
1469    fn opaque_payload_is_payload_primitive() {
1470        let schemas = extract_schemas(crate::Payload::<'static>::SHAPE)
1471            .unwrap()
1472            .schemas;
1473        assert_eq!(schemas.len(), 1);
1474        assert!(matches!(
1475            schemas[0].kind,
1476            SchemaKind::Primitive {
1477                primitive_type: PrimitiveType::Payload
1478            }
1479        ));
1480    }
1481
1482    #[test]
1483    fn infallible_is_never_primitive() {
1484        let schemas = extract_schemas(<std::convert::Infallible as Facet>::SHAPE)
1485            .unwrap()
1486            .schemas;
1487        assert_eq!(schemas.len(), 1);
1488        assert!(matches!(
1489            schemas[0].kind,
1490            SchemaKind::Primitive {
1491                primitive_type: PrimitiveType::Never
1492            }
1493        ));
1494    }
1495
1496    // r[verify schema.principles.once-per-type]
1497    #[test]
1498    fn deduplication_two_u32_fields() {
1499        #[derive(Facet)]
1500        struct TwoU32 {
1501            a: u32,
1502            b: u32,
1503        }
1504
1505        let schemas = extract_schemas(TwoU32::SHAPE).unwrap().schemas;
1506        let u32_count = schemas
1507            .iter()
1508            .filter(|s| {
1509                matches!(
1510                    s.kind,
1511                    SchemaKind::Primitive {
1512                        primitive_type: PrimitiveType::U32
1513                    }
1514                )
1515            })
1516            .count();
1517        assert_eq!(u32_count, 1, "u32 schema should appear exactly once");
1518        assert_eq!(schemas.len(), 2);
1519    }
1520
1521    // r[verify schema.format.container]
1522    #[test]
1523    fn container_map() {
1524        let schemas = extract_schemas(<std::collections::HashMap<String, u32> as Facet>::SHAPE)
1525            .unwrap()
1526            .schemas;
1527        let map_schema = schemas.last().unwrap();
1528        assert!(matches!(map_schema.kind, SchemaKind::Map { .. }));
1529    }
1530
1531    // r[verify schema.format.container]
1532    #[test]
1533    fn container_array() {
1534        let schemas = extract_schemas(<[u32; 4] as Facet>::SHAPE).unwrap().schemas;
1535        let arr_schema = schemas.last().unwrap();
1536        match &arr_schema.kind {
1537            SchemaKind::Array { length, .. } => assert_eq!(*length, 4),
1538            other => panic!("expected Array, got {other:?}"),
1539        }
1540    }
1541
1542    // r[verify schema.format.tuple]
1543    #[test]
1544    fn tuple_type() {
1545        let schemas = extract_schemas(<(u32, String) as Facet>::SHAPE)
1546            .unwrap()
1547            .schemas;
1548        let tuple_schema = schemas.last().unwrap();
1549        match &tuple_schema.kind {
1550            SchemaKind::Tuple { elements } => {
1551                assert_eq!(elements.len(), 2);
1552                assert_ne!(elements[0], elements[1]);
1553            }
1554            other => panic!("expected Tuple, got {other:?}"),
1555        }
1556    }
1557
1558    // r[verify schema.format]
1559    #[test]
1560    fn extract_schemas_returns_all_kinds() {
1561        #[derive(Facet)]
1562        struct Mixed {
1563            count: u32,
1564            tags: Vec<String>,
1565            pair: (u8, u8),
1566        }
1567
1568        let schemas = extract_schemas(Mixed::SHAPE).unwrap().schemas;
1569        assert!(schemas.len() >= 4);
1570    }
1571
1572    // r[verify schema.principles.once-per-type]
1573    // r[verify schema.exchange.idempotent]
1574    #[test]
1575    fn tracker_prepare_send_returns_payload_then_empty() {
1576        let mut tracker = SchemaSendTracker::new();
1577        let method = MethodId(1);
1578        let mut schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1579        let first = tracker
1580            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1581            .unwrap();
1582        assert!(
1583            !first.is_empty(),
1584            "first prepare_send should return payload"
1585        );
1586        assert_eq!(schematic.attached.0, first.0);
1587        let second = tracker
1588            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1589            .unwrap();
1590        assert!(
1591            second.is_empty(),
1592            "second prepare_send for same method should return empty"
1593        );
1594        assert!(schematic.attached.is_empty());
1595    }
1596
1597    // r[verify schema.tracking.transitive]
1598    // r[verify schema.tracking.sent]
1599    #[test]
1600    fn tracker_prepare_send_includes_transitive_deps() {
1601        #[derive(Facet)]
1602        struct Outer {
1603            inner: u32,
1604            name: String,
1605        }
1606
1607        let mut tracker = SchemaSendTracker::new();
1608        let method = MethodId(1);
1609        let mut schematic = TestSchematic::new(BindingDirection::Args, Outer::SHAPE);
1610        let first = tracker
1611            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1612            .unwrap();
1613        assert!(!first.is_empty(), "should return schemas");
1614        let parsed = SchemaPayload::from_cbor(&first.0).expect("should parse CBOR");
1615        assert!(
1616            parsed.schemas.len() >= 3,
1617            "should include transitive deps, got {}",
1618            parsed.schemas.len()
1619        );
1620
1621        // Same method again — nothing to send
1622        schematic.shape = <u32 as Facet>::SHAPE;
1623        let again = tracker
1624            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1625            .unwrap();
1626        assert!(
1627            again.is_empty(),
1628            "u32 was already sent as transitive dep, method already bound"
1629        );
1630    }
1631
1632    // r[verify schema.tracking.received]
1633    #[test]
1634    fn tracker_record_and_get_received() {
1635        let tracker = SchemaRecvTracker::new();
1636        let schemas = extract_schemas(<u32 as Facet>::SHAPE).unwrap().schemas;
1637        let id = schemas[0].id;
1638        assert!(tracker.get_received(&id).is_none());
1639        tracker
1640            .record_received(
1641                MethodId(7),
1642                BindingDirection::Args,
1643                SchemaPayload {
1644                    schemas,
1645                    root: TypeRef::concrete(id),
1646                },
1647            )
1648            .expect("first record should succeed");
1649        assert!(tracker.get_received(&id).is_some());
1650        assert_eq!(
1651            tracker.get_remote_args_root(MethodId(7)),
1652            Some(TypeRef::concrete(id))
1653        );
1654    }
1655
1656    // r[verify schema.type-id]
1657    // r[verify schema.type-id.hash]
1658    #[test]
1659    fn type_ids_are_content_hashes() {
1660        let mut tracker = SchemaSendTracker::new();
1661        let extracted = tracker
1662            .extract_schemas(<(u32, String) as Facet>::SHAPE)
1663            .unwrap();
1664        let schemas = extracted.schemas;
1665        assert!(schemas.len() >= 3);
1666
1667        // Same type extracted again must produce the same content hash.
1668        let mut tracker2 = SchemaSendTracker::new();
1669        let schemas2 = tracker2
1670            .extract_schemas(<(u32, String) as Facet>::SHAPE)
1671            .unwrap()
1672            .schemas;
1673        assert_eq!(schemas.len(), schemas2.len());
1674        for (a, b) in schemas.iter().zip(schemas2.iter()) {
1675            assert_eq!(a.id, b.id, "content hash should be deterministic");
1676        }
1677
1678        // Different types must produce different hashes.
1679        let mut tracker3 = SchemaSendTracker::new();
1680        let extracted3 = tracker3
1681            .extract_schemas(<(u64, String) as Facet>::SHAPE)
1682            .unwrap();
1683        assert_ne!(
1684            extracted.root, extracted3.root,
1685            "different types should produce different root refs"
1686        );
1687    }
1688
1689    // r[verify schema.type-id.hash.primitives]
1690    #[test]
1691    fn primitive_content_hashes_are_stable() {
1692        // These are the canonical hash values for primitive types.
1693        // Other implementations MUST produce identical values.
1694        let primitives = [
1695            PrimitiveType::Bool,
1696            PrimitiveType::U8,
1697            PrimitiveType::U16,
1698            PrimitiveType::U32,
1699            PrimitiveType::U64,
1700            PrimitiveType::U128,
1701            PrimitiveType::I8,
1702            PrimitiveType::I16,
1703            PrimitiveType::I32,
1704            PrimitiveType::I64,
1705            PrimitiveType::I128,
1706            PrimitiveType::F32,
1707            PrimitiveType::F64,
1708            PrimitiveType::Char,
1709            PrimitiveType::String,
1710            PrimitiveType::Unit,
1711            PrimitiveType::Never,
1712            PrimitiveType::Bytes,
1713            PrimitiveType::Payload,
1714        ];
1715
1716        // All primitive hashes must be unique.
1717        let hashes: Vec<SchemaHash> = primitives
1718            .iter()
1719            .map(|p| {
1720                compute_content_hash(&SchemaKind::Primitive { primitive_type: *p }, &[], &|id| id)
1721            })
1722            .collect();
1723        let unique: HashSet<SchemaHash> = hashes.iter().copied().collect();
1724        assert_eq!(
1725            unique.len(),
1726            hashes.len(),
1727            "all primitive hashes must be unique"
1728        );
1729
1730        // Verify they're deterministic (same computation, same result).
1731        for (i, p) in primitives.iter().enumerate() {
1732            let hash2 =
1733                compute_content_hash(&SchemaKind::Primitive { primitive_type: *p }, &[], &|id| id);
1734            assert_eq!(hashes[i], hash2, "hash for {:?} must be deterministic", p);
1735        }
1736    }
1737
1738    // r[verify schema.type-id.hash.struct]
1739    #[test]
1740    fn struct_hash_is_deterministic() {
1741        #[derive(Facet)]
1742        struct Point {
1743            x: f64,
1744            y: f64,
1745        }
1746
1747        let schemas1 = extract_schemas(Point::SHAPE).unwrap().schemas;
1748        let schemas2 = extract_schemas(Point::SHAPE).unwrap().schemas;
1749        assert_eq!(
1750            schemas1.last().unwrap().id,
1751            schemas2.last().unwrap().id,
1752            "same struct must produce the same content hash"
1753        );
1754    }
1755
1756    // r[verify schema.hash.recursive]
1757    #[test]
1758    fn recursive_type_hash_is_deterministic() {
1759        #[derive(Facet)]
1760        struct TreeNode {
1761            label: String,
1762            children: Vec<TreeNode>,
1763        }
1764
1765        let schemas1 = extract_schemas(TreeNode::SHAPE).unwrap().schemas;
1766        let schemas2 = extract_schemas(TreeNode::SHAPE).unwrap().schemas;
1767
1768        // Must have at least String, Vec<TreeNode>, TreeNode
1769        assert!(schemas1.len() >= 2);
1770
1771        // Same recursive type must produce identical hashes.
1772        let root1 = schemas1.last().unwrap().id;
1773        let root2 = schemas2.last().unwrap().id;
1774        assert_eq!(root1, root2, "recursive type hash must be deterministic");
1775
1776        // All type IDs in the output must be valid content hashes (non-zero).
1777        for s in &schemas1 {
1778            assert_ne!(s.id.0, 0, "content hash must not be zero");
1779        }
1780    }
1781
1782    #[test]
1783    fn bidirectional_bindings_are_independent() {
1784        let mut tracker = SchemaSendTracker::new();
1785        let method = MethodId(1);
1786
1787        // Send args binding
1788        let mut args_schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1789        let args = tracker
1790            .attach_schemas_for_shape_if_needed(method, args_schematic.shape, &mut args_schematic)
1791            .unwrap();
1792        assert!(!args.is_empty(), "should send args");
1793        let args_parsed = SchemaPayload::from_cbor(&args.0).expect("parse args CBOR");
1794
1795        // Send response binding for the same method — should NOT be deduplicated
1796        let mut response_schematic =
1797            TestSchematic::new(BindingDirection::Response, <String as Facet>::SHAPE);
1798        let response = tracker
1799            .attach_schemas_for_shape_if_needed(
1800                method,
1801                response_schematic.shape,
1802                &mut response_schematic,
1803            )
1804            .unwrap();
1805        assert!(!response.is_empty(), "should send response");
1806        let response_parsed = SchemaPayload::from_cbor(&response.0).expect("parse response CBOR");
1807        assert_ne!(args_parsed.root, response_parsed.root);
1808
1809        // Record received bindings and verify they go to separate maps
1810        let recv_tracker = SchemaRecvTracker::new();
1811        recv_tracker
1812            .record_received(
1813                MethodId(42),
1814                BindingDirection::Args,
1815                SchemaPayload {
1816                    schemas: extract_schemas(<u64 as Facet>::SHAPE).unwrap().schemas,
1817                    root: TypeRef::concrete(SchemaHash(100)),
1818                },
1819            )
1820            .expect("record should succeed");
1821        recv_tracker
1822            .record_received(
1823                MethodId(42),
1824                BindingDirection::Response,
1825                SchemaPayload {
1826                    schemas: vec![],
1827                    root: TypeRef::concrete(SchemaHash(200)),
1828                },
1829            )
1830            .expect("record should succeed");
1831
1832        assert_eq!(
1833            recv_tracker.get_remote_args_root(MethodId(42)),
1834            Some(TypeRef::concrete(SchemaHash(100)))
1835        );
1836        assert_eq!(
1837            recv_tracker.get_remote_response_root(MethodId(42)),
1838            Some(TypeRef::concrete(SchemaHash(200)))
1839        );
1840    }
1841
1842    #[test]
1843    fn duplicate_schema_is_protocol_error() {
1844        let tracker = SchemaRecvTracker::new();
1845        let schemas = extract_schemas(<u32 as Facet>::SHAPE).unwrap().schemas;
1846        tracker
1847            .record_received(
1848                MethodId(9),
1849                BindingDirection::Args,
1850                SchemaPayload {
1851                    schemas: schemas.clone(),
1852                    root: TypeRef::concrete(schemas[0].id),
1853                },
1854            )
1855            .expect("first record should succeed");
1856        let err = tracker
1857            .record_received(
1858                MethodId(9),
1859                BindingDirection::Args,
1860                SchemaPayload {
1861                    schemas: schemas.clone(),
1862                    root: TypeRef::concrete(schemas[0].id),
1863                },
1864            )
1865            .expect_err("duplicate should fail");
1866        assert_eq!(err.type_id, schemas[0].id);
1867    }
1868
1869    #[test]
1870    fn send_tracker_reset_clears_all_state() {
1871        let mut tracker = SchemaSendTracker::new();
1872        let method = MethodId(1);
1873        let mut schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1874        let first = tracker
1875            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1876            .unwrap();
1877        assert!(!first.is_empty(), "first should return payload");
1878
1879        tracker.reset();
1880
1881        let after_reset = tracker
1882            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1883            .unwrap();
1884        assert!(
1885            !after_reset.is_empty(),
1886            "after reset, prepare_send should return payload again"
1887        );
1888    }
1889
1890    // ========================================================================
1891    // Generic type deduplication tests
1892    // ========================================================================
1893
1894    #[test]
1895    fn generic_vec_uses_var_in_body() {
1896        let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE).unwrap().schemas;
1897        let list_schema = schemas
1898            .iter()
1899            .find(|s| matches!(s.kind, SchemaKind::List { .. }))
1900            .unwrap();
1901        assert_eq!(
1902            list_schema.type_params.len(),
1903            1,
1904            "Vec should have 1 type param"
1905        );
1906        match &list_schema.kind {
1907            SchemaKind::List { element } => {
1908                assert!(
1909                    matches!(element, TypeRef::Var { .. }),
1910                    "element should be Var, got {element:?}"
1911                );
1912            }
1913            other => panic!("expected List, got {other:?}"),
1914        }
1915    }
1916
1917    #[test]
1918    fn generic_option_uses_var_in_body() {
1919        let schemas = extract_schemas(<Option<String> as Facet>::SHAPE)
1920            .unwrap()
1921            .schemas;
1922        let opt_schema = schemas
1923            .iter()
1924            .find(|s| matches!(s.kind, SchemaKind::Option { .. }))
1925            .unwrap();
1926        assert_eq!(
1927            opt_schema.type_params.len(),
1928            1,
1929            "Option should have 1 type param"
1930        );
1931        match &opt_schema.kind {
1932            SchemaKind::Option { element } => {
1933                assert!(
1934                    matches!(element, TypeRef::Var { .. }),
1935                    "element should be Var, got {element:?}"
1936                );
1937            }
1938            other => panic!("expected Option, got {other:?}"),
1939        }
1940    }
1941
1942    #[test]
1943    fn generic_tuple_uses_vars_in_body() {
1944        let schemas = extract_schemas(<(u32, String) as Facet>::SHAPE)
1945            .unwrap()
1946            .schemas;
1947        let tuple_schema = schemas
1948            .iter()
1949            .find(|s| matches!(s.kind, SchemaKind::Tuple { .. }))
1950            .unwrap();
1951        assert_eq!(
1952            tuple_schema.type_params.len(),
1953            2,
1954            "tuple arity 2 should have 2 type params"
1955        );
1956        match &tuple_schema.kind {
1957            SchemaKind::Tuple { elements } => {
1958                assert_eq!(elements.len(), 2);
1959                assert!(matches!(elements[0], TypeRef::Var { .. }));
1960                assert!(matches!(elements[1], TypeRef::Var { .. }));
1961            }
1962            other => panic!("expected Tuple, got {other:?}"),
1963        }
1964    }
1965
1966    #[test]
1967    fn generic_vox_error_uses_var_in_user_payload() {
1968        use crate::VoxError;
1969
1970        let schemas = extract_schemas(<VoxError<::core::convert::Infallible> as Facet>::SHAPE)
1971            .unwrap()
1972            .schemas;
1973        let vox_error_schema = schemas
1974            .iter()
1975            .find(|s| matches!(&s.kind, SchemaKind::Enum { name, .. } if name == "VoxError"))
1976            .expect("VoxError schema should be present");
1977        match &vox_error_schema.kind {
1978            SchemaKind::Enum { variants, .. } => {
1979                let user = variants
1980                    .iter()
1981                    .find(|variant| variant.name == "User")
1982                    .expect("VoxError should have User variant");
1983                let VariantPayload::Newtype { type_ref } = &user.payload else {
1984                    panic!("User variant should be newtype");
1985                };
1986                assert!(
1987                    matches!(type_ref, TypeRef::Var { .. }),
1988                    "User payload should be a type variable, got {type_ref:?}"
1989                );
1990            }
1991            other => panic!("expected enum, got {other:?}"),
1992        }
1993    }
1994
1995    #[test]
1996    fn vec_of_option_of_u32_deduplicates() {
1997        // Vec<Option<u32>> should produce: u32, Option<T>, Vec<T>
1998        // NOT: u32, Option<u32>, Vec<Option<u32>>
1999        let schemas = extract_schemas(<Vec<Option<u32>> as Facet>::SHAPE)
2000            .unwrap()
2001            .schemas;
2002
2003        let list_count = schemas
2004            .iter()
2005            .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2006            .count();
2007        let option_count = schemas
2008            .iter()
2009            .filter(|s| matches!(s.kind, SchemaKind::Option { .. }))
2010            .count();
2011        assert_eq!(list_count, 1, "should have exactly 1 List schema");
2012        assert_eq!(option_count, 1, "should have exactly 1 Option schema");
2013    }
2014
2015    #[test]
2016    fn vec_u32_and_vec_string_share_one_list_schema() {
2017        #[derive(Facet)]
2018        struct Both {
2019            a: Vec<u32>,
2020            b: Vec<String>,
2021        }
2022
2023        let schemas = extract_schemas(Both::SHAPE).unwrap().schemas;
2024        let list_count = schemas
2025            .iter()
2026            .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2027            .count();
2028        assert_eq!(
2029            list_count, 1,
2030            "Vec<u32> and Vec<String> should share one List schema"
2031        );
2032    }
2033
2034    #[test]
2035    fn resolve_kind_substitutes_vars() {
2036        let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE).unwrap().schemas;
2037        let registry = build_registry(&schemas);
2038
2039        // The root schema is Vec<u32> — find it
2040        let root = schemas.last().unwrap();
2041        assert!(matches!(root.kind, SchemaKind::List { .. }));
2042
2043        // Build a TypeRef that says "Vec applied to u32"
2044        let u32_schema = schemas
2045            .iter()
2046            .find(|s| {
2047                matches!(
2048                    s.kind,
2049                    SchemaKind::Primitive {
2050                        primitive_type: PrimitiveType::U32
2051                    }
2052                )
2053            })
2054            .unwrap();
2055        let type_ref = TypeRef::generic(root.id, vec![TypeRef::concrete(u32_schema.id)]);
2056
2057        // resolve_kind should substitute Var("T") → concrete u32 id
2058        let resolved = type_ref.resolve_kind(&registry).expect("should resolve");
2059        match &resolved {
2060            SchemaKind::List { element } => match element {
2061                TypeRef::Concrete { type_id, args } => {
2062                    assert_eq!(*type_id, u32_schema.id);
2063                    assert!(args.is_empty());
2064                }
2065                other => panic!("expected concrete after resolution, got {other:?}"),
2066            },
2067            other => panic!("expected List, got {other:?}"),
2068        }
2069    }
2070
2071    #[test]
2072    fn extract_result_tuple_root_preserves_ok_tuple() {
2073        use crate::VoxError;
2074
2075        let extracted = extract_schemas(
2076            <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE,
2077        )
2078        .unwrap();
2079        let registry = build_registry(&extracted.schemas);
2080        let root = extracted
2081            .root
2082            .resolve_kind(&registry)
2083            .expect("result root should resolve");
2084
2085        let SchemaKind::Enum { variants, .. } = root else {
2086            panic!("expected Result enum root");
2087        };
2088        let ok_variant = variants
2089            .iter()
2090            .find(|variant| variant.name == "Ok")
2091            .expect("Result should have Ok variant");
2092        let VariantPayload::Newtype { type_ref } = &ok_variant.payload else {
2093            panic!("Ok variant should be newtype");
2094        };
2095        let ok_kind = type_ref
2096            .resolve_kind(&registry)
2097            .expect("Ok payload should resolve");
2098        match ok_kind {
2099            SchemaKind::Tuple { elements } => {
2100                assert_eq!(elements.len(), 2, "Ok tuple should have two elements");
2101            }
2102            other => panic!("expected Ok payload to be tuple, got {other:?}"),
2103        }
2104    }
2105
2106    #[test]
2107    fn result_ok_tuple_uses_generic_tuple_schema() {
2108        use crate::VoxError;
2109
2110        let result_shape =
2111            <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE;
2112        let ok_shape = result_shape.type_params[0].shape;
2113        let extracted = extract_schemas(
2114            <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE,
2115        )
2116        .unwrap();
2117        let TypeRef::Concrete { args, .. } = &extracted.root else {
2118            panic!("Result root should be concrete");
2119        };
2120        assert_eq!(
2121            args.len(),
2122            2,
2123            "Result root should have Ok and Err type args"
2124        );
2125        let TypeRef::Concrete { args: ok_args, .. } = &args[0] else {
2126            panic!("Ok type arg should be concrete tuple ref");
2127        };
2128        assert_eq!(
2129            ok_args.len(),
2130            2,
2131            "Ok tuple ref should carry concrete tuple element args; root={:?}; ok_shape={}; ok_shape_ty={:?}",
2132            extracted.root,
2133            ok_shape.type_identifier,
2134            ok_shape.ty
2135        );
2136    }
2137
2138    #[test]
2139    fn unary_tuple_root_preserves_nested_tuple() {
2140        let extracted = extract_schemas(<((i32, String),) as Facet>::SHAPE).unwrap();
2141        let registry = build_registry(&extracted.schemas);
2142
2143        let root = extracted
2144            .root
2145            .resolve_kind(&registry)
2146            .expect("root should resolve");
2147        let SchemaKind::Tuple { elements } = root else {
2148            panic!("expected unary tuple root");
2149        };
2150        assert_eq!(elements.len(), 1, "outer tuple should remain unary");
2151
2152        let inner = elements[0]
2153            .resolve_kind(&registry)
2154            .expect("inner tuple should resolve");
2155        match inner {
2156            SchemaKind::Tuple { elements } => {
2157                assert_eq!(elements.len(), 2, "inner tuple should remain binary");
2158            }
2159            other => panic!("expected inner tuple, got {other:?}"),
2160        }
2161
2162        let tuple_count = extracted
2163            .schemas
2164            .iter()
2165            .filter(|schema| matches!(schema.kind, SchemaKind::Tuple { .. }))
2166            .count();
2167        assert_eq!(tuple_count, 2, "should emit one tuple schema per arity");
2168    }
2169
2170    #[test]
2171    fn nested_generic_vec_of_vec_of_u32() {
2172        // Vec<Vec<u32>> — should produce u32, Vec<T>, not u32, Vec<u32>, Vec<Vec<u32>>
2173        let schemas = extract_schemas(<Vec<Vec<u32>> as Facet>::SHAPE)
2174            .unwrap()
2175            .schemas;
2176        let list_count = schemas
2177            .iter()
2178            .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2179            .count();
2180        assert_eq!(
2181            list_count, 1,
2182            "Vec<Vec<u32>> should have exactly 1 List schema (Vec<T>)"
2183        );
2184    }
2185
2186    #[test]
2187    fn recursive_type_with_option_box() {
2188        #[derive(Facet)]
2189        struct Node {
2190            value: u32,
2191            next: Option<Box<Node>>,
2192        }
2193
2194        let schemas = extract_schemas(Node::SHAPE).unwrap().schemas;
2195        // Should have: u32, Option<T>, Node
2196        let option_count = schemas
2197            .iter()
2198            .filter(|s| matches!(s.kind, SchemaKind::Option { .. }))
2199            .count();
2200        assert_eq!(option_count, 1, "should have exactly 1 Option schema");
2201
2202        // The Option schema should use Var, not concrete
2203        let opt_schema = schemas
2204            .iter()
2205            .find(|s| matches!(s.kind, SchemaKind::Option { .. }))
2206            .unwrap();
2207        match &opt_schema.kind {
2208            SchemaKind::Option { element } => {
2209                assert!(
2210                    matches!(element, TypeRef::Var { .. }),
2211                    "element should be Var"
2212                );
2213            }
2214            _ => unreachable!(),
2215        }
2216
2217        // All type IDs should be non-zero (properly hashed)
2218        for s in &schemas {
2219            assert_ne!(s.id.0, 0, "content hash must not be zero: {:?}", s.kind);
2220        }
2221    }
2222
2223    #[test]
2224    fn map_schema_is_generic() {
2225        let schemas = extract_schemas(<std::collections::HashMap<String, u32> as Facet>::SHAPE)
2226            .unwrap()
2227            .schemas;
2228        let map_schema = schemas
2229            .iter()
2230            .find(|s| matches!(s.kind, SchemaKind::Map { .. }))
2231            .unwrap();
2232        assert_eq!(
2233            map_schema.type_params.len(),
2234            2,
2235            "HashMap should have 2 type params"
2236        );
2237        match &map_schema.kind {
2238            SchemaKind::Map { key, value } => {
2239                assert!(matches!(key, TypeRef::Var { .. }), "key should be Var");
2240                assert!(matches!(value, TypeRef::Var { .. }), "value should be Var");
2241            }
2242            _ => unreachable!(),
2243        }
2244    }
2245
2246    #[test]
2247    fn schema_payload_cbor_round_trip() {
2248        let payload = SchemaPayload {
2249            schemas: vec![],
2250            root: TypeRef::Concrete {
2251                type_id: SchemaHash(123),
2252                args: vec![TypeRef::concrete(SchemaHash(456))],
2253            },
2254        };
2255        let bytes = payload.to_cbor();
2256        let parsed = SchemaPayload::from_cbor(&bytes.0).expect("should parse CBOR");
2257        match &parsed.root {
2258            TypeRef::Concrete { type_id, args } => {
2259                assert_eq!(*type_id, SchemaHash(123));
2260                assert_eq!(args.len(), 1);
2261                match &args[0] {
2262                    TypeRef::Concrete { type_id, args } => {
2263                        assert_eq!(*type_id, SchemaHash(456));
2264                        assert!(args.is_empty());
2265                    }
2266                    other => panic!("expected concrete arg, got {other:?}"),
2267                }
2268            }
2269            other => panic!("expected concrete root, got {other:?}"),
2270        }
2271    }
2272}