1use crate::schema::{Ontology, RelationshipKind, SchemaRegistry, SemanticTag};
6use crate::workspace;
7use anyhow::{Context, Result};
8use colored::*;
9use std::path::Path;
10
11pub 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 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
100pub 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 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 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 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 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 if !schema.notes.is_empty() {
200 println!("\n{}", "Notes".bold());
201 for note in &schema.notes {
202 println!(" • {}", note);
203 }
204 }
205
206 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 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
223pub 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 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 let resolved_path = resolve_detect_path(path)?;
238 let p = Path::new(&resolved_path);
239
240 if p.is_dir() {
241 if p.join("chatSessions").exists() {
243 registry.detect_schema_from_workspace(p)?
244 } else {
245 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 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 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
310pub 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
330pub 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 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 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 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 println!(
398 "\n{} ({})",
399 "Cross-Provider Mappings".bold(),
400 ontology.mappings.len()
401 );
402 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 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 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
446pub 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
504fn 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}