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