Skip to main content

legalis_interop/
lib.rs

1//! Legalis-Interop: Interoperability layer for legal DSL formats.
2//!
3//! This crate enables Legalis-RS to import from and export to other legal DSL formats:
4//! - **Catala**: French legal DSL for tax and benefits legislation (Inria)
5//! - **Stipula**: Italian legal DSL for smart contracts (University of Bologna)
6//! - **L4**: Singapore's legal DSL with deontic logic support
7//! - **Akoma Ntoso**: XML standard for legislative documents (OASIS)
8//! - **LegalRuleML**: XML standard for legal rules
9//! - **LegalDocML**: OASIS legal document markup standard
10//! - **LKIF**: Legal Knowledge Interchange Format (ESTRELLA)
11//! - **LegalCite**: OASIS standard for legal citation (TC LegalCiteM)
12//! - **MetaLex**: CEN standard for legal document metadata (CWA 15710)
13//! - **MPEG-21 REL**: ISO standard for rights expression (ISO/IEC 21000-5)
14//! - **Creative Commons**: CC license format (RDF/XML)
15//! - **SPDX**: Software Package Data Exchange license expressions (ISO/IEC 5962:2021)
16
17pub mod ai_converter;
18pub mod akoma_ntoso;
19#[cfg(feature = "async")]
20pub mod async_converter;
21pub mod basel3;
22#[cfg(feature = "batch")]
23pub mod batch;
24pub mod blockchain_docs;
25pub mod bpmn;
26pub mod cache;
27pub mod cadence;
28pub mod catala;
29pub mod cicero;
30pub mod clauseio;
31pub mod cli;
32pub mod cmmn;
33pub mod commonform;
34pub mod compatibility;
35pub mod contractexpress;
36pub mod coverage;
37pub mod creative_commons;
38pub mod dmn;
39pub mod dms;
40pub mod docusign;
41#[cfg(test)]
42mod edge_cases_tests;
43pub mod enhanced;
44pub mod error_handling;
45pub mod errors;
46pub mod fidelity;
47pub mod finreg;
48pub mod format_detection;
49pub mod format_validation;
50pub mod formex;
51pub mod incremental;
52pub mod l4;
53pub mod legalcite;
54pub mod legaldocml;
55pub mod legalruleml;
56pub mod lkif;
57pub mod metalex;
58pub mod metrics;
59pub mod mifid2;
60pub mod move_lang;
61pub mod mpeg21_rel;
62pub mod msword_legal;
63pub mod niem;
64pub mod openlaw;
65pub mod optimizations;
66pub mod pdf_legal;
67pub mod performance;
68pub mod quality;
69pub mod regml;
70pub mod rest_api;
71pub mod ruleml;
72pub mod salesforce_contract;
73pub mod sap_legal;
74pub mod sbvr;
75pub mod schema;
76pub mod solidity;
77pub mod spdx;
78pub mod stipula;
79pub mod streaming;
80pub mod streaming_v2;
81pub mod transformation;
82pub mod universal_format;
83pub mod validation;
84pub mod vyper;
85pub mod webhooks;
86pub mod xbrl;
87
88use legalis_core::Statute;
89use serde::{Deserialize, Serialize};
90use thiserror::Error;
91
92/// Errors during interop operations.
93#[derive(Debug, Error)]
94pub enum InteropError {
95    #[error("Parse error: {0}")]
96    ParseError(String),
97
98    #[error("Unsupported format: {0}")]
99    UnsupportedFormat(String),
100
101    #[error("Conversion error: {0}")]
102    ConversionError(String),
103
104    #[error("Feature not supported in target format: {0}")]
105    UnsupportedFeature(String),
106
107    #[error("IO error: {0}")]
108    IoError(#[from] std::io::Error),
109
110    #[error("Serialization error: {0}")]
111    SerializationError(String),
112
113    #[error("Validation error: {0}")]
114    ValidationError(String),
115}
116
117/// Result type for interop operations.
118pub type InteropResult<T> = Result<T, InteropError>;
119
120/// Supported legal DSL formats.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
122pub enum LegalFormat {
123    /// Catala - French legal DSL (Inria)
124    Catala,
125    /// Stipula - Italian smart contract DSL (Bologna)
126    Stipula,
127    /// L4 - Singapore legal DSL with deontic logic
128    L4,
129    /// Akoma Ntoso XML standard
130    AkomaNtoso,
131    /// LegalRuleML XML standard
132    LegalRuleML,
133    /// LegalDocML - OASIS legal document markup standard
134    LegalDocML,
135    /// LKIF - Legal Knowledge Interchange Format
136    LKIF,
137    /// LegalCite - OASIS standard for legal citation
138    LegalCite,
139    /// MetaLex - CEN standard for legal document metadata
140    MetaLex,
141    /// MPEG-21 REL - ISO standard for rights expression
142    Mpeg21Rel,
143    /// Creative Commons license format
144    CreativeCommons,
145    /// SPDX license expression format
146    Spdx,
147    /// Native Legalis DSL format
148    Legalis,
149    /// BPMN - Business Process Model and Notation (OMG)
150    Bpmn,
151    /// DMN - Decision Model and Notation (OMG)
152    Dmn,
153    /// CMMN - Case Management Model and Notation (OMG)
154    Cmmn,
155    /// RuleML - Rule Markup Language
156    RuleML,
157    /// SBVR - Semantics of Business Vocabulary and Business Rules
158    Sbvr,
159    /// OpenLaw - Protocol for creating and executing legal agreements
160    OpenLaw,
161    /// Cicero - Accord Project smart legal contract templates
162    Cicero,
163    /// CommonForm - Format for legal forms and contracts (JSON)
164    CommonForm,
165    /// Clause.io - Contract automation platform templates
166    ClauseIo,
167    /// ContractExpress - Document automation platform
168    ContractExpress,
169    /// FORMEX - EU Official Journal format
170    Formex,
171    /// NIEM - National Information Exchange Model
172    Niem,
173    /// FinReg - Financial Regulatory format
174    FinReg,
175    /// XBRL - eXtensible Business Reporting Language
176    Xbrl,
177    /// RegML - Regulation Markup Language
178    RegML,
179    /// MiFID II - Markets in Financial Instruments Directive II
180    MiFID2,
181    /// Basel III - International regulatory framework for banks
182    Basel3,
183    /// SAP Legal Module - Enterprise legal management system
184    SapLegal,
185    /// Salesforce Contract - Salesforce CPQ contract management
186    SalesforceContract,
187    /// DocuSign - Electronic signature and digital transaction platform
188    DocuSign,
189    /// MS Word Legal - Microsoft Word legal add-in format
190    MsWordLegal,
191    /// PDF Legal - Adobe PDF legal annotations and form fields
192    PdfLegal,
193    /// Solidity - Ethereum smart contract language
194    Solidity,
195    /// Vyper - Pythonic Ethereum smart contract language
196    Vyper,
197    /// Cadence - Flow blockchain smart contract language
198    Cadence,
199    /// Move - Aptos/Sui blockchain smart contract language
200    Move,
201}
202
203impl LegalFormat {
204    /// Returns the typical file extension for this format.
205    pub fn extension(&self) -> &'static str {
206        match self {
207            LegalFormat::Catala => "catala_en",
208            LegalFormat::Stipula => "stipula",
209            LegalFormat::L4 => "l4",
210            LegalFormat::AkomaNtoso => "xml",
211            LegalFormat::LegalRuleML => "xml",
212            LegalFormat::LegalDocML => "xml",
213            LegalFormat::LKIF => "xml",
214            LegalFormat::LegalCite => "xml",
215            LegalFormat::MetaLex => "xml",
216            LegalFormat::Mpeg21Rel => "xml",
217            LegalFormat::CreativeCommons => "rdf",
218            LegalFormat::Spdx => "spdx",
219            LegalFormat::Legalis => "legal",
220            LegalFormat::Bpmn => "bpmn",
221            LegalFormat::Dmn => "dmn",
222            LegalFormat::Cmmn => "cmmn",
223            LegalFormat::RuleML => "ruleml",
224            LegalFormat::Sbvr => "sbvr",
225            LegalFormat::OpenLaw => "openlaw",
226            LegalFormat::Cicero => "cicero",
227            LegalFormat::CommonForm => "json",
228            LegalFormat::ClauseIo => "json",
229            LegalFormat::ContractExpress => "docx",
230            LegalFormat::Formex => "xml",
231            LegalFormat::Niem => "xml",
232            LegalFormat::FinReg => "json",
233            LegalFormat::Xbrl => "xbrl",
234            LegalFormat::RegML => "xml",
235            LegalFormat::MiFID2 => "json",
236            LegalFormat::Basel3 => "json",
237            LegalFormat::SapLegal => "json",
238            LegalFormat::SalesforceContract => "json",
239            LegalFormat::DocuSign => "json",
240            LegalFormat::MsWordLegal => "json",
241            LegalFormat::PdfLegal => "json",
242            LegalFormat::Solidity => "sol",
243            LegalFormat::Vyper => "vy",
244            LegalFormat::Cadence => "cdc",
245            LegalFormat::Move => "move",
246        }
247    }
248
249    /// Attempts to detect format from file extension.
250    pub fn from_extension(ext: &str) -> Option<Self> {
251        match ext.to_lowercase().as_str() {
252            "catala_en" | "catala_fr" | "catala" => Some(LegalFormat::Catala),
253            "stipula" => Some(LegalFormat::Stipula),
254            "l4" => Some(LegalFormat::L4),
255            "lkif" => Some(LegalFormat::LKIF),
256            "rdf" => Some(LegalFormat::CreativeCommons),
257            "spdx" => Some(LegalFormat::Spdx),
258            "legal" => Some(LegalFormat::Legalis),
259            "bpmn" => Some(LegalFormat::Bpmn),
260            "dmn" => Some(LegalFormat::Dmn),
261            "cmmn" => Some(LegalFormat::Cmmn),
262            "ruleml" => Some(LegalFormat::RuleML),
263            "sbvr" => Some(LegalFormat::Sbvr),
264            "openlaw" => Some(LegalFormat::OpenLaw),
265            "cicero" => Some(LegalFormat::Cicero),
266            "commonform" | "commonform.json" => Some(LegalFormat::CommonForm),
267            "clauseio" | "clauseio.json" => Some(LegalFormat::ClauseIo),
268            "contractexpress" | "docx" => Some(LegalFormat::ContractExpress),
269            "formex" => Some(LegalFormat::Formex),
270            "niem" => Some(LegalFormat::Niem),
271            "finreg" | "finreg.json" => Some(LegalFormat::FinReg),
272            "xbrl" => Some(LegalFormat::Xbrl),
273            "regml" | "regml.xml" => Some(LegalFormat::RegML),
274            "mifid2" | "mifid2.json" => Some(LegalFormat::MiFID2),
275            "basel3" | "basel3.json" => Some(LegalFormat::Basel3),
276            "saplegal" | "sap.json" | "sap-legal.json" => Some(LegalFormat::SapLegal),
277            "salesforce" | "sfdc.json" | "salesforce-contract.json" => {
278                Some(LegalFormat::SalesforceContract)
279            }
280            "docusign" | "docusign.json" | "envelope.json" => Some(LegalFormat::DocuSign),
281            "msword" | "word-legal.json" | "msword-legal.json" => Some(LegalFormat::MsWordLegal),
282            "pdf-legal" | "pdf-annotations.json" | "pdf-legal.json" => Some(LegalFormat::PdfLegal),
283            "sol" | "solidity" => Some(LegalFormat::Solidity),
284            "vy" | "vyper" => Some(LegalFormat::Vyper),
285            "cdc" | "cadence" => Some(LegalFormat::Cadence),
286            "move" => Some(LegalFormat::Move),
287            _ => None,
288        }
289    }
290}
291
292/// Report of conversion quality and potential data loss.
293#[derive(Debug, Clone, Default, Serialize, Deserialize)]
294pub struct ConversionReport {
295    /// Source format
296    pub source_format: Option<LegalFormat>,
297    /// Target format
298    pub target_format: Option<LegalFormat>,
299    /// Features that could not be converted
300    pub unsupported_features: Vec<String>,
301    /// Warnings about potential semantic changes
302    pub warnings: Vec<String>,
303    /// Conversion confidence score (0.0 - 1.0)
304    pub confidence: f64,
305    /// Number of statutes converted
306    pub statutes_converted: usize,
307}
308
309impl ConversionReport {
310    /// Creates a new report.
311    pub fn new(source: LegalFormat, target: LegalFormat) -> Self {
312        Self {
313            source_format: Some(source),
314            target_format: Some(target),
315            confidence: 1.0,
316            ..Default::default()
317        }
318    }
319
320    /// Adds an unsupported feature warning.
321    pub fn add_unsupported(&mut self, feature: impl Into<String>) {
322        self.unsupported_features.push(feature.into());
323        self.confidence = (self.confidence - 0.1).max(0.0);
324    }
325
326    /// Adds a warning.
327    pub fn add_warning(&mut self, warning: impl Into<String>) {
328        self.warnings.push(warning.into());
329        self.confidence = (self.confidence - 0.05).max(0.0);
330    }
331
332    /// Returns true if the conversion is considered high quality (confidence >= 0.8).
333    pub fn is_high_quality(&self) -> bool {
334        self.confidence >= 0.8
335    }
336
337    /// Returns true if the conversion is lossless (confidence == 1.0 and no warnings).
338    pub fn is_lossless(&self) -> bool {
339        self.confidence >= 1.0 && self.unsupported_features.is_empty() && self.warnings.is_empty()
340    }
341}
342
343/// Trait for importing from external formats.
344pub trait FormatImporter: Send + Sync {
345    /// Returns the format this importer handles.
346    fn format(&self) -> LegalFormat;
347
348    /// Parses source code into statutes.
349    fn import(&self, source: &str) -> InteropResult<(Vec<Statute>, ConversionReport)>;
350
351    /// Validates that the source is in the expected format.
352    fn validate(&self, source: &str) -> bool;
353}
354
355/// Trait for exporting to external formats.
356pub trait FormatExporter: Send + Sync {
357    /// Returns the format this exporter produces.
358    fn format(&self) -> LegalFormat;
359
360    /// Exports statutes to the target format.
361    fn export(&self, statutes: &[Statute]) -> InteropResult<(String, ConversionReport)>;
362
363    /// Checks if a statute can be fully represented in this format.
364    fn can_represent(&self, statute: &Statute) -> Vec<String>;
365}
366
367/// Universal converter between legal DSL formats.
368pub struct LegalConverter {
369    importers: Vec<Box<dyn FormatImporter>>,
370    exporters: Vec<Box<dyn FormatExporter>>,
371    cache: Option<cache::ConversionCache>,
372}
373
374impl Default for LegalConverter {
375    fn default() -> Self {
376        Self::new()
377    }
378}
379
380impl LegalConverter {
381    /// Creates a new converter with default importers/exporters (without caching).
382    pub fn new() -> Self {
383        Self {
384            importers: vec![
385                Box::new(catala::CatalaImporter::new()),
386                Box::new(stipula::StipulaImporter::new()),
387                Box::new(l4::L4Importer::new()),
388                Box::new(akoma_ntoso::AkomaNtosoImporter::new()),
389                Box::new(legalruleml::LegalRuleMLImporter::new()),
390                Box::new(legaldocml::LegalDocMLImporter::new()),
391                Box::new(lkif::LkifImporter::new()),
392                Box::new(legalcite::LegalCiteImporter::new()),
393                Box::new(metalex::MetaLexImporter::new()),
394                Box::new(mpeg21_rel::Mpeg21RelImporter::new()),
395                Box::new(creative_commons::CreativeCommonsImporter::new()),
396                Box::new(spdx::SpdxImporter::new()),
397                Box::new(bpmn::BpmnImporter::new()),
398                Box::new(dmn::DmnImporter::new()),
399                Box::new(cmmn::CmmnImporter::new()),
400                Box::new(ruleml::RuleMLImporter::new()),
401                Box::new(sbvr::SbvrImporter::new()),
402                Box::new(openlaw::OpenLawImporter::new()),
403                Box::new(cicero::CiceroImporter::new()),
404                Box::new(commonform::CommonFormImporter::new()),
405                Box::new(clauseio::ClauseIoImporter::new()),
406                Box::new(contractexpress::ContractExpressImporter::new()),
407                Box::new(formex::FormexImporter::new()),
408                Box::new(niem::NiemImporter::new()),
409                Box::new(finreg::FinRegImporter::new()),
410                Box::new(xbrl::XbrlImporter::new()),
411                Box::new(regml::RegMLImporter::new()),
412                Box::new(mifid2::MiFID2Importer::new()),
413                Box::new(basel3::Basel3Importer::new()),
414                Box::new(sap_legal::SapLegalImporter::new()),
415                Box::new(salesforce_contract::SalesforceContractImporter::new()),
416                Box::new(docusign::DocuSignImporter::new()),
417                Box::new(msword_legal::MsWordLegalImporter::new()),
418                Box::new(pdf_legal::PdfLegalImporter::new()),
419                Box::new(solidity::SolidityImporter::new()),
420                Box::new(vyper::VyperImporter::new()),
421                Box::new(cadence::CadenceImporter::new()),
422                Box::new(move_lang::MoveImporter::new()),
423            ],
424            exporters: vec![
425                Box::new(catala::CatalaExporter::new()),
426                Box::new(stipula::StipulaExporter::new()),
427                Box::new(l4::L4Exporter::new()),
428                Box::new(akoma_ntoso::AkomaNtosoExporter::new()),
429                Box::new(legalruleml::LegalRuleMLExporter::new()),
430                Box::new(legaldocml::LegalDocMLExporter::new()),
431                Box::new(lkif::LkifExporter::new()),
432                Box::new(legalcite::LegalCiteExporter::new()),
433                Box::new(metalex::MetaLexExporter::new()),
434                Box::new(mpeg21_rel::Mpeg21RelExporter::new()),
435                Box::new(creative_commons::CreativeCommonsExporter::new()),
436                Box::new(spdx::SpdxExporter::new()),
437                Box::new(bpmn::BpmnExporter::new()),
438                Box::new(dmn::DmnExporter::new()),
439                Box::new(cmmn::CmmnExporter::new()),
440                Box::new(ruleml::RuleMLExporter::new()),
441                Box::new(sbvr::SbvrExporter::new()),
442                Box::new(openlaw::OpenLawExporter::new()),
443                Box::new(cicero::CiceroExporter::new()),
444                Box::new(commonform::CommonFormExporter::new()),
445                Box::new(clauseio::ClauseIoExporter::new()),
446                Box::new(contractexpress::ContractExpressExporter::new()),
447                Box::new(formex::FormexExporter::new()),
448                Box::new(niem::NiemExporter::new()),
449                Box::new(finreg::FinRegExporter::new()),
450                Box::new(xbrl::XbrlExporter::new()),
451                Box::new(regml::RegMLExporter::new()),
452                Box::new(mifid2::MiFID2Exporter::new()),
453                Box::new(basel3::Basel3Exporter::new()),
454                Box::new(sap_legal::SapLegalExporter::new()),
455                Box::new(salesforce_contract::SalesforceContractExporter::new()),
456                Box::new(docusign::DocuSignExporter::new()),
457                Box::new(msword_legal::MsWordLegalExporter::new()),
458                Box::new(pdf_legal::PdfLegalExporter::new()),
459                Box::new(solidity::SolidityExporter::new()),
460                Box::new(vyper::VyperExporter::new()),
461                Box::new(cadence::CadenceExporter::new()),
462                Box::new(move_lang::MoveExporter::new()),
463            ],
464            cache: None,
465        }
466    }
467
468    /// Creates a new converter with caching enabled.
469    pub fn with_cache(cache_size: usize) -> Self {
470        let mut converter = Self::new();
471        converter.cache = Some(cache::ConversionCache::with_capacity(cache_size));
472        converter
473    }
474
475    /// Enables caching with the specified capacity.
476    pub fn enable_cache(&mut self, cache_size: usize) {
477        self.cache = Some(cache::ConversionCache::with_capacity(cache_size));
478    }
479
480    /// Disables caching.
481    pub fn disable_cache(&mut self) {
482        self.cache = None;
483    }
484
485    /// Clears the cache if enabled.
486    pub fn clear_cache(&mut self) {
487        if let Some(cache) = &mut self.cache {
488            cache.clear();
489        }
490    }
491
492    /// Returns cache statistics if caching is enabled.
493    pub fn cache_stats(&self) -> Option<cache::CacheStats> {
494        self.cache.as_ref().map(|c| c.stats())
495    }
496
497    /// Imports from a specific format.
498    pub fn import(
499        &mut self,
500        source: &str,
501        format: LegalFormat,
502    ) -> InteropResult<(Vec<Statute>, ConversionReport)> {
503        // Check cache first
504        if let Some(cache) = &mut self.cache
505            && let Some(cached) = cache.get_import(source, format)
506        {
507            return Ok(cached);
508        }
509
510        let importer = self
511            .importers
512            .iter()
513            .find(|i| i.format() == format)
514            .ok_or_else(|| InteropError::UnsupportedFormat(format!("{:?}", format)))?;
515
516        let result = importer.import(source)?;
517
518        // Store in cache
519        if let Some(cache) = &mut self.cache {
520            cache.put_import(source, format, result.0.clone(), result.1.clone());
521        }
522
523        Ok(result)
524    }
525
526    /// Exports to a specific format.
527    pub fn export(
528        &mut self,
529        statutes: &[Statute],
530        format: LegalFormat,
531    ) -> InteropResult<(String, ConversionReport)> {
532        let exporter = self
533            .exporters
534            .iter()
535            .find(|e| e.format() == format)
536            .ok_or_else(|| InteropError::UnsupportedFormat(format!("{:?}", format)))?;
537
538        exporter.export(statutes)
539    }
540
541    /// Converts between formats.
542    pub fn convert(
543        &mut self,
544        source: &str,
545        from: LegalFormat,
546        to: LegalFormat,
547    ) -> InteropResult<(String, ConversionReport)> {
548        // Check cache first
549        if let Some(cache) = &mut self.cache
550            && let Some(cached) = cache.get_export(source, from, to)
551        {
552            return Ok(cached);
553        }
554
555        let (statutes, mut import_report) = self.import(source, from)?;
556        let (output, export_report) = self.export(&statutes, to)?;
557
558        // Merge reports
559        import_report.target_format = Some(to);
560        import_report
561            .unsupported_features
562            .extend(export_report.unsupported_features);
563        import_report.warnings.extend(export_report.warnings);
564        import_report.confidence = (import_report.confidence * export_report.confidence).max(0.0);
565
566        // Store in cache
567        if let Some(cache) = &mut self.cache {
568            cache.put_export(source, from, to, output.clone(), import_report.clone());
569        }
570
571        Ok((output, import_report))
572    }
573
574    /// Auto-detects format and imports.
575    pub fn auto_import(&mut self, source: &str) -> InteropResult<(Vec<Statute>, ConversionReport)> {
576        for importer in &self.importers {
577            if importer.validate(source) {
578                let format = importer.format();
579                return self.import(source, format);
580            }
581        }
582        Err(InteropError::UnsupportedFormat(
583            "Could not auto-detect format".to_string(),
584        ))
585    }
586
587    /// Returns supported import formats.
588    pub fn supported_imports(&self) -> Vec<LegalFormat> {
589        self.importers.iter().map(|i| i.format()).collect()
590    }
591
592    /// Returns supported export formats.
593    pub fn supported_exports(&self) -> Vec<LegalFormat> {
594        self.exporters.iter().map(|e| e.format()).collect()
595    }
596
597    /// Batch converts multiple source documents.
598    ///
599    /// # Arguments
600    /// * `sources` - Vector of (source_text, source_format) tuples
601    /// * `target_format` - Target format for all conversions
602    ///
603    /// # Returns
604    /// Vector of (converted_text, report) tuples, one for each source
605    pub fn batch_convert(
606        &mut self,
607        sources: &[(String, LegalFormat)],
608        target_format: LegalFormat,
609    ) -> InteropResult<Vec<(String, ConversionReport)>> {
610        let mut results = Vec::with_capacity(sources.len());
611
612        for (source_text, source_format) in sources {
613            match self.convert(source_text, *source_format, target_format) {
614                Ok(result) => results.push(result),
615                Err(e) => {
616                    // Create error report for failed conversion
617                    let mut report = ConversionReport::new(*source_format, target_format);
618                    report.add_warning(format!("Conversion failed: {}", e));
619                    report.confidence = 0.0;
620                    results.push((String::new(), report));
621                }
622            }
623        }
624
625        Ok(results)
626    }
627
628    /// Batch imports multiple source documents.
629    ///
630    /// # Arguments
631    /// * `sources` - Vector of (source_text, source_format) tuples
632    ///
633    /// # Returns
634    /// Vector of (statutes, report) tuples, one for each source
635    pub fn batch_import(
636        &mut self,
637        sources: &[(String, LegalFormat)],
638    ) -> InteropResult<Vec<(Vec<Statute>, ConversionReport)>> {
639        let mut results = Vec::with_capacity(sources.len());
640
641        for (source_text, source_format) in sources {
642            match self.import(source_text, *source_format) {
643                Ok(result) => results.push(result),
644                Err(e) => {
645                    // Create error report for failed import
646                    let mut report = ConversionReport::new(*source_format, LegalFormat::Legalis);
647                    report.add_warning(format!("Import failed: {}", e));
648                    report.confidence = 0.0;
649                    results.push((Vec::new(), report));
650                }
651            }
652        }
653
654        Ok(results)
655    }
656
657    /// Batch exports statutes to multiple formats.
658    ///
659    /// # Arguments
660    /// * `statutes` - Statutes to export
661    /// * `target_formats` - Vector of target formats
662    ///
663    /// # Returns
664    /// Vector of (format, converted_text, report) tuples
665    pub fn batch_export(
666        &mut self,
667        statutes: &[Statute],
668        target_formats: &[LegalFormat],
669    ) -> InteropResult<Vec<(LegalFormat, String, ConversionReport)>> {
670        let mut results = Vec::with_capacity(target_formats.len());
671
672        for &format in target_formats {
673            match self.export(statutes, format) {
674                Ok((output, report)) => results.push((format, output, report)),
675                Err(e) => {
676                    // Create error report for failed export
677                    let mut report = ConversionReport::new(LegalFormat::Legalis, format);
678                    report.add_warning(format!("Export failed: {}", e));
679                    report.confidence = 0.0;
680                    results.push((format, String::new(), report));
681                }
682            }
683        }
684
685        Ok(results)
686    }
687
688    /// Parallel batch converts multiple source documents.
689    ///
690    /// Uses rayon for parallel processing to speed up conversion of multiple documents.
691    /// Note: This method requires mutable self but processes items in parallel safely.
692    ///
693    /// # Arguments
694    /// * `sources` - Vector of (source_text, source_format) tuples
695    /// * `target_format` - Target format for all conversions
696    ///
697    /// # Returns
698    /// Vector of (converted_text, report) tuples, one for each source
699    #[cfg(feature = "parallel")]
700    pub fn batch_convert_parallel(
701        sources: &[(String, LegalFormat)],
702        target_format: LegalFormat,
703    ) -> InteropResult<Vec<(String, ConversionReport)>> {
704        use rayon::prelude::*;
705
706        let results: Vec<_> = sources
707            .par_iter()
708            .map(|(source_text, source_format)| {
709                let mut converter = Self::new();
710                match converter.convert(source_text, *source_format, target_format) {
711                    Ok(result) => result,
712                    Err(e) => {
713                        let mut report = ConversionReport::new(*source_format, target_format);
714                        report.add_warning(format!("Conversion failed: {}", e));
715                        report.confidence = 0.0;
716                        (String::new(), report)
717                    }
718                }
719            })
720            .collect();
721
722        Ok(results)
723    }
724
725    /// Parallel batch imports multiple source documents.
726    ///
727    /// Uses rayon for parallel processing to speed up importing of multiple documents.
728    ///
729    /// # Arguments
730    /// * `sources` - Vector of (source_text, source_format) tuples
731    ///
732    /// # Returns
733    /// Vector of (statutes, report) tuples, one for each source
734    #[cfg(feature = "parallel")]
735    pub fn batch_import_parallel(
736        sources: &[(String, LegalFormat)],
737    ) -> InteropResult<Vec<(Vec<Statute>, ConversionReport)>> {
738        use rayon::prelude::*;
739
740        let results: Vec<_> = sources
741            .par_iter()
742            .map(|(source_text, source_format)| {
743                let mut converter = Self::new();
744                match converter.import(source_text, *source_format) {
745                    Ok(result) => result,
746                    Err(e) => {
747                        let mut report =
748                            ConversionReport::new(*source_format, LegalFormat::Legalis);
749                        report.add_warning(format!("Import failed: {}", e));
750                        report.confidence = 0.0;
751                        (Vec::new(), report)
752                    }
753                }
754            })
755            .collect();
756
757        Ok(results)
758    }
759
760    /// Parallel batch exports statutes to multiple formats.
761    ///
762    /// Uses rayon for parallel processing to speed up exporting to multiple formats.
763    ///
764    /// # Arguments
765    /// * `statutes` - Statutes to export
766    /// * `target_formats` - Vector of target formats
767    ///
768    /// # Returns
769    /// Vector of (format, converted_text, report) tuples
770    #[cfg(feature = "parallel")]
771    pub fn batch_export_parallel(
772        statutes: &[Statute],
773        target_formats: &[LegalFormat],
774    ) -> InteropResult<Vec<(LegalFormat, String, ConversionReport)>> {
775        use rayon::prelude::*;
776
777        let results: Vec<_> = target_formats
778            .par_iter()
779            .map(|&format| {
780                let mut converter = Self::new();
781                match converter.export(statutes, format) {
782                    Ok((output, report)) => (format, output, report),
783                    Err(e) => {
784                        let mut report = ConversionReport::new(LegalFormat::Legalis, format);
785                        report.add_warning(format!("Export failed: {}", e));
786                        report.confidence = 0.0;
787                        (format, String::new(), report)
788                    }
789                }
790            })
791            .collect();
792
793        Ok(results)
794    }
795
796    /// Validates semantic preservation through roundtrip conversion.
797    ///
798    /// Converts to target format and back, then compares statute counts and structure.
799    ///
800    /// # Arguments
801    /// * `source` - Source text
802    /// * `source_format` - Source format
803    /// * `target_format` - Target format to test
804    ///
805    /// # Returns
806    /// Validation report with findings
807    pub fn validate_roundtrip(
808        &mut self,
809        source: &str,
810        source_format: LegalFormat,
811        target_format: LegalFormat,
812    ) -> InteropResult<SemanticValidation> {
813        // Import original
814        let (original_statutes, import_report) = self.import(source, source_format)?;
815
816        // Convert to target format
817        let (target_output, convert_report) = self.export(&original_statutes, target_format)?;
818
819        // Convert back to source format
820        let (roundtrip_statutes, reimport_report) = self.import(&target_output, target_format)?;
821
822        // Compare
823        let mut validation = SemanticValidation::new(source_format, target_format);
824
825        // Check statute count preservation
826        if original_statutes.len() != roundtrip_statutes.len() {
827            validation.add_issue(format!(
828                "Statute count changed: {} -> {}",
829                original_statutes.len(),
830                roundtrip_statutes.len()
831            ));
832        }
833
834        // Check individual statutes
835        for (i, (original, roundtrip)) in original_statutes
836            .iter()
837            .zip(roundtrip_statutes.iter())
838            .enumerate()
839        {
840            // Compare precondition counts
841            if original.preconditions.len() != roundtrip.preconditions.len() {
842                validation.add_issue(format!(
843                    "Statute {}: Precondition count changed: {} -> {}",
844                    i,
845                    original.preconditions.len(),
846                    roundtrip.preconditions.len()
847                ));
848            }
849
850            // Compare effect types
851            if original.effect.effect_type != roundtrip.effect.effect_type {
852                validation.add_issue(format!(
853                    "Statute {}: Effect type changed: {:?} -> {:?}",
854                    i, original.effect.effect_type, roundtrip.effect.effect_type
855                ));
856            }
857        }
858
859        // Aggregate confidence from all reports
860        validation.confidence =
861            (import_report.confidence * convert_report.confidence * reimport_report.confidence)
862                .max(0.0);
863
864        validation.import_report = import_report;
865        validation.convert_report = convert_report;
866        validation.reimport_report = reimport_report;
867
868        Ok(validation)
869    }
870}
871
872/// Semantic preservation validation result.
873#[derive(Debug, Clone)]
874pub struct SemanticValidation {
875    /// Source format
876    pub source_format: LegalFormat,
877    /// Target format tested
878    pub target_format: LegalFormat,
879    /// Issues found during validation
880    pub issues: Vec<String>,
881    /// Overall confidence in semantic preservation (0.0 - 1.0)
882    pub confidence: f64,
883    /// Import report
884    pub import_report: ConversionReport,
885    /// Conversion report
886    pub convert_report: ConversionReport,
887    /// Re-import report
888    pub reimport_report: ConversionReport,
889}
890
891impl SemanticValidation {
892    /// Creates a new validation result.
893    pub fn new(source: LegalFormat, target: LegalFormat) -> Self {
894        Self {
895            source_format: source,
896            target_format: target,
897            issues: Vec::new(),
898            confidence: 1.0,
899            import_report: ConversionReport::new(source, LegalFormat::Legalis),
900            convert_report: ConversionReport::new(LegalFormat::Legalis, target),
901            reimport_report: ConversionReport::new(target, LegalFormat::Legalis),
902        }
903    }
904
905    /// Adds a validation issue.
906    pub fn add_issue(&mut self, issue: impl Into<String>) {
907        self.issues.push(issue.into());
908        self.confidence = (self.confidence - 0.1).max(0.0);
909    }
910
911    /// Returns true if validation passed (no issues and high confidence).
912    pub fn passed(&self) -> bool {
913        self.issues.is_empty() && self.confidence >= 0.8
914    }
915
916    /// Returns true if semantic preservation is perfect (no issues, confidence 1.0).
917    pub fn is_perfect(&self) -> bool {
918        self.issues.is_empty() && self.confidence >= 1.0
919    }
920}
921
922#[cfg(test)]
923mod tests {
924    use super::*;
925    use legalis_core::{ComparisonOp, Condition, Effect, EffectType};
926
927    #[test]
928    fn test_format_extension() {
929        assert_eq!(LegalFormat::Catala.extension(), "catala_en");
930        assert_eq!(LegalFormat::Stipula.extension(), "stipula");
931        assert_eq!(LegalFormat::L4.extension(), "l4");
932    }
933
934    #[test]
935    fn test_format_from_extension() {
936        assert_eq!(
937            LegalFormat::from_extension("catala_en"),
938            Some(LegalFormat::Catala)
939        );
940        assert_eq!(
941            LegalFormat::from_extension("stipula"),
942            Some(LegalFormat::Stipula)
943        );
944        assert_eq!(LegalFormat::from_extension("l4"), Some(LegalFormat::L4));
945        assert_eq!(LegalFormat::from_extension("unknown"), None);
946    }
947
948    #[test]
949    fn test_conversion_report() {
950        let mut report = ConversionReport::new(LegalFormat::Catala, LegalFormat::Legalis);
951        assert_eq!(report.confidence, 1.0);
952
953        report.add_unsupported("scopes");
954        assert!(report.confidence < 1.0);
955
956        report.add_warning("Date format normalized");
957        assert!(report.unsupported_features.contains(&"scopes".to_string()));
958    }
959
960    #[test]
961    fn test_converter_supported_formats() {
962        let converter = LegalConverter::new();
963        let imports = converter.supported_imports();
964        let exports = converter.supported_exports();
965
966        assert!(imports.contains(&LegalFormat::Catala));
967        assert!(imports.contains(&LegalFormat::Stipula));
968        assert!(imports.contains(&LegalFormat::L4));
969        assert!(imports.contains(&LegalFormat::AkomaNtoso));
970
971        assert!(exports.contains(&LegalFormat::Catala));
972        assert!(exports.contains(&LegalFormat::Stipula));
973        assert!(exports.contains(&LegalFormat::L4));
974        assert!(exports.contains(&LegalFormat::AkomaNtoso));
975    }
976
977    #[test]
978    fn test_catala_export_import_roundtrip() {
979        let mut converter = LegalConverter::new();
980
981        // Create a statute
982        let statute = Statute::new(
983            "voting-rights",
984            "Voting Rights",
985            Effect::new(EffectType::Grant, "vote"),
986        )
987        .with_precondition(Condition::Age {
988            operator: ComparisonOp::GreaterOrEqual,
989            value: 18,
990        });
991
992        // Export to Catala
993        let (catala_output, export_report) =
994            converter.export(&[statute], LegalFormat::Catala).unwrap();
995        assert_eq!(export_report.statutes_converted, 1);
996        assert!(catala_output.contains("declaration scope VotingRights"));
997        assert!(catala_output.contains("input.age >= 18"));
998
999        // Import from Catala
1000        let (imported, import_report) = converter
1001            .import(&catala_output, LegalFormat::Catala)
1002            .unwrap();
1003        assert_eq!(import_report.statutes_converted, 1);
1004        assert_eq!(imported.len(), 1);
1005        assert_eq!(imported[0].id, "votingrights");
1006    }
1007
1008    #[test]
1009    fn test_stipula_export_import_roundtrip() {
1010        let mut converter = LegalConverter::new();
1011
1012        // Create a statute
1013        let statute = Statute::new(
1014            "simple-contract",
1015            "Simple Contract",
1016            Effect::new(EffectType::Grant, "execute"),
1017        )
1018        .with_precondition(Condition::Age {
1019            operator: ComparisonOp::GreaterOrEqual,
1020            value: 21,
1021        });
1022
1023        // Export to Stipula
1024        let (stipula_output, export_report) =
1025            converter.export(&[statute], LegalFormat::Stipula).unwrap();
1026        assert_eq!(export_report.statutes_converted, 1);
1027        assert!(stipula_output.contains("agreement SimpleContract"));
1028        assert!(stipula_output.contains("age >= 21"));
1029
1030        // Import from Stipula
1031        let (imported, import_report) = converter
1032            .import(&stipula_output, LegalFormat::Stipula)
1033            .unwrap();
1034        assert_eq!(import_report.statutes_converted, 1);
1035        assert_eq!(imported.len(), 1);
1036        assert_eq!(imported[0].id, "simplecontract");
1037    }
1038
1039    #[test]
1040    fn test_l4_export_import_roundtrip() {
1041        let mut converter = LegalConverter::new();
1042
1043        // Create a statute
1044        let statute = Statute::new(
1045            "adult-rights",
1046            "Adult Rights",
1047            Effect::new(EffectType::Grant, "full_capacity"),
1048        )
1049        .with_precondition(Condition::Age {
1050            operator: ComparisonOp::GreaterOrEqual,
1051            value: 18,
1052        });
1053
1054        // Export to L4
1055        let (l4_output, export_report) = converter.export(&[statute], LegalFormat::L4).unwrap();
1056        assert_eq!(export_report.statutes_converted, 1);
1057        assert!(l4_output.contains("RULE AdultRights"));
1058        assert!(l4_output.contains("age >= 18"));
1059        assert!(l4_output.contains("MAY"));
1060
1061        // Import from L4
1062        let (imported, import_report) = converter.import(&l4_output, LegalFormat::L4).unwrap();
1063        assert_eq!(import_report.statutes_converted, 1);
1064        assert_eq!(imported.len(), 1);
1065    }
1066
1067    #[test]
1068    fn test_catala_to_l4_conversion() {
1069        let mut converter = LegalConverter::new();
1070
1071        let catala_source = r#"
1072```catala
1073declaration scope TaxBenefit:
1074  context input content Input
1075  context output content Output
1076```
1077
1078```catala
1079scope TaxBenefit:
1080  definition output.eligible equals
1081    input.age >= 65
1082```
1083"#;
1084
1085        // Convert Catala to L4
1086        let (l4_output, report) = converter
1087            .convert(catala_source, LegalFormat::Catala, LegalFormat::L4)
1088            .unwrap();
1089
1090        assert!(report.statutes_converted >= 1);
1091        assert!(l4_output.contains("RULE"));
1092    }
1093
1094    #[test]
1095    fn test_auto_detect_catala() {
1096        let mut converter = LegalConverter::new();
1097
1098        let catala_source = r#"
1099declaration scope Test:
1100  context input content integer
1101"#;
1102
1103        let (statutes, report) = converter.auto_import(catala_source).unwrap();
1104        assert_eq!(report.source_format, Some(LegalFormat::Catala));
1105        assert!(!statutes.is_empty());
1106    }
1107
1108    #[test]
1109    fn test_auto_detect_stipula() {
1110        let mut converter = LegalConverter::new();
1111
1112        let stipula_source = "agreement TestContract(Alice, Bob) { }";
1113
1114        let (statutes, report) = converter.auto_import(stipula_source).unwrap();
1115        assert_eq!(report.source_format, Some(LegalFormat::Stipula));
1116        assert!(!statutes.is_empty());
1117    }
1118
1119    #[test]
1120    fn test_auto_detect_l4() {
1121        let mut converter = LegalConverter::new();
1122
1123        let l4_source = "RULE TestRule WHEN age >= 18 THEN Person MAY vote";
1124
1125        let (statutes, report) = converter.auto_import(l4_source).unwrap();
1126        assert_eq!(report.source_format, Some(LegalFormat::L4));
1127        assert!(!statutes.is_empty());
1128    }
1129
1130    #[test]
1131    fn test_akoma_ntoso_export_import_roundtrip() {
1132        let mut converter = LegalConverter::new();
1133
1134        // Create a statute
1135        let statute = Statute::new(
1136            "adult-capacity",
1137            "Adult Capacity Act",
1138            Effect::new(EffectType::Grant, "Full legal capacity"),
1139        )
1140        .with_precondition(Condition::Age {
1141            operator: ComparisonOp::GreaterOrEqual,
1142            value: 18,
1143        });
1144
1145        // Export to Akoma Ntoso
1146        let (akn_output, export_report) = converter
1147            .export(&[statute], LegalFormat::AkomaNtoso)
1148            .unwrap();
1149        assert_eq!(export_report.statutes_converted, 1);
1150        assert!(akn_output.contains("<akomaNtoso"));
1151        assert!(akn_output.contains("Adult Capacity Act"));
1152
1153        // Import from Akoma Ntoso
1154        let (imported, import_report) = converter
1155            .import(&akn_output, LegalFormat::AkomaNtoso)
1156            .unwrap();
1157        assert_eq!(import_report.statutes_converted, 1);
1158        assert_eq!(imported.len(), 1);
1159        assert_eq!(imported[0].title, "Adult Capacity Act");
1160    }
1161
1162    #[test]
1163    fn test_auto_detect_akoma_ntoso() {
1164        let mut converter = LegalConverter::new();
1165
1166        let akn_source = r#"
1167        <akomaNtoso>
1168            <act>
1169                <body>
1170                    <article eId="art_1">
1171                        <heading>Test Article</heading>
1172                    </article>
1173                </body>
1174            </act>
1175        </akomaNtoso>
1176        "#;
1177
1178        let (statutes, report) = converter.auto_import(akn_source).unwrap();
1179        assert_eq!(report.source_format, Some(LegalFormat::AkomaNtoso));
1180        assert!(!statutes.is_empty());
1181    }
1182
1183    #[test]
1184    fn test_catala_to_akoma_ntoso_conversion() {
1185        let mut converter = LegalConverter::new();
1186
1187        let catala_source = r#"
1188declaration scope AdultRights:
1189  context input content integer
1190"#;
1191
1192        // Convert Catala to Akoma Ntoso
1193        let (akn_output, report) = converter
1194            .convert(catala_source, LegalFormat::Catala, LegalFormat::AkomaNtoso)
1195            .unwrap();
1196
1197        assert!(report.statutes_converted >= 1);
1198        assert!(akn_output.contains("<akomaNtoso"));
1199        assert!(akn_output.contains("<article"));
1200    }
1201
1202    #[test]
1203    fn test_legalruleml_export_import_roundtrip() {
1204        let mut converter = LegalConverter::new();
1205
1206        // Create a statute
1207        let statute = Statute::new(
1208            "legal-rule",
1209            "Legal Rule Example",
1210            Effect::new(EffectType::Grant, "Legal capacity"),
1211        )
1212        .with_precondition(Condition::Age {
1213            operator: ComparisonOp::GreaterOrEqual,
1214            value: 18,
1215        });
1216
1217        // Export to LegalRuleML
1218        let (lrml_output, export_report) = converter
1219            .export(&[statute], LegalFormat::LegalRuleML)
1220            .unwrap();
1221        assert_eq!(export_report.statutes_converted, 1);
1222        assert!(lrml_output.contains("<legalruleml"));
1223        assert!(lrml_output.contains("Legal Rule Example"));
1224
1225        // Import from LegalRuleML
1226        let (imported, import_report) = converter
1227            .import(&lrml_output, LegalFormat::LegalRuleML)
1228            .unwrap();
1229        assert_eq!(import_report.statutes_converted, 1);
1230        assert_eq!(imported.len(), 1);
1231        assert_eq!(imported[0].title, "Legal Rule Example");
1232    }
1233
1234    #[test]
1235    fn test_auto_detect_legalruleml() {
1236        let mut converter = LegalConverter::new();
1237
1238        let lrml_source = r#"
1239        <legalruleml>
1240            <Statements>
1241                <LegalRule key="test">
1242                    <Name>Test</Name>
1243                    <if><Premise>age >= 18</Premise></if>
1244                    <then><Conclusion>Grant</Conclusion></then>
1245                </LegalRule>
1246            </Statements>
1247        </legalruleml>
1248        "#;
1249
1250        let (statutes, report) = converter.auto_import(lrml_source).unwrap();
1251        assert_eq!(report.source_format, Some(LegalFormat::LegalRuleML));
1252        assert!(!statutes.is_empty());
1253    }
1254
1255    #[test]
1256    fn test_catala_to_legalruleml_conversion() {
1257        let mut converter = LegalConverter::new();
1258
1259        let catala_source = r#"
1260declaration scope TaxRule:
1261  context input content integer
1262"#;
1263
1264        // Convert Catala to LegalRuleML
1265        let (lrml_output, report) = converter
1266            .convert(catala_source, LegalFormat::Catala, LegalFormat::LegalRuleML)
1267            .unwrap();
1268
1269        assert!(report.statutes_converted >= 1);
1270        assert!(lrml_output.contains("<legalruleml"));
1271        assert!(lrml_output.contains("<LegalRule"));
1272    }
1273
1274    #[test]
1275    fn test_batch_convert() {
1276        let mut converter = LegalConverter::new();
1277
1278        let sources = vec![
1279            (
1280                "declaration scope Test1:\n  context input content integer".to_string(),
1281                LegalFormat::Catala,
1282            ),
1283            (
1284                "agreement Test2(A, B) { }".to_string(),
1285                LegalFormat::Stipula,
1286            ),
1287        ];
1288
1289        let results = converter.batch_convert(&sources, LegalFormat::L4).unwrap();
1290
1291        assert_eq!(results.len(), 2);
1292        assert!(results[0].0.contains("RULE"));
1293        assert!(results[1].0.contains("RULE"));
1294    }
1295
1296    #[test]
1297    fn test_batch_import() {
1298        let mut converter = LegalConverter::new();
1299
1300        let sources = vec![
1301            (
1302                "declaration scope Test1:\n  context input content integer".to_string(),
1303                LegalFormat::Catala,
1304            ),
1305            (
1306                "agreement Test2(A, B) { }".to_string(),
1307                LegalFormat::Stipula,
1308            ),
1309        ];
1310
1311        let results = converter.batch_import(&sources).unwrap();
1312
1313        assert_eq!(results.len(), 2);
1314        assert!(!results[0].0.is_empty());
1315        assert!(!results[1].0.is_empty());
1316    }
1317
1318    #[test]
1319    fn test_batch_export() {
1320        let mut converter = LegalConverter::new();
1321
1322        let statute = Statute::new(
1323            "test",
1324            "Test Statute",
1325            Effect::new(EffectType::Grant, "Test"),
1326        );
1327
1328        let formats = vec![LegalFormat::Catala, LegalFormat::L4, LegalFormat::Stipula];
1329
1330        let results = converter.batch_export(&[statute], &formats).unwrap();
1331
1332        assert_eq!(results.len(), 3);
1333        assert_eq!(results[0].0, LegalFormat::Catala);
1334        assert_eq!(results[1].0, LegalFormat::L4);
1335        assert_eq!(results[2].0, LegalFormat::Stipula);
1336    }
1337
1338    #[test]
1339    fn test_conversion_caching() {
1340        let mut converter = LegalConverter::with_cache(10);
1341
1342        let catala_source = "declaration scope Test:\n  context input content integer";
1343
1344        // First conversion - cache miss
1345        let (output1, report1) = converter
1346            .convert(catala_source, LegalFormat::Catala, LegalFormat::L4)
1347            .unwrap();
1348
1349        // Second conversion - cache hit
1350        let (output2, report2) = converter
1351            .convert(catala_source, LegalFormat::Catala, LegalFormat::L4)
1352            .unwrap();
1353
1354        // Results should be identical
1355        assert_eq!(output1, output2);
1356        assert_eq!(report1.statutes_converted, report2.statutes_converted);
1357
1358        // Check cache stats
1359        // Note: We cache both import and conversion, so first run creates 2 entries
1360        // Second run is a cache hit on conversion
1361        let stats = converter.cache_stats().unwrap();
1362        assert_eq!(stats.entries, 2); // import + conversion cached
1363        assert!(stats.access_count >= 3); // Multiple puts and gets
1364    }
1365
1366    #[test]
1367    fn test_cache_enable_disable() {
1368        let mut converter = LegalConverter::new();
1369
1370        // Initially no cache
1371        assert!(converter.cache_stats().is_none());
1372
1373        // Enable cache
1374        converter.enable_cache(5);
1375        assert!(converter.cache_stats().is_some());
1376
1377        // Disable cache
1378        converter.disable_cache();
1379        assert!(converter.cache_stats().is_none());
1380    }
1381
1382    #[test]
1383    fn test_semantic_validation_roundtrip() {
1384        let mut converter = LegalConverter::new();
1385
1386        let l4_source = "RULE VotingAge WHEN age >= 18 THEN Person MAY vote";
1387
1388        let validation = converter
1389            .validate_roundtrip(l4_source, LegalFormat::L4, LegalFormat::Catala)
1390            .unwrap();
1391
1392        // Should preserve basic structure
1393        assert!(validation.confidence > 0.0);
1394        assert!(!validation.issues.is_empty() || validation.passed());
1395    }
1396
1397    #[test]
1398    fn test_conversion_report_quality() {
1399        let mut report = ConversionReport::new(LegalFormat::Catala, LegalFormat::L4);
1400
1401        assert!(report.is_lossless());
1402        assert!(report.is_high_quality());
1403
1404        report.add_warning("Minor issue");
1405        assert!(!report.is_lossless());
1406        assert!(report.is_high_quality());
1407
1408        report.add_unsupported("Major feature");
1409        report.add_unsupported("Another feature");
1410        report.add_unsupported("Yet another");
1411        assert!(!report.is_high_quality());
1412    }
1413
1414    #[test]
1415    fn test_semantic_validation_structure() {
1416        let mut converter = LegalConverter::new();
1417
1418        let catala_source = r#"
1419declaration scope AdultRights:
1420  context input content integer
1421"#;
1422
1423        let validation = converter
1424            .validate_roundtrip(catala_source, LegalFormat::Catala, LegalFormat::L4)
1425            .unwrap();
1426
1427        // Validation structure should be populated
1428        assert_eq!(validation.source_format, LegalFormat::Catala);
1429        assert_eq!(validation.target_format, LegalFormat::L4);
1430        assert!(validation.confidence <= 1.0);
1431    }
1432
1433    // Tests for new formats (v0.1.1)
1434
1435    #[test]
1436    fn test_legalcite_export_import_roundtrip() {
1437        let mut converter = LegalConverter::new();
1438
1439        let statute = Statute::new(
1440            "test-statute",
1441            "Test Statute",
1442            Effect::new(EffectType::Grant, "legal_reference"),
1443        )
1444        .with_jurisdiction("US");
1445
1446        let (legalcite_output, export_report) = converter
1447            .export(&[statute], LegalFormat::LegalCite)
1448            .unwrap();
1449        assert_eq!(export_report.statutes_converted, 1);
1450        assert!(legalcite_output.contains("legalCite"));
1451
1452        let (imported, import_report) = converter
1453            .import(&legalcite_output, LegalFormat::LegalCite)
1454            .unwrap();
1455        assert_eq!(import_report.statutes_converted, 1);
1456        assert_eq!(imported.len(), 1);
1457    }
1458
1459    #[test]
1460    fn test_metalex_export_import_roundtrip() {
1461        let mut converter = LegalConverter::new();
1462
1463        let statute = Statute::new(
1464            "article-1",
1465            "Article 1",
1466            Effect::new(EffectType::Grant, "provision"),
1467        );
1468
1469        let (metalex_output, export_report) =
1470            converter.export(&[statute], LegalFormat::MetaLex).unwrap();
1471        assert_eq!(export_report.statutes_converted, 1);
1472        assert!(metalex_output.contains("metalex"));
1473
1474        let (imported, import_report) = converter
1475            .import(&metalex_output, LegalFormat::MetaLex)
1476            .unwrap();
1477        assert_eq!(import_report.statutes_converted, 1);
1478        assert_eq!(imported.len(), 1);
1479    }
1480
1481    #[test]
1482    fn test_mpeg21_rel_export_import_roundtrip() {
1483        let mut converter = LegalConverter::new();
1484
1485        let statute = Statute::new(
1486            "play-right",
1487            "Play Right",
1488            Effect::new(EffectType::Grant, "play"),
1489        );
1490
1491        let (mpeg21_output, export_report) = converter
1492            .export(&[statute], LegalFormat::Mpeg21Rel)
1493            .unwrap();
1494        assert_eq!(export_report.statutes_converted, 1);
1495
1496        let (imported, import_report) = converter
1497            .import(&mpeg21_output, LegalFormat::Mpeg21Rel)
1498            .unwrap();
1499        assert_eq!(import_report.statutes_converted, 1);
1500        assert_eq!(imported.len(), 1);
1501    }
1502
1503    #[test]
1504    fn test_creative_commons_export_import_roundtrip() {
1505        let mut converter = LegalConverter::new();
1506
1507        let statute = Statute::new(
1508            "cc-permit",
1509            "Permit Reproduction",
1510            Effect::new(EffectType::Grant, "Reproduction"),
1511        );
1512
1513        let (cc_output, export_report) = converter
1514            .export(&[statute], LegalFormat::CreativeCommons)
1515            .unwrap();
1516        assert_eq!(export_report.statutes_converted, 1);
1517
1518        let (imported, import_report) = converter
1519            .import(&cc_output, LegalFormat::CreativeCommons)
1520            .unwrap();
1521        assert_eq!(import_report.statutes_converted, 1);
1522        assert!(!imported.is_empty());
1523    }
1524
1525    #[test]
1526    fn test_spdx_export_import_roundtrip() {
1527        let mut converter = LegalConverter::new();
1528
1529        let mut effect = Effect::new(EffectType::Grant, "use");
1530        effect
1531            .parameters
1532            .insert("spdx_id".to_string(), "MIT".to_string());
1533        let statute = Statute::new("mit_license", "License: MIT", effect);
1534
1535        let (spdx_output, export_report) = converter.export(&[statute], LegalFormat::Spdx).unwrap();
1536        assert_eq!(export_report.statutes_converted, 1);
1537        assert_eq!(spdx_output, "MIT");
1538
1539        let (imported, import_report) = converter.import(&spdx_output, LegalFormat::Spdx).unwrap();
1540        assert_eq!(import_report.statutes_converted, 1);
1541        assert_eq!(imported.len(), 1);
1542    }
1543
1544    #[test]
1545    fn test_auto_detect_legalcite() {
1546        let mut converter = LegalConverter::new();
1547
1548        let legalcite_source = r#"<LegalCiteDocument>
1549            <legalCite>
1550                <citations>
1551                    <id>test-1</id>
1552                    <title>Test Citation</title>
1553                    <citation_type>statute</citation_type>
1554                </citations>
1555            </legalCite>
1556        </LegalCiteDocument>"#;
1557
1558        let (statutes, report) = converter.auto_import(legalcite_source).unwrap();
1559        assert_eq!(report.source_format, Some(LegalFormat::LegalCite));
1560        assert!(!statutes.is_empty());
1561    }
1562
1563    #[test]
1564    fn test_auto_detect_metalex() {
1565        let mut converter = LegalConverter::new();
1566
1567        let metalex_source = r#"<MetaLexDocument>
1568            <metalex>
1569                <Body>
1570                    <Article id="art-1">
1571                        <Title>Test Article</Title>
1572                    </Article>
1573                </Body>
1574            </metalex>
1575        </MetaLexDocument>"#;
1576
1577        let (statutes, report) = converter.auto_import(metalex_source).unwrap();
1578        assert_eq!(report.source_format, Some(LegalFormat::MetaLex));
1579        assert!(!statutes.is_empty());
1580    }
1581
1582    #[test]
1583    fn test_auto_detect_mpeg21_rel() {
1584        let mut converter = LegalConverter::new();
1585
1586        let mpeg21_source = r#"<Mpeg21RelDocument>
1587            <license>
1588                <grant>
1589                    <right>play</right>
1590                </grant>
1591            </license>
1592        </Mpeg21RelDocument>"#;
1593
1594        let (statutes, report) = converter.auto_import(mpeg21_source).unwrap();
1595        assert_eq!(report.source_format, Some(LegalFormat::Mpeg21Rel));
1596        assert!(!statutes.is_empty());
1597    }
1598
1599    #[test]
1600    fn test_auto_detect_creative_commons() {
1601        let mut converter = LegalConverter::new();
1602
1603        let cc_source = "https://creativecommons.org/licenses/by/4.0/";
1604
1605        let (statutes, report) = converter.auto_import(cc_source).unwrap();
1606        assert_eq!(report.source_format, Some(LegalFormat::CreativeCommons));
1607        assert!(!statutes.is_empty());
1608    }
1609
1610    #[test]
1611    fn test_auto_detect_spdx() {
1612        let mut converter = LegalConverter::new();
1613
1614        let spdx_source = "MIT AND Apache-2.0";
1615
1616        let (statutes, report) = converter.auto_import(spdx_source).unwrap();
1617        assert_eq!(report.source_format, Some(LegalFormat::Spdx));
1618        assert!(!statutes.is_empty());
1619    }
1620
1621    #[test]
1622    fn test_new_formats_in_converter() {
1623        let converter = LegalConverter::new();
1624        let imports = converter.supported_imports();
1625        let exports = converter.supported_exports();
1626
1627        // Check all new formats are registered
1628        assert!(imports.contains(&LegalFormat::LegalCite));
1629        assert!(imports.contains(&LegalFormat::MetaLex));
1630        assert!(imports.contains(&LegalFormat::Mpeg21Rel));
1631        assert!(imports.contains(&LegalFormat::CreativeCommons));
1632        assert!(imports.contains(&LegalFormat::Spdx));
1633
1634        assert!(exports.contains(&LegalFormat::LegalCite));
1635        assert!(exports.contains(&LegalFormat::MetaLex));
1636        assert!(exports.contains(&LegalFormat::Mpeg21Rel));
1637        assert!(exports.contains(&LegalFormat::CreativeCommons));
1638        assert!(exports.contains(&LegalFormat::Spdx));
1639    }
1640
1641    // Blockchain format tests (v0.2.9)
1642
1643    #[test]
1644    fn test_blockchain_formats_registered() {
1645        let converter = LegalConverter::new();
1646        let imports = converter.supported_imports();
1647        let exports = converter.supported_exports();
1648
1649        // Check all blockchain formats are registered
1650        assert!(imports.contains(&LegalFormat::Solidity));
1651        assert!(imports.contains(&LegalFormat::Vyper));
1652        assert!(imports.contains(&LegalFormat::Cadence));
1653        assert!(imports.contains(&LegalFormat::Move));
1654
1655        assert!(exports.contains(&LegalFormat::Solidity));
1656        assert!(exports.contains(&LegalFormat::Vyper));
1657        assert!(exports.contains(&LegalFormat::Cadence));
1658        assert!(exports.contains(&LegalFormat::Move));
1659    }
1660
1661    #[test]
1662    fn test_solidity_import_export_roundtrip() {
1663        let mut converter = LegalConverter::new();
1664
1665        let mut statute = Statute::new(
1666            "token_transfer",
1667            "Token Transfer",
1668            Effect::new(EffectType::MonetaryTransfer, "Transfer tokens"),
1669        );
1670        statute
1671            .effect
1672            .parameters
1673            .insert("contract".to_string(), "ERC20".to_string());
1674        statute
1675            .effect
1676            .parameters
1677            .insert("function".to_string(), "transfer".to_string());
1678        statute
1679            .effect
1680            .parameters
1681            .insert("license".to_string(), "MIT".to_string());
1682
1683        let (solidity_output, export_report) =
1684            converter.export(&[statute], LegalFormat::Solidity).unwrap();
1685        assert_eq!(export_report.statutes_converted, 1);
1686        assert!(solidity_output.contains("contract ERC20"));
1687        assert!(solidity_output.contains("function transfer()"));
1688
1689        let (imported, import_report) = converter
1690            .import(&solidity_output, LegalFormat::Solidity)
1691            .unwrap();
1692        assert_eq!(import_report.statutes_converted, 1);
1693        assert!(!imported.is_empty());
1694    }
1695
1696    #[test]
1697    fn test_vyper_import_export_roundtrip() {
1698        let mut converter = LegalConverter::new();
1699
1700        let mut statute = Statute::new(
1701            "vote",
1702            "Vote Function",
1703            Effect::new(EffectType::Grant, "Cast a vote"),
1704        );
1705        statute
1706            .effect
1707            .parameters
1708            .insert("contract".to_string(), "Voting".to_string());
1709        statute
1710            .effect
1711            .parameters
1712            .insert("function".to_string(), "vote".to_string());
1713        statute
1714            .effect
1715            .parameters
1716            .insert("license".to_string(), "MIT".to_string());
1717
1718        let (vyper_output, export_report) =
1719            converter.export(&[statute], LegalFormat::Vyper).unwrap();
1720        assert_eq!(export_report.statutes_converted, 1);
1721        assert!(vyper_output.contains("# Voting"));
1722        assert!(vyper_output.contains("def vote()"));
1723
1724        let (imported, import_report) =
1725            converter.import(&vyper_output, LegalFormat::Vyper).unwrap();
1726        assert_eq!(import_report.statutes_converted, 1);
1727        assert!(!imported.is_empty());
1728    }
1729
1730    #[test]
1731    fn test_cadence_import_export_roundtrip() {
1732        let mut converter = LegalConverter::new();
1733
1734        let mut statute = Statute::new(
1735            "mint_nft",
1736            "Mint NFT",
1737            Effect::new(EffectType::Grant, "Mint new NFT"),
1738        );
1739        statute
1740            .effect
1741            .parameters
1742            .insert("contract".to_string(), "NFT".to_string());
1743        statute
1744            .effect
1745            .parameters
1746            .insert("function".to_string(), "mintNFT".to_string());
1747
1748        let (cadence_output, export_report) =
1749            converter.export(&[statute], LegalFormat::Cadence).unwrap();
1750        assert_eq!(export_report.statutes_converted, 1);
1751        assert!(cadence_output.contains("pub contract NFT"));
1752        assert!(cadence_output.contains("pub fun mintNFT()"));
1753
1754        let (imported, import_report) = converter
1755            .import(&cadence_output, LegalFormat::Cadence)
1756            .unwrap();
1757        assert_eq!(import_report.statutes_converted, 1);
1758        assert!(!imported.is_empty());
1759    }
1760
1761    #[test]
1762    fn test_move_import_export_roundtrip() {
1763        let mut converter = LegalConverter::new();
1764
1765        let mut statute = Statute::new(
1766            "transfer_coin",
1767            "Transfer Coin",
1768            Effect::new(EffectType::MonetaryTransfer, "Transfer coins"),
1769        );
1770        statute
1771            .effect
1772            .parameters
1773            .insert("module_address".to_string(), "0x1".to_string());
1774        statute
1775            .effect
1776            .parameters
1777            .insert("module_name".to_string(), "Coin".to_string());
1778        statute
1779            .effect
1780            .parameters
1781            .insert("function".to_string(), "transfer".to_string());
1782        statute
1783            .effect
1784            .parameters
1785            .insert("entry".to_string(), "true".to_string());
1786
1787        let (move_output, export_report) = converter.export(&[statute], LegalFormat::Move).unwrap();
1788        assert_eq!(export_report.statutes_converted, 1);
1789        assert!(move_output.contains("module 0x1::Coin"));
1790        assert!(move_output.contains("public entry fun transfer()"));
1791
1792        let (imported, import_report) = converter.import(&move_output, LegalFormat::Move).unwrap();
1793        assert_eq!(import_report.statutes_converted, 1);
1794        assert!(!imported.is_empty());
1795    }
1796
1797    #[test]
1798    fn test_blockchain_format_extensions() {
1799        assert_eq!(LegalFormat::Solidity.extension(), "sol");
1800        assert_eq!(LegalFormat::Vyper.extension(), "vy");
1801        assert_eq!(LegalFormat::Cadence.extension(), "cdc");
1802        assert_eq!(LegalFormat::Move.extension(), "move");
1803    }
1804
1805    #[test]
1806    fn test_blockchain_format_from_extension() {
1807        assert_eq!(
1808            LegalFormat::from_extension("sol"),
1809            Some(LegalFormat::Solidity)
1810        );
1811        assert_eq!(
1812            LegalFormat::from_extension("solidity"),
1813            Some(LegalFormat::Solidity)
1814        );
1815        assert_eq!(LegalFormat::from_extension("vy"), Some(LegalFormat::Vyper));
1816        assert_eq!(
1817            LegalFormat::from_extension("vyper"),
1818            Some(LegalFormat::Vyper)
1819        );
1820        assert_eq!(
1821            LegalFormat::from_extension("cdc"),
1822            Some(LegalFormat::Cadence)
1823        );
1824        assert_eq!(
1825            LegalFormat::from_extension("cadence"),
1826            Some(LegalFormat::Cadence)
1827        );
1828        assert_eq!(LegalFormat::from_extension("move"), Some(LegalFormat::Move));
1829    }
1830
1831    #[test]
1832    fn test_solidity_source_import() {
1833        let mut converter = LegalConverter::new();
1834
1835        let source = r#"
1836        // SPDX-License-Identifier: MIT
1837        pragma solidity ^0.8.0;
1838
1839        contract SimpleStorage {
1840            function store(uint256 value) public {
1841            }
1842        }
1843        "#;
1844
1845        let (statutes, report) = converter.import(source, LegalFormat::Solidity).unwrap();
1846        assert_eq!(report.statutes_converted, 1);
1847        assert!(!statutes.is_empty());
1848        assert_eq!(
1849            statutes[0].effect.parameters.get("contract"),
1850            Some(&"SimpleStorage".to_string())
1851        );
1852    }
1853
1854    #[test]
1855    fn test_vyper_source_import() {
1856        let mut converter = LegalConverter::new();
1857
1858        let source = r#"
1859        # @version 0.3.0
1860        # @license MIT
1861
1862        @external
1863        def transfer():
1864            pass
1865        "#;
1866
1867        let (statutes, report) = converter.import(source, LegalFormat::Vyper).unwrap();
1868        assert_eq!(report.statutes_converted, 1);
1869        assert!(!statutes.is_empty());
1870    }
1871
1872    #[test]
1873    fn test_cadence_source_import() {
1874        let mut converter = LegalConverter::new();
1875
1876        let source = r#"
1877        pub contract FlowToken {
1878            pub fun transfer() {
1879            }
1880        }
1881        "#;
1882
1883        let (statutes, report) = converter.import(source, LegalFormat::Cadence).unwrap();
1884        assert_eq!(report.statutes_converted, 1);
1885        assert!(!statutes.is_empty());
1886        assert_eq!(
1887            statutes[0].effect.parameters.get("blockchain"),
1888            Some(&"Flow".to_string())
1889        );
1890    }
1891
1892    #[test]
1893    fn test_move_source_import() {
1894        let mut converter = LegalConverter::new();
1895
1896        let source = r#"
1897        module 0x1::Coin {
1898            public entry fun mint() {
1899            }
1900        }
1901        "#;
1902
1903        let (statutes, report) = converter.import(source, LegalFormat::Move).unwrap();
1904        assert_eq!(report.statutes_converted, 1);
1905        assert!(!statutes.is_empty());
1906        assert_eq!(
1907            statutes[0].effect.parameters.get("blockchain"),
1908            Some(&"Move".to_string())
1909        );
1910    }
1911
1912    #[test]
1913    fn test_cross_blockchain_conversion() {
1914        let mut converter = LegalConverter::new();
1915
1916        // Import from Solidity
1917        let solidity_source = r#"
1918        pragma solidity ^0.8.0;
1919        contract Token {
1920            function transfer() public {
1921            }
1922        }
1923        "#;
1924
1925        let (statutes, _) = converter
1926            .import(solidity_source, LegalFormat::Solidity)
1927            .unwrap();
1928
1929        // Export to Vyper
1930        let (vyper_output, report) = converter.export(&statutes, LegalFormat::Vyper).unwrap();
1931        assert_eq!(report.statutes_converted, 1);
1932        assert!(vyper_output.contains("def transfer()"));
1933
1934        // Export to Cadence
1935        let (cadence_output, report) = converter.export(&statutes, LegalFormat::Cadence).unwrap();
1936        assert_eq!(report.statutes_converted, 1);
1937        assert!(cadence_output.contains("pub fun transfer()"));
1938
1939        // Export to Move
1940        let (move_output, report) = converter.export(&statutes, LegalFormat::Move).unwrap();
1941        assert_eq!(report.statutes_converted, 1);
1942        assert!(move_output.contains("fun transfer()"));
1943    }
1944}