Skip to main content

rosetta_aisp/
converter.rs

1//! AISP Converter - Main conversion API
2//!
3//! Provides 3-tier conversion:
4//! - Minimal: Direct symbol substitution (0.5-1x tokens)
5//! - Standard: + Header + evidence block (1.5-2x tokens)
6//! - Full: + All blocks + proofs (4-8x tokens)
7
8use crate::rosetta::RosettaStone;
9use chrono::Utc;
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12
13/// Conversion tier
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum ConversionTier {
17    Minimal,
18    Standard,
19    Full,
20}
21
22impl std::fmt::Display for ConversionTier {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            ConversionTier::Minimal => write!(f, "minimal"),
26            ConversionTier::Standard => write!(f, "standard"),
27            ConversionTier::Full => write!(f, "full"),
28        }
29    }
30}
31
32/// Conversion options
33#[derive(Debug, Clone, Default)]
34pub struct ConversionOptions {
35    /// Force specific tier (auto-detect if None)
36    pub tier: Option<ConversionTier>,
37    /// Confidence threshold (default: 0.8)
38    pub confidence_threshold: Option<f64>,
39}
40
41/// Token statistics
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct TokenStats {
44    pub input: usize,
45    pub output: usize,
46    pub ratio: f64,
47}
48
49/// Conversion result
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ConversionResult {
52    /// Converted AISP output
53    pub output: String,
54    /// Confidence score (0.0 - 1.0)
55    pub confidence: f64,
56    /// Words that couldn't be mapped
57    pub unmapped: Vec<String>,
58    /// Conversion tier used
59    pub tier: ConversionTier,
60    /// Token statistics
61    pub tokens: TokenStats,
62    /// Whether LLM fallback was used (for gear-core integration)
63    #[serde(default)]
64    pub used_fallback: bool,
65}
66
67/// AISP Converter
68///
69/// Provides deterministic prose ↔ AISP conversion using Rosetta Stone mappings.
70pub struct AispConverter;
71
72impl AispConverter {
73    /// Convert prose to AISP with specified options
74    ///
75    /// # Example
76    /// ```
77    /// use rosetta_aisp::{AispConverter, ConversionOptions, ConversionTier};
78    ///
79    /// let result = AispConverter::convert("Define x as 5", None);
80    /// assert!(result.output.contains("≜"));
81    ///
82    /// // Force a specific tier
83    /// let result = AispConverter::convert("Define x as 5", Some(ConversionOptions {
84    ///     tier: Some(ConversionTier::Standard),
85    ///     ..Default::default()
86    /// }));
87    /// assert!(result.output.contains("𝔸5.1"));
88    /// ```
89    pub fn convert(prose: &str, options: Option<ConversionOptions>) -> ConversionResult {
90        let opts = options.unwrap_or_default();
91        let tier = opts.tier.unwrap_or_else(|| Self::detect_tier(prose));
92
93        let result = match tier {
94            ConversionTier::Minimal => Self::convert_minimal(prose),
95            ConversionTier::Standard => Self::convert_standard(prose),
96            ConversionTier::Full => Self::convert_full(prose),
97        };
98
99        ConversionResult {
100            tokens: TokenStats {
101                input: prose.len(),
102                output: result.output.len(),
103                ratio: if prose.is_empty() {
104                    0.0
105                } else {
106                    (result.output.len() as f64 / prose.len() as f64 * 100.0).round() / 100.0
107                },
108            },
109            ..result
110        }
111    }
112
113    /// Auto-detect appropriate tier based on prose complexity
114    ///
115    /// # Example
116    /// ```
117    /// use rosetta_aisp::{AispConverter, ConversionTier};
118    ///
119    /// assert_eq!(AispConverter::detect_tier("Define x as 5"), ConversionTier::Minimal);
120    /// assert_eq!(
121    ///     AispConverter::detect_tier("The user must authenticate to access the API"),
122    ///     ConversionTier::Standard
123    /// );
124    /// ```
125    pub fn detect_tier(prose: &str) -> ConversionTier {
126        let word_count = prose.split_whitespace().count();
127
128        let types_regex =
129            Regex::new(r"(?i)\b(type|class|struct|interface|schema|model|entity)\b").unwrap();
130        let rules_regex = Regex::new(
131            r"(?i)\b(must|should|always|never|require|ensure|guarantee|constraint|rule)\b",
132        )
133        .unwrap();
134        let proof_regex =
135            Regex::new(r"(?i)\b(prove|verify|validate|certify|demonstrate|qed|proven)\b").unwrap();
136        let complex_regex =
137            Regex::new(r"(?i)\b(for all|there exists|if and only if|implies|therefore)\b").unwrap();
138        let api_regex =
139            Regex::new(r"(?i)\b(api|endpoint|route|controller|handler|service)\b").unwrap();
140        let contractor_regex =
141            Regex::new(r"(?i)\b(delta|invariant|precondition|postcondition|requires|ensures)\b")
142                .unwrap();
143        let intent_regex =
144            Regex::new(r"(?i)\b(intent|goal|purpose|objective|fitness|risk|utility)\b").unwrap();
145
146        let has_types = types_regex.is_match(prose);
147        let has_rules = rules_regex.is_match(prose);
148        let has_proof = proof_regex.is_match(prose);
149        let has_complex = complex_regex.is_match(prose);
150        let has_api = api_regex.is_match(prose);
151        let has_contractor = contractor_regex.is_match(prose);
152        let has_intent = intent_regex.is_match(prose);
153
154        // Full tier: proofs, contractors, intents required, or types + rules together
155        if has_proof || has_contractor || has_intent || (has_types && has_rules) {
156            return ConversionTier::Full;
157        }
158
159        // Standard tier: types OR rules OR complex logic OR API OR longer text
160        if has_types || has_rules || has_complex || has_api || word_count > 20 {
161            return ConversionTier::Standard;
162        }
163
164        // Minimal tier: simple, short prose
165        ConversionTier::Minimal
166    }
167
168    /// Minimal conversion - direct Rosetta mapping
169    fn convert_minimal(prose: &str) -> ConversionResult {
170        let (output, mapped_chars, unmapped) = RosettaStone::convert(prose);
171        let confidence = RosettaStone::confidence(prose.len(), mapped_chars);
172
173        ConversionResult {
174            output,
175            confidence,
176            unmapped,
177            tier: ConversionTier::Minimal,
178            tokens: TokenStats {
179                input: 0,
180                output: 0,
181                ratio: 0.0,
182            },
183            used_fallback: false,
184        }
185    }
186
187    /// Standard conversion - minimal + header + evidence
188    fn convert_standard(prose: &str) -> ConversionResult {
189        let minimal = Self::convert_minimal(prose);
190        let domain = Self::extract_domain(prose);
191        let date = Utc::now().format("%Y-%m-%d").to_string();
192
193        let output = format!(
194            r#"𝔸5.1.{domain}@{date}
195γ≔{domain}
196
197⟦Ω:Meta⟧{{
198  domain≜{domain}
199  version≜1.0.0
200}}
201
202⟦Σ:Types⟧{{
203204}}
205
206⟦Γ:Rules⟧{{
207208}}
209
210⟦Λ:Funcs⟧{{
211  {body}
212}}
213
214⟦Ε⟧⟨δ≜0.70;τ≜◊⁺⟩"#,
215            domain = domain,
216            date = date,
217            body = minimal.output
218        );
219
220        ConversionResult {
221            output,
222            confidence: minimal.confidence,
223            unmapped: minimal.unmapped,
224            tier: ConversionTier::Standard,
225            tokens: TokenStats {
226                input: 0,
227                output: 0,
228                ratio: 0.0,
229            },
230            used_fallback: false,
231        }
232    }
233
234    /// Full conversion - complete AISP document
235    fn convert_full(prose: &str) -> ConversionResult {
236        let minimal = Self::convert_minimal(prose);
237        let domain = Self::extract_domain(prose);
238        let date = Utc::now().format("%Y-%m-%d").to_string();
239        let types = Self::infer_types(prose);
240        let rules = Self::infer_rules(prose);
241        let errors = Self::infer_errors(prose);
242
243        let output = format!(
244            r#"𝔸5.1.{domain}@{date}
245γ≔{domain}.definitions
246ρ≔⟨{domain},types,rules⟩
247
248⟦Ω:Meta⟧{{
249  domain≜{domain}
250  version≜1.0.0
251  ∀D∈AISP:Ambig(D)<0.02
252}}
253
254⟦Σ:Types⟧{{
255{types}
256}}
257
258⟦Γ:Rules⟧{{
259{rules}
260}}
261
262⟦Λ:Funcs⟧{{
263  {body}
264}}
265
266⟦Χ:Errors⟧{{
267{errors}
268}}
269
270⟦Ε⟧⟨δ≜0.82;φ≜100;τ≜◊⁺⁺;⊢valid;∎⟩"#,
271            domain = domain,
272            date = date,
273            types = types,
274            rules = rules,
275            body = minimal.output,
276            errors = errors
277        );
278
279        ConversionResult {
280            output,
281            confidence: minimal.confidence,
282            unmapped: minimal.unmapped,
283            tier: ConversionTier::Full,
284            tokens: TokenStats {
285                input: 0,
286                output: 0,
287                ratio: 0.0,
288            },
289            used_fallback: false,
290        }
291    }
292
293    /// Extract domain from prose
294    fn extract_domain(prose: &str) -> &'static str {
295        let lower = prose.to_lowercase();
296
297        if lower.contains("api") || lower.contains("endpoint") {
298            return "api";
299        }
300        if lower.contains("auth") || lower.contains("login") || lower.contains("password") {
301            return "auth";
302        }
303        if lower.contains("math") || lower.contains("sum") || lower.contains("calculate") {
304            return "math";
305        }
306        if lower.contains("database") || lower.contains("store") || lower.contains("persist") {
307            return "data";
308        }
309        if lower.contains("file") || lower.contains("read") || lower.contains("write") {
310            return "io";
311        }
312        if lower.contains("test") || lower.contains("assert") || lower.contains("expect") {
313            return "test";
314        }
315        if lower.contains("user") {
316            return "user";
317        }
318
319        "domain"
320    }
321
322    /// Infer types from prose
323    fn infer_types(prose: &str) -> String {
324        let lower = prose.to_lowercase();
325        let mut types = Vec::new();
326
327        if lower.contains("number") || lower.contains("integer") || lower.contains("count") {
328            types.push("  ℕ≜natural_numbers");
329        }
330        if lower.contains("string") || lower.contains("text") || lower.contains("name") {
331            types.push("  𝕊≜strings");
332        }
333        if lower.contains("bool")
334            || lower.contains("flag")
335            || lower.contains("true")
336            || lower.contains("false")
337        {
338            types.push("  𝔹≜booleans");
339        }
340        if lower.contains("function") || lower.contains("lambda") {
341            types.push("  Fn⟨A,B⟩≜A→B");
342        }
343        if lower.contains("user") {
344            types.push("  User≜⟨id:ℕ,name:𝕊⟩");
345        }
346        if lower.contains("list") || lower.contains("array") {
347            types.push("  List⟨T⟩≜⟨items:T*⟩");
348        }
349
350        if types.is_empty() {
351            types.push("  T≜⟨value:Any⟩");
352        }
353
354        types.join("\n")
355    }
356
357    /// Infer rules from prose
358    fn infer_rules(prose: &str) -> String {
359        let lower = prose.to_lowercase();
360        let mut rules = Vec::new();
361
362        if lower.contains("constant") || lower.contains("immutable") {
363            rules.push("  ∀c∈Const:c.immutable≡⊤");
364        }
365        if lower.contains("valid") || lower.contains("check") {
366            rules.push("  ∀x:T:valid(x)⇒accept(x)");
367        }
368        if lower.contains("all") || lower.contains("every") {
369            rules.push("  ∀x∈S:P(x)");
370        }
371        if lower.contains("must") || lower.contains("require") {
372            rules.push("  ∀x:T:require(x)⇒proceed(x)");
373        }
374        if lower.contains("unique") {
375            rules.push("  ∃!x:T:unique(x)");
376        }
377        if lower.contains("admin") {
378            rules.push("  ∀u∈User:u.admin≡⊤⇒allow(u)");
379        }
380
381        // Contractor detections
382        if lower.contains("invariant") || lower.contains("always true") {
383            rules.push("  Inv(s)≜always(s)");
384        }
385        if lower.contains("precondition") || lower.contains("before") {
386            rules.push("  Pre(f)≜req(args)");
387        }
388        if lower.contains("postcondition") || lower.contains("after") || lower.contains("ensures") {
389            rules.push("  Post(f)≜guarantee(result)");
390        }
391        if lower.contains("delta") || lower.contains("change") {
392            rules.push("  Δ(s)≜s'−s");
393        }
394
395        if rules.is_empty() {
396            rules.push("  ∀x:T:⊤");
397        }
398
399        rules.join("\n")
400    }
401
402    /// Infer errors from prose
403    fn infer_errors(prose: &str) -> String {
404        let lower = prose.to_lowercase();
405        let mut errors = Vec::new();
406
407        if lower.contains("error") || lower.contains("exception") {
408            errors.push("  E≜GenericError");
409        }
410        if lower.contains("fail") || lower.contains("failure") {
411            errors.push("  fail(x)⇒⊥");
412        }
413        if lower.contains("crash") || lower.contains("panic") {
414            errors.push("  crash⇒⊥⊥");
415        }
416        if lower.contains("not found") || lower.contains("missing") {
417            errors.push("  NotFound⇒∅");
418        }
419        if lower.contains("unauthorized") || lower.contains("forbidden") || lower.contains("denied")
420        {
421            errors.push("  AuthError⇒⊘");
422        }
423
424        if errors.is_empty() {
425            errors.push("  ∅");
426        }
427
428        errors.join("\n")
429    }
430
431    /// Convert AISP back to prose
432    ///
433    /// # Example
434    /// ```
435    /// use rosetta_aisp::AispConverter;
436    ///
437    /// let prose = AispConverter::to_prose("∀x∈S");
438    /// assert!(prose.contains("for all"));
439    /// assert!(prose.contains("in"));
440    /// ```
441    pub fn to_prose(aisp: &str) -> String {
442        RosettaStone::to_prose(aisp)
443    }
444
445    /// Validate AISP document using the aisp crate
446    pub fn validate(aisp: &str) -> aisp::ValidationResult {
447        aisp::validate(aisp)
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_detect_tier_minimal() {
457        assert_eq!(
458            AispConverter::detect_tier("Define x as 5"),
459            ConversionTier::Minimal
460        );
461    }
462
463    #[test]
464    fn test_detect_tier_standard() {
465        assert_eq!(
466            AispConverter::detect_tier("The user must provide valid authentication"),
467            ConversionTier::Standard
468        );
469    }
470
471    #[test]
472    fn test_detect_tier_full() {
473        assert_eq!(
474            AispConverter::detect_tier("Define a type User and verify all users are valid"),
475            ConversionTier::Full
476        );
477    }
478
479    #[test]
480    fn test_convert_minimal() {
481        let result = AispConverter::convert("Define x as 5", None);
482        assert!(result.output.contains("≜"));
483        assert_eq!(result.tier, ConversionTier::Minimal);
484    }
485
486    #[test]
487    fn test_convert_standard() {
488        let result = AispConverter::convert(
489            "Define x as 5",
490            Some(ConversionOptions {
491                tier: Some(ConversionTier::Standard),
492                ..Default::default()
493            }),
494        );
495        assert!(result.output.contains("𝔸5.1"));
496        assert!(result.output.contains("⟦Ω:Meta⟧"));
497        assert!(result.output.contains("⟦Σ:Types⟧"));
498        assert!(result.output.contains("⟦Γ:Rules⟧"));
499        assert!(result.output.contains("⟦Λ:Funcs⟧"));
500    }
501
502    #[test]
503    fn test_convert_full() {
504        let result = AispConverter::convert(
505            "Define x as 5",
506            Some(ConversionOptions {
507                tier: Some(ConversionTier::Full),
508                ..Default::default()
509            }),
510        );
511        assert!(result.output.contains("⟦Ω:Meta⟧"));
512        assert!(result.output.contains("⟦Σ:Types⟧"));
513        assert!(result.output.contains("⟦Γ:Rules⟧"));
514        assert!(result.output.contains("⟦Λ:Funcs⟧"));
515        assert!(result.output.contains("⟦Χ:Errors⟧"));
516    }
517
518    #[test]
519    fn test_to_prose() {
520        let prose = AispConverter::to_prose("∀x∈S");
521        assert!(prose.contains("for all"));
522        assert!(prose.contains("in"));
523    }
524}