Skip to main content

quiver/
serialize.rs

1//! Serialization and Persistence
2//!
3//! This module provides types and utilities for saving and loading patches,
4//! including module registry and patch definitions.
5
6use crate::analog::{AnalogVco, Saturator, Wavefolder};
7use crate::graph::{NodeHandle, Patch, PatchError};
8use crate::modules::*;
9use crate::port::{GraphModule, PortSpec};
10use crate::StdMap;
11use alloc::boxed::Box;
12use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec;
15use alloc::vec::Vec;
16use serde::{Deserialize, Serialize};
17
18/// Current patch schema version written by [`Patch::to_def`].
19///
20/// Version policy: the field is a monotonic integer. A loader accepts any patch whose
21/// `version <= CURRENT_PATCH_VERSION` (older or equal — the format only grows additively, so
22/// old files keep loading) and rejects anything newer with a descriptive error rather than
23/// silently misreading fields it does not understand. New *additive* fields (like `output`)
24/// do **not** bump the version; only a breaking change to existing field meaning would.
25pub const CURRENT_PATCH_VERSION: u32 = 1;
26
27/// Serializable patch definition
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
30#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
31pub struct PatchDef {
32    /// Schema version. See [`CURRENT_PATCH_VERSION`] for the compatibility policy: loaders
33    /// accept `version <= CURRENT_PATCH_VERSION` and reject newer patches.
34    pub version: u32,
35
36    /// Patch metadata
37    pub name: String,
38    pub author: Option<String>,
39    pub description: Option<String>,
40
41    /// Optional and additive: the published JSON schema marks `tags` optional (default `[]`),
42    /// so a hand-authored/tool-generated patch that omits the key must still load. `serde(default)`
43    /// keeps deserialization in sync with the schema.
44    #[serde(default)]
45    pub tags: Vec<String>,
46
47    /// Name of the module whose output feeds the patch's stereo out.
48    ///
49    /// Optional and additive: patches written before this field existed omit it, and
50    /// [`Patch::from_def`] then falls back to its output-node heuristic. Written by
51    /// [`Patch::to_def`] whenever the patch has an output node set.
52    #[serde(default)]
53    pub output: Option<String>,
54
55    /// Module instances
56    pub modules: Vec<ModuleDef>,
57
58    /// Cable connections
59    pub cables: Vec<CableDef>,
60
61    /// Parameter values (key: `"module_name.param_id"`).
62    ///
63    /// `param_id` is either a control-input port name (its unpatched base value) or an
64    /// internal parameter id exposed via `ModuleIntrospection`. Applied by
65    /// [`Patch::from_def`] through [`Patch::set_param_by_id`]; unknown keys are ignored so
66    /// hand-edited files degrade gracefully.
67    ///
68    /// Optional and additive: the schema marks `parameters` optional (default `{}`), so a patch
69    /// that omits the key must still load. `serde(default)` keeps deserialization in sync.
70    #[serde(default)]
71    pub parameters: StdMap<String, f64>,
72}
73
74impl PatchDef {
75    /// Create a new empty patch definition
76    pub fn new(name: impl Into<String>) -> Self {
77        Self {
78            version: CURRENT_PATCH_VERSION,
79            name: name.into(),
80            author: None,
81            description: None,
82            tags: vec![],
83            output: None,
84            modules: vec![],
85            cables: vec![],
86            parameters: StdMap::new(),
87        }
88    }
89
90    /// Set the author
91    pub fn with_author(mut self, author: impl Into<String>) -> Self {
92        self.author = Some(author.into());
93        self
94    }
95
96    /// Set the description
97    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
98        self.description = Some(desc.into());
99        self
100    }
101
102    /// Add a tag
103    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
104        self.tags.push(tag.into());
105        self
106    }
107
108    /// Serialize to JSON string
109    pub fn to_json(&self) -> Result<String, serde_json::Error> {
110        serde_json::to_string_pretty(self)
111    }
112
113    /// Deserialize from JSON string
114    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
115        serde_json::from_str(json)
116    }
117}
118
119impl Default for PatchDef {
120    fn default() -> Self {
121        Self::new("Untitled")
122    }
123}
124
125/// Serializable module definition
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
128pub struct ModuleDef {
129    /// Unique instance name
130    pub name: String,
131
132    /// Module type identifier
133    pub module_type: String,
134
135    /// UI position (optional)
136    pub position: Option<(f32, f32)>,
137
138    /// Module-specific state
139    pub state: Option<serde_json::Value>,
140}
141
142impl ModuleDef {
143    pub fn new(name: impl Into<String>, module_type: impl Into<String>) -> Self {
144        Self {
145            name: name.into(),
146            module_type: module_type.into(),
147            position: None,
148            state: None,
149        }
150    }
151
152    pub fn with_position(mut self, x: f32, y: f32) -> Self {
153        self.position = Some((x, y));
154        self
155    }
156}
157
158/// Serializable cable definition
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
161pub struct CableDef {
162    /// Source: "module_name.port_name"
163    pub from: String,
164
165    /// Destination: "module_name.port_name"
166    pub to: String,
167
168    /// Optional attenuation/gain (-2.0 to 2.0)
169    pub attenuation: Option<f64>,
170
171    /// Optional DC offset (-10.0 to 10.0V)
172    pub offset: Option<f64>,
173}
174
175impl CableDef {
176    pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
177        Self {
178            from: from.into(),
179            to: to.into(),
180            attenuation: None,
181            offset: None,
182        }
183    }
184
185    pub fn with_attenuation(mut self, attenuation: f64) -> Self {
186        self.attenuation = Some(attenuation);
187        self
188    }
189
190    pub fn with_offset(mut self, offset: f64) -> Self {
191        self.offset = Some(offset);
192        self
193    }
194
195    pub fn with_modulation(mut self, attenuation: f64, offset: f64) -> Self {
196        self.attenuation = Some(attenuation);
197        self.offset = Some(offset);
198        self
199    }
200}
201
202/// Module factory function type
203pub type ModuleFactory = Box<dyn Fn(f64) -> Box<dyn GraphModule> + Send + Sync>;
204
205/// Metadata about a registered module type
206#[derive(Debug, Clone)]
207pub struct ModuleMetadata {
208    pub type_id: String,
209    pub name: String,
210    pub category: String,
211    pub description: String,
212    pub port_spec: PortSpec,
213    /// Keywords for search functionality
214    pub keywords: Vec<String>,
215    /// Tags for filtering (e.g., "essential", "advanced")
216    pub tags: Vec<String>,
217}
218
219// =============================================================================
220// Module Catalog Types (Phase 3: GUI Framework)
221// =============================================================================
222
223/// Summary of a module's port configuration for the catalog UI
224#[derive(Debug, Clone, Serialize, Deserialize)]
225#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
226pub struct PortSummary {
227    /// Number of input ports
228    pub inputs: u8,
229    /// Number of output ports
230    pub outputs: u8,
231    /// Whether the module has audio input(s)
232    pub has_audio_in: bool,
233    /// Whether the module has audio output(s)
234    pub has_audio_out: bool,
235}
236
237impl PortSummary {
238    /// Create a port summary from a PortSpec
239    pub fn from_port_spec(spec: &PortSpec) -> Self {
240        use crate::port::SignalKind;
241
242        let has_audio_in = spec.inputs.iter().any(|p| p.kind == SignalKind::Audio);
243        let has_audio_out = spec.outputs.iter().any(|p| p.kind == SignalKind::Audio);
244
245        Self {
246            inputs: spec.inputs.len() as u8,
247            outputs: spec.outputs.len() as u8,
248            has_audio_in,
249            has_audio_out,
250        }
251    }
252}
253
254/// A catalog entry for the "add module" UI
255#[derive(Debug, Clone, Serialize, Deserialize)]
256#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
257pub struct ModuleCatalogEntry {
258    /// Module type identifier (e.g., "vco", "svf")
259    pub type_id: String,
260    /// Human-readable name (e.g., "VCO", "State Variable Filter")
261    pub name: String,
262    /// Category for grouping (e.g., "Oscillators", "Filters")
263    pub category: String,
264    /// Longer description for tooltips/help
265    pub description: String,
266    /// Search keywords (e.g., ["oscillator", "sine", "saw", "pulse"])
267    pub keywords: Vec<String>,
268    /// Port configuration summary
269    pub ports: PortSummary,
270    /// Tags for filtering (e.g., ["essential", "advanced", "analog"])
271    pub tags: Vec<String>,
272}
273
274impl ModuleCatalogEntry {
275    /// Create a catalog entry from ModuleMetadata
276    pub fn from_metadata(metadata: &ModuleMetadata) -> Self {
277        Self {
278            type_id: metadata.type_id.clone(),
279            name: metadata.name.clone(),
280            category: metadata.category.clone(),
281            description: metadata.description.clone(),
282            keywords: metadata.keywords.clone(),
283            ports: PortSummary::from_port_spec(&metadata.port_spec),
284            tags: metadata.tags.clone(),
285        }
286    }
287}
288
289/// Response from catalog() containing all modules and categories
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
292#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
293pub struct CatalogResponse {
294    /// All available modules
295    pub modules: Vec<ModuleCatalogEntry>,
296    /// All unique categories (sorted)
297    pub categories: Vec<String>,
298}
299
300/// Registry of available module types for instantiation
301pub struct ModuleRegistry {
302    factories: StdMap<String, ModuleFactory>,
303    metadata: StdMap<String, ModuleMetadata>,
304}
305
306impl ModuleRegistry {
307    /// Create a new empty registry
308    pub fn new() -> Self {
309        let mut registry = Self {
310            factories: StdMap::new(),
311            metadata: StdMap::new(),
312        };
313
314        // Register built-in modules
315        registry.register_builtin();
316        registry
317    }
318
319    fn register_builtin(&mut self) {
320        // =====================================================================
321        // Oscillators
322        // =====================================================================
323        self.register_factory_with_keywords(
324            "vco",
325            "VCO",
326            "Oscillators",
327            "Voltage-controlled oscillator with multiple waveforms",
328            &[
329                "oscillator",
330                "sine",
331                "saw",
332                "pulse",
333                "triangle",
334                "waveform",
335                "pitch",
336            ],
337            &["essential"],
338            |sr| Box::new(Vco::new(sr)),
339        );
340
341        self.register_factory_with_keywords(
342            "analog_vco",
343            "Analog VCO",
344            "Oscillators",
345            "VCO with analog modeling (drift, saturation)",
346            &[
347                "oscillator",
348                "analog",
349                "drift",
350                "warm",
351                "vintage",
352                "detuned",
353            ],
354            &["analog"],
355            |sr| Box::new(AnalogVco::new(sr)),
356        );
357
358        self.register_factory_with_keywords(
359            "lfo",
360            "LFO",
361            "Modulation",
362            "Low-frequency oscillator for modulation",
363            &[
364                "oscillator",
365                "modulation",
366                "vibrato",
367                "tremolo",
368                "slow",
369                "sweep",
370            ],
371            &["essential"],
372            |sr| Box::new(Lfo::new(sr)),
373        );
374
375        // =====================================================================
376        // Filters
377        // =====================================================================
378        self.register_factory_with_keywords(
379            "svf",
380            "SVF",
381            "Filters",
382            "State-variable filter with LP/BP/HP/Notch outputs",
383            &[
384                "filter",
385                "lowpass",
386                "highpass",
387                "bandpass",
388                "notch",
389                "resonance",
390                "cutoff",
391            ],
392            &["essential"],
393            |sr| Box::new(Svf::new(sr)),
394        );
395
396        self.register_factory_with_keywords(
397            "diode_ladder",
398            "Diode Ladder Filter",
399            "Filters",
400            "24dB/oct ladder filter with diode saturation",
401            &[
402                "filter",
403                "ladder",
404                "moog",
405                "lowpass",
406                "resonance",
407                "saturation",
408                "analog",
409            ],
410            &["analog"],
411            |sr| Box::new(DiodeLadderFilter::new(sr)),
412        );
413
414        // =====================================================================
415        // Envelopes
416        // =====================================================================
417        self.register_factory_with_keywords(
418            "adsr",
419            "ADSR",
420            "Envelopes",
421            "Attack-Decay-Sustain-Release envelope generator",
422            &[
423                "envelope", "attack", "decay", "sustain", "release", "eg", "contour",
424            ],
425            &["essential"],
426            |sr| Box::new(Adsr::new(sr)),
427        );
428
429        // =====================================================================
430        // Amplifiers & VCAs
431        // =====================================================================
432        self.register_factory_with_keywords(
433            "vca",
434            "VCA",
435            "Utilities",
436            "Voltage-controlled amplifier",
437            &["amplifier", "gain", "volume", "level", "cv"],
438            &["essential"],
439            |_| Box::new(Vca::new()),
440        );
441
442        // =====================================================================
443        // Mixers & Utilities
444        // =====================================================================
445        self.register_factory_with_keywords(
446            "mixer",
447            "Mixer",
448            "Utilities",
449            "4-channel audio mixer",
450            &["mix", "combine", "sum", "blend", "audio"],
451            &["essential"],
452            |_| Box::new(Mixer::new(4)),
453        );
454
455        self.register_factory_with_keywords(
456            "mixer8",
457            "Mixer 8",
458            "Utilities",
459            "8-channel audio mixer for polyphony",
460            &["mix", "combine", "sum", "blend", "audio", "poly", "voices"],
461            &[],
462            |_| Box::new(Mixer::new(8)),
463        );
464
465        self.register_factory_with_keywords(
466            "offset",
467            "Offset",
468            "Utilities",
469            "DC offset / voltage source",
470            &["dc", "voltage", "constant", "bias", "source"],
471            &[],
472            |_| Box::new(Offset::new(0.0)),
473        );
474
475        self.register_factory_with_keywords(
476            "unit_delay",
477            "Unit Delay",
478            "Utilities",
479            "Single-sample delay for feedback",
480            &["delay", "feedback", "sample", "z-1"],
481            &["advanced"],
482            |_| Box::new(UnitDelay::new()),
483        );
484
485        self.register_factory_with_keywords(
486            "delay_line",
487            "Delay Line",
488            "Effects",
489            "Multi-tap delay with feedback and wet/dry mix",
490            &["delay", "echo", "feedback", "time", "effect"],
491            &[],
492            |sr| Box::new(DelayLine::new(sr)),
493        );
494
495        self.register_factory_with_keywords(
496            "chorus",
497            "Chorus",
498            "Effects",
499            "Classic chorus effect with modulated delay lines",
500            &[
501                "chorus",
502                "modulation",
503                "detune",
504                "ensemble",
505                "effect",
506                "stereo",
507            ],
508            &[],
509            |sr| Box::new(Chorus::new(sr)),
510        );
511
512        self.register_factory_with_keywords(
513            "flanger",
514            "Flanger",
515            "Effects",
516            "Classic flanging effect with modulated delay",
517            &["flanger", "modulation", "sweep", "jet", "effect"],
518            &[],
519            |sr| Box::new(Flanger::new(sr)),
520        );
521
522        self.register_factory_with_keywords(
523            "phaser",
524            "Phaser",
525            "Effects",
526            "Classic phaser effect with all-pass filters",
527            &["phaser", "modulation", "sweep", "effect", "allpass"],
528            &[],
529            |sr| Box::new(Phaser::new(sr)),
530        );
531
532        self.register_factory_with_keywords(
533            "limiter",
534            "Limiter",
535            "Dynamics",
536            "Prevents signals from exceeding threshold",
537            &["limiter", "dynamics", "ceiling", "clip", "loudness"],
538            &[],
539            |sr| Box::new(Limiter::new(sr)),
540        );
541
542        self.register_factory_with_keywords(
543            "noise_gate",
544            "Noise Gate",
545            "Dynamics",
546            "Attenuates signals below threshold",
547            &["gate", "dynamics", "noise", "threshold", "mute"],
548            &[],
549            |sr| Box::new(NoiseGate::new(sr)),
550        );
551
552        self.register_factory_with_keywords(
553            "compressor",
554            "Compressor",
555            "Dynamics",
556            "Dynamic range compression with sidechain",
557            &["compressor", "dynamics", "squeeze", "punch", "sidechain"],
558            &[],
559            |sr| Box::new(Compressor::new(sr)),
560        );
561
562        self.register_factory_with_keywords(
563            "envelope_follower",
564            "Envelope Follower",
565            "Utilities",
566            "Extracts amplitude envelope from audio",
567            &["envelope", "follower", "detector", "cv", "ducking"],
568            &[],
569            |sr| Box::new(EnvelopeFollower::new(sr)),
570        );
571
572        self.register_factory_with_keywords(
573            "ducker",
574            "Ducker",
575            "Dynamics",
576            "Sidechain ducking driven by a key input",
577            &["ducker", "duck", "sidechain", "key", "dynamics", "pump"],
578            &[],
579            |sr| Box::new(Ducker::new(sr)),
580        );
581
582        self.register_factory_with_keywords(
583            "sample_player",
584            "Sample Player",
585            "Oscillators",
586            "Mono sample playback with V/Oct pitch and looping",
587            &["sample", "player", "playback", "sampler", "wav", "loop"],
588            &[],
589            // Serialized default is an empty (silent) buffer; load audio via the
590            // non-RT SamplePlayer::set_buffer setter after instantiation.
591            |sr| Box::new(SamplePlayer::empty(sr)),
592        );
593
594        self.register_factory_with_keywords(
595            "mid_side_encode",
596            "Mid/Side Encode",
597            "Utilities",
598            "Encode left/right stereo to mid/side",
599            &["mid", "side", "ms", "stereo", "encode", "matrix"],
600            &[],
601            |_| Box::new(MidSideEncode::new()),
602        );
603
604        self.register_factory_with_keywords(
605            "mid_side_decode",
606            "Mid/Side Decode",
607            "Utilities",
608            "Decode mid/side to left/right with width control",
609            &["mid", "side", "ms", "stereo", "decode", "width"],
610            &[],
611            |_| Box::new(MidSideDecode::new()),
612        );
613
614        self.register_factory_with_keywords(
615            "bitcrusher",
616            "Bitcrusher",
617            "Effects",
618            "Lo-fi bit depth and sample rate reduction",
619            &["bitcrusher", "lofi", "distortion", "digital", "retro"],
620            &[],
621            |_| Box::new(Bitcrusher::new()),
622        );
623
624        // P3 Effects
625        self.register_factory_with_keywords(
626            "tremolo",
627            "Tremolo",
628            "Effects",
629            "Amplitude modulation effect with rate and depth control",
630            &["tremolo", "amplitude", "modulation", "wobble", "lfo"],
631            &[],
632            |sr| Box::new(Tremolo::new(sr)),
633        );
634
635        self.register_factory_with_keywords(
636            "vibrato",
637            "Vibrato",
638            "Effects",
639            "Pitch modulation effect using modulated delay",
640            &["vibrato", "pitch", "modulation", "wobble", "lfo"],
641            &[],
642            |sr| Box::new(Vibrato::new(sr)),
643        );
644
645        self.register_factory_with_keywords(
646            "distortion",
647            "Distortion",
648            "Effects",
649            "Waveshaping distortion with multiple modes",
650            &["distortion", "overdrive", "fuzz", "saturation", "clip"],
651            &[],
652            |sr| Box::new(Distortion::new(sr)),
653        );
654
655        // P3 Oscillators
656        self.register_factory_with_keywords(
657            "supersaw",
658            "Supersaw",
659            "Oscillators",
660            "JP-8000 style 7-voice detuned supersaw oscillator",
661            &["supersaw", "trance", "unison", "detune", "thick"],
662            &[],
663            |sr| Box::new(Supersaw::new(sr)),
664        );
665
666        self.register_factory_with_keywords(
667            "karplus_strong",
668            "Karplus-Strong",
669            "Oscillators",
670            "Physical modeling plucked string synthesis",
671            &["karplus", "string", "pluck", "physical", "modeling"],
672            &[],
673            |sr| Box::new(KarplusStrong::new(sr)),
674        );
675
676        // P3 Utilities
677        self.register_factory_with_keywords(
678            "scale_quantizer",
679            "Scale Quantizer",
680            "Utilities",
681            "Quantize CV to musical scale notes",
682            &["quantizer", "scale", "music", "notes", "pitch"],
683            &[],
684            |sr| Box::new(ScaleQuantizer::new(sr)),
685        );
686
687        self.register_factory_with_keywords(
688            "euclidean",
689            "Euclidean Rhythm",
690            "Sequencers",
691            "Euclidean rhythm generator for evenly distributed pulses",
692            &["euclidean", "rhythm", "pattern", "trigger", "clock"],
693            &[],
694            |sr| Box::new(Euclidean::new(sr)),
695        );
696
697        self.register_factory_with_keywords(
698            "attenuverter",
699            "Attenuverter",
700            "Utilities",
701            "Attenuate, invert, and offset signals",
702            &["attenuator", "invert", "scale", "offset", "gain"],
703            &["essential"],
704            |_| Box::new(Attenuverter::new()),
705        );
706
707        self.register_factory_with_keywords(
708            "multiple",
709            "Multiple",
710            "Utilities",
711            "Signal splitter (1 input to 4 outputs)",
712            &["split", "copy", "mult", "buffer", "distribute"],
713            &["essential"],
714            |_| Box::new(Multiple::new()),
715        );
716
717        self.register_factory_with_keywords(
718            "crossfader",
719            "Crossfader/Panner",
720            "Utilities",
721            "Crossfade between inputs or pan stereo",
722            &["crossfade", "pan", "stereo", "balance", "mix"],
723            &[],
724            |_| Box::new(Crossfader::new()),
725        );
726
727        self.register_factory_with_keywords(
728            "precision_adder",
729            "Precision Adder",
730            "Utilities",
731            "High-precision CV adder for V/Oct signals",
732            &["add", "sum", "transpose", "octave", "voct", "pitch"],
733            &[],
734            |_| Box::new(PrecisionAdder::new()),
735        );
736
737        self.register_factory_with_keywords(
738            "vc_switch",
739            "VC Switch",
740            "Utilities",
741            "Voltage-controlled signal router",
742            &["switch", "router", "selector", "mux", "demux"],
743            &[],
744            |_| Box::new(VcSwitch::new()),
745        );
746
747        self.register_factory_with_keywords(
748            "min",
749            "Min",
750            "Utilities",
751            "Output minimum of two signals",
752            &["minimum", "compare", "math", "lowest"],
753            &[],
754            |_| Box::new(Min::new()),
755        );
756
757        self.register_factory_with_keywords(
758            "max",
759            "Max",
760            "Utilities",
761            "Output maximum of two signals",
762            &["maximum", "compare", "math", "highest"],
763            &[],
764            |_| Box::new(Max::new()),
765        );
766
767        self.register_factory_with_keywords(
768            "sample_and_hold",
769            "Sample & Hold",
770            "Utilities",
771            "Sample input value on trigger",
772            &["sample", "hold", "trigger", "freeze", "snapshot"],
773            &[],
774            |_| Box::new(SampleAndHold::new()),
775        );
776
777        self.register_factory_with_keywords(
778            "slew_limiter",
779            "Slew Limiter",
780            "Utilities",
781            "Limits rate of change (portamento/glide)",
782            &["slew", "portamento", "glide", "lag", "smooth"],
783            &[],
784            |sr| Box::new(SlewLimiter::new(sr)),
785        );
786
787        self.register_factory_with_keywords(
788            "quantizer",
789            "Quantizer",
790            "Utilities",
791            "Quantize V/Oct to musical scales",
792            &["quantize", "scale", "pitch", "chromatic", "note", "tune"],
793            &[],
794            |_| Box::new(Quantizer::new(Scale::Chromatic)),
795        );
796
797        // =====================================================================
798        // Sources
799        // =====================================================================
800        self.register_factory_with_keywords(
801            "noise",
802            "Noise",
803            "Sources",
804            "White and pink noise generator",
805            &["noise", "white", "pink", "random", "hiss"],
806            &["essential"],
807            |_| Box::new(NoiseGenerator::new()),
808        );
809
810        // =====================================================================
811        // Sequencing
812        // =====================================================================
813        self.register_factory_with_keywords(
814            "step_sequencer",
815            "Step Sequencer",
816            "Sequencing",
817            "8-step CV/gate sequencer",
818            &["sequencer", "step", "pattern", "melody", "cv", "gate"],
819            &["essential"],
820            |_| Box::new(StepSequencer::new()),
821        );
822
823        self.register_factory_with_keywords(
824            "clock",
825            "Clock",
826            "Sequencing",
827            "Master clock with tempo control",
828            &["clock", "tempo", "bpm", "trigger", "pulse", "sync"],
829            &["essential"],
830            |sr| Box::new(Clock::new(sr)),
831        );
832
833        // =====================================================================
834        // I/O
835        // =====================================================================
836        self.register_factory_with_keywords(
837            "stereo_output",
838            "Stereo Output",
839            "I/O",
840            "Final stereo audio output",
841            &["output", "stereo", "main", "master", "speaker", "audio"],
842            &["essential"],
843            |_| Box::new(StereoOutput::new()),
844        );
845
846        // =====================================================================
847        // Effects
848        // =====================================================================
849        self.register_factory_with_keywords(
850            "saturator",
851            "Saturator",
852            "Effects",
853            "Soft saturation / overdrive",
854            &[
855                "saturation",
856                "overdrive",
857                "distortion",
858                "warm",
859                "tube",
860                "tape",
861            ],
862            &["analog"],
863            |_| Box::new(Saturator::default()),
864        );
865
866        self.register_factory_with_keywords(
867            "wavefolder",
868            "Wavefolder",
869            "Effects",
870            "Wavefolder for complex harmonics",
871            &["wavefolder", "fold", "harmonics", "timbre", "west coast"],
872            &[],
873            |_| Box::new(Wavefolder::default()),
874        );
875
876        self.register_factory_with_keywords(
877            "ring_mod",
878            "Ring Modulator",
879            "Effects",
880            "Multiplies two signals for metallic/bell sounds",
881            &["ring", "modulator", "multiply", "bell", "metallic", "am"],
882            &[],
883            |_| Box::new(RingModulator::new()),
884        );
885
886        self.register_factory_with_keywords(
887            "rectifier",
888            "Rectifier",
889            "Effects",
890            "Full-wave and half-wave rectification",
891            &["rectify", "absolute", "waveshape", "fold"],
892            &[],
893            |_| Box::new(Rectifier::new()),
894        );
895
896        // =====================================================================
897        // Logic
898        // =====================================================================
899        self.register_factory_with_keywords(
900            "logic_and",
901            "Logic AND",
902            "Logic",
903            "Output high when both inputs are high",
904            &["and", "gate", "boolean", "logic", "digital"],
905            &[],
906            |_| Box::new(LogicAnd::new()),
907        );
908
909        self.register_factory_with_keywords(
910            "logic_or",
911            "Logic OR",
912            "Logic",
913            "Output high when either input is high",
914            &["or", "gate", "boolean", "logic", "digital"],
915            &[],
916            |_| Box::new(LogicOr::new()),
917        );
918
919        self.register_factory_with_keywords(
920            "logic_xor",
921            "Logic XOR",
922            "Logic",
923            "Output high when exactly one input is high",
924            &["xor", "exclusive", "gate", "boolean", "logic", "digital"],
925            &[],
926            |_| Box::new(LogicXor::new()),
927        );
928
929        self.register_factory_with_keywords(
930            "logic_not",
931            "Logic NOT",
932            "Logic",
933            "Invert gate signal",
934            &["not", "invert", "gate", "boolean", "logic", "digital"],
935            &[],
936            |_| Box::new(LogicNot::new()),
937        );
938
939        self.register_factory_with_keywords(
940            "comparator",
941            "Comparator",
942            "Logic",
943            "Compare two CVs, output gates for greater/less/equal",
944            &["compare", "greater", "less", "equal", "threshold", "cv"],
945            &[],
946            |_| Box::new(Comparator::new()),
947        );
948
949        // =====================================================================
950        // Random
951        // =====================================================================
952        self.register_factory_with_keywords(
953            "bernoulli_gate",
954            "Bernoulli Gate",
955            "Random",
956            "Probabilistic trigger router",
957            &[
958                "random",
959                "probability",
960                "chance",
961                "coin",
962                "trigger",
963                "router",
964            ],
965            &[],
966            |_| Box::new(BernoulliGate::new()),
967        );
968
969        // =====================================================================
970        // Analog Modeling
971        // =====================================================================
972        self.register_factory_with_keywords(
973            "crosstalk",
974            "Crosstalk",
975            "Analog Modeling",
976            "Channel crosstalk simulation",
977            &["crosstalk", "bleed", "stereo", "channel", "analog"],
978            &["analog"],
979            |sr| Box::new(Crosstalk::new(sr)),
980        );
981
982        self.register_factory_with_keywords(
983            "ground_loop",
984            "Ground Loop",
985            "Analog Modeling",
986            "Ground loop hum simulation (50/60 Hz)",
987            &["ground", "hum", "buzz", "50hz", "60hz", "mains", "analog"],
988            &["analog"],
989            |sr| Box::new(GroundLoop::new(sr)),
990        );
991
992        // =====================================================================
993        // Phase 4: Advanced DSP Modules
994        // =====================================================================
995
996        // Oscillators
997        self.register_factory_with_keywords(
998            "wavetable",
999            "Wavetable",
1000            "Oscillators",
1001            "Wavetable oscillator with 8 tables and morphing",
1002            &["wavetable", "oscillator", "morph", "digital", "synthesis"],
1003            &[],
1004            |sr| Box::new(Wavetable::new(sr)),
1005        );
1006
1007        self.register_factory_with_keywords(
1008            "formant_osc",
1009            "Formant Oscillator",
1010            "Oscillators",
1011            "Formant oscillator for vocal synthesis (a/e/i/o/u)",
1012            &["formant", "vocal", "vowel", "voice", "speech", "oscillator"],
1013            &[],
1014            |sr| Box::new(FormantOsc::new(sr)),
1015        );
1016
1017        // Effects
1018        self.register_factory_with_keywords(
1019            "reverb",
1020            "Reverb",
1021            "Effects",
1022            "Algorithmic reverb (Freeverb-style) with stereo output",
1023            &["reverb", "room", "hall", "space", "ambience", "freeverb"],
1024            &["essential"],
1025            |sr| Box::new(Reverb::new(sr)),
1026        );
1027
1028        self.register_factory_with_keywords(
1029            "parametric_eq",
1030            "Parametric EQ",
1031            "Effects",
1032            "3-band parametric equalizer (low shelf, mid peak, high shelf)",
1033            &["eq", "equalizer", "tone", "parametric", "shelf", "filter"],
1034            &[],
1035            |sr| Box::new(ParametricEq::new(sr)),
1036        );
1037
1038        self.register_factory_with_keywords(
1039            "vocoder",
1040            "Vocoder",
1041            "Effects",
1042            "16-band vocoder with carrier/modulator inputs",
1043            &["vocoder", "voice", "robot", "spectral", "filter", "bands"],
1044            &[],
1045            |sr| Box::new(Vocoder::new(sr)),
1046        );
1047
1048        self.register_factory_with_keywords(
1049            "pitch_shifter",
1050            "Pitch Shifter",
1051            "Effects",
1052            "Granular pitch shifter (±24 semitones)",
1053            &["pitch", "shift", "transpose", "semitone", "granular"],
1054            &[],
1055            |sr| Box::new(PitchShifter::new(sr)),
1056        );
1057
1058        self.register_factory_with_keywords(
1059            "granular",
1060            "Granular",
1061            "Effects",
1062            "Granular synthesis/processing with 16 concurrent grains",
1063            &[
1064                "granular", "grain", "texture", "freeze", "clouds", "ambient",
1065            ],
1066            &["advanced"],
1067            |sr| Box::new(Granular::new(sr)),
1068        );
1069
1070        // Utilities
1071        self.register_factory_with_keywords(
1072            "chord_memory",
1073            "Chord Memory",
1074            "Utilities",
1075            "Generate chord voicings from root note (9 chord types)",
1076            &["chord", "harmony", "voicing", "major", "minor", "seventh"],
1077            &[],
1078            |_| Box::new(ChordMemory::new()),
1079        );
1080
1081        self.register_factory_with_keywords(
1082            "arpeggiator",
1083            "Arpeggiator",
1084            "Sequencers",
1085            "Pattern-based arpeggiator (up/down/up-down/random)",
1086            &[
1087                "arpeggiator",
1088                "arp",
1089                "pattern",
1090                "sequence",
1091                "melody",
1092                "clock",
1093            ],
1094            &[],
1095            |sr| Box::new(Arpeggiator::new(sr)),
1096        );
1097    }
1098
1099    /// Register a module factory with metadata
1100    pub fn register_factory<F>(
1101        &mut self,
1102        type_id: &str,
1103        name: &str,
1104        category: &str,
1105        description: &str,
1106        factory: F,
1107    ) where
1108        F: Fn(f64) -> Box<dyn GraphModule> + Send + Sync + 'static,
1109    {
1110        self.register_factory_with_keywords(
1111            type_id,
1112            name,
1113            category,
1114            description,
1115            &[],
1116            &[],
1117            factory,
1118        );
1119    }
1120
1121    /// Register a module factory with metadata, keywords, and tags
1122    #[allow(clippy::too_many_arguments)]
1123    pub fn register_factory_with_keywords<F>(
1124        &mut self,
1125        type_id: &str,
1126        name: &str,
1127        category: &str,
1128        description: &str,
1129        keywords: &[&str],
1130        tags: &[&str],
1131        factory: F,
1132    ) where
1133        F: Fn(f64) -> Box<dyn GraphModule> + Send + Sync + 'static,
1134    {
1135        // Get port spec from a temporary instance
1136        let temp_instance = factory(44100.0);
1137        let port_spec = temp_instance.port_spec().clone();
1138
1139        self.factories
1140            .insert(type_id.to_string(), Box::new(factory));
1141
1142        self.metadata.insert(
1143            type_id.to_string(),
1144            ModuleMetadata {
1145                type_id: type_id.to_string(),
1146                name: name.to_string(),
1147                category: category.to_string(),
1148                description: description.to_string(),
1149                port_spec,
1150                keywords: keywords.iter().map(|s| s.to_string()).collect(),
1151                tags: tags.iter().map(|s| s.to_string()).collect(),
1152            },
1153        );
1154    }
1155
1156    /// Instantiate a module by type ID
1157    pub fn instantiate(&self, type_id: &str, sample_rate: f64) -> Option<Box<dyn GraphModule>> {
1158        self.factories.get(type_id).map(|f| f(sample_rate))
1159    }
1160
1161    /// List all registered module types
1162    pub fn list_modules(&self) -> impl Iterator<Item = &ModuleMetadata> {
1163        self.metadata.values()
1164    }
1165
1166    /// Get metadata for a specific module type
1167    pub fn get_metadata(&self, type_id: &str) -> Option<&ModuleMetadata> {
1168        self.metadata.get(type_id)
1169    }
1170
1171    /// List modules in a specific category
1172    pub fn list_by_category<'a>(
1173        &'a self,
1174        category: &'a str,
1175    ) -> impl Iterator<Item = &'a ModuleMetadata> {
1176        self.metadata
1177            .values()
1178            .filter(move |m| m.category == category)
1179    }
1180
1181    /// Get all unique categories
1182    pub fn categories(&self) -> Vec<String> {
1183        let mut cats: Vec<_> = self.metadata.values().map(|m| m.category.clone()).collect();
1184        cats.sort();
1185        cats.dedup();
1186        cats
1187    }
1188
1189    // =========================================================================
1190    // Catalog API (Phase 3: GUI Framework)
1191    // =========================================================================
1192
1193    /// Get the full module catalog for the "add module" UI
1194    pub fn catalog(&self) -> CatalogResponse {
1195        let mut modules: Vec<ModuleCatalogEntry> = self
1196            .metadata
1197            .values()
1198            .map(ModuleCatalogEntry::from_metadata)
1199            .collect();
1200
1201        // Sort by category, then by name for consistent ordering
1202        modules.sort_by(|a, b| (&a.category, &a.name).cmp(&(&b.category, &b.name)));
1203
1204        CatalogResponse {
1205            modules,
1206            categories: self.categories(),
1207        }
1208    }
1209
1210    /// Search modules by query string
1211    ///
1212    /// Matches against type_id, name, description, and keywords (case-insensitive)
1213    pub fn search(&self, query: &str) -> Vec<ModuleCatalogEntry> {
1214        let query_lower = query.to_lowercase();
1215
1216        let mut results: Vec<(ModuleCatalogEntry, u8)> = self
1217            .metadata
1218            .values()
1219            .filter_map(|m| {
1220                // Calculate match score (higher = better match)
1221                let mut score: u8 = 0;
1222
1223                // Exact type_id match (highest priority)
1224                if m.type_id.to_lowercase() == query_lower {
1225                    score += 100;
1226                }
1227                // Exact name match
1228                else if m.name.to_lowercase() == query_lower {
1229                    score += 90;
1230                }
1231                // type_id contains query
1232                else if m.type_id.to_lowercase().contains(&query_lower) {
1233                    score += 70;
1234                }
1235                // name contains query
1236                else if m.name.to_lowercase().contains(&query_lower) {
1237                    score += 60;
1238                }
1239                // keyword exact match
1240                else if m.keywords.iter().any(|k| k.to_lowercase() == query_lower) {
1241                    score += 50;
1242                }
1243                // keyword contains query
1244                else if m
1245                    .keywords
1246                    .iter()
1247                    .any(|k| k.to_lowercase().contains(&query_lower))
1248                {
1249                    score += 40;
1250                }
1251                // description contains query
1252                else if m.description.to_lowercase().contains(&query_lower) {
1253                    score += 20;
1254                }
1255                // category contains query
1256                else if m.category.to_lowercase().contains(&query_lower) {
1257                    score += 10;
1258                }
1259
1260                if score > 0 {
1261                    Some((ModuleCatalogEntry::from_metadata(m), score))
1262                } else {
1263                    None
1264                }
1265            })
1266            .collect();
1267
1268        // Sort by score (descending), then by name
1269        results.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.name.cmp(&b.0.name)));
1270
1271        results.into_iter().map(|(entry, _)| entry).collect()
1272    }
1273
1274    /// Get modules in a specific category
1275    pub fn by_category(&self, category: &str) -> Vec<ModuleCatalogEntry> {
1276        let mut modules: Vec<ModuleCatalogEntry> = self
1277            .metadata
1278            .values()
1279            .filter(|m| m.category.eq_ignore_ascii_case(category))
1280            .map(ModuleCatalogEntry::from_metadata)
1281            .collect();
1282
1283        // Sort by name for consistent ordering
1284        modules.sort_by(|a, b| a.name.cmp(&b.name));
1285        modules
1286    }
1287}
1288
1289impl Default for ModuleRegistry {
1290    fn default() -> Self {
1291        Self::new()
1292    }
1293}
1294
1295/// Extension methods for Patch to support serialization
1296impl Patch {
1297    /// Convert patch to a serializable definition
1298    pub fn to_def(&self, name: &str) -> PatchDef {
1299        let modules: Vec<ModuleDef> = self
1300            .nodes()
1301            .map(|(node_id, node_name, module)| ModuleDef {
1302                name: node_name.to_string(),
1303                module_type: module.type_id().to_string(),
1304                position: self.get_position(node_id),
1305                state: module.serialize_state(),
1306            })
1307            .collect();
1308
1309        let cables: Vec<CableDef> = self
1310            .cables()
1311            .iter()
1312            .filter_map(|cable| {
1313                // Find node names and port names
1314                let from_name = self.get_name(cable.from.node)?;
1315                let to_name = self.get_name(cable.to.node)?;
1316
1317                // Find port names from the modules
1318                let (_, _, from_module) = self.nodes().find(|(id, _, _)| *id == cable.from.node)?;
1319                let (_, _, to_module) = self.nodes().find(|(id, _, _)| *id == cable.to.node)?;
1320
1321                let from_port = from_module
1322                    .port_spec()
1323                    .outputs
1324                    .iter()
1325                    .find(|p| p.id == cable.from.port)
1326                    .map(|p| p.name.as_str())?;
1327
1328                let to_port = to_module
1329                    .port_spec()
1330                    .inputs
1331                    .iter()
1332                    .find(|p| p.id == cable.to.port)
1333                    .map(|p| p.name.as_str())?;
1334
1335                Some(CableDef {
1336                    from: format!("{}.{}", from_name, from_port),
1337                    to: format!("{}.{}", to_name, to_port),
1338                    attenuation: cable.attenuation,
1339                    offset: cable.offset,
1340                })
1341            })
1342            .collect();
1343
1344        // Record every parameter whose current value differs from its default, keyed
1345        // "module_name.param_id". Covers both control-input port base values and internal
1346        // (introspection) parameters; unchanged params are omitted to keep the JSON lean.
1347        let mut parameters: StdMap<String, f64> = StdMap::new();
1348        for (node_id, node_name, _) in self.nodes() {
1349            for p in self.param_infos(node_id) {
1350                if (p.value - p.default).abs() > f64::EPSILON {
1351                    parameters.insert(format!("{}.{}", node_name, p.id), p.value);
1352                }
1353            }
1354        }
1355
1356        // Serialize the designated output node by name (Q087), so loading is deterministic
1357        // instead of relying on from_def's heuristic.
1358        let output = self
1359            .output_node()
1360            .and_then(|id| self.get_name(id))
1361            .map(|n| n.to_string());
1362
1363        let meta = self.meta();
1364        PatchDef {
1365            version: CURRENT_PATCH_VERSION,
1366            name: name.to_string(),
1367            author: meta.author.clone(),
1368            description: meta.description.clone(),
1369            tags: meta.tags.clone(),
1370            output,
1371            modules,
1372            cables,
1373            parameters,
1374        }
1375    }
1376
1377    /// Load a patch from a definition
1378    pub fn from_def(
1379        def: &PatchDef,
1380        registry: &ModuleRegistry,
1381        sample_rate: f64,
1382    ) -> Result<Self, PatchError> {
1383        // Version policy (Q090): reject patches newer than we understand rather than
1384        // silently misreading them; older/equal versions load (the format only grows).
1385        if def.version > CURRENT_PATCH_VERSION {
1386            return Err(PatchError::CompilationFailed(format!(
1387                "Unsupported patch version {} (this build supports up to {})",
1388                def.version, CURRENT_PATCH_VERSION
1389            )));
1390        }
1391
1392        let mut patch = Patch::new(sample_rate);
1393        let mut name_to_handle: StdMap<String, NodeHandle> = StdMap::new();
1394
1395        // Instantiate modules
1396        for module_def in &def.modules {
1397            let module = registry
1398                .instantiate(&module_def.module_type, sample_rate)
1399                .ok_or_else(|| {
1400                    PatchError::CompilationFailed(format!(
1401                        "Unknown module type: {}",
1402                        module_def.module_type
1403                    ))
1404                })?;
1405
1406            let handle = patch.add_boxed(&module_def.name, module);
1407
1408            // Set position if available
1409            if let Some((x, y)) = module_def.position {
1410                patch.set_position(handle.id(), (x, y));
1411            }
1412
1413            // Restore opaque module-specific state (e.g. a ScaleQuantizer custom/Scala tuning
1414            // table) captured in `ModuleDef.state` by `GraphModule::serialize_state`. Modules
1415            // without such state serialize `None`, so this is skipped for them.
1416            if let Some(state) = &module_def.state {
1417                patch
1418                    .deserialize_module_state(handle.id(), state)
1419                    .map_err(PatchError::CompilationFailed)?;
1420            }
1421
1422            name_to_handle.insert(module_def.name.clone(), handle);
1423        }
1424
1425        // Create cables
1426        for cable_def in &def.cables {
1427            let (from_module, from_port) = parse_port_ref(&cable_def.from)?;
1428            let (to_module, to_port) = parse_port_ref(&cable_def.to)?;
1429
1430            let from_handle = name_to_handle.get(from_module).ok_or_else(|| {
1431                PatchError::CompilationFailed(format!("Unknown module: {}", from_module))
1432            })?;
1433
1434            let to_handle = name_to_handle.get(to_module).ok_or_else(|| {
1435                PatchError::CompilationFailed(format!("Unknown module: {}", to_module))
1436            })?;
1437
1438            // Resolve port refs with the fallible NodeHandle::output/input so a mistyped
1439            // port name in hand-edited/untrusted JSON returns a descriptive Err (module
1440            // name, requested port, and the module's valid ports) instead of panicking via
1441            // the convenience out()/in_() methods.
1442            let from_ref = from_handle.output(from_port).map_err(|_| {
1443                PatchError::CompilationFailed(format!(
1444                    "Unknown output port '{}' on module '{}' (available: {})",
1445                    from_port,
1446                    from_module,
1447                    from_handle.output_names().join(", ")
1448                ))
1449            })?;
1450            let to_ref = to_handle.input(to_port).map_err(|_| {
1451                PatchError::CompilationFailed(format!(
1452                    "Unknown input port '{}' on module '{}' (available: {})",
1453                    to_port,
1454                    to_module,
1455                    to_handle.input_names().join(", ")
1456                ))
1457            })?;
1458
1459            match (cable_def.attenuation, cable_def.offset) {
1460                (Some(attenuation), Some(offset)) => {
1461                    patch.connect_modulated(from_ref, to_ref, attenuation, offset)?;
1462                }
1463                (Some(attenuation), None) => {
1464                    patch.connect_attenuated(from_ref, to_ref, attenuation)?;
1465                }
1466                (None, Some(offset)) => {
1467                    patch.connect_modulated(
1468                        from_ref, to_ref, 1.0, // Unity gain
1469                        offset,
1470                    )?;
1471                }
1472                (None, None) => {
1473                    patch.connect(from_ref, to_ref)?;
1474                }
1475            }
1476        }
1477
1478        // Apply parameters (Q086): "module_name.param_id" -> value, via the introspection /
1479        // port-default dispatch. Unknown modules/params are skipped so hand-edited or
1480        // legacy files degrade gracefully rather than failing to load. Applied before
1481        // compile so port-default overrides are baked into the routing.
1482        for (key, &value) in &def.parameters {
1483            if let Some((module_name, param_id)) = key.split_once('.') {
1484                if let Some(handle) = name_to_handle.get(module_name) {
1485                    patch.set_param_by_id(handle.id(), param_id, value);
1486                }
1487            }
1488        }
1489
1490        // Set the output node. Honor the serialized `output` field when present (Q087),
1491        // otherwise fall back to the legacy heuristic for patches written before it existed.
1492        let output_set = def
1493            .output
1494            .as_ref()
1495            .and_then(|name| name_to_handle.get(name))
1496            .map(|handle| patch.set_output(handle.id()))
1497            .is_some();
1498        if !output_set {
1499            if let Some(handle) = name_to_handle.get("output") {
1500                patch.set_output(handle.id());
1501            } else if let Some(handle) = name_to_handle.values().find(|h| {
1502                h.spec()
1503                    .outputs
1504                    .iter()
1505                    .any(|p| p.name == "left" || p.name == "right")
1506            }) {
1507                patch.set_output(handle.id());
1508            }
1509        }
1510
1511        // Preserve patch metadata (Q090) so a subsequent to_def round-trips it.
1512        patch.set_meta(crate::graph::PatchMeta {
1513            name: Some(def.name.clone()),
1514            author: def.author.clone(),
1515            description: def.description.clone(),
1516            tags: def.tags.clone(),
1517        });
1518
1519        patch.compile()?;
1520        Ok(patch)
1521    }
1522}
1523
1524fn parse_port_ref(s: &str) -> Result<(&str, &str), PatchError> {
1525    let parts: Vec<&str> = s.splitn(2, '.').collect();
1526    if parts.len() != 2 {
1527        return Err(PatchError::CompilationFailed(format!(
1528            "Invalid port reference: {}",
1529            s
1530        )));
1531    }
1532    Ok((parts[0], parts[1]))
1533}
1534
1535// =============================================================================
1536// Patch Validation
1537// =============================================================================
1538
1539/// A validation error with path and message
1540#[derive(Debug, Clone, Serialize, Deserialize)]
1541#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1542pub struct ValidationError {
1543    /// JSON path to the error location (e.g., `modules[0].name`)
1544    pub path: String,
1545    /// Human-readable error message
1546    pub message: String,
1547}
1548
1549impl ValidationError {
1550    pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
1551        Self {
1552            path: path.into(),
1553            message: message.into(),
1554        }
1555    }
1556}
1557
1558impl core::fmt::Display for ValidationError {
1559    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1560        write!(f, "{}: {}", self.path, self.message)
1561    }
1562}
1563
1564/// Result of patch validation
1565#[derive(Debug, Clone, Serialize, Deserialize)]
1566#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1567#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1568pub struct ValidationResult {
1569    /// Whether the patch is valid
1570    pub valid: bool,
1571    /// List of validation errors (empty if valid)
1572    pub errors: Vec<ValidationError>,
1573}
1574
1575impl ValidationResult {
1576    pub fn ok() -> Self {
1577        Self {
1578            valid: true,
1579            errors: Vec::new(),
1580        }
1581    }
1582
1583    pub fn with_errors(errors: Vec<ValidationError>) -> Self {
1584        Self {
1585            valid: errors.is_empty(),
1586            errors,
1587        }
1588    }
1589}
1590
1591impl PatchDef {
1592    /// Validate the patch definition without loading it
1593    ///
1594    /// This performs structural validation to catch errors early before
1595    /// attempting to instantiate modules. For full semantic validation
1596    /// (e.g., checking that port names exist), use `validate_with_registry`.
1597    pub fn validate(&self) -> ValidationResult {
1598        let mut errors = Vec::new();
1599
1600        // Validate version
1601        if self.version < 1 {
1602            errors.push(ValidationError::new(
1603                "version",
1604                "Version must be a positive integer",
1605            ));
1606        }
1607
1608        // Validate name
1609        if self.name.is_empty() {
1610            errors.push(ValidationError::new(
1611                "name",
1612                "Name must be a non-empty string",
1613            ));
1614        }
1615
1616        // Collect module names for duplicate checking
1617        let mut module_names = alloc::collections::BTreeSet::new();
1618
1619        // Validate modules
1620        for (i, module) in self.modules.iter().enumerate() {
1621            let path = format!("modules[{}]", i);
1622
1623            if module.name.is_empty() {
1624                errors.push(ValidationError::new(
1625                    format!("{}.name", path),
1626                    "Module name must be a non-empty string",
1627                ));
1628            } else if !module_names.insert(&module.name) {
1629                errors.push(ValidationError::new(
1630                    format!("{}.name", path),
1631                    format!("Duplicate module name: {}", module.name),
1632                ));
1633            }
1634
1635            if module.module_type.is_empty() {
1636                errors.push(ValidationError::new(
1637                    format!("{}.module_type", path),
1638                    "Module type must be a non-empty string",
1639                ));
1640            }
1641        }
1642
1643        // Validate cables
1644        for (i, cable) in self.cables.iter().enumerate() {
1645            let path = format!("cables[{}]", i);
1646
1647            // Validate port reference format
1648            if !is_valid_port_ref(&cable.from) {
1649                errors.push(ValidationError::new(
1650                    format!("{}.from", path),
1651                    "From must be a port reference in format 'module_name.port_name'",
1652                ));
1653            }
1654
1655            if !is_valid_port_ref(&cable.to) {
1656                errors.push(ValidationError::new(
1657                    format!("{}.to", path),
1658                    "To must be a port reference in format 'module_name.port_name'",
1659                ));
1660            }
1661
1662            // Validate attenuation range
1663            if let Some(attenuation) = cable.attenuation {
1664                if !(-2.0..=2.0).contains(&attenuation) {
1665                    errors.push(ValidationError::new(
1666                        format!("{}.attenuation", path),
1667                        "Attenuation must be between -2.0 and 2.0",
1668                    ));
1669                }
1670            }
1671
1672            // Validate offset range
1673            if let Some(offset) = cable.offset {
1674                if !(-10.0..=10.0).contains(&offset) {
1675                    errors.push(ValidationError::new(
1676                        format!("{}.offset", path),
1677                        "Offset must be between -10.0 and 10.0",
1678                    ));
1679                }
1680            }
1681        }
1682
1683        ValidationResult::with_errors(errors)
1684    }
1685
1686    /// Validate the patch definition with registry context
1687    ///
1688    /// This performs full semantic validation including checking that:
1689    /// - All module types exist in the registry
1690    /// - All port references point to existing modules
1691    /// - All port names exist on their respective modules
1692    pub fn validate_with_registry(&self, registry: &ModuleRegistry) -> ValidationResult {
1693        // First do structural validation
1694        let mut result = self.validate();
1695        if !result.valid {
1696            return result;
1697        }
1698
1699        let mut errors = Vec::new();
1700
1701        // Collect module names for reference checking
1702        let module_names: alloc::collections::BTreeSet<_> =
1703            self.modules.iter().map(|m| m.name.as_str()).collect();
1704
1705        // Validate module types exist
1706        for (i, module) in self.modules.iter().enumerate() {
1707            if registry.get_metadata(&module.module_type).is_none() {
1708                errors.push(ValidationError::new(
1709                    format!("modules[{}].module_type", i),
1710                    format!("Unknown module type: {}", module.module_type),
1711                ));
1712            }
1713        }
1714
1715        // Validate cable references
1716        for (i, cable) in self.cables.iter().enumerate() {
1717            let path = format!("cables[{}]", i);
1718
1719            // Check source module exists
1720            if let Ok((from_module, from_port)) = parse_port_ref(&cable.from) {
1721                if !module_names.contains(from_module) {
1722                    errors.push(ValidationError::new(
1723                        format!("{}.from", path),
1724                        format!("Unknown module: {}", from_module),
1725                    ));
1726                } else {
1727                    // Check source port exists
1728                    if let Some(module_def) = self.modules.iter().find(|m| m.name == from_module) {
1729                        if let Some(metadata) = registry.get_metadata(&module_def.module_type) {
1730                            if metadata.port_spec.output_by_name(from_port).is_none() {
1731                                errors.push(ValidationError::new(
1732                                    format!("{}.from", path),
1733                                    format!(
1734                                        "Unknown output port '{}' on module '{}'",
1735                                        from_port, from_module
1736                                    ),
1737                                ));
1738                            }
1739                        }
1740                    }
1741                }
1742            }
1743
1744            // Check destination module exists
1745            if let Ok((to_module, to_port)) = parse_port_ref(&cable.to) {
1746                if !module_names.contains(to_module) {
1747                    errors.push(ValidationError::new(
1748                        format!("{}.to", path),
1749                        format!("Unknown module: {}", to_module),
1750                    ));
1751                } else {
1752                    // Check destination port exists
1753                    if let Some(module_def) = self.modules.iter().find(|m| m.name == to_module) {
1754                        if let Some(metadata) = registry.get_metadata(&module_def.module_type) {
1755                            if metadata.port_spec.input_by_name(to_port).is_none() {
1756                                errors.push(ValidationError::new(
1757                                    format!("{}.to", path),
1758                                    format!(
1759                                        "Unknown input port '{}' on module '{}'",
1760                                        to_port, to_module
1761                                    ),
1762                                ));
1763                            }
1764                        }
1765                    }
1766                }
1767            }
1768        }
1769
1770        if errors.is_empty() {
1771            result
1772        } else {
1773            result.valid = false;
1774            result.errors.extend(errors);
1775            result
1776        }
1777    }
1778}
1779
1780/// Check if a string is a valid port reference (module.port format)
1781fn is_valid_port_ref(s: &str) -> bool {
1782    let parts: Vec<&str> = s.splitn(2, '.').collect();
1783    if parts.len() != 2 {
1784        return false;
1785    }
1786
1787    // Check that both parts are non-empty and contain valid characters
1788    let valid_chars = |s: &str| {
1789        !s.is_empty()
1790            && s.chars()
1791                .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
1792    };
1793
1794    valid_chars(parts[0]) && valid_chars(parts[1])
1795}
1796
1797#[cfg(test)]
1798mod tests {
1799    use super::*;
1800
1801    #[test]
1802    fn test_patch_def_serialization() {
1803        let def = PatchDef::new("Test Patch")
1804            .with_author("Test Author")
1805            .with_description("A test patch")
1806            .with_tag("test");
1807
1808        let json = def.to_json().unwrap();
1809        let loaded = PatchDef::from_json(&json).unwrap();
1810
1811        assert_eq!(loaded.name, "Test Patch");
1812        assert_eq!(loaded.author, Some("Test Author".to_string()));
1813    }
1814
1815    #[test]
1816    fn test_cable_def() {
1817        let cable = CableDef::new("vco.saw", "vcf.in").with_attenuation(0.5);
1818        assert_eq!(cable.from, "vco.saw");
1819        assert_eq!(cable.to, "vcf.in");
1820        assert_eq!(cable.attenuation, Some(0.5));
1821    }
1822
1823    #[test]
1824    fn test_patch_def_default() {
1825        let def = PatchDef::default();
1826        assert_eq!(def.name, "Untitled");
1827    }
1828
1829    #[test]
1830    fn test_module_def_with_position() {
1831        let def = ModuleDef::new("vco1", "vco").with_position(100.0, 200.0);
1832        assert_eq!(def.position, Some((100.0, 200.0)));
1833    }
1834
1835    #[test]
1836    fn test_cable_def_with_offset() {
1837        let cable = CableDef::new("a.out", "b.in").with_offset(2.5);
1838        assert_eq!(cable.offset, Some(2.5));
1839    }
1840
1841    #[test]
1842    fn test_cable_def_with_modulation() {
1843        let cable = CableDef::new("a.out", "b.in").with_modulation(0.5, 1.0);
1844        assert_eq!(cable.attenuation, Some(0.5));
1845        assert_eq!(cable.offset, Some(1.0));
1846    }
1847
1848    // =============================================================================
1849    // Validation Tests
1850    // =============================================================================
1851
1852    #[test]
1853    fn test_valid_patch_validation() {
1854        let mut def = PatchDef::new("Test Patch");
1855        def.modules.push(ModuleDef::new("vco1", "vco"));
1856        def.modules.push(ModuleDef::new("output", "stereo_output"));
1857        def.cables.push(CableDef::new("vco1.saw", "output.left"));
1858
1859        let result = def.validate();
1860        assert!(
1861            result.valid,
1862            "Expected valid patch, got errors: {:?}",
1863            result.errors
1864        );
1865        assert!(result.errors.is_empty());
1866    }
1867
1868    #[test]
1869    fn test_empty_name_validation() {
1870        let mut def = PatchDef::new("");
1871        def.modules.push(ModuleDef::new("vco1", "vco"));
1872
1873        let result = def.validate();
1874        assert!(!result.valid);
1875        assert!(result.errors.iter().any(|e| e.path == "name"));
1876    }
1877
1878    #[test]
1879    fn test_duplicate_module_name_validation() {
1880        let mut def = PatchDef::new("Test");
1881        def.modules.push(ModuleDef::new("vco1", "vco"));
1882        def.modules.push(ModuleDef::new("vco1", "vco")); // Duplicate!
1883
1884        let result = def.validate();
1885        assert!(!result.valid);
1886        assert!(result
1887            .errors
1888            .iter()
1889            .any(|e| e.message.contains("Duplicate")));
1890    }
1891
1892    #[test]
1893    fn test_invalid_port_reference_validation() {
1894        let mut def = PatchDef::new("Test");
1895        def.modules.push(ModuleDef::new("vco1", "vco"));
1896        def.cables.push(CableDef::new("invalid", "also_invalid")); // Missing dots
1897
1898        let result = def.validate();
1899        assert!(!result.valid);
1900        assert!(result.errors.len() >= 2);
1901    }
1902
1903    #[test]
1904    fn test_attenuation_range_validation() {
1905        let mut def = PatchDef::new("Test");
1906        def.modules.push(ModuleDef::new("vco1", "vco"));
1907        def.cables
1908            .push(CableDef::new("a.out", "b.in").with_attenuation(5.0)); // Out of range
1909
1910        let result = def.validate();
1911        assert!(!result.valid);
1912        assert!(result.errors.iter().any(|e| e.path.contains("attenuation")));
1913    }
1914
1915    #[test]
1916    fn test_offset_range_validation() {
1917        let mut def = PatchDef::new("Test");
1918        def.modules.push(ModuleDef::new("vco1", "vco"));
1919        def.cables
1920            .push(CableDef::new("a.out", "b.in").with_offset(15.0)); // Out of range
1921
1922        let result = def.validate();
1923        assert!(!result.valid);
1924        assert!(result.errors.iter().any(|e| e.path.contains("offset")));
1925    }
1926
1927    #[test]
1928    fn test_validate_with_registry_unknown_module_type() {
1929        let registry = ModuleRegistry::new();
1930
1931        let mut def = PatchDef::new("Test");
1932        def.modules.push(ModuleDef::new("foo", "nonexistent_type"));
1933
1934        let result = def.validate_with_registry(&registry);
1935        assert!(!result.valid);
1936        assert!(result
1937            .errors
1938            .iter()
1939            .any(|e| e.message.contains("Unknown module type")));
1940    }
1941
1942    #[test]
1943    fn test_validate_with_registry_unknown_module_reference() {
1944        let registry = ModuleRegistry::new();
1945
1946        let mut def = PatchDef::new("Test");
1947        def.modules.push(ModuleDef::new("vco1", "vco"));
1948        def.cables
1949            .push(CableDef::new("nonexistent.out", "vco1.voct"));
1950
1951        let result = def.validate_with_registry(&registry);
1952        assert!(!result.valid);
1953        assert!(result
1954            .errors
1955            .iter()
1956            .any(|e| e.message.contains("Unknown module")));
1957    }
1958
1959    #[test]
1960    fn test_validate_with_registry_unknown_port() {
1961        let registry = ModuleRegistry::new();
1962
1963        let mut def = PatchDef::new("Test");
1964        def.modules.push(ModuleDef::new("vco1", "vco"));
1965        def.modules.push(ModuleDef::new("output", "stereo_output"));
1966        def.cables
1967            .push(CableDef::new("vco1.nonexistent_port", "output.left"));
1968
1969        let result = def.validate_with_registry(&registry);
1970        assert!(!result.valid);
1971        assert!(result
1972            .errors
1973            .iter()
1974            .any(|e| e.message.contains("Unknown output port")));
1975    }
1976
1977    #[test]
1978    fn test_validate_with_registry_valid_patch() {
1979        let registry = ModuleRegistry::new();
1980
1981        let mut def = PatchDef::new("Valid Patch");
1982        def.modules.push(ModuleDef::new("vco1", "vco"));
1983        def.modules.push(ModuleDef::new("output", "stereo_output"));
1984        def.cables.push(CableDef::new("vco1.saw", "output.left"));
1985        def.cables.push(CableDef::new("vco1.sin", "output.right"));
1986
1987        let result = def.validate_with_registry(&registry);
1988        assert!(
1989            result.valid,
1990            "Expected valid patch, got errors: {:?}",
1991            result.errors
1992        );
1993    }
1994
1995    #[test]
1996    fn test_is_valid_port_ref() {
1997        assert!(is_valid_port_ref("vco1.out"));
1998        assert!(is_valid_port_ref("module_name.port_name"));
1999        assert!(is_valid_port_ref("a.b"));
2000        assert!(is_valid_port_ref("my-module.my-port"));
2001
2002        assert!(!is_valid_port_ref("nodot"));
2003        assert!(!is_valid_port_ref(".startswithdot"));
2004        assert!(!is_valid_port_ref("endswithdot."));
2005        assert!(!is_valid_port_ref(""));
2006        assert!(!is_valid_port_ref("has spaces.port"));
2007    }
2008
2009    #[test]
2010    fn test_validation_error_display() {
2011        let error = ValidationError::new("modules[0].name", "Name is empty");
2012        let display = format!("{}", error);
2013        assert_eq!(display, "modules[0].name: Name is empty");
2014    }
2015
2016    // =============================================================================
2017    // Module Catalog Tests (Phase 3: GUI Framework)
2018    // =============================================================================
2019
2020    #[test]
2021    fn test_catalog_returns_all_modules() {
2022        let registry = ModuleRegistry::new();
2023        let catalog = registry.catalog();
2024
2025        // Should have all 36 built-in modules
2026        assert!(catalog.modules.len() >= 36, "Expected at least 36 modules");
2027
2028        // Check some known modules exist
2029        assert!(catalog.modules.iter().any(|m| m.type_id == "vco"));
2030        assert!(catalog.modules.iter().any(|m| m.type_id == "svf"));
2031        assert!(catalog.modules.iter().any(|m| m.type_id == "adsr"));
2032    }
2033
2034    #[test]
2035    fn test_remediation_modules_registered_and_instantiate() {
2036        let registry = ModuleRegistry::new();
2037        // Each new module (Q142/Q148/Q150) must instantiate and report the
2038        // matching type_id it registered under.
2039        for id in [
2040            "sample_player",
2041            "ducker",
2042            "mid_side_encode",
2043            "mid_side_decode",
2044        ] {
2045            let module = registry
2046                .instantiate(id, 44100.0)
2047                .unwrap_or_else(|| panic!("module '{id}' not registered"));
2048            assert_eq!(module.type_id(), id, "type_id mismatch for '{id}'");
2049        }
2050    }
2051
2052    #[test]
2053    fn test_catalog_categories() {
2054        let registry = ModuleRegistry::new();
2055        let catalog = registry.catalog();
2056
2057        // Should have expected categories
2058        assert!(catalog.categories.contains(&"Oscillators".to_string()));
2059        assert!(catalog.categories.contains(&"Filters".to_string()));
2060        assert!(catalog.categories.contains(&"Utilities".to_string()));
2061        assert!(catalog.categories.contains(&"Effects".to_string()));
2062
2063        // Categories should be sorted
2064        let mut sorted_cats = catalog.categories.clone();
2065        sorted_cats.sort();
2066        assert_eq!(catalog.categories, sorted_cats);
2067    }
2068
2069    #[test]
2070    fn test_catalog_entry_has_port_summary() {
2071        let registry = ModuleRegistry::new();
2072        let catalog = registry.catalog();
2073
2074        let vco = catalog.modules.iter().find(|m| m.type_id == "vco").unwrap();
2075        assert!(vco.ports.outputs > 0, "VCO should have outputs");
2076        assert!(vco.ports.has_audio_out, "VCO should have audio output");
2077
2078        let stereo_out = catalog
2079            .modules
2080            .iter()
2081            .find(|m| m.type_id == "stereo_output")
2082            .unwrap();
2083        assert!(
2084            stereo_out.ports.inputs > 0,
2085            "Stereo output should have inputs"
2086        );
2087        assert!(
2088            stereo_out.ports.has_audio_in,
2089            "Stereo output should have audio input"
2090        );
2091    }
2092
2093    #[test]
2094    fn test_search_exact_type_id_match() {
2095        let registry = ModuleRegistry::new();
2096        let results = registry.search("vco");
2097
2098        assert!(!results.is_empty());
2099        // Exact match should be first
2100        assert_eq!(results[0].type_id, "vco");
2101    }
2102
2103    #[test]
2104    fn test_search_by_keyword() {
2105        let registry = ModuleRegistry::new();
2106        let results = registry.search("oscillator");
2107
2108        // Should find VCO, Analog VCO, LFO (all have "oscillator" keyword)
2109        assert!(results.len() >= 3);
2110        assert!(results.iter().any(|m| m.type_id == "vco"));
2111        assert!(results.iter().any(|m| m.type_id == "analog_vco"));
2112        assert!(results.iter().any(|m| m.type_id == "lfo"));
2113    }
2114
2115    #[test]
2116    fn test_search_case_insensitive() {
2117        let registry = ModuleRegistry::new();
2118        let results_lower = registry.search("filter");
2119        let results_upper = registry.search("FILTER");
2120        let results_mixed = registry.search("FiLtEr");
2121
2122        assert_eq!(results_lower.len(), results_upper.len());
2123        assert_eq!(results_lower.len(), results_mixed.len());
2124    }
2125
2126    #[test]
2127    fn test_search_by_description() {
2128        let registry = ModuleRegistry::new();
2129        let results = registry.search("saturation");
2130
2131        // Should find saturator, diode_ladder, analog_vco (all mention saturation)
2132        assert!(!results.is_empty());
2133        assert!(results.iter().any(|m| m.type_id == "saturator"));
2134    }
2135
2136    #[test]
2137    fn test_search_no_results() {
2138        let registry = ModuleRegistry::new();
2139        let results = registry.search("nonexistent_xyz_123");
2140
2141        assert!(results.is_empty());
2142    }
2143
2144    #[test]
2145    fn test_by_category() {
2146        let registry = ModuleRegistry::new();
2147        let oscillators = registry.by_category("Oscillators");
2148
2149        assert!(oscillators.len() >= 2);
2150        assert!(oscillators.iter().all(|m| m.category == "Oscillators"));
2151        assert!(oscillators.iter().any(|m| m.type_id == "vco"));
2152        assert!(oscillators.iter().any(|m| m.type_id == "analog_vco"));
2153    }
2154
2155    #[test]
2156    fn test_by_category_case_insensitive() {
2157        let registry = ModuleRegistry::new();
2158        let filters1 = registry.by_category("Filters");
2159        let filters2 = registry.by_category("filters");
2160        let filters3 = registry.by_category("FILTERS");
2161
2162        assert_eq!(filters1.len(), filters2.len());
2163        assert_eq!(filters1.len(), filters3.len());
2164    }
2165
2166    #[test]
2167    fn test_by_category_sorted_by_name() {
2168        let registry = ModuleRegistry::new();
2169        let utilities = registry.by_category("Utilities");
2170
2171        // Check that results are sorted by name
2172        let names: Vec<_> = utilities.iter().map(|m| &m.name).collect();
2173        let mut sorted_names = names.clone();
2174        sorted_names.sort();
2175        assert_eq!(names, sorted_names);
2176    }
2177
2178    #[test]
2179    fn test_catalog_entry_serialization() {
2180        let registry = ModuleRegistry::new();
2181        let catalog = registry.catalog();
2182
2183        // Should serialize to JSON without errors
2184        let json = serde_json::to_string(&catalog).unwrap();
2185        assert!(json.contains("\"type_id\""));
2186        assert!(json.contains("\"category\""));
2187        assert!(json.contains("\"keywords\""));
2188
2189        // Should deserialize back
2190        let deserialized: CatalogResponse = serde_json::from_str(&json).unwrap();
2191        assert_eq!(deserialized.modules.len(), catalog.modules.len());
2192    }
2193
2194    #[test]
2195    fn test_module_has_keywords_and_tags() {
2196        let registry = ModuleRegistry::new();
2197        let metadata = registry.get_metadata("vco").unwrap();
2198
2199        // VCO should have keywords
2200        assert!(!metadata.keywords.is_empty());
2201        assert!(metadata.keywords.contains(&"oscillator".to_string()));
2202
2203        // VCO should have "essential" tag
2204        assert!(metadata.tags.contains(&"essential".to_string()));
2205    }
2206
2207    #[test]
2208    fn test_from_def_mistyped_cable_port_returns_err() {
2209        // Q180: a valid module type with a mistyped port name in a cable must return Err
2210        // from from_def (no panic via the convenience out()/in_() methods).
2211        let registry = ModuleRegistry::new();
2212        let mut def = PatchDef::new("Bad Ports");
2213        def.modules.push(ModuleDef::new("vco", "vco"));
2214        def.modules.push(ModuleDef::new("output", "stereo_output"));
2215        // "definitely_not_a_port" is not a real VCO output port.
2216        def.cables
2217            .push(CableDef::new("vco.definitely_not_a_port", "output.left"));
2218
2219        let result = Patch::from_def(&def, &registry, 44100.0);
2220        assert!(result.is_err(), "expected Err for mistyped cable port");
2221        match result {
2222            Err(PatchError::CompilationFailed(msg)) => {
2223                assert!(
2224                    msg.contains("definitely_not_a_port") && msg.contains("vco"),
2225                    "error should name the bad port and module: {}",
2226                    msg
2227                );
2228            }
2229            other => panic!("expected CompilationFailed, got {:?}", other),
2230        }
2231    }
2232
2233    #[test]
2234    fn test_from_def_unknown_module_type_returns_err() {
2235        // Q180 sweep: an unknown module type must be a descriptive Err, never a panic.
2236        let registry = ModuleRegistry::new();
2237        let mut def = PatchDef::new("Bad Type");
2238        def.modules.push(ModuleDef::new("x", "not_a_real_module"));
2239        match Patch::from_def(&def, &registry, 44100.0) {
2240            Err(PatchError::CompilationFailed(msg)) => {
2241                assert!(msg.contains("not_a_real_module"), "msg: {msg}");
2242            }
2243            other => panic!("expected CompilationFailed, got {other:?}"),
2244        }
2245    }
2246
2247    #[test]
2248    fn test_from_def_malformed_cable_ref_returns_err() {
2249        // Q180 sweep: a cable reference missing the "module.port" dot must Err, not panic.
2250        let registry = ModuleRegistry::new();
2251        let mut def = PatchDef::new("Malformed");
2252        def.modules.push(ModuleDef::new("vco", "vco"));
2253        def.modules.push(ModuleDef::new("output", "stereo_output"));
2254        def.cables.push(CableDef::new("no_dot_here", "output.left"));
2255        assert!(Patch::from_def(&def, &registry, 44100.0).is_err());
2256    }
2257
2258    #[test]
2259    fn test_from_def_rejects_newer_version() {
2260        // Q090: a patch from a newer schema version must be rejected with a clear error.
2261        let registry = ModuleRegistry::new();
2262        let mut def = PatchDef::new("Future");
2263        def.version = CURRENT_PATCH_VERSION + 1;
2264        def.modules.push(ModuleDef::new("output", "stereo_output"));
2265        match Patch::from_def(&def, &registry, 44100.0) {
2266            Err(PatchError::CompilationFailed(msg)) => {
2267                assert!(msg.contains("version"), "msg: {msg}");
2268            }
2269            other => panic!("expected version rejection, got {other:?}"),
2270        }
2271        // A patch at the current version still loads.
2272        def.version = CURRENT_PATCH_VERSION;
2273        assert!(Patch::from_def(&def, &registry, 44100.0).is_ok());
2274    }
2275
2276    #[test]
2277    fn test_roundtrip_preserves_params_and_output() {
2278        // Q086: build a patch, change parameters (port-backed and internal), round-trip
2279        // through JSON, and verify parameters survive AND the audio output is identical.
2280        use crate::modules::{Distortion, StereoOutput, Svf, Vco};
2281
2282        let build = || -> (Patch, crate::graph::NodeId, crate::graph::NodeId, crate::graph::NodeId) {
2283            let mut patch = Patch::new(44100.0);
2284            let osc = patch.add("osc", Vco::new(44100.0));
2285            let dist = patch.add("dist", Distortion::new(44100.0));
2286            let flt = patch.add("flt", Svf::new(44100.0));
2287            let out = patch.add("output", StereoOutput::new());
2288            patch.connect(osc.out("saw"), dist.in_("in")).unwrap();
2289            patch.connect(dist.out("out"), flt.in_("in")).unwrap();
2290            patch.connect(flt.out("lp"), out.in_("left")).unwrap();
2291            patch.connect(flt.out("lp"), out.in_("right")).unwrap();
2292            patch.set_output(out.id());
2293            (patch, osc.id(), dist.id(), flt.id())
2294        };
2295
2296        let (mut original, osc_id, dist_id, flt_id) = build();
2297        // Port-backed params: filter cutoff and oscillator pitch (V/Oct).
2298        assert!(original.set_param_by_id(flt_id, "cutoff", 0.35));
2299        assert!(original.set_param_by_id(osc_id, "voct", 1.0));
2300        // Internal (non-port) param dispatched through ModuleIntrospection: oversampling.
2301        assert!(original.set_param_by_id(dist_id, "oversample", 1.0));
2302        // A bogus id must be rejected.
2303        assert!(!original.set_param_by_id(flt_id, "no_such_param", 1.0));
2304
2305        // Serialize -> JSON -> deserialize -> rebuild.
2306        let def = original.to_def("Round Trip");
2307        assert_eq!(def.output.as_deref(), Some("output"));
2308        let json = def.to_json().unwrap();
2309        let reloaded = PatchDef::from_json(&json).unwrap();
2310        let registry = ModuleRegistry::new();
2311        let mut rebuilt = Patch::from_def(&reloaded, &registry, 44100.0).unwrap();
2312
2313        // Params survive, read back through the live-patch introspection getters.
2314        let r_osc = rebuilt.get_node_id_by_name("osc").unwrap();
2315        let r_dist = rebuilt.get_node_id_by_name("dist").unwrap();
2316        let r_flt = rebuilt.get_node_id_by_name("flt").unwrap();
2317        assert!((rebuilt.get_param_by_id(r_flt, "cutoff").unwrap() - 0.35).abs() < 1e-9);
2318        assert!((rebuilt.get_param_by_id(r_osc, "voct").unwrap() - 1.0).abs() < 1e-9);
2319        assert!((rebuilt.get_param_by_id(r_dist, "oversample").unwrap() - 1.0).abs() < 1e-9);
2320        // The rebuilt output node is honored from the serialized field.
2321        assert_eq!(rebuilt.output_node(), Some(r_out_of(&rebuilt)));
2322
2323        // Deterministic output must match sample-for-sample.
2324        for i in 0..256 {
2325            let a = original.tick();
2326            let b = rebuilt.tick();
2327            assert!(
2328                (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9,
2329                "sample {i} diverged: {a:?} vs {b:?}"
2330            );
2331        }
2332    }
2333
2334    // Helper: the output node id of a patch (its module named "output").
2335    fn r_out_of(p: &Patch) -> crate::graph::NodeId {
2336        p.get_node_id_by_name("output").unwrap()
2337    }
2338
2339    #[test]
2340    fn test_roundtrip_preserves_metadata() {
2341        // Q090: name/author/description/tags survive a to_def -> JSON -> from_def round-trip.
2342        use crate::graph::PatchMeta;
2343        use crate::modules::StereoOutput;
2344
2345        let mut patch = Patch::new(44100.0);
2346        let out = patch.add("output", StereoOutput::new());
2347        patch.set_output(out.id());
2348        patch.set_meta(PatchMeta {
2349            name: Some("My Patch".into()),
2350            author: Some("Ada".into()),
2351            description: Some("A lovely patch".into()),
2352            tags: vec!["demo".into(), "test".into()],
2353        });
2354
2355        let def = patch.to_def("My Patch");
2356        assert_eq!(def.author.as_deref(), Some("Ada"));
2357        assert_eq!(def.description.as_deref(), Some("A lovely patch"));
2358        assert_eq!(def.tags, vec!["demo".to_string(), "test".to_string()]);
2359
2360        let json = def.to_json().unwrap();
2361        let reloaded = PatchDef::from_json(&json).unwrap();
2362        let registry = ModuleRegistry::new();
2363        let rebuilt = Patch::from_def(&reloaded, &registry, 44100.0).unwrap();
2364        let meta = rebuilt.meta();
2365        assert_eq!(meta.author.as_deref(), Some("Ada"));
2366        assert_eq!(meta.description.as_deref(), Some("A lovely patch"));
2367        assert_eq!(meta.tags, vec!["demo".to_string(), "test".to_string()]);
2368    }
2369
2370    #[test]
2371    fn test_old_json_without_output_field_still_loads() {
2372        // Q087 back-compat: JSON predating the `output` field must deserialize (serde default)
2373        // and fall back to the output heuristic.
2374        let json = r#"{
2375            "version": 1,
2376            "name": "Legacy",
2377            "author": null,
2378            "description": null,
2379            "tags": [],
2380            "modules": [
2381                {"name": "vco", "module_type": "vco", "position": null, "state": null},
2382                {"name": "output", "module_type": "stereo_output", "position": null, "state": null}
2383            ],
2384            "cables": [
2385                {"from": "vco.saw", "to": "output.left", "attenuation": null, "offset": null}
2386            ],
2387            "parameters": {}
2388        }"#;
2389        let def = PatchDef::from_json(json).unwrap();
2390        assert!(def.output.is_none());
2391        let registry = ModuleRegistry::new();
2392        let patch = Patch::from_def(&def, &registry, 44100.0).unwrap();
2393        // Heuristic still selects the stereo output node named "output".
2394        assert_eq!(patch.output_node(), patch.get_node_id_by_name("output"));
2395    }
2396
2397    /// Q088 drift-guard: the shipped JSON schema's `module_type` enum and the
2398    /// `ModuleRegistry` built-ins must stay in exact 1:1 correspondence. Reads the schema
2399    /// file via `include_str!` so a stale schema (or a newly registered module with no enum
2400    /// entry) fails the build instead of shipping a patch that valid code cannot round-trip.
2401    #[test]
2402    fn test_schema_enum_matches_registry() {
2403        const SCHEMA: &str = include_str!("../schemas/patch.schema.json");
2404        let schema: serde_json::Value =
2405            serde_json::from_str(SCHEMA).expect("patch.schema.json must be valid JSON");
2406        let enum_vals = schema["$defs"]["ModuleDef"]["properties"]["module_type"]["enum"]
2407            .as_array()
2408            .expect("schema module_type must define an enum array");
2409        let enum_ids: Vec<&str> = enum_vals
2410            .iter()
2411            .map(|v| v.as_str().expect("enum entries must be strings"))
2412            .collect();
2413
2414        let registry = ModuleRegistry::new();
2415        let registry_ids: Vec<String> =
2416            registry.list_modules().map(|m| m.type_id.clone()).collect();
2417
2418        // Every registered type must appear in the schema enum.
2419        for id in &registry_ids {
2420            assert!(
2421                enum_ids.contains(&id.as_str()),
2422                "registry type '{id}' is missing from the schema module_type enum"
2423            );
2424        }
2425        // And no schema enum entry may be a dead type unknown to the registry.
2426        for id in &enum_ids {
2427            assert!(
2428                registry_ids.iter().any(|r| r == id),
2429                "schema module_type enum lists '{id}', which the registry does not register"
2430            );
2431        }
2432        // Counts must match exactly (guards against duplicate enum entries too).
2433        assert_eq!(
2434            enum_ids.len(),
2435            registry_ids.len(),
2436            "schema enum ({}) and registry ({}) type counts diverge",
2437            enum_ids.len(),
2438            registry_ids.len()
2439        );
2440    }
2441
2442    // ---- Q162: round-trip with a modulated cable + a mult, behavioral equality ----
2443
2444    #[test]
2445    fn test_roundtrip_modulated_cable_and_mult_behavioral_equality() {
2446        // The existing round-trip test covers plain cables + a mult; this one
2447        // closes the remaining gap: a modulated cable (with attenuation AND
2448        // offset) must survive to_def -> JSON -> from_def, and the reloaded
2449        // patch must produce a bit-identical tick() sequence on a multi-module
2450        // graph that also contains a mult (one output -> two inputs).
2451        use crate::modules::{Lfo, StereoOutput, Svf, Vco};
2452
2453        let build = || -> Patch {
2454            let mut patch = Patch::new(44100.0);
2455            let lfo = patch.add("lfo", Lfo::new(44100.0));
2456            let osc = patch.add("osc", Vco::new(44100.0));
2457            let flt = patch.add("flt", Svf::new(44100.0));
2458            let out = patch.add("output", StereoOutput::new());
2459            // Plain audio cable.
2460            patch.connect(osc.out("saw"), flt.in_("in")).unwrap();
2461            // Modulated cable: attenuation 0.5, offset +0.1V into the filter FM
2462            // input (both endpoints are CvBipolar, so no signal-kind warning).
2463            patch
2464                .connect_modulated(lfo.out("sin"), flt.in_("fm"), 0.5, 0.1)
2465                .unwrap();
2466            // Mult: the filter's low-pass output feeds BOTH stereo inputs.
2467            patch.connect(flt.out("lp"), out.in_("left")).unwrap();
2468            patch.connect(flt.out("lp"), out.in_("right")).unwrap();
2469            patch.set_output(out.id());
2470            // A non-default modulation rate so the LFO actually moves.
2471            let lfo_id = lfo.id();
2472            assert!(patch.set_param_by_id(lfo_id, "rate", 0.6));
2473            patch
2474        };
2475
2476        let mut original = build();
2477
2478        // Serialize -> JSON -> deserialize -> rebuild.
2479        let def = original.to_def("Modulated Round Trip");
2480        assert_eq!(def.cables.len(), 4, "all four cables must serialize");
2481        // Exactly one cable carries modulation (attenuation AND offset).
2482        let modulated: Vec<_> = def
2483            .cables
2484            .iter()
2485            .filter(|c| c.attenuation.is_some() && c.offset.is_some())
2486            .collect();
2487        assert_eq!(modulated.len(), 1, "the modulated cable must be preserved");
2488        assert!((modulated[0].attenuation.unwrap() - 0.5).abs() < 1e-9);
2489        assert!((modulated[0].offset.unwrap() - 0.1).abs() < 1e-9);
2490        // The mult shows up as two cables sharing the same source port.
2491        let from_lp = def.cables.iter().filter(|c| c.from == "flt.lp").count();
2492        assert_eq!(
2493            from_lp, 2,
2494            "the mult must serialize as two cables from flt.lp"
2495        );
2496
2497        let json = def.to_json().unwrap();
2498        let reloaded = PatchDef::from_json(&json).unwrap();
2499        let registry = ModuleRegistry::new();
2500        let mut rebuilt = Patch::from_def(&reloaded, &registry, 44100.0).unwrap();
2501
2502        assert_eq!(rebuilt.cable_count(), original.cable_count());
2503
2504        // Behavioral equality: identical stereo output sample-for-sample.
2505        for i in 0..512 {
2506            let a = original.tick();
2507            let b = rebuilt.tick();
2508            assert!(
2509                (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9,
2510                "sample {i} diverged after round-trip: {a:?} vs {b:?}"
2511            );
2512        }
2513    }
2514
2515    // ---- Schema/serde agreement: minimal JSON without tags/parameters loads ----
2516
2517    #[test]
2518    fn test_minimal_json_without_tags_or_parameters_loads() {
2519        // The published schema marks `tags` and `parameters` optional (defaults `[]`/`{}`), so a
2520        // hand-authored/tool-generated patch that omits them must deserialize (serde defaults)
2521        // and load. Before `#[serde(default)]` this failed with "missing field `tags`".
2522        let json = r#"{
2523            "version": 1,
2524            "name": "Minimal",
2525            "modules": [
2526                {"name": "output", "module_type": "stereo_output", "position": null, "state": null}
2527            ],
2528            "cables": []
2529        }"#;
2530        let def = PatchDef::from_json(json).expect("minimal schema-valid JSON must deserialize");
2531        assert!(def.tags.is_empty());
2532        assert!(def.parameters.is_empty());
2533        assert!(def.author.is_none());
2534
2535        let registry = ModuleRegistry::new();
2536        let patch = Patch::from_def(&def, &registry, 44100.0).expect("minimal patch must load");
2537        assert_eq!(patch.output_node(), patch.get_node_id_by_name("output"));
2538    }
2539
2540    // ---- StepSequencer: a muted (gate-off) step must survive round-trip ----
2541
2542    #[test]
2543    fn test_step_sequencer_gate_off_survives_roundtrip() {
2544        use crate::modules::{StepSequencer, StereoOutput};
2545
2546        let mut patch = Patch::new(44100.0);
2547        let seq = patch.add("seq", StepSequencer::new());
2548        let out = patch.add("output", StereoOutput::new());
2549        patch.set_output(out.id());
2550        let seq_id = seq.id();
2551
2552        // Mute step 3 (gate OFF) with a non-zero CV, and mute step 5 at default CV. All other
2553        // steps keep their (ON) defaults.
2554        assert!(patch.set_param_by_id(seq_id, "step_3_cv", 2.0));
2555        assert!(patch.set_param_by_id(seq_id, "step_3_gate", 0.0));
2556        assert!(patch.set_param_by_id(seq_id, "step_5_gate", 0.0));
2557
2558        // A gate turned OFF (value 0.0) must be recorded: the toggle default is now 1.0 (gates
2559        // default ON), so the "differs from default" filter no longer drops it.
2560        let def = patch.to_def("Seq");
2561        assert_eq!(def.parameters.get("seq.step_3_gate"), Some(&0.0));
2562        assert_eq!(def.parameters.get("seq.step_3_cv"), Some(&2.0));
2563        assert_eq!(def.parameters.get("seq.step_5_gate"), Some(&0.0));
2564        // An ON (default) gate equals its default and is correctly omitted.
2565        assert!(!def.parameters.contains_key("seq.step_0_gate"));
2566
2567        let json = def.to_json().unwrap();
2568        let reloaded = PatchDef::from_json(&json).unwrap();
2569        let registry = ModuleRegistry::new();
2570        let rebuilt = Patch::from_def(&reloaded, &registry, 44100.0).unwrap();
2571        let rid = rebuilt.get_node_id_by_name("seq").unwrap();
2572
2573        // Muted steps stay muted and step CV survives; untouched steps stay ON.
2574        assert_eq!(rebuilt.get_param_by_id(rid, "step_3_gate"), Some(0.0));
2575        assert_eq!(rebuilt.get_param_by_id(rid, "step_3_cv"), Some(2.0));
2576        assert_eq!(rebuilt.get_param_by_id(rid, "step_5_gate"), Some(0.0));
2577        assert_eq!(rebuilt.get_param_by_id(rid, "step_0_gate"), Some(1.0));
2578    }
2579
2580    // ---- Ducker: depth/threshold knobs reachable via Patch and round-trip ----
2581
2582    #[test]
2583    fn test_ducker_knobs_reachable_and_survive_roundtrip() {
2584        use crate::modules::{Ducker, StereoOutput};
2585
2586        let mut patch = Patch::new(44100.0);
2587        let d = patch.add("duck", Ducker::default());
2588        let out = patch.add("output", StereoOutput::new());
2589        patch.connect(d.out("out"), out.in_("left")).unwrap();
2590        patch.set_output(out.id());
2591        let did = d.id();
2592
2593        // The depth/thresh KNOBS (introspection) are discoverable, distinct from the same-named
2594        // bipolar CV input PORTS which remain exposed alongside them.
2595        let ids: Vec<String> = patch.param_infos(did).into_iter().map(|p| p.id).collect();
2596        assert!(
2597            ids.iter().any(|i| i == "depth"),
2598            "depth knob missing: {ids:?}"
2599        );
2600        assert!(
2601            ids.iter().any(|i| i == "thresh"),
2602            "thresh knob missing: {ids:?}"
2603        );
2604        assert!(
2605            ids.iter().any(|i| i == "amount"),
2606            "amount CV port missing: {ids:?}"
2607        );
2608        assert!(
2609            ids.iter().any(|i| i == "threshold"),
2610            "threshold CV port missing: {ids:?}"
2611        );
2612
2613        // Setting the knob moves the knob (not the CV port).
2614        assert!(patch.set_param_by_id(did, "depth", 0.4));
2615        assert!(patch.set_param_by_id(did, "thresh", 0.7));
2616        assert_eq!(patch.get_param_by_id(did, "depth"), Some(0.4));
2617        assert_eq!(patch.get_param_by_id(did, "thresh"), Some(0.7));
2618
2619        let def = patch.to_def("Duck");
2620        assert_eq!(def.parameters.get("duck.depth"), Some(&0.4));
2621        assert_eq!(def.parameters.get("duck.thresh"), Some(&0.7));
2622
2623        let json = def.to_json().unwrap();
2624        let reloaded = PatchDef::from_json(&json).unwrap();
2625        let registry = ModuleRegistry::new();
2626        let rebuilt = Patch::from_def(&reloaded, &registry, 44100.0).unwrap();
2627        let rid = rebuilt.get_node_id_by_name("duck").unwrap();
2628        assert_eq!(rebuilt.get_param_by_id(rid, "depth"), Some(0.4));
2629        assert_eq!(rebuilt.get_param_by_id(rid, "thresh"), Some(0.7));
2630    }
2631
2632    // ---- ScaleQuantizer: a custom/microtuning scale must survive round-trip ----
2633
2634    #[test]
2635    fn test_scale_quantizer_custom_scale_survives_roundtrip() {
2636        use crate::modules::{ScaleQuantizer, StereoOutput};
2637
2638        // Whole-tone tuning, distinct from the built-in 12-TET chromatic default.
2639        let whole_tone = [0.0, 200.0, 400.0, 600.0, 800.0, 1000.0];
2640        let test_voct = 0.1; // 120 cents: quantizes differently under whole-tone vs 12-TET.
2641
2642        // Build a patch whose ScaleQuantizer has the custom scale installed, with its "in"
2643        // pitch parked at `test_voct` and "out" wired downstream so it gets ticked.
2644        let build_custom = || -> Patch {
2645            let mut patch = Patch::new(44100.0);
2646            let mut q = ScaleQuantizer::new(44100.0);
2647            q.set_custom_scale(&whole_tone);
2648            let qh = patch.add("q", q);
2649            let out = patch.add("output", StereoOutput::new());
2650            patch.connect(qh.out("out"), out.in_("left")).unwrap();
2651            patch.set_output(out.id());
2652            assert!(patch.set_param_by_id(qh.id(), "in", test_voct));
2653            patch.compile().unwrap();
2654            patch
2655        };
2656
2657        let mut original = build_custom();
2658        let qid_o = original.get_node_id_by_name("q").unwrap();
2659
2660        // The custom scale must ride in ModuleDef.state (not the scalar parameters map).
2661        let def = original.to_def("Tuned");
2662        let q_state = &def.modules.iter().find(|m| m.name == "q").unwrap().state;
2663        assert!(
2664            q_state.is_some(),
2665            "custom scale must serialize into ModuleDef.state"
2666        );
2667
2668        let json = def.to_json().unwrap();
2669        let reloaded = PatchDef::from_json(&json).unwrap();
2670        let registry = ModuleRegistry::new();
2671        let mut rebuilt = Patch::from_def(&reloaded, &registry, 44100.0).unwrap();
2672        let qid_r = rebuilt.get_node_id_by_name("q").unwrap();
2673
2674        // Reference: an untuned (12-TET) quantizer at the same input.
2675        let mut plain = {
2676            let mut patch = Patch::new(44100.0);
2677            let qh = patch.add("q", ScaleQuantizer::new(44100.0));
2678            let out = patch.add("output", StereoOutput::new());
2679            patch.connect(qh.out("out"), out.in_("left")).unwrap();
2680            patch.set_output(out.id());
2681            assert!(patch.set_param_by_id(qh.id(), "in", test_voct));
2682            patch.compile().unwrap();
2683            patch
2684        };
2685        let qid_p = plain.get_node_id_by_name("q").unwrap();
2686
2687        original.tick();
2688        rebuilt.tick();
2689        plain.tick();
2690        // Port id 10 is the ScaleQuantizer "out" (V/Oct).
2691        let o = original.get_output_value(qid_o, 10).unwrap();
2692        let r = rebuilt.get_output_value(qid_r, 10).unwrap();
2693        let p = plain.get_output_value(qid_p, 10).unwrap();
2694
2695        assert!(
2696            (o - r).abs() < 1e-9,
2697            "custom scale must survive round-trip: {o} vs {r}"
2698        );
2699        assert!(
2700            (o - p).abs() > 1e-6,
2701            "custom (whole-tone) quantization must differ from default 12-TET: {o} vs {p}"
2702        );
2703    }
2704}