Skip to main content

knowdit_repo_model/
lang.rs

1//! Project source language enum.
2//!
3//! Used in two distinct roles:
4//!
5//! * **File-extension auto-detection** at project load time, via
6//!   `knowdit_project::ProjectData::language`. That detector
7//!   classifies a project as Solidity / Move purely from the
8//!   `.sol` vs `.move` file extensions it found while walking
9//!   the scope.
10//! * **Profile-agent declaration** as part of [`crate::ProjectProfile`].
11//!   The profile agent reads the project's actual source and
12//!   commits the language it observed — this is the
13//!   authoritative answer the downstream phases (mapper /
14//!   spec / reflect / regen) read back.
15//!
16//! Language-flavored prompt material lives on the
17//! `HarnessBackend` trait — each per-language impl carries its
18//! own prefix const. This module is just the enum + lightweight
19//! display / parsing helpers.
20
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
24pub enum SourceLanguage {
25    Solidity,
26    Move,
27}
28
29impl SourceLanguage {
30    pub fn display_name(self) -> &'static str {
31        match self {
32            Self::Solidity => "Solidity",
33            Self::Move => "Move",
34        }
35    }
36
37    pub fn extension(self) -> &'static str {
38        match self {
39            Self::Solidity => "sol",
40            Self::Move => "move",
41        }
42    }
43
44    pub fn code_fence(self) -> &'static str {
45        match self {
46            Self::Solidity => "solidity",
47            Self::Move => "move",
48        }
49    }
50
51    /// Parse a free-form string the LLM might emit
52    /// (`"Solidity"` / `"solidity"` / `"SOLIDITY"` /
53    /// `"Move"` / `"move"` / `"Sui Move"`).
54    /// Returns `None` for anything unrecognized so the caller
55    /// (typically the profile agent's `finalize_profile` tool)
56    /// can refuse to commit a malformed language.
57    pub fn parse_lenient(s: &str) -> Option<Self> {
58        match s.trim().to_ascii_lowercase().as_str() {
59            "solidity" | "sol" => Some(Self::Solidity),
60            "move" | "sui move" | "sui_move" => Some(Self::Move),
61            _ => None,
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn parse_lenient_known_spellings() {
72        for s in ["Solidity", "solidity", "SOLIDITY", "sol", " Solidity "] {
73            assert_eq!(
74                SourceLanguage::parse_lenient(s),
75                Some(SourceLanguage::Solidity),
76                "input={s:?}"
77            );
78        }
79        for s in ["Move", "move", "MOVE", "Sui Move", "sui_move"] {
80            assert_eq!(
81                SourceLanguage::parse_lenient(s),
82                Some(SourceLanguage::Move),
83                "input={s:?}"
84            );
85        }
86    }
87
88    #[test]
89    fn parse_lenient_rejects_unknown() {
90        for s in ["", "rust", "Solana", "vyper", "Solidity!", "Move 2", "??"] {
91            assert!(
92                SourceLanguage::parse_lenient(s).is_none(),
93                "should reject {s:?}"
94            );
95        }
96    }
97
98    #[test]
99    fn json_roundtrip() {
100        for lang in [SourceLanguage::Solidity, SourceLanguage::Move] {
101            let s = serde_json::to_string(&lang).unwrap();
102            let back: SourceLanguage = serde_json::from_str(&s).unwrap();
103            assert_eq!(back, lang);
104        }
105    }
106}