Skip to main content

syster_cli/
lib.rs

1//! syster-cli library - Core analysis functionality
2//!
3//! This module provides the `run_analysis` function for parsing and analyzing
4//! SysML v2 and KerML files using the syster-base library.
5
6use serde::Serialize;
7use std::path::{Path, PathBuf};
8use syster::hir::{Severity, check_file};
9use syster::ide::AnalysisHost;
10use walkdir::WalkDir;
11
12/// Result of analyzing SysML/KerML files.
13#[derive(Debug, Serialize)]
14pub struct AnalysisResult {
15    /// Number of files analyzed.
16    pub file_count: usize,
17    /// Total number of symbols found.
18    pub symbol_count: usize,
19    /// Number of errors found.
20    pub error_count: usize,
21    /// Number of warnings found.
22    pub warning_count: usize,
23    /// All diagnostics collected.
24    pub diagnostics: Vec<DiagnosticInfo>,
25}
26
27/// A diagnostic message with location information.
28#[derive(Debug, Clone, Serialize)]
29pub struct DiagnosticInfo {
30    /// File path containing the diagnostic.
31    pub file: String,
32    /// Start line (1-indexed).
33    pub line: u32,
34    /// Start column (1-indexed).
35    pub col: u32,
36    /// End line (1-indexed).
37    pub end_line: u32,
38    /// End column (1-indexed).
39    pub end_col: u32,
40    /// The diagnostic message.
41    pub message: String,
42    /// Severity level.
43    #[serde(serialize_with = "serialize_severity")]
44    pub severity: Severity,
45    /// Optional error code.
46    pub code: Option<String>,
47}
48
49/// Serialize Severity as a string
50fn serialize_severity<S>(severity: &Severity, serializer: S) -> Result<S::Ok, S::Error>
51where
52    S: serde::Serializer,
53{
54    let s = match severity {
55        Severity::Error => "error",
56        Severity::Warning => "warning",
57        Severity::Info => "info",
58        Severity::Hint => "hint",
59    };
60    serializer.serialize_str(s)
61}
62
63/// Run analysis on input file or directory.
64///
65/// # Arguments
66/// * `input` - Path to a file or directory to analyze
67/// * `verbose` - Enable verbose output
68/// * `load_stdlib` - Whether to load the standard library
69/// * `stdlib_path` - Optional custom path to the standard library
70///
71/// # Returns
72/// An `AnalysisResult` with file count, symbol count, and diagnostics.
73pub fn run_analysis(
74    input: &Path,
75    verbose: bool,
76    load_stdlib: bool,
77    stdlib_path: Option<&Path>,
78) -> Result<AnalysisResult, String> {
79    let mut host = AnalysisHost::new();
80
81    // 1. Load stdlib if requested
82    if load_stdlib {
83        load_stdlib_files(&mut host, stdlib_path, verbose)?;
84    }
85
86    // 2. Load input file(s)
87    load_input(&mut host, input, verbose)?;
88
89    // 3. Trigger index rebuild and get analysis
90    let _analysis = host.analysis();
91
92    // 4. Collect diagnostics from all files
93    let diagnostics = collect_diagnostics(&host);
94
95    // 5. Build result
96    let error_count = diagnostics
97        .iter()
98        .filter(|d| matches!(d.severity, Severity::Error))
99        .count();
100    let warning_count = diagnostics
101        .iter()
102        .filter(|d| matches!(d.severity, Severity::Warning))
103        .count();
104
105    Ok(AnalysisResult {
106        file_count: host.file_count(),
107        symbol_count: host.symbol_index().all_symbols().count(),
108        error_count,
109        warning_count,
110        diagnostics,
111    })
112}
113
114/// Load input file or directory.
115pub fn load_input(host: &mut AnalysisHost, input: &Path, verbose: bool) -> Result<(), String> {
116    if input.is_file() {
117        load_file(host, input, verbose)
118    } else if input.is_dir() {
119        load_directory(host, input, verbose)
120    } else {
121        Err(format!("Path does not exist: {}", input.display()))
122    }
123}
124
125/// Load a single file into the analysis host.
126fn load_file(host: &mut AnalysisHost, path: &Path, verbose: bool) -> Result<(), String> {
127    if verbose {
128        println!("  Loading: {}", path.display());
129    }
130
131    let content = std::fs::read_to_string(path)
132        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
133
134    let path_str = path.to_string_lossy();
135    let parse_errors = host.set_file_content(&path_str, &content);
136
137    // Parse errors are reported but don't fail the load
138    for err in parse_errors {
139        eprintln!(
140            "parse error: {}:{}:{}: {}",
141            path.display(),
142            err.position.line,
143            err.position.column,
144            err.message
145        );
146    }
147
148    Ok(())
149}
150
151/// Load all SysML/KerML files from a directory.
152fn load_directory(host: &mut AnalysisHost, dir: &Path, verbose: bool) -> Result<(), String> {
153    if verbose {
154        println!("Scanning directory: {}", dir.display());
155    }
156
157    for entry in WalkDir::new(dir).follow_links(true) {
158        let entry = entry.map_err(|e| format!("Walk error: {}", e))?;
159        let path = entry.path();
160
161        if is_sysml_file(path) {
162            load_file(host, path, verbose)?;
163        }
164    }
165
166    Ok(())
167}
168
169/// Check if a path is a SysML or KerML file.
170fn is_sysml_file(path: &Path) -> bool {
171    path.is_file()
172        && matches!(
173            path.extension().and_then(|e| e.to_str()),
174            Some("sysml") | Some("kerml")
175        )
176}
177
178/// Load standard library files.
179pub fn load_stdlib_files(
180    host: &mut AnalysisHost,
181    custom_path: Option<&Path>,
182    verbose: bool,
183) -> Result<(), String> {
184    if verbose {
185        println!("Loading standard library...");
186    }
187
188    // Try custom path first
189    if let Some(path) = custom_path {
190        if path.exists() {
191            return load_directory(host, path, verbose);
192        } else {
193            return Err(format!("Stdlib path does not exist: {}", path.display()));
194        }
195    }
196
197    // Try default locations
198    let default_paths = [
199        PathBuf::from("sysml.library"),
200        PathBuf::from("../sysml.library"),
201        PathBuf::from("../base/sysml.library"),
202    ];
203
204    for path in &default_paths {
205        if path.exists() {
206            return load_directory(host, path, verbose);
207        }
208    }
209
210    if verbose {
211        println!("  Warning: Standard library not found");
212    }
213
214    Ok(())
215}
216
217/// Collect diagnostics from all files in the host.
218fn collect_diagnostics(host: &AnalysisHost) -> Vec<DiagnosticInfo> {
219    let mut all_diagnostics = Vec::new();
220
221    for path in host.files().keys() {
222        if let Some(file_id) = host.get_file_id_for_path(path) {
223            let file_path = path.to_string_lossy().to_string();
224            let diagnostics = check_file(host.symbol_index(), file_id);
225
226            for diag in diagnostics {
227                all_diagnostics.push(DiagnosticInfo {
228                    file: file_path.clone(),
229                    line: diag.start_line + 1, // 1-indexed for display
230                    col: diag.start_col + 1,
231                    end_line: diag.end_line + 1,
232                    end_col: diag.end_col + 1,
233                    message: diag.message.to_string(),
234                    severity: diag.severity,
235                    code: diag.code.map(|c| c.to_string()),
236                });
237            }
238        }
239    }
240
241    // Sort by file, then line, then column
242    all_diagnostics.sort_by(|a, b| (&a.file, a.line, a.col).cmp(&(&b.file, b.line, b.col)));
243
244    all_diagnostics
245}
246
247// ============================================================================
248// EXPORT FUNCTIONS
249// ============================================================================
250
251/// A symbol for JSON export (simplified from HirSymbol).
252#[derive(Debug, Serialize)]
253pub struct ExportSymbol {
254    pub name: String,
255    pub qualified_name: String,
256    pub kind: String,
257    pub file: String,
258    pub start_line: u32,
259    pub start_col: u32,
260    pub end_line: u32,
261    pub end_col: u32,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub doc: Option<String>,
264    #[serde(skip_serializing_if = "Vec::is_empty")]
265    pub supertypes: Vec<String>,
266}
267
268/// AST export result.
269#[derive(Debug, Serialize)]
270pub struct AstExport {
271    pub files: Vec<FileAst>,
272}
273
274/// AST for a single file.
275#[derive(Debug, Serialize)]
276pub struct FileAst {
277    pub path: String,
278    pub symbols: Vec<ExportSymbol>,
279}
280
281/// Export AST (symbols) for all files.
282pub fn export_ast(
283    input: &Path,
284    verbose: bool,
285    load_stdlib: bool,
286    stdlib_path: Option<&Path>,
287) -> Result<String, String> {
288    let mut host = AnalysisHost::new();
289
290    if load_stdlib {
291        load_stdlib_files(&mut host, stdlib_path, verbose)?;
292    }
293
294    load_input(&mut host, input, verbose)?;
295    let _analysis = host.analysis();
296
297    let mut files = Vec::new();
298
299    // Only export user files, not stdlib
300    for path in host.files().keys() {
301        let path_str = path.to_string_lossy().to_string();
302
303        // Skip stdlib files
304        if path_str.contains("sysml.library") {
305            continue;
306        }
307
308        if let Some(file_id) = host.get_file_id_for_path(path) {
309            let symbols: Vec<ExportSymbol> = host
310                .symbol_index()
311                .symbols_in_file(file_id)
312                .into_iter()
313                .map(|sym| ExportSymbol {
314                    name: sym.name.to_string(),
315                    qualified_name: sym.qualified_name.to_string(),
316                    kind: format!("{:?}", sym.kind),
317                    file: path_str.clone(),
318                    start_line: sym.start_line + 1,
319                    start_col: sym.start_col + 1,
320                    end_line: sym.end_line + 1,
321                    end_col: sym.end_col + 1,
322                    doc: sym.doc.as_ref().map(|d| d.to_string()),
323                    supertypes: sym.supertypes.iter().map(|s| s.to_string()).collect(),
324                })
325                .collect();
326
327            files.push(FileAst {
328                path: path_str,
329                symbols,
330            });
331        }
332    }
333
334    // Sort files by path for consistent output
335    files.sort_by(|a, b| a.path.cmp(&b.path));
336
337    let export = AstExport { files };
338    serde_json::to_string_pretty(&export).map_err(|e| format!("Failed to serialize AST: {}", e))
339}
340
341/// Export analysis result as JSON.
342pub fn export_json(result: &AnalysisResult) -> Result<String, String> {
343    serde_json::to_string_pretty(result).map_err(|e| format!("Failed to serialize result: {}", e))
344}
345
346// ============================================================================
347// INTERCHANGE EXPORT
348// ============================================================================
349
350/// Export a model to an interchange format.
351///
352/// Supported formats:
353/// - `xmi` - XML Model Interchange
354/// - `kpar` - Kernel Package Archive (ZIP)
355/// - `jsonld` - JSON-LD
356///
357/// # Arguments
358/// * `input` - Path to a file or directory to analyze
359/// * `format` - Output format (xmi, kpar, jsonld)
360/// * `verbose` - Enable verbose output
361/// * `load_stdlib` - Whether to load the standard library
362/// * `stdlib_path` - Optional custom path to the standard library
363///
364/// # Returns
365/// The serialized model as bytes.
366#[cfg(feature = "interchange")]
367pub fn export_model(
368    input: &Path,
369    format: &str,
370    verbose: bool,
371    load_stdlib: bool,
372    stdlib_path: Option<&Path>,
373    self_contained: bool,
374) -> Result<Vec<u8>, String> {
375    use syster::interchange::{
376        JsonLd, Kpar, ModelFormat, Xmi, Yaml, model_from_symbols, restore_ids_from_symbols,
377    };
378
379    let mut host = AnalysisHost::new();
380
381    // 1. Load stdlib if requested
382    if load_stdlib {
383        load_stdlib_files(&mut host, stdlib_path, verbose)?;
384    }
385
386    // 2. Load input file(s)
387    load_input(&mut host, input, verbose)?;
388
389    // 2.5. Load metadata if present (for ID preservation on round-trip)
390    #[cfg(feature = "interchange")]
391    {
392        use syster::project::WorkspaceLoader;
393        let loader = WorkspaceLoader::new();
394
395        // If input is a file, check for companion metadata
396        if input.is_file() {
397            let parent_dir = input.parent().unwrap_or(input);
398            if let Err(e) = loader.load_metadata_from_directory(parent_dir, &mut host) {
399                if verbose {
400                    eprintln!("Note: Could not load metadata: {}", e);
401                }
402            } else if verbose {
403                println!("Loaded metadata from {}", parent_dir.display());
404            }
405        } else if input.is_dir() {
406            // For directories, load metadata from that directory
407            if let Err(e) = loader.load_metadata_from_directory(input, &mut host) {
408                if verbose {
409                    eprintln!("Note: Could not load metadata: {}", e);
410                }
411            } else if verbose {
412                println!("Loaded metadata from {}", input.display());
413            }
414        }
415    }
416
417    // 3. Trigger index rebuild
418    let analysis = host.analysis();
419
420    // 4. Get symbols from the index (filtered unless self_contained)
421    let symbols: Vec<_> = if self_contained {
422        // Include all symbols (user model + stdlib)
423        analysis.symbol_index().all_symbols().cloned().collect()
424    } else {
425        // Filter to only user files (exclude stdlib)
426        analysis
427            .symbol_index()
428            .all_symbols()
429            .filter(|sym| {
430                // Get the file path for this symbol and check if it's stdlib
431                if let Some(path) = analysis.get_file_path(sym.file) {
432                    !path.contains("sysml.library")
433                } else {
434                    true // Include if we can't determine the path
435                }
436            })
437            .cloned()
438            .collect()
439    };
440
441    if verbose {
442        println!(
443            "Collecting {} symbols (self_contained={})",
444            symbols.len(),
445            self_contained
446        );
447    }
448
449    // 5. Convert to interchange model
450    let mut model = model_from_symbols(&symbols);
451
452    // 6. Restore original element IDs from symbols (if they exist)
453    model = restore_ids_from_symbols(model, analysis.symbol_index());
454    if verbose {
455        println!("Restored element IDs from symbol database");
456    }
457
458    if verbose {
459        println!(
460            "Exported model: {} elements, {} relationships",
461            model.elements.len(),
462            model.relationship_count()
463        );
464    }
465
466    // 8. Serialize to requested format
467    match format.to_lowercase().as_str() {
468        "xmi" => Xmi.write(&model).map_err(|e| e.to_string()),
469        "kpar" => Kpar.write(&model).map_err(|e| e.to_string()),
470        "jsonld" | "json-ld" => JsonLd.write(&model).map_err(|e| e.to_string()),
471        "yaml" | "yml" => Yaml.write(&model).map_err(|e| e.to_string()),
472        _ => Err(format!(
473            "Unsupported format: {}. Use xmi, kpar, jsonld, or yaml.",
474            format
475        )),
476    }
477}
478
479/// Export model from an existing AnalysisHost to an interchange format.
480///
481/// This allows exporting from a pre-populated host (e.g., after import_model_into_host).
482/// Element IDs are preserved from the symbol database.
483///
484/// Set `self_contained` to true to include all symbols (including stdlib),
485/// or false to only include user model symbols.
486#[cfg(feature = "interchange")]
487pub fn export_from_host(
488    host: &mut AnalysisHost,
489    format: &str,
490    verbose: bool,
491    self_contained: bool,
492) -> Result<Vec<u8>, String> {
493    use syster::interchange::{
494        JsonLd, Kpar, ModelFormat, Xmi, Yaml, model_from_symbols, restore_ids_from_symbols,
495    };
496
497    let analysis = host.analysis();
498    let symbols: Vec<_> = if self_contained {
499        analysis.symbol_index().all_symbols().cloned().collect()
500    } else {
501        analysis
502            .symbol_index()
503            .all_symbols()
504            .filter(|sym| {
505                if let Some(path) = analysis.get_file_path(sym.file) {
506                    !path.contains("sysml.library")
507                } else {
508                    true
509                }
510            })
511            .cloned()
512            .collect()
513    };
514
515    if verbose {
516        println!(
517            "Collecting {} symbols (self_contained={})",
518            symbols.len(),
519            self_contained
520        );
521    }
522
523    let mut model = model_from_symbols(&symbols);
524    model = restore_ids_from_symbols(model, analysis.symbol_index());
525
526    if verbose {
527        println!(
528            "Exported model: {} elements, {} relationships",
529            model.elements.len(),
530            model.relationship_count()
531        );
532    }
533
534    match format.to_lowercase().as_str() {
535        "xmi" => Xmi.write(&model).map_err(|e| e.to_string()),
536        "kpar" => Kpar.write(&model).map_err(|e| e.to_string()),
537        "jsonld" | "json-ld" => JsonLd.write(&model).map_err(|e| e.to_string()),
538        "yaml" | "yml" => Yaml.write(&model).map_err(|e| e.to_string()),
539        _ => Err(format!(
540            "Unsupported format: {}. Use xmi, kpar, jsonld, or yaml.",
541            format
542        )),
543    }
544}
545
546/// Result of importing a model from an interchange format.
547#[cfg(feature = "interchange")]
548#[derive(Debug)]
549pub struct ImportResult {
550    /// Number of elements imported.
551    pub element_count: usize,
552    /// Number of relationships imported.
553    pub relationship_count: usize,
554    /// Number of validation errors (hard failures).
555    pub error_count: usize,
556    /// Number of validation warnings (non-fatal).
557    pub warning_count: usize,
558    /// Validation messages.
559    pub messages: Vec<String>,
560}
561
562/// Import a model from an interchange format file (validation only).
563///
564/// This validates the model but doesn't load it into a workspace.
565/// For importing into a workspace, use `import_model_into_host()`.
566///
567/// Supported formats are detected from file extension:
568/// - `.xmi` - XML Model Interchange
569/// - `.kpar` - Kernel Package Archive (ZIP)
570/// - `.jsonld`, `.json` - JSON-LD
571///
572/// # Arguments
573/// * `input` - Path to the interchange file
574/// * `format` - Optional format override (otherwise detected from extension)
575/// * `verbose` - Enable verbose output
576///
577/// # Returns
578/// An `ImportResult` with element count and symbol info.
579#[cfg(feature = "interchange")]
580pub fn import_model_into_host(
581    host: &mut AnalysisHost,
582    input: &Path,
583    format: Option<&str>,
584    verbose: bool,
585) -> Result<ImportResult, String> {
586    use syster::interchange::{JsonLd, Kpar, ModelFormat, Xmi, detect_format};
587
588    // Read the input file
589    let bytes =
590        std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
591
592    // Determine format
593    let format_str = format.map(String::from).unwrap_or_else(|| {
594        input
595            .extension()
596            .and_then(|e| e.to_str())
597            .unwrap_or("xmi")
598            .to_string()
599    });
600
601    if verbose {
602        println!(
603            "Importing {} as {} into workspace",
604            input.display(),
605            format_str
606        );
607    }
608
609    // Parse the model
610    let model = match format_str.to_lowercase().as_str() {
611        "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
612        "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
613        "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
614        _ => {
615            // Try to detect from file extension
616            if let Some(format_impl) = detect_format(input) {
617                format_impl.read(&bytes).map_err(|e| e.to_string())?
618            } else {
619                return Err(format!(
620                    "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
621                    format_str
622                ));
623            }
624        }
625    };
626
627    let element_count = model.elements.len();
628    let relationship_count = model.relationship_count();
629
630    if verbose {
631        println!(
632            "Parsed {} elements and {} relationships",
633            element_count, relationship_count
634        );
635    }
636
637    // Add model to host using the new add_model API
638    // This decompiles the model to SysML and parses it, preserving element IDs
639    let virtual_path = input.to_string_lossy().to_string() + ".sysml";
640    let errors = host.add_model(&model, &virtual_path);
641
642    if verbose {
643        if errors.is_empty() {
644            println!("Loaded model into workspace with preserved element IDs");
645        } else {
646            println!("Loaded model with {} parse warnings", errors.len());
647        }
648    }
649
650    Ok(ImportResult {
651        element_count,
652        relationship_count,
653        error_count: errors.len(),
654        warning_count: 0,
655        messages: vec![format!("Successfully imported {} elements", element_count)],
656    })
657}
658
659/// Import and validate a model from an interchange format (legacy version).
660///
661/// This validates the model but doesn't load it into a workspace.
662/// For importing into a workspace, use `import_model_into_host()`.
663///
664/// # Arguments
665/// * `input` - Path to the model file
666/// * `format` - Optional format override (xmi, kpar, jsonld)
667/// * `verbose` - Enable verbose output
668///
669/// # Returns
670/// An `ImportResult` with element count and validation info.
671#[cfg(feature = "interchange")]
672pub fn import_model(
673    input: &Path,
674    format: Option<&str>,
675    verbose: bool,
676) -> Result<ImportResult, String> {
677    use syster::interchange::{JsonLd, Kpar, ModelFormat, Xmi, detect_format};
678
679    // Read the input file
680    let bytes =
681        std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
682
683    // Determine format
684    let format_str = format.map(String::from).unwrap_or_else(|| {
685        input
686            .extension()
687            .and_then(|e| e.to_str())
688            .unwrap_or("xmi")
689            .to_string()
690    });
691
692    if verbose {
693        println!("Importing {} as {}", input.display(), format_str);
694    }
695
696    // Parse the model
697    let model = match format_str.to_lowercase().as_str() {
698        "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
699        "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
700        "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
701        _ => {
702            // Try to detect from file extension
703            if let Some(format_impl) = detect_format(input) {
704                format_impl.read(&bytes).map_err(|e| e.to_string())?
705            } else {
706                return Err(format!(
707                    "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
708                    format_str
709                ));
710            }
711        }
712    };
713
714    // Basic validation
715    let mut messages = Vec::new();
716    let mut error_count = 0;
717    let mut warning_count = 0;
718
719    // Check for orphan relationships (references to non-existent elements)
720    // Missing targets are warnings (often stdlib types not included in export).
721    // Missing sources are errors (indicates corrupt data).
722    // Targets that resolve to `_external` stub elements also count as
723    // warnings — they were unresolved external references at export time.
724    for rel in model.iter_relationship_elements() {
725        if let Some(rd) = &rel.relationship {
726            if let Some(src) = rd.source.first() {
727                if model.elements.get(src).is_none() {
728                    messages.push(format!("Error: Relationship source '{}' not found", src));
729                    error_count += 1;
730                }
731            }
732            if let Some(tgt) = rd.target.first() {
733                let target_el = model.elements.get(tgt);
734                let is_missing = target_el.is_none();
735                // Targets whose ID starts with `_ext_` are stub elements
736                // created by model_from_symbols() for unresolved external
737                // references (e.g. stdlib types not included in export).
738                let is_external_stub = tgt.as_str().starts_with("_ext_");
739                if is_missing || is_external_stub {
740                    let name = target_el
741                        .and_then(|el| el.name.as_deref())
742                        .unwrap_or(tgt.as_str());
743                    messages.push(format!(
744                        "Warning: Relationship target '{}' not found (may be a stdlib type)",
745                        name
746                    ));
747                    warning_count += 1;
748                }
749            }
750        }
751    }
752
753    if verbose {
754        println!(
755            "Imported: {} elements, {} relationships, {} errors, {} warnings",
756            model.elements.len(),
757            model.relationship_count(),
758            error_count,
759            warning_count
760        );
761        for msg in &messages {
762            println!("  {}", msg);
763        }
764    }
765
766    Ok(ImportResult {
767        element_count: model.elements.len(),
768        relationship_count: model.relationship_count(),
769        error_count,
770        warning_count,
771        messages,
772    })
773}
774
775// ============================================================================
776// SEMANTIC MODEL COMMANDS (AnalysisHost-based)
777// ============================================================================
778
779/// Result of querying a model.
780#[cfg(feature = "interchange")]
781#[derive(Debug, Serialize)]
782pub struct QueryResult {
783    /// Number of elements matching the query.
784    pub match_count: usize,
785    /// The matching elements.
786    pub elements: Vec<ElementInfo>,
787}
788
789/// Information about a single model element (JSON-serializable).
790#[cfg(feature = "interchange")]
791#[derive(Debug, Clone, Serialize)]
792pub struct ElementInfo {
793    /// Element ID.
794    pub id: String,
795    /// Declared name.
796    pub name: Option<String>,
797    /// Qualified name.
798    pub qualified_name: Option<String>,
799    /// Metaclass kind (e.g., "PartDefinition", "PartUsage").
800    pub kind: String,
801    /// Whether the element is abstract.
802    pub is_abstract: bool,
803    /// Owner element name (if any).
804    pub owner: Option<String>,
805    /// Number of owned members.
806    pub owned_member_count: usize,
807    /// Type names (for usages typed by a definition).
808    pub typed_by: Vec<String>,
809    /// Supertype names (for specializations).
810    pub supertypes: Vec<String>,
811    /// Documentation text.
812    pub documentation: Option<String>,
813}
814
815/// Inspect result — detailed view of one element and its surroundings.
816#[cfg(feature = "interchange")]
817#[derive(Debug, Serialize)]
818pub struct InspectResult {
819    /// The inspected element.
820    pub element: ElementInfo,
821    /// Direct children.
822    pub children: Vec<ElementInfo>,
823    /// Relationships from this element.
824    pub relationships_from: Vec<RelationshipInfo>,
825    /// Relationships to this element.
826    pub relationships_to: Vec<RelationshipInfo>,
827}
828
829/// A relationship in human-readable form.
830#[cfg(feature = "interchange")]
831#[derive(Debug, Clone, Serialize)]
832pub struct RelationshipInfo {
833    /// Relationship kind (e.g., "Specialization", "FeatureTyping").
834    pub kind: String,
835    /// Source element name.
836    pub source: String,
837    /// Target element name.
838    pub target: String,
839}
840
841/// Result of a rename operation.
842#[cfg(feature = "interchange")]
843#[derive(Debug)]
844pub struct RenameResult {
845    /// Old name.
846    pub old_name: String,
847    /// New name.
848    pub new_name: String,
849    /// Rendered SysML text after rename.
850    pub rendered_text: String,
851    /// Updated metadata JSON (if metadata was loaded).
852    pub metadata_json: Option<String>,
853}
854
855/// Load a SysML file into an `AnalysisHost` with companion metadata applied.
856///
857/// This is the standard entry-point for CLI commands that need a parsed
858/// `Model` — it replaces the old `ModelHost::from_text()` pattern,
859/// using `AnalysisHost` directly instead.
860///
861/// Returns the host (with model cached) and the raw source text.
862#[cfg(feature = "interchange")]
863fn load_model_from_file(
864    input: &Path,
865    verbose: bool,
866) -> Result<(syster::ide::AnalysisHost, String), String> {
867    let source = std::fs::read_to_string(input)
868        .map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
869
870    let path_str = input.to_string_lossy().to_string();
871    let mut host = syster::ide::AnalysisHost::new();
872    let errors = host.set_file_content(&path_str, &source);
873
874    if !errors.is_empty() && verbose {
875        eprintln!("Parse warnings: {}", errors.len());
876    }
877
878    // Apply companion metadata (restores original element IDs)
879    // Take the model, apply metadata, put it back.
880    let model = host.take_model().unwrap();
881    let model = load_companion_metadata(input, model, verbose);
882    host.set_model_cache(model);
883
884    if verbose {
885        eprintln!(
886            "Loaded {} elements, {} relationships",
887            host.model().element_count(),
888            host.model().relationship_count()
889        );
890    }
891
892    Ok((host, source))
893}
894
895/// Try to load a companion `.metadata.json` file for ID preservation.
896///
897/// Given `/path/to/model.sysml`, looks for `/path/to/model.metadata.json`.
898/// If found, applies the original element IDs to the model via
899/// `restore_element_ids`.
900#[cfg(feature = "interchange")]
901fn load_companion_metadata(
902    input: &Path,
903    model: syster::interchange::model::Model,
904    verbose: bool,
905) -> syster::interchange::model::Model {
906    use syster::interchange::metadata::ImportMetadata;
907    use syster::interchange::recompile::restore_element_ids;
908
909    let metadata_path = input.with_extension("metadata.json");
910    if metadata_path.exists() {
911        match ImportMetadata::read_from_file(&metadata_path) {
912            Ok(metadata) => {
913                if verbose {
914                    eprintln!(
915                        "Loaded metadata from {} ({} elements)",
916                        metadata_path.display(),
917                        metadata.elements.len()
918                    );
919                }
920                restore_element_ids(model, &metadata)
921            }
922            Err(e) => {
923                if verbose {
924                    eprintln!(
925                        "Note: Could not load metadata from {}: {}",
926                        metadata_path.display(),
927                        e
928                    );
929                }
930                model
931            }
932        }
933    } else {
934        model
935    }
936}
937
938/// Build updated metadata JSON after a rename operation.
939///
940/// Takes the renamed model and produces a new `ImportMetadata` that maps
941/// qualified names to their (preserved) element IDs.
942#[cfg(feature = "interchange")]
943fn build_updated_metadata(
944    model: &syster::interchange::model::Model,
945    source_path: &Path,
946) -> Option<String> {
947    use syster::interchange::metadata::{ElementMeta, ImportMetadata, SourceInfo};
948
949    let mut metadata = ImportMetadata::new().with_source(SourceInfo::from_path(
950        source_path.to_string_lossy().to_string(),
951    ));
952
953    for element in model.elements.values() {
954        if let Some(qn) = &element.qualified_name {
955            let meta = ElementMeta::with_id(element.id.as_str());
956            metadata.add_element(qn.as_ref(), meta);
957        }
958    }
959
960    serde_json::to_string_pretty(&metadata).ok()
961}
962
963/// Query a SysML model by name, kind, or qualified name.
964///
965/// Loads the file via `AnalysisHost` and searches for elements
966/// matching the given criteria. If a companion `.metadata.json` exists,
967/// original element IDs are restored.
968///
969/// # Arguments
970/// * `input` - Path to a `.sysml` file
971/// * `name` - Optional name filter (substring match)
972/// * `kind` - Optional kind filter (e.g., "PartDefinition")
973/// * `qualified_name` - Optional exact qualified name lookup
974/// * `verbose` - Enable verbose output
975#[cfg(feature = "interchange")]
976pub fn query_model(
977    input: &Path,
978    name: Option<&str>,
979    kind: Option<&str>,
980    qualified_name: Option<&str>,
981    verbose: bool,
982) -> Result<QueryResult, String> {
983    let (mut host, _source) = load_model_from_file(input, verbose)?;
984
985    // Get the model reference and query through it directly
986    let model = host.model();
987
988    // Collect matching elements
989    let views: Vec<_> = if let Some(qn) = qualified_name {
990        // Exact qualified name lookup
991        model.find_by_qualified_name(qn).into_iter().collect()
992    } else {
993        // Get all elements, then filter
994        model
995            .elements
996            .values()
997            .filter_map(|el| {
998                let view = model.view(&el.id)?;
999
1000                // Name filter (substring)
1001                if let Some(n) = name {
1002                    let matches_name = view.name().map(|vn| vn.contains(n)).unwrap_or(false);
1003                    if !matches_name {
1004                        return None;
1005                    }
1006                }
1007
1008                // Kind filter
1009                if let Some(k) = kind {
1010                    let kind_str = format!("{:?}", view.kind());
1011                    if !kind_str.eq_ignore_ascii_case(k) {
1012                        return None;
1013                    }
1014                }
1015
1016                // Skip relationship-only elements if no explicit kind filter
1017                if kind.is_none() && view.kind().is_relationship() {
1018                    return None;
1019                }
1020
1021                Some(view)
1022            })
1023            .collect()
1024    };
1025
1026    let elements: Vec<ElementInfo> = views.iter().map(|v| element_info(v)).collect();
1027
1028    Ok(QueryResult {
1029        match_count: elements.len(),
1030        elements,
1031    })
1032}
1033
1034/// Inspect a single element by name or qualified name — shows the element
1035/// plus its children and relationships.
1036///
1037/// # Arguments
1038/// * `input` - Path to a `.sysml` file
1039/// * `target` - Name or qualified name of the element to inspect
1040/// * `verbose` - Enable verbose output
1041#[cfg(feature = "interchange")]
1042pub fn inspect_element(input: &Path, target: &str, verbose: bool) -> Result<InspectResult, String> {
1043    let (mut host, _source) = load_model_from_file(input, verbose)?;
1044
1045    let model = host.model();
1046
1047    // Try qualified name first, then plain name
1048    let view = model
1049        .find_by_qualified_name(target)
1050        .or_else(|| model.find_by_name(target).into_iter().next())
1051        .ok_or_else(|| format!("Element '{}' not found", target))?;
1052
1053    if verbose {
1054        eprintln!("Found: {:?} '{}'", view.kind(), view.name().unwrap_or("?"));
1055    }
1056
1057    let children: Vec<ElementInfo> = view
1058        .owned_members()
1059        .iter()
1060        .map(|c| element_info(c))
1061        .collect();
1062
1063    let relationships_from: Vec<RelationshipInfo> = view
1064        .relationships_from()
1065        .iter()
1066        .map(|r| {
1067            let source_name = r
1068                .source()
1069                .and_then(|sid| model.view(sid))
1070                .and_then(|v| v.name().map(String::from))
1071                .unwrap_or_else(|| {
1072                    r.source()
1073                        .map(|s| s.as_str().to_string())
1074                        .unwrap_or_default()
1075                });
1076            let target_name = r
1077                .target()
1078                .and_then(|tid| model.view(tid))
1079                .and_then(|v| v.name().map(String::from))
1080                .unwrap_or_else(|| {
1081                    r.target()
1082                        .map(|t| t.as_str().to_string())
1083                        .unwrap_or_default()
1084                });
1085            RelationshipInfo {
1086                kind: format!("{:?}", r.kind),
1087                source: source_name,
1088                target: target_name,
1089            }
1090        })
1091        .collect();
1092
1093    let relationships_to: Vec<RelationshipInfo> = view
1094        .relationships_to()
1095        .iter()
1096        .map(|r| {
1097            let source_name = r
1098                .source()
1099                .and_then(|sid| model.view(sid))
1100                .and_then(|v| v.name().map(String::from))
1101                .unwrap_or_else(|| {
1102                    r.source()
1103                        .map(|s| s.as_str().to_string())
1104                        .unwrap_or_default()
1105                });
1106            let target_name = r
1107                .target()
1108                .and_then(|tid| model.view(tid))
1109                .and_then(|v| v.name().map(String::from))
1110                .unwrap_or_else(|| {
1111                    r.target()
1112                        .map(|t| t.as_str().to_string())
1113                        .unwrap_or_default()
1114                });
1115            RelationshipInfo {
1116                kind: format!("{:?}", r.kind),
1117                source: source_name,
1118                target: target_name,
1119            }
1120        })
1121        .collect();
1122
1123    Ok(InspectResult {
1124        element: element_info(&view),
1125        children,
1126        relationships_from,
1127        relationships_to,
1128    })
1129}
1130
1131/// Rename an element in a SysML file and output the modified text.
1132///
1133/// Uses `AnalysisHost::apply_model_edit()` to perform the rename via
1134/// the semantic Model layer, with automatic text re-rendering and
1135/// SymbolIndex sync.
1136///
1137/// # Arguments
1138/// * `input` - Path to a `.sysml` file
1139/// * `target` - Name or qualified name of the element to rename
1140/// * `new_name` - The new name for the element
1141/// * `verbose` - Enable verbose output
1142#[cfg(feature = "interchange")]
1143pub fn rename_element(
1144    input: &Path,
1145    target: &str,
1146    new_name: &str,
1147    verbose: bool,
1148) -> Result<RenameResult, String> {
1149    let (mut host, _source) = load_model_from_file(input, verbose)?;
1150    let path_str = input.to_string_lossy().to_string();
1151
1152    // Find the element (immutable access, then release borrow)
1153    let (old_name, element_id) = {
1154        let model = host.model();
1155        let view = model
1156            .find_by_qualified_name(target)
1157            .or_else(|| model.find_by_name(target).into_iter().next())
1158            .ok_or_else(|| format!("Element '{}' not found", target))?;
1159        let old = view.name().unwrap_or("(anonymous)").to_string();
1160        let id = view.id().clone();
1161        if verbose {
1162            eprintln!("Renaming {:?} '{}' -> '{}'", view.kind(), old, new_name);
1163        }
1164        (old, id)
1165    };
1166
1167    // Apply rename via apply_model_edit (handles SourceMap, render, re-parse, ID restore)
1168    let result = host.apply_model_edit(&path_str, |model, tracker| {
1169        tracker.rename(model, &element_id, new_name);
1170    });
1171
1172    if verbose {
1173        eprintln!("Renamed '{}' -> '{}'", old_name, new_name);
1174    }
1175
1176    // Build updated metadata JSON (preserves element IDs after rename)
1177    let metadata_json = build_updated_metadata(host.model(), input);
1178
1179    Ok(RenameResult {
1180        old_name,
1181        new_name: new_name.to_string(),
1182        rendered_text: result.rendered_text,
1183        metadata_json,
1184    })
1185}
1186
1187/// Result of adding a member to an element.
1188#[cfg(feature = "interchange")]
1189#[derive(Debug)]
1190pub struct AddMemberResult {
1191    /// Parent element name.
1192    pub parent_name: String,
1193    /// Added element name.
1194    pub member_name: String,
1195    /// Added element kind.
1196    pub member_kind: String,
1197    /// Generated element ID for the new member.
1198    pub member_id: String,
1199    /// Rendered SysML text after the addition.
1200    pub rendered_text: String,
1201    /// Updated metadata JSON.
1202    pub metadata_json: Option<String>,
1203}
1204
1205/// Result of removing an element.
1206#[cfg(feature = "interchange")]
1207#[derive(Debug)]
1208pub struct RemoveMemberResult {
1209    /// Removed element name.
1210    pub removed_name: String,
1211    /// Rendered SysML text after removal.
1212    pub rendered_text: String,
1213    /// Updated metadata JSON.
1214    pub metadata_json: Option<String>,
1215}
1216
1217/// Add a new member (part, attribute, etc.) to an existing element.
1218///
1219/// Uses `AnalysisHost::apply_model_edit()` to add a child element via
1220/// the semantic Model layer, with automatic text re-rendering and
1221/// SymbolIndex sync.
1222///
1223/// # Arguments
1224/// * `input` - Path to a `.sysml` file
1225/// * `parent` - Name or qualified name of the parent element
1226/// * `member_name` - Name for the new member
1227/// * `member_kind` - Kind string (e.g., "PartUsage", "AttributeUsage", "PartDefinition")
1228/// * `type_name` - Optional type to assign (e.g., "Engine" for `part engine : Engine`)
1229/// * `verbose` - Enable verbose output
1230#[cfg(feature = "interchange")]
1231pub fn add_member(
1232    input: &Path,
1233    parent: &str,
1234    member_name: &str,
1235    member_kind: &str,
1236    type_name: Option<&str>,
1237    verbose: bool,
1238) -> Result<AddMemberResult, String> {
1239    use syster::interchange::model::{Element, ElementId, ElementKind as EK};
1240
1241    let (mut host, _source) = load_model_from_file(input, verbose)?;
1242    let path_str = input.to_string_lossy().to_string();
1243
1244    // Find the parent element (immutable access, then release borrow)
1245    let (parent_id, parent_display_name, new_qn) = {
1246        let model = host.model();
1247        let parent_view = model
1248            .find_by_qualified_name(parent)
1249            .or_else(|| model.find_by_name(parent).into_iter().next())
1250            .ok_or_else(|| format!("Parent element '{}' not found", parent))?;
1251
1252        let pid = parent_view.id().clone();
1253        let pname = parent_view.name().unwrap_or("(anonymous)").to_string();
1254        let parent_qn = parent_view.qualified_name().unwrap_or(pname.as_str());
1255        let qn = format!("{}::{}", parent_qn, member_name);
1256
1257        if verbose {
1258            eprintln!(
1259                "Adding {:?} '{}' to {:?} '{}'",
1260                parse_element_kind(member_kind)
1261                    .unwrap_or(syster::interchange::ElementKind::Package),
1262                member_name,
1263                parent_view.kind(),
1264                pname
1265            );
1266        }
1267
1268        (pid, pname, qn)
1269    };
1270
1271    // Parse the kind string
1272    let kind = parse_element_kind(member_kind)?;
1273
1274    // Resolve type ID before entering the edit closure (if needed)
1275    let type_id = if let Some(tn) = type_name {
1276        let model = host.model();
1277        Some(
1278            model
1279                .find_by_qualified_name(tn)
1280                .or_else(|| model.find_by_name(tn).into_iter().next())
1281                .map(|v| v.id().clone())
1282                .ok_or_else(|| format!("Type '{}' not found", tn))?,
1283        )
1284    } else {
1285        None
1286    };
1287
1288    // Create the new element ID upfront (needed for result)
1289    let new_id = ElementId::generate();
1290    let new_id_for_closure = new_id.clone();
1291
1292    // Apply via apply_model_edit (handles SourceMap, render, re-parse, ID restore)
1293    let result = host.apply_model_edit(&path_str, move |model, tracker| {
1294        let element = Element::new(new_id_for_closure.clone(), kind)
1295            .with_name(member_name)
1296            .with_qualified_name(new_qn);
1297        tracker.add_element(model, element, Some(&parent_id));
1298
1299        // If a type is specified, add a FeatureTyping relationship
1300        if let Some(tid) = type_id {
1301            let rel_id = ElementId::generate();
1302            tracker.add_relationship(model, rel_id, EK::FeatureTyping, new_id_for_closure, tid);
1303        }
1304    });
1305
1306    if verbose {
1307        eprintln!(
1308            "Added {} '{}' to '{}'",
1309            member_kind, member_name, parent_display_name
1310        );
1311    }
1312
1313    let metadata_json = build_updated_metadata(host.model(), input);
1314
1315    Ok(AddMemberResult {
1316        parent_name: parent_display_name,
1317        member_name: member_name.to_string(),
1318        member_kind: member_kind.to_string(),
1319        member_id: new_id.as_str().to_string(),
1320        rendered_text: result.rendered_text,
1321        metadata_json,
1322    })
1323}
1324
1325/// Remove an element from the model by name or qualified name.
1326///
1327/// Uses `AnalysisHost::apply_model_edit()` to remove the element via
1328/// the semantic Model layer, with automatic text re-rendering and
1329/// SymbolIndex sync.
1330///
1331/// # Arguments
1332/// * `input` - Path to a `.sysml` file
1333/// * `target` - Name or qualified name of the element to remove
1334/// * `verbose` - Enable verbose output
1335#[cfg(feature = "interchange")]
1336pub fn remove_member(
1337    input: &Path,
1338    target: &str,
1339    verbose: bool,
1340) -> Result<RemoveMemberResult, String> {
1341    let (mut host, _source) = load_model_from_file(input, verbose)?;
1342    let path_str = input.to_string_lossy().to_string();
1343
1344    // Find the element (immutable access, then release borrow)
1345    let (element_id, removed_name) = {
1346        let model = host.model();
1347        let view = model
1348            .find_by_qualified_name(target)
1349            .or_else(|| model.find_by_name(target).into_iter().next())
1350            .ok_or_else(|| format!("Element '{}' not found", target))?;
1351
1352        let id = view.id().clone();
1353        let name = view.name().unwrap_or("(anonymous)").to_string();
1354        if verbose {
1355            eprintln!("Removing {:?} '{}'", view.kind(), name);
1356        }
1357        (id, name)
1358    };
1359
1360    // Apply removal via apply_model_edit (handles SourceMap, render, re-parse, ID restore)
1361    let result = host.apply_model_edit(&path_str, |model, tracker| {
1362        tracker.remove_element(model, &element_id);
1363    });
1364
1365    if verbose {
1366        eprintln!("Removed '{}'", removed_name);
1367    }
1368
1369    let metadata_json = build_updated_metadata(host.model(), input);
1370
1371    Ok(RemoveMemberResult {
1372        removed_name,
1373        rendered_text: result.rendered_text,
1374        metadata_json,
1375    })
1376}
1377
1378/// Parse a kind string into an ElementKind.
1379#[cfg(feature = "interchange")]
1380fn parse_element_kind(kind_str: &str) -> Result<syster::interchange::model::ElementKind, String> {
1381    use syster::interchange::model::ElementKind;
1382
1383    match kind_str {
1384        // Namespaces and Packages
1385        "Namespace" | "namespace" => Ok(ElementKind::Namespace),
1386        "Package" | "package" => Ok(ElementKind::Package),
1387        "LibraryPackage" | "library package" => Ok(ElementKind::LibraryPackage),
1388
1389        // KerML Classifiers
1390        "Class" | "class" => Ok(ElementKind::Class),
1391        "DataType" | "datatype" => Ok(ElementKind::DataType),
1392        "Structure" | "struct" => Ok(ElementKind::Structure),
1393        "Association" | "assoc" => Ok(ElementKind::Association),
1394        "AssociationStructure" | "assoc struct" => Ok(ElementKind::AssociationStructure),
1395        "Interaction" | "interaction" => Ok(ElementKind::Interaction),
1396        "Behavior" | "behavior" => Ok(ElementKind::Behavior),
1397        "Function" | "function" => Ok(ElementKind::Function),
1398        "Predicate" | "predicate" => Ok(ElementKind::Predicate),
1399
1400        // SysML Definitions
1401        "PartDefinition" | "part def" => Ok(ElementKind::PartDefinition),
1402        "ItemDefinition" | "item def" => Ok(ElementKind::ItemDefinition),
1403        "ActionDefinition" | "action def" => Ok(ElementKind::ActionDefinition),
1404        "PortDefinition" | "port def" => Ok(ElementKind::PortDefinition),
1405        "AttributeDefinition" | "attribute def" => Ok(ElementKind::AttributeDefinition),
1406        "ConnectionDefinition" | "connection def" => Ok(ElementKind::ConnectionDefinition),
1407        "InterfaceDefinition" | "interface def" => Ok(ElementKind::InterfaceDefinition),
1408        "AllocationDefinition" | "allocation def" => Ok(ElementKind::AllocationDefinition),
1409        "RequirementDefinition" | "requirement def" => Ok(ElementKind::RequirementDefinition),
1410        "ConstraintDefinition" | "constraint def" => Ok(ElementKind::ConstraintDefinition),
1411        "StateDefinition" | "state def" => Ok(ElementKind::StateDefinition),
1412        "CalculationDefinition" | "calc def" => Ok(ElementKind::CalculationDefinition),
1413        "UseCaseDefinition" | "use case def" => Ok(ElementKind::UseCaseDefinition),
1414        "AnalysisCaseDefinition" | "analysis case def" => Ok(ElementKind::AnalysisCaseDefinition),
1415        "ConcernDefinition" | "concern def" => Ok(ElementKind::ConcernDefinition),
1416        "ViewDefinition" | "view def" => Ok(ElementKind::ViewDefinition),
1417        "ViewpointDefinition" | "viewpoint def" => Ok(ElementKind::ViewpointDefinition),
1418        "RenderingDefinition" | "rendering def" => Ok(ElementKind::RenderingDefinition),
1419        "EnumerationDefinition" | "enum def" => Ok(ElementKind::EnumerationDefinition),
1420        "MetadataDefinition" | "metadata def" => Ok(ElementKind::MetadataDefinition),
1421
1422        // SysML Usages
1423        "PartUsage" | "part" => Ok(ElementKind::PartUsage),
1424        "ItemUsage" | "item" => Ok(ElementKind::ItemUsage),
1425        "ActionUsage" | "action" => Ok(ElementKind::ActionUsage),
1426        "PortUsage" | "port" => Ok(ElementKind::PortUsage),
1427        "AttributeUsage" | "attribute" => Ok(ElementKind::AttributeUsage),
1428        "ConnectionUsage" | "connection" => Ok(ElementKind::ConnectionUsage),
1429        "InterfaceUsage" | "interface" => Ok(ElementKind::InterfaceUsage),
1430        "AllocationUsage" | "allocation" => Ok(ElementKind::AllocationUsage),
1431        "RequirementUsage" | "requirement" => Ok(ElementKind::RequirementUsage),
1432        "ConstraintUsage" | "constraint" => Ok(ElementKind::ConstraintUsage),
1433        "StateUsage" | "state" => Ok(ElementKind::StateUsage),
1434        "TransitionUsage" | "transition" => Ok(ElementKind::TransitionUsage),
1435        "CalculationUsage" | "calc" => Ok(ElementKind::CalculationUsage),
1436        "ReferenceUsage" | "ref" => Ok(ElementKind::ReferenceUsage),
1437        "OccurrenceUsage" | "occurrence" => Ok(ElementKind::OccurrenceUsage),
1438        "FlowConnectionUsage" | "flow" => Ok(ElementKind::FlowConnectionUsage),
1439        "SuccessionFlowConnectionUsage" | "succession flow" => {
1440            Ok(ElementKind::SuccessionFlowConnectionUsage)
1441        }
1442        "MetadataUsage" | "metadata" => Ok(ElementKind::MetadataUsage),
1443
1444        // KerML Features
1445        "Feature" | "feature" => Ok(ElementKind::Feature),
1446        "Step" | "step" => Ok(ElementKind::Step),
1447        "Connector" | "connector" => Ok(ElementKind::Connector),
1448        "BindingConnector" | "binding" => Ok(ElementKind::BindingConnector),
1449        "Succession" | "succession" => Ok(ElementKind::Succession),
1450
1451        // Comments and documentation
1452        "Comment" | "comment" => Ok(ElementKind::Comment),
1453        "Documentation" | "doc" => Ok(ElementKind::Documentation),
1454
1455        _ => Err(format!(
1456            "Unknown element kind: '{}'. Examples: part, part def, attribute, connection def, etc.",
1457            kind_str
1458        )),
1459    }
1460}
1461
1462/// Helper: convert an ElementView to a serializable ElementInfo.
1463#[cfg(feature = "interchange")]
1464fn element_info(view: &syster::interchange::views::ElementView<'_>) -> ElementInfo {
1465    ElementInfo {
1466        id: view.id().as_str().to_string(),
1467        name: view.name().map(String::from),
1468        qualified_name: view.qualified_name().map(String::from),
1469        kind: format!("{:?}", view.kind()),
1470        is_abstract: view.is_abstract(),
1471        owner: view.owner().and_then(|o| o.name().map(String::from)),
1472        owned_member_count: view.owned_members().len(),
1473        typed_by: view
1474            .typing()
1475            .iter()
1476            .filter_map(|t| t.name().map(String::from))
1477            .collect(),
1478        supertypes: view
1479            .supertypes()
1480            .iter()
1481            .filter_map(|s| s.name().map(String::from))
1482            .collect(),
1483        documentation: view.documentation().map(String::from),
1484    }
1485}
1486
1487/// Result of decompiling a model to SysML files.
1488#[cfg(feature = "interchange")]
1489#[derive(Debug)]
1490pub struct DecompileResult {
1491    /// Generated SysML text.
1492    pub sysml_text: String,
1493    /// Metadata JSON for preserving element IDs.
1494    pub metadata_json: String,
1495    /// Number of elements decompiled.
1496    pub element_count: usize,
1497    /// Source file path.
1498    pub source_path: String,
1499}
1500
1501/// Decompile an interchange file to SysML text with metadata.
1502///
1503/// This function converts an XMI/KPAR/JSON-LD file to SysML text plus
1504/// a companion metadata JSON file that preserves element IDs for
1505/// lossless round-tripping.
1506///
1507/// # Arguments
1508/// * `input` - Path to the interchange file
1509/// * `format` - Optional format override (otherwise detected from extension)
1510/// * `verbose` - Enable verbose output
1511///
1512/// # Returns
1513/// A `DecompileResult` with SysML text and metadata JSON.
1514#[cfg(feature = "interchange")]
1515pub fn decompile_model(
1516    input: &Path,
1517    format: Option<&str>,
1518    verbose: bool,
1519) -> Result<DecompileResult, String> {
1520    use syster::interchange::{
1521        JsonLd, Kpar, ModelFormat, SourceInfo, Xmi, decompile_with_source, detect_format,
1522    };
1523
1524    // Read the input file
1525    let bytes =
1526        std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
1527
1528    // Determine format
1529    let format_str = format.map(String::from).unwrap_or_else(|| {
1530        input
1531            .extension()
1532            .and_then(|e| e.to_str())
1533            .unwrap_or("xmi")
1534            .to_string()
1535    });
1536
1537    if verbose {
1538        println!("Decompiling {} as {}", input.display(), format_str);
1539    }
1540
1541    // Parse the model
1542    let model = match format_str.to_lowercase().as_str() {
1543        "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
1544        "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
1545        "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
1546        _ => {
1547            if let Some(format_impl) = detect_format(input) {
1548                format_impl.read(&bytes).map_err(|e| e.to_string())?
1549            } else {
1550                return Err(format!(
1551                    "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
1552                    format_str
1553                ));
1554            }
1555        }
1556    };
1557
1558    let element_count = model.elements.len();
1559
1560    // Create source info
1561    let source = SourceInfo::from_path(input.to_string_lossy()).with_format(&format_str);
1562
1563    // Decompile to SysML
1564    let result = decompile_with_source(&model, source);
1565
1566    if verbose {
1567        println!(
1568            "Decompiled: {} elements -> {} chars of SysML, {} metadata entries",
1569            element_count,
1570            result.text.len(),
1571            result.metadata.elements.len()
1572        );
1573    }
1574
1575    // Serialize metadata to JSON
1576    let metadata_json = serde_json::to_string_pretty(&result.metadata)
1577        .map_err(|e| format!("Failed to serialize metadata: {}", e))?;
1578
1579    Ok(DecompileResult {
1580        sysml_text: result.text,
1581        metadata_json,
1582        element_count,
1583        source_path: input.to_string_lossy().to_string(),
1584    })
1585}