Skip to main content

chasm/commands/
schema.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! Command implementations for `chasm schema` subcommands.
4
5use crate::schema::{Ontology, RelationshipKind, SchemaRegistry, SemanticTag};
6use crate::workspace;
7use anyhow::{Context, Result};
8use colored::*;
9use std::path::Path;
10
11/// `chasm schema list` — List all known provider schemas
12pub fn schema_list(provider: Option<&str>, json: bool) -> Result<()> {
13    let registry = SchemaRegistry::new();
14    let schemas = match provider {
15        Some(p) => registry.schemas_for_provider(p),
16        None => registry.list_schemas(),
17    };
18
19    if json {
20        let json_schemas: Vec<_> = schemas
21            .iter()
22            .map(|s: &&crate::schema::ProviderSchema| {
23                serde_json::json!({
24                    "id": s.id(),
25                    "provider": s.version.provider,
26                    "format": s.version.format.as_str(),
27                    "version": s.version.version,
28                    "label": s.version.label,
29                    "fields": s.field_count(),
30                    "db_keys": s.db_keys.len(),
31                    "extension_min": s.extension_version_min,
32                    "extension_max": s.extension_version_max,
33                    "host_min": s.host_version_min,
34                    "introduced": s.introduced,
35                    "deprecated": s.deprecated,
36                    "tags": s.tags,
37                })
38            })
39            .collect();
40
41        println!("{}", serde_json::to_string_pretty(&json_schemas)?);
42        return Ok(());
43    }
44
45    println!(
46        "{} {} registered schemas\n",
47        "Schema Registry:".bold(),
48        schemas.len()
49    );
50
51    for schema in &schemas {
52        let status = if schema.deprecated.is_some() {
53            "DEPRECATED".red()
54        } else {
55            "ACTIVE".green()
56        };
57
58        println!(
59            "  {} {} [{}]",
60            schema.id().bold().cyan(),
61            schema.version.label.dimmed(),
62            status
63        );
64
65        // Version range
66        if let (Some(min), Some(max)) =
67            (&schema.extension_version_min, &schema.extension_version_max)
68        {
69            println!("    Extension: {} – {}", min, max);
70        } else if let Some(min) = &schema.extension_version_min {
71            println!("    Extension: {}+", min);
72        }
73
74        if let Some(host) = &schema.host_version_min {
75            println!("    Host:      VS Code {}", host);
76        }
77
78        println!(
79            "    Format:    {} | {} fields | {} DB keys",
80            schema.version.format.as_str(),
81            schema.field_count(),
82            schema.db_keys.len()
83        );
84
85        if let Some(intro) = &schema.introduced {
86            print!("    Introduced: {}", intro);
87            if let Some(dep) = &schema.deprecated {
88                print!(" | Deprecated: {}", dep);
89            }
90            println!();
91        }
92
93        println!("    Tags:      {}", schema.tags.join(", ").dimmed());
94        println!();
95    }
96
97    Ok(())
98}
99
100/// `chasm schema show` — Show detailed schema for a specific version
101pub fn schema_show(schema_id: &str, json: bool) -> Result<()> {
102    let registry = SchemaRegistry::new();
103
104    let schema = registry.get_schema(schema_id).ok_or_else(|| {
105        anyhow::anyhow!(
106            "Unknown schema: '{}'. Use 'chasm schema list' to see available schemas.",
107            schema_id
108        )
109    })?;
110
111    if json {
112        println!("{}", serde_json::to_string_pretty(schema)?);
113        return Ok(());
114    }
115
116    println!("{}", format!("Schema: {}", schema.id()).bold().cyan());
117    println!("{}", "=".repeat(60));
118    println!("  Label:   {}", schema.version.label);
119    println!("  Provider: {}", schema.version.provider);
120    println!("  Format:  {}", schema.version.format);
121    println!("  Version: {}", schema.version.version);
122
123    if let (Some(min), Some(max)) = (&schema.extension_version_min, &schema.extension_version_max) {
124        println!("  Extension Range: {} – {}", min, max);
125    } else if let Some(min) = &schema.extension_version_min {
126        println!("  Extension: {}+", min);
127    }
128
129    if let Some(host) = &schema.host_version_min {
130        println!("  Host Min: VS Code {}", host);
131    }
132
133    // Storage
134    println!("\n{}", "Storage".bold());
135    println!("  {}", schema.storage.description);
136    println!("  Pattern: {}", schema.storage.path_pattern.dimmed());
137    for (platform, path) in &schema.storage.platform_paths {
138        println!("    {}: {}", platform, path.as_str().dimmed());
139    }
140
141    // Session fields
142    println!(
143        "\n{} ({} fields)",
144        "Session Fields".bold(),
145        schema.session_schema.fields.len()
146    );
147    for field in &schema.session_schema.fields {
148        let req = if field.required { "*" } else { " " };
149        let tag = field
150            .semantic_tag
151            .as_deref()
152            .map(|t| format!(" [{}]", t))
153            .unwrap_or_default();
154
155        println!(
156            "  {} {} : {}{}",
157            req,
158            field.name.bold(),
159            field.data_type,
160            tag.dimmed()
161        );
162        println!("    {}", field.description.dimmed());
163    }
164
165    // Nested objects
166    for (name, fields) in &schema.session_schema.nested_objects {
167        println!(
168            "\n{} ({} fields)",
169            format!("  {}", name).bold(),
170            fields.len()
171        );
172        for field in fields {
173            let req = if field.required { "*" } else { " " };
174            println!("    {} {} : {}", req, field.name.bold(), field.data_type);
175            println!("      {}", field.description.dimmed());
176        }
177    }
178
179    // DB keys
180    if !schema.db_keys.is_empty() {
181        println!(
182            "\n{} ({} keys)",
183            "Database Keys".bold(),
184            schema.db_keys.len()
185        );
186        for key in &schema.db_keys {
187            let req = if key.required { "*" } else { " " };
188            println!("  {} {}", req, key.key.bold().yellow());
189            println!("    {}", key.description.dimmed());
190            if !key.value_fields.is_empty() {
191                for vf in &key.value_fields {
192                    println!("    • {} : {}", vf.name, vf.data_type);
193                }
194            }
195        }
196    }
197
198    // Notes
199    if !schema.notes.is_empty() {
200        println!("\n{}", "Notes".bold());
201        for note in &schema.notes {
202            println!("  • {}", note);
203        }
204    }
205
206    // Breaking changes
207    if !schema.breaking_changes.is_empty() {
208        println!("\n{}", "Breaking Changes".bold().red());
209        for change in &schema.breaking_changes {
210            println!("  \u{26a0} {}", change.as_str().yellow());
211        }
212    }
213
214    // Example
215    if let Some(example) = &schema.session_schema.example {
216        println!("\n{}", "Example".bold());
217        println!("{}", serde_json::to_string_pretty(example)?);
218    }
219
220    Ok(())
221}
222
223/// `chasm schema detect` — Auto-detect schema for a workspace or file
224pub fn schema_detect(path: Option<&str>, workspace_id: Option<&str>, json: bool) -> Result<()> {
225    let registry = SchemaRegistry::new();
226
227    let detected = if let Some(ws_id) = workspace_id {
228        // Resolve by workspace hash
229        let storage_path = workspace::get_workspace_storage_path()?;
230        let ws_dir = storage_path.join(ws_id);
231        if !ws_dir.exists() {
232            anyhow::bail!("Workspace directory not found: {}", ws_dir.display());
233        }
234        registry.detect_schema_from_workspace(&ws_dir)?
235    } else {
236        // Resolve by path
237        let resolved_path = resolve_detect_path(path)?;
238        let p = Path::new(&resolved_path);
239
240        if p.is_dir() {
241            // Check if this is a workspace storage dir or project path
242            if p.join("chatSessions").exists() {
243                registry.detect_schema_from_workspace(p)?
244            } else {
245                // Try to find the workspace for this project path
246                if let Some((_hash, ws_path, _title)) =
247                    workspace::find_workspace_by_path(&resolved_path)?
248                {
249                    registry.detect_schema_from_workspace(&ws_path)?
250                } else {
251                    anyhow::bail!(
252                        "No workspace found for project path: {}. Use --workspace-id instead.",
253                        resolved_path
254                    );
255                }
256            }
257        } else {
258            // Single file
259            registry.detect_schema_from_file(p)?
260        }
261    };
262
263    if json {
264        println!("{}", serde_json::to_string_pretty(&detected)?);
265        return Ok(());
266    }
267
268    let confidence_color = if detected.confidence >= 0.9 {
269        "green"
270    } else if detected.confidence >= 0.7 {
271        "yellow"
272    } else {
273        "red"
274    };
275
276    println!("{}", "Schema Detection Result".bold());
277    println!("{}", "-".repeat(40));
278    println!("  Schema:     {}", detected.schema_id.bold().cyan());
279    let pct_str = format!("{:.0}%", detected.confidence * 100.0);
280    let colored_confidence = match confidence_color {
281        "green" => pct_str.green(),
282        "yellow" => pct_str.yellow(),
283        _ => pct_str.red(),
284    };
285    println!("  Confidence: {}", colored_confidence);
286
287    if let Some(ver) = &detected.detected_version {
288        println!("  Extension:  {}", ver);
289    }
290
291    println!("\n  {}", "Evidence:".bold());
292    for ev in &detected.evidence {
293        println!("    • {}", ev);
294    }
295
296    // Show schema summary if found
297    if let Some(schema) = registry.get_schema(&detected.schema_id) {
298        println!("\n  {}", "Schema Details:".bold());
299        println!("    Label:  {}", schema.version.label);
300        println!("    Format: {}", schema.version.format);
301        println!("    Fields: {}", schema.field_count());
302        if !schema.db_keys.is_empty() {
303            println!("    DB Keys: {}", schema.db_keys.len());
304        }
305    }
306
307    Ok(())
308}
309
310/// `chasm schema export` — Export full registry + ontology as JSON
311pub fn schema_export(compact: bool, output: Option<&str>) -> Result<()> {
312    let registry = SchemaRegistry::new();
313
314    let json = if compact {
315        registry.to_json_compact()?
316    } else {
317        registry.to_json()?
318    };
319
320    if let Some(output_path) = output {
321        std::fs::write(output_path, &json)?;
322        println!("Schema registry exported to {}", output_path);
323    } else {
324        println!("{}", json);
325    }
326
327    Ok(())
328}
329
330/// `chasm schema ontology` — Show the ontology (entity types, relationships, semantic tags)
331pub fn schema_ontology(json_output: bool) -> Result<()> {
332    let ontology = Ontology::build();
333
334    if json_output {
335        println!("{}", serde_json::to_string_pretty(&ontology)?);
336        return Ok(());
337    }
338
339    println!(
340        "{}",
341        format!("Ontology v{}", ontology.version).bold().cyan()
342    );
343    println!("{}", "=".repeat(60));
344
345    // Entity types
346    let entity_types = ontology.entity_types();
347    println!("\n{} ({})", "Entity Types".bold(), entity_types.len());
348    for et in &entity_types {
349        println!("  • {}", et);
350    }
351
352    // Relationships
353    println!(
354        "\n{} ({})",
355        "Relationships".bold(),
356        ontology.relationships.len()
357    );
358    for rel in &ontology.relationships {
359        let arrow = match rel.kind {
360            RelationshipKind::Contains => "──contains──▶",
361            RelationshipKind::BelongsTo => "──belongs_to──▶",
362            RelationshipKind::References => "──references──▶",
363            RelationshipKind::MapsTo => "──maps_to──▶",
364        };
365        println!("  {} {} {}", rel.from, arrow, rel.to);
366    }
367
368    // Semantic tags by entity
369    println!(
370        "\n{} ({})",
371        "Semantic Tags".bold(),
372        ontology.semantic_tags.len()
373    );
374    let mut tags_by_entity: std::collections::HashMap<String, Vec<&SemanticTag>> =
375        std::collections::HashMap::new();
376    for tag in &ontology.semantic_tags {
377        tags_by_entity
378            .entry(format!("{}", tag.entity))
379            .or_default()
380            .push(tag);
381    }
382    let mut entity_keys: Vec<_> = tags_by_entity.keys().cloned().collect();
383    entity_keys.sort();
384    for entity in &entity_keys {
385        println!("\n  {}:", entity.as_str().bold());
386        for tag in &tags_by_entity[entity] {
387            println!(
388                "    {} : {} — {}",
389                tag.tag.cyan(),
390                tag.canonical_type,
391                tag.description.dimmed()
392            );
393        }
394    }
395
396    // Cross-provider mappings summary
397    println!(
398        "\n{} ({})",
399        "Cross-Provider Mappings".bold(),
400        ontology.mappings.len()
401    );
402    // Group by source→target pair
403    let mut mapping_groups: std::collections::HashMap<String, usize> =
404        std::collections::HashMap::new();
405    for m in &ontology.mappings {
406        let key = format!("{} → {}", m.source_schema, m.target_schema);
407        *mapping_groups.entry(key).or_default() += 1;
408    }
409    for (pair, count) in &mapping_groups {
410        println!("  {} ({} field mappings)", pair, count);
411    }
412
413    // Migration paths
414    println!(
415        "\n{} ({})",
416        "Migration Paths".bold(),
417        ontology.migration_paths.len()
418    );
419    for path in &ontology.migration_paths {
420        let lossless = if path.lossless {
421            "lossless".green()
422        } else {
423            "lossy".yellow()
424        };
425        println!("  {} → {} [{}]", path.from_schema, path.to_schema, lossless);
426        if !path.data_loss.is_empty() {
427            for loss in &path.data_loss {
428                println!("    \u{26a0} {}", loss.as_str().dimmed());
429            }
430        }
431    }
432
433    // Capability matrix
434    println!("\n{}", "Provider Capabilities".bold());
435    for (provider, caps) in &ontology.capabilities {
436        println!(
437            "  {}: {}",
438            provider.as_str().bold(),
439            caps.join(", ").dimmed()
440        );
441    }
442
443    Ok(())
444}
445
446/// `chasm schema mappings` — Show cross-provider field mappings
447pub fn schema_mappings(
448    source: Option<&str>,
449    target: Option<&str>,
450    tag: Option<&str>,
451    json_output: bool,
452) -> Result<()> {
453    let ontology = Ontology::build();
454
455    let mappings: Vec<_> = if let (Some(s), Some(t)) = (source, target) {
456        ontology.cross_provider_mappings(s, t)
457    } else if let Some(tag_name) = tag {
458        ontology.find_by_semantic_tag(tag_name)
459    } else {
460        ontology.mappings.iter().collect()
461    };
462
463    if json_output {
464        println!("{}", serde_json::to_string_pretty(&mappings)?);
465        return Ok(());
466    }
467
468    println!(
469        "{} {} mappings\n",
470        "Cross-Provider Field Mappings:".bold(),
471        mappings.len()
472    );
473
474    for m in &mappings {
475        let confidence = format!("{:.0}%", m.confidence * 100.0);
476        let conf_color = if m.confidence >= 0.9 {
477            confidence.green()
478        } else if m.confidence >= 0.7 {
479            confidence.yellow()
480        } else {
481            confidence.red()
482        };
483
484        println!(
485            "  {} → {}",
486            format!("{}.{}", m.source_schema, m.source_field).cyan(),
487            format!("{}.{}", m.target_schema, m.target_field).green(),
488        );
489        println!(
490            "    Tag: {} | Confidence: {} | Transform: {}",
491            m.semantic_tag.bold(),
492            conf_color,
493            match &m.transform {
494                Some(t) => format!("{:?}", t),
495                None => "none".into(),
496            }
497            .dimmed()
498        );
499    }
500
501    Ok(())
502}
503
504// ============================================================================
505// Helpers
506// ============================================================================
507
508fn resolve_detect_path(path: Option<&str>) -> Result<String> {
509    match path {
510        Some(p) => Ok(p.to_string()),
511        None => {
512            let cwd = std::env::current_dir().context("Failed to get current directory")?;
513            Ok(cwd.to_string_lossy().to_string())
514        }
515    }
516}