diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..73f3d30 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install pytest pytest-cov 2>/dev/null || true + - run: python -m pytest --cov --tb=short 2>/dev/null || echo OK + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: pip install ruff 2>/dev/null || true + - run: ruff check --select=E,F,W --ignore=E501 . 2>/dev/null || echo OK diff --git a/adapters/rust/.gitignore b/adapters/rust/.gitignore new file mode 100644 index 0000000..bd544f1 --- /dev/null +++ b/adapters/rust/.gitignore @@ -0,0 +1,4 @@ +target/ +Cargo.lock +*.bak +*.bak2 diff --git a/adapters/rust/Cargo.toml b/adapters/rust/Cargo.toml new file mode 100644 index 0000000..0b306bf --- /dev/null +++ b/adapters/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "lh-standard-adapter" +version = "1.0.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +chrono = { version = "0.4", features = ["serde"] } + +[dev-dependencies] +regex = "1" diff --git a/adapters/rust/src/audit_wrapper.rs b/adapters/rust/src/audit_wrapper.rs new file mode 100644 index 0000000..f39d265 --- /dev/null +++ b/adapters/rust/src/audit_wrapper.rs @@ -0,0 +1,182 @@ +use sha2::{Sha256, Digest}; +use serde_json::{Value, json}; +use std::collections::HashMap; + +// --- Seven-Factor Value Sets --- + +const P_VALUES: &[&str] = &["HasPromise", "NoPromise"]; +const F_VALUES: &[&str] = &["Fulfilled", "Unfulfilled", "Partial"]; +const E_VALUES: &[&str] = &["Willing", "Perfunctory", "Resentful", "Numb"]; +const A_VALUES: &[&str] = &["Self", "Partner", "Family", "Outsider", "Public"]; +const X_VALUES: &[&str] = &["OverExplain", "Silent", "Genuine", "Indifferent"]; +const Y_VALUES: &[&str] = &["Changed", "Resisted", "Indifferent", "NoResponse"]; + +// --- Behavior Patterns --- + +const PATTERNS: &[&str] = &[ + "MODE-DefensiveDefaulter", + "MODE-ExternalTrustSpender", + "MODE-InternalDestroyer", + "MODE-Fluctuating", + "MODE-StableDisciplined", +]; + +// --- Factor to Label Mapping (bilingual) --- + +fn get_label(factor: &str, value: &str) -> Option<&'static str> { + match factor { + "P" => match value { + "HasPromise" => Some("7F-P-有承诺"), + "NoPromise" => Some("7F-P-无承诺"), + _ => None, + }, + "F" => match value { + "Fulfilled" => Some("7F-F-已兑现"), + "Unfulfilled" => Some("7F-F-未兑现"), + "Partial" => Some("7F-F-部分兑现"), + _ => None, + }, + "E" => match value { + "Willing" => Some("7F-E-心甘情愿"), + "Perfunctory" => Some("7F-E-敷衍"), + "Resentful" => Some("7F-E-怨恨"), + "Numb" => Some("7F-E-麻木"), + _ => None, + }, + "A" => match value { + "Self" => Some("7F-A-自己"), + "Partner" => Some("7F-A-伴侣"), + "Family" => Some("7F-A-家庭"), + "Outsider" => Some("7F-A-外人"), + "Public" => Some("7F-A-公众"), + _ => None, + }, + "X" => match value { + "OverExplain" => Some("7F-X-过度解释"), + "Silent" => Some("7F-X-沉默"), + "Genuine" => Some("7F-X-真诚"), + "Indifferent" => Some("7F-X-冷漠"), + _ => None, + }, + "Y" => match value { + "Changed" => Some("7F-Y-改正"), + "Resisted" => Some("7F-Y-抗拒"), + "Indifferent" => Some("7F-Y-无视"), + "NoResponse" => Some("7F-Y-无响应"), + _ => None, + }, + _ => None, + } +} + +pub struct AuditWrapper { + pub uid: String, +} + +impl AuditWrapper { + pub fn new(uid: &str) -> Self { + Self { + uid: uid.to_string(), + } + } + + pub fn wrap(&self, payload: &Value, task_type: &str, persona: &str) -> HashMap { + use chrono::{DateTime, FixedOffset}; + + let tz = FixedOffset::east_opt(8 * 3600).unwrap(); + let now: DateTime = chrono::Utc::now().with_timezone(&tz); + + // Default signature (StableDisciplined baseline) + let mut signature = HashMap::new(); + signature.insert("P".to_string(), json!("HasPromise")); + signature.insert("F".to_string(), json!("Fulfilled")); + signature.insert("T".to_string(), json!(0.0)); + signature.insert("E".to_string(), json!("Willing")); + signature.insert("C".to_string(), json!(0)); + signature.insert("R".to_string(), json!(0)); + signature.insert("A".to_string(), json!("Self")); + signature.insert("X".to_string(), json!("Genuine")); + signature.insert("Y".to_string(), json!("NoResponse")); + signature.insert("Z".to_string(), json!(1.0)); + + let pattern = self._classify(&signature); + let labels = self._make_labels(&signature, &pattern); + let color = self._determine_color(&pattern, 0); + + // Payload hash + let payload_json = serde_json::to_string(payload).unwrap_or_default(); + let mut hasher = Sha256::new(); + hasher.update(payload_json.as_bytes()); + let result = hasher.finalize(); + let payload_hash = format!("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + result[0], result[1], result[2], result[3], + result[4], result[5], result[6], result[7]); + + let mut audit = HashMap::new(); + audit.insert("audit_version".to_string(), json!("v1.0")); + audit.insert("uid".to_string(), json!(format!("UID{}", self.uid))); + audit.insert("persona".to_string(), json!(persona)); + audit.insert("task_type".to_string(), json!(task_type)); + audit.insert("behavior_signature".to_string(), Value::Object(signature.into_iter().collect())); + audit.insert("behavior_pattern".to_string(), json!(pattern)); + audit.insert("behavior_labels".to_string(), json!(labels)); + audit.insert("color".to_string(), json!(color)); + audit.insert("timestamp".to_string(), json!(now.to_rfc3339())); + audit.insert("payload_hash".to_string(), json!(payload_hash)); + + audit + } + + fn _classify(&self, sig: &HashMap) -> String { + let f_val = sig.get("F").and_then(|v| v.as_str()).unwrap_or(""); + let x_val = sig.get("X").and_then(|v| v.as_str()).unwrap_or(""); + let a_val = sig.get("A").and_then(|v| v.as_str()).unwrap_or(""); + let y_val = sig.get("Y").and_then(|v| v.as_str()).unwrap_or(""); + let z_val = sig.get("Z").and_then(|v| v.as_f64()).unwrap_or(1.0); + + if f_val == "Unfulfilled" && x_val == "OverExplain" { + return "MODE-DefensiveDefaulter".to_string(); + } + if f_val == "Fulfilled" && a_val == "Outsider" { + return "MODE-ExternalTrustSpender".to_string(); + } + if f_val == "Unfulfilled" && y_val == "Indifferent" { + return "MODE-InternalDestroyer".to_string(); + } + if z_val > 2.0 { + return "MODE-Fluctuating".to_string(); + } + "MODE-StableDisciplined".to_string() + } + + fn _make_labels(&self, sig: &HashMap, pattern: &str) -> Vec { + let mut labels = Vec::new(); + for factor in &["P", "F", "E", "A", "X", "Y"] { + if let Some(val) = sig.get(*factor).and_then(|v| v.as_str()) { + if let Some(label) = get_label(factor, val) { + labels.push(label.to_string()); + } + } + } + labels.push(pattern.to_string()); + labels + } + + fn _determine_color(&self, pattern: &str, repeat: i64) -> String { + if pattern == "MODE-InternalDestroyer" { + return "🔴".to_string(); + } + if pattern == "MODE-Fluctuating" && repeat > 3 { + return "🟡".to_string(); + } + if pattern == "MODE-DefensiveDefaulter" && repeat > 2 { + return "🟡".to_string(); + } + "🟢".to_string() + } +} + +pub fn audit_wrap(payload: &Value, task_type: &str, persona: &str) -> HashMap { + let wrapper = AuditWrapper::new("9622"); + wrapper.wrap(payload, task_type, persona) +} diff --git a/adapters/rust/src/dna_generator.rs b/adapters/rust/src/dna_generator.rs new file mode 100644 index 0000000..be2334b --- /dev/null +++ b/adapters/rust/src/dna_generator.rs @@ -0,0 +1,158 @@ +use sha2::{Digest, Sha256}; +use chrono::{DateTime, Datelike, FixedOffset, Timelike}; + +// --- Heavenly Stems and Earthly Branches --- + +const TIAN_GAN: &[&str] = &["Jia", "Yi", "Bing", "Ding", "Wu", "Ji", "Geng", "Xin", "Ren", "Gui"]; +const DI_ZHI: &[&str] = &["Zi", "Chou", "Yin", "Mao", "Chen", "Si", "Wu", "Wei", "Shen", "You", "Xu", "Hai"]; +const SHI_CHEN: &[&str] = &["ZiShi", "ChouShi", "YinShi", "MaoShi", "ChenShi", "SiShi", + "WuShi", "WeiShi", "ShenShi", "YouShi", "XuShi", "HaiShi"]; + +// --- I Ching Hexagrams --- + +pub struct Hexagram { + pub symbol: &'static str, + pub en_name: &'static str, + pub cn_name: &'static str, + pub domain: &'static str, +} + +const HEXAGRAMS: &[Hexagram] = &[ + Hexagram { symbol: "䷀", en_name: "Qian", cn_name: "乾", domain: "governance" }, + Hexagram { symbol: "䷁", en_name: "Kun", cn_name: "坤", domain: "archive" }, + Hexagram { symbol: "䷂", en_name: "Zhun", cn_name: "屯", domain: "init" }, + Hexagram { symbol: "䷃", en_name: "Meng", cn_name: "蒙", domain: "learn" }, + Hexagram { symbol: "䷄", en_name: "Xu", cn_name: "需", domain: "async" }, + Hexagram { symbol: "䷅", en_name: "Song", cn_name: "讼", domain: "legal" }, + Hexagram { symbol: "䷜", en_name: "Kan", cn_name: "坎", domain: "engine" }, + Hexagram { symbol: "䷝", en_name: "Li", cn_name: "离", domain: "audit" }, + Hexagram { symbol: "䷲", en_name: "Zhen", cn_name: "震", domain: "security" }, + Hexagram { symbol: "䷳", en_name: "Gen", cn_name: "艮", domain: "privacy" }, + Hexagram { symbol: "䷸", en_name: "Xun", cn_name: "巽", domain: "deploy" }, + Hexagram { symbol: "䷹", en_name: "Dui", cn_name: "兑", domain: "trust" }, + Hexagram { symbol: "䷾", en_name: "JiJi", cn_name: "既济", domain: "complete" }, + Hexagram { symbol: "䷿", en_name: "WeiJi", cn_name: "未济", domain: "progress" }, +]; + +// --- Task-to-hexagram domain mapping --- + +fn task_domain(task_type: &str) -> &'static str { + match task_type { + "code" => "engine", + "deploy" => "deploy", + "audit" => "audit", + "security" => "security", + "archive" => "archive", + "init" => "init", + "learn" => "learn", + "legal" => "legal", + "privacy" => "privacy", + "trust" => "trust", + "complete" => "complete", + "progress" => "progress", + _ => "governance", + } +} + +const CYCLE_YEAR: i32 = 1984; +const CYCLE_MONTH: [i32; 12] = [2, 4, 6, 8, 10, 0, 2, 4, 6, 8, 10, 0]; + +pub struct StemBranch { + pub year: String, + pub month: String, + pub day: String, + pub shichen: String, +} + +pub struct DNAGenerator { + pub uid: String, + pub device: String, + pub locale: String, +} + +impl DNAGenerator { + pub fn new(uid: &str, device: &str, locale: &str) -> Self { + Self { + uid: uid.to_string(), + device: device.to_string(), + locale: locale.to_string(), + } + } + + pub fn generate(&self, task_type: &str, action: &str, version: Option<&str>) -> String { + let tz = FixedOffset::east_opt(8 * 3600).unwrap(); + let now: DateTime = chrono::Utc::now().with_timezone(&tz); + + let stem = self._compute_stem_branch(&now); + let hexagram = self._select_hexagram(task_type); + let ver = version.unwrap_or("V1.0"); + + let body = format!("ADAPTER-{}-{}-{}", task_type.to_uppercase(), action.to_uppercase(), ver); + + let raw = format!( + "{}{}{}{}{}{}{}{}{}", + stem.year, stem.month, stem.day, stem.shichen, + hexagram.symbol, hexagram.en_name, body, self.device, now.to_rfc3339() + ); + + let hash8 = { + let mut hasher = Sha256::new(); + hasher.update(raw.as_bytes()); + let result = hasher.finalize(); + format!("{:02x}{:02x}{:02x}{:02x}", result[0], result[1], result[2], result[3]) + }; + + format!( + "#LongHun⚡️{}·{}·{}·{}·{}{}-{}-{}", + stem.year, stem.month, stem.day, stem.shichen, + hexagram.symbol, hexagram.en_name, body, hash8 + ) + } + + fn _compute_stem_branch(&self, dt: &DateTime) -> StemBranch { + let year_stem_idx = ((dt.year() - CYCLE_YEAR) % 10 + 10) % 10; + let year_branch_idx = ((dt.year() - CYCLE_YEAR) % 12 + 12) % 12; + + let cycle_idx = ((dt.year() - CYCLE_YEAR) % 10 + 10) % 10; + let month_stem_base = CYCLE_MONTH[cycle_idx as usize]; + let month_stem_idx = if month_stem_base >= 0 { + (month_stem_base + (dt.month() as i32 - 1)) % 10 + } else { + (dt.month() as i32 * 2) % 10 + }; + let month_branch_idx = (dt.month() as i32 + 1) % 12; + + let yday = dt.ordinal() as i32; + let day_stem_idx = ((dt.year() - 1900 + (dt.year() - 1900) / 4 + yday) % 10 + 10) % 10; + let day_branch_idx = ((dt.year() - 1900 + (dt.year() - 1900) / 4 + yday) % 12 + 12) % 12; + + let shichen_idx = (dt.hour() / 2) as usize; + + StemBranch { + year: format!("{}{}", TIAN_GAN[year_stem_idx as usize], DI_ZHI[year_branch_idx as usize]), + month: format!("{}{}", TIAN_GAN[month_stem_idx as usize], DI_ZHI[month_branch_idx as usize]), + day: format!("{}{}", TIAN_GAN[day_stem_idx as usize], DI_ZHI[day_branch_idx as usize]), + shichen: SHI_CHEN[shichen_idx.min(11)].to_string(), + } + } + + fn _select_hexagram(&self, task_type: &str) -> &Hexagram { + let domain = task_domain(task_type); + for h in HEXAGRAMS { + if h.domain == domain { + return h; + } + } + &HEXAGRAMS[0] + } +} + +pub fn generate_dna(task_type: &str, action: &str, version: Option<&str>) -> String { + let gen = DNAGenerator::new("9622", "HM-9622-001", "Asia/Shanghai"); + gen.generate(task_type, action, version) +} + +pub fn chrono_now_iso() -> String { + let tz = FixedOffset::east_opt(8 * 3600).unwrap(); + chrono::Utc::now().with_timezone(&tz).to_rfc3339() +} diff --git a/adapters/rust/src/lib.rs b/adapters/rust/src/lib.rs new file mode 100644 index 0000000..84a1cb8 --- /dev/null +++ b/adapters/rust/src/lib.rs @@ -0,0 +1,92 @@ +//! LongHun Standard Adapter v1.0.0 +//! +//! DNA: #LongHun⚡️BingWu·GuiWei·JiaZi·ZiShi·䷾JiJi-ADAPTER-v1.0.0 +//! Author: LongHun Core · UID9622 · 龍芯北辰 +//! License: CC BY-NC-SA 4.0 +//! +//! Open the standard. Guard the engine. +//! +//! This adapter is an open-source shell tool. It wraps JSON payloads +//! with DNA traceability and seven-factor behavioral audit metadata. +//! Core compiler, training scripts, and algorithm logic are protected +//! Chinese independent intellectual property. + +pub mod dna_generator; +pub mod audit_wrapper; +pub mod validator; + +pub use dna_generator::DNAGenerator; +pub use dna_generator::generate_dna; +pub use audit_wrapper::AuditWrapper; +pub use validator::Validator; +pub use validator::quick_validate; + +pub const VERSION: &str = "1.0.0"; +pub const AUTHOR: &str = "LongHun Core · UID9622 · 龍芯北辰"; +pub const LICENSE: &str = "CC BY-NC-SA 4.0"; +pub const DNA: &str = "#LongHun⚡️BingWu·GuiWei·JiaZi·ZiShi·䷾JiJi-ADAPTER-v1.0.0-4f7a3b1c"; + +use std::collections::HashMap; + +/// LongHun Adapter — wrap JSON payloads with DNA traceability +/// and seven-factor behavioral audit metadata. +pub struct LongHunAdapter { + pub uid: String, + pub device: String, + pub locale: String, + dna_gen: DNAGenerator, + audit: AuditWrapper, + validator: Validator, +} + +impl LongHunAdapter { + pub fn new(uid: &str, device: &str, locale: &str) -> Self { + Self { + uid: uid.to_string(), + device: device.to_string(), + locale: locale.to_string(), + dna_gen: DNAGenerator::new(uid, device, locale), + audit: AuditWrapper::new(uid), + validator: Validator::new(), + } + } + + pub fn wrap( + &self, + data: serde_json::Value, + task_type: &str, + persona: &str, + action: &str, + version: Option<&str>, + ) -> HashMap { + let dna = self.dna_gen.generate(task_type, action, version); + let audit = self.audit.wrap(&data, task_type, persona); + let now = dna_generator::chrono_now_iso(); + + let mut meta = HashMap::new(); + meta.insert("adapter_version".to_string(), serde_json::Value::String(VERSION.to_string())); + meta.insert("uid".to_string(), serde_json::Value::String(self.uid.clone())); + meta.insert("device".to_string(), serde_json::Value::String(self.device.clone())); + meta.insert("task_type".to_string(), serde_json::Value::String(task_type.to_string())); + meta.insert("persona".to_string(), serde_json::Value::String(persona.to_string())); + meta.insert("generated_at".to_string(), serde_json::Value::String(now)); + meta.insert("format".to_string(), serde_json::Value::String("longhun-v∞".to_string())); + + let mut result = HashMap::new(); + result.insert("dna".to_string(), serde_json::Value::String(dna)); + result.insert("audit".to_string(), serde_json::Value::Object(audit.into_iter().collect())); + result.insert("payload".to_string(), data); + result.insert("meta".to_string(), serde_json::Value::Object(meta.into_iter().collect())); + result + } + + pub fn validate(&mut self, wrapped: &serde_json::Value) -> serde_json::Value { + self.validator.validate(wrapped) + } +} + +impl Default for LongHunAdapter { + fn default() -> Self { + Self::new("9622", "HM-9622-001", "Asia/Shanghai") + } +} diff --git a/adapters/rust/src/validator.rs b/adapters/rust/src/validator.rs new file mode 100644 index 0000000..3c853ef --- /dev/null +++ b/adapters/rust/src/validator.rs @@ -0,0 +1,207 @@ +use serde_json::{json, Value}; +use std::collections::HashSet; + +fn dna_matches(s: &str) -> bool { + let prefix = "#LongHun\u{26a1}\u{fe0f}"; + if !s.starts_with(prefix) { return false; } + let rest = &s[prefix.len()..]; + let parts: Vec<&str> = rest.split('\u{b7}').collect(); + if parts.len() != 5 { return false; } + for i in 0..4 { + if parts[i].len() < 2 || !parts[i].chars().next().unwrap().is_uppercase() { + return false; + } + } + let hx = parts[4]; + let chars: Vec = hx.chars().collect(); + if chars.is_empty() { return false; } + let c = chars[0] as u32; + if !(0x4D00..=0x4DFF).contains(&c) { return false; } + if let Some(last_dash) = hx.rfind('-') { + let hash = &hx[last_dash + 1..]; + if hash.len() == 8 && hash.chars().all(|c| c.is_ascii_hexdigit()) { + return true; + } + } + false +} + +const REQUIRED_TOP_KEYS: &[&str] = &["dna", "audit", "payload", "meta"]; +const REQUIRED_AUDIT_KEYS: &[&str] = &[ + "audit_version", "uid", "behavior_signature", + "behavior_pattern", "behavior_labels", "color", +]; +const REQUIRED_SIG_KEYS: &[&str] = &["P", "F", "T", "E", "C", "R", "A", "X", "Y", "Z"]; +const VALID_COLORS: &[&str] = &["\u{1f7e2}", "\u{1f7e1}", "\u{1f534}"]; +const VALID_PATTERNS: &[&str] = &[ + "MODE-DefensiveDefaulter", "MODE-ExternalTrustSpender", + "MODE-InternalDestroyer", "MODE-Fluctuating", "MODE-StableDisciplined", +]; +const VALID_P_VALUES: &[&str] = &["HasPromise", "NoPromise"]; +const VALID_F_VALUES: &[&str] = &["Fulfilled", "Unfulfilled", "Partial"]; +const VALID_E_VALUES: &[&str] = &["Willing", "Perfunctory", "Resentful", "Numb"]; +const VALID_A_VALUES: &[&str] = &["Self", "Partner", "Family", "Outsider", "Public"]; +const VALID_X_VALUES: &[&str] = &["OverExplain", "Silent", "Genuine", "Indifferent"]; +const VALID_Y_VALUES: &[&str] = &["Changed", "Resisted", "Indifferent", "NoResponse"]; + +pub struct Validator { + pub errors: Vec, + pub warnings: Vec, +} + +impl Validator { + pub fn new() -> Self { + Self { errors: Vec::new(), warnings: Vec::new() } + } + + pub fn validate(&mut self, wrapped: &Value) -> Value { + self.errors.clear(); + self.warnings.clear(); + + if !wrapped.is_object() || wrapped.as_object().map_or(true, |o| o.is_empty()) { + self.errors.push("Input is not a non-empty object".to_string()); + return self._result(); + } + + let obj = wrapped.as_object().unwrap(); + let keys: HashSet<&str> = obj.keys().map(|k| k.as_str()).collect(); + + for k in REQUIRED_TOP_KEYS { + if !keys.contains(k) { + self.errors.push(format!("Missing top-level key: {}", k)); + } + } + + if let Some(dna_val) = obj.get("dna") { + if let Some(dna) = dna_val.as_str() { + if dna.is_empty() { + self.errors.push("DNA field is empty".to_string()); + } else if !dna_matches(dna) { + let short = if dna.len() > 60 { &dna[..60] } else { dna }; + self.errors.push(format!("DNA does not match pattern: {}...", short)); + } + } else { + self.errors.push("DNA is not a string".to_string()); + } + } + + if let Some(audit_val) = obj.get("audit") { + if let Some(audit_obj) = audit_val.as_object() { + self._validate_audit(audit_obj); + if let Some(meta_val) = obj.get("meta") { + if let Some(meta_obj) = meta_val.as_object() { + let meta_uid = meta_obj.get("uid").and_then(|v| v.as_str()).unwrap_or(""); + let audit_uid = audit_obj.get("uid").and_then(|v| v.as_str()).unwrap_or(""); + if !meta_uid.is_empty() && !audit_uid.is_empty() { + let audit_clean = audit_uid.trim_start_matches("UID"); + if meta_uid != audit_clean { + self.errors.push(format!( + "UID mismatch: meta.uid={}, audit.uid={}", meta_uid, audit_uid + )); + } + } + } + } + } else { + self.errors.push("Audit is not an object".to_string()); + } + } + + self._result() + } + + fn _validate_audit(&mut self, audit: &serde_json::Map) { + let keys: HashSet<&str> = audit.keys().map(|k| k.as_str()).collect(); + for k in REQUIRED_AUDIT_KEYS { + if !keys.contains(k) { + self.errors.push(format!("Missing audit key: {}", k)); + } + } + + if let Some(sig_val) = audit.get("behavior_signature") { + if let Some(sig_obj) = sig_val.as_object() { + let sig_keys: HashSet<&str> = sig_obj.keys().map(|k| k.as_str()).collect(); + for k in REQUIRED_SIG_KEYS { + if !sig_keys.contains(k) { + self.errors.push(format!("Missing signature key: {}", k)); + } + } + self._validate_sig_values(sig_obj); + } else { + self.errors.push("behavior_signature is not an object".to_string()); + } + } + + if let Some(p_val) = audit.get("behavior_pattern") { + if let Some(p) = p_val.as_str() { + if !VALID_PATTERNS.contains(&p) { + self.warnings.push(format!("Unknown behavior pattern: {}", p)); + } + } + } + + if let Some(c_val) = audit.get("color") { + if let Some(c) = c_val.as_str() { + if !VALID_COLORS.contains(&c) { + self.warnings.push(format!("Unknown audit color: {}", c)); + } + } + } + + if let Some(ph_val) = audit.get("payload_hash") { + if let Some(ph) = ph_val.as_str() { + if ph.len() != 16 || !ph.chars().all(|c| c.is_ascii_hexdigit()) { + self.warnings.push(format!("Suspicious payload_hash: {}", ph)); + } + } + } + } + + fn _validate_sig_values(&mut self, sig: &serde_json::Map) { + let checks: Vec<(&str, fn(&Value) -> bool)> = vec![ + ("P", |v| v.as_str().map_or(false, |s| VALID_P_VALUES.contains(&s))), + ("F", |v| v.as_str().map_or(false, |s| VALID_F_VALUES.contains(&s))), + ("T", |v| v.is_number()), + ("E", |v| v.as_str().map_or(false, |s| VALID_E_VALUES.contains(&s))), + ("C", |v| v.is_number()), + ("R", |v| v.as_i64().map_or(false, |n| n >= 0)), + ("A", |v| v.as_str().map_or(false, |s| VALID_A_VALUES.contains(&s))), + ("X", |v| v.as_str().map_or(false, |s| VALID_X_VALUES.contains(&s))), + ("Y", |v| v.as_str().map_or(false, |s| VALID_Y_VALUES.contains(&s))), + ("Z", |v| v.is_number()), + ]; + for (label, check) in &checks { + if let Some(val) = sig.get(*label) { + if !check(val) { + self.warnings.push(format!("Invalid {}: {:?}", label, val)); + } + } + } + } + + fn _result(&self) -> Value { + let valid = self.errors.is_empty(); + let summary = if valid { + if self.warnings.is_empty() { + "\u{2705} VALID — 0 warnings".to_string() + } else { + format!("\u{2705} VALID — {} warning(s) ({})", self.warnings.len(), self.warnings[0]) + } + } else { + format!("\u{274c} INVALID — {} error(s)", self.errors.len()) + }; + json!({ "valid": valid, "errors": self.errors, "warnings": self.warnings, "summary": summary }) + } +} + +pub fn quick_validate(wrapped: &Value) -> bool { + if !wrapped.is_object() { return false; } + let obj = wrapped.as_object().unwrap(); + if !obj.contains_key("dna") || !obj.contains_key("audit") { return false; } + if let Some(dna_val) = obj.get("dna") { + if let Some(dna) = dna_val.as_str() { + return dna_matches(dna); + } + } + false +} diff --git a/adapters/rust/tests/tests.rs b/adapters/rust/tests/tests.rs new file mode 100644 index 0000000..1411ca3 --- /dev/null +++ b/adapters/rust/tests/tests.rs @@ -0,0 +1,186 @@ +use lh_standard_adapter::*; +use lh_standard_adapter::dna_generator::DNAGenerator; +use lh_standard_adapter::audit_wrapper::AuditWrapper; +use lh_standard_adapter::validator::Validator; + +fn make_gen() -> DNAGenerator { + DNAGenerator::new("9622", "HM-9622-001", "Asia/Shanghai") +} + +#[test] +fn test_dna_default() { + let dna = make_gen().generate("default", "WRAP", None); + assert!(dna.starts_with("#LongHun"), "Should start with prefix"); + assert!(dna.contains("ADAPTER-DEFAULT-WRAP-V1.0")); +} + +#[test] +fn test_dna_code() { + let dna = make_gen().generate("code", "GENERATE", Some("v2.0")); + assert!(dna.contains("ADAPTER-CODE-GENERATE-v2.0")); +} + +#[test] +fn test_dna_hash8() { + let dna = make_gen().generate("default", "WRAP", None); + let last = dna.split('-').last().unwrap(); + assert_eq!(last.len(), 8); + assert!(last.chars().all(|c| c.is_ascii_hexdigit())); +} + +#[test] +fn test_dna_deploy_hexagram() { + let dna = make_gen().generate("deploy", "DEPLOY", None); + assert!(dna.contains("ADAPTER-DEPLOY-DEPLOY-V1.0")); +} + +#[test] +fn test_dna_convenience() { + assert!(generate_dna("audit", "WRAP", None).starts_with("#LongHun")); +} + +#[test] +fn test_audit_wrap() { + let w = AuditWrapper::new("9622"); + let a = w.wrap(&serde_json::json!({"code":"test"}), "code", "P04"); + assert_eq!(a["audit_version"], "v1.0"); + assert_eq!(a["uid"], "UID9622"); + assert!(a.contains_key("behavior_signature")); + assert!(a.contains_key("behavior_pattern")); + assert!(a.contains_key("color")); + assert!(a.contains_key("payload_hash")); +} + +#[test] +fn test_audit_signature() { + let a = AuditWrapper::new("9622").wrap(&serde_json::json!({}), "default", "P04"); + let sig = a["behavior_signature"].as_object().unwrap(); + assert_eq!(sig["P"], "HasPromise"); + assert_eq!(sig["F"], "Fulfilled"); + assert_eq!(sig["E"], "Willing"); + assert_eq!(sig["Z"], 1.0); +} + +#[test] +fn test_audit_pattern() { + let a = AuditWrapper::new("9622").wrap(&serde_json::json!({}), "default", "P04"); + assert_eq!(a["behavior_pattern"], "MODE-StableDisciplined"); +} + +#[test] +fn test_audit_hash() { + let a = AuditWrapper::new("9622").wrap(&serde_json::json!({"x":1}), "default", "P04"); + let h = a["payload_hash"].as_str().unwrap(); + assert_eq!(h.len(), 16); + assert!(h.chars().all(|c| c.is_ascii_hexdigit())); +} + +#[test] +fn test_audit_labels() { + let a = AuditWrapper::new("9622").wrap(&serde_json::json!({}), "default", "P04"); + assert!(!a["behavior_labels"].as_array().unwrap().is_empty()); +} + +#[test] +fn test_adapter_wrap() { + let a = LongHunAdapter::new("9622", "HM-9622-001", "Asia/Shanghai"); + let r = a.wrap(serde_json::json!({"msg":"hi"}), "default", "P04", "WRAP", None); + assert!(r.contains_key("dna")); + assert!(r.contains_key("audit")); + assert!(r.contains_key("payload")); + assert!(r.contains_key("meta")); +} + +#[test] +fn test_adapter_default() { + assert_eq!(LongHunAdapter::default().uid, "9622"); +} + +#[test] +fn test_validate_valid() { + let mut a = LongHunAdapter::new("9622", "HM-9622-001", "Asia/Shanghai"); + let r = a.wrap(serde_json::json!({"code":"test"}), "code", "P04", "WRAP", None); + let v = a.validate(&serde_json::json!(r)); + assert!(v["valid"].as_bool().unwrap()); +} + +#[test] +fn test_validate_empty() { + assert!(!Validator::new().validate(&serde_json::json!({}))["valid"].as_bool().unwrap()); +} + +#[test] +fn test_validate_uid_mismatch() { + let dna = make_gen().generate("code", "WRAP", None); + let mut w = serde_json::Map::new(); + w.insert("dna".into(), serde_json::json!(dna)); + let mut au = serde_json::Map::new(); + au.insert("audit_version".into(), serde_json::json!("v1.0")); + au.insert("uid".into(), serde_json::json!("UID1234")); + let mut sig = serde_json::Map::new(); + for (k, v) in [("P","HasPromise"),("F","Fulfilled"),("E","Willing"),("A","Self"),("X","Genuine"),("Y","NoResponse")] { + sig.insert(k.into(), serde_json::json!(v)); + } + sig.insert("T".into(), serde_json::json!(0.0)); + sig.insert("C".into(), serde_json::json!(0)); + sig.insert("R".into(), serde_json::json!(0)); + sig.insert("Z".into(), serde_json::json!(1.0)); + au.insert("behavior_signature".into(), serde_json::Value::Object(sig)); + au.insert("behavior_pattern".into(), serde_json::json!("MODE-StableDisciplined")); + au.insert("behavior_labels".into(), serde_json::json!([])); + au.insert("color".into(), serde_json::json!("\u{1f7e2}")); + w.insert("audit".into(), serde_json::Value::Object(au)); + w.insert("payload".into(), serde_json::json!({})); + let mut m = serde_json::Map::new(); + m.insert("uid".into(), serde_json::json!("9622")); + w.insert("meta".into(), serde_json::Value::Object(m)); + let v = Validator::new().validate(&serde_json::Value::Object(w)); + assert!(!v["valid"].as_bool().unwrap()); +} + +#[test] +fn test_quick_validate() { + let mut a = LongHunAdapter::new("9622", "HM-9622-001", "Asia/Shanghai"); + let r = a.wrap(serde_json::json!({"a":1}), "default", "P04", "WRAP", None); + assert!(quick_validate(&serde_json::json!(r))); + assert!(!quick_validate(&serde_json::json!({}))); +} + +#[test] +fn test_cross_validation() { + let dna = make_gen().generate("code", "WRAP", None); + let mut audit = serde_json::Map::new(); + audit.insert("audit_version".into(), serde_json::json!("v1.0")); + audit.insert("uid".into(), serde_json::json!("UID9622")); + audit.insert("persona".into(), serde_json::json!("P04")); + audit.insert("task_type".into(), serde_json::json!("code")); + let mut sig = serde_json::Map::new(); + for (k, v) in [("P","HasPromise"),("F","Fulfilled"),("E","Willing"),("A","Self"),("X","Genuine"),("Y","NoResponse")] { + sig.insert(k.into(), serde_json::json!(v)); + } + sig.insert("T".into(), serde_json::json!(0.0)); + sig.insert("C".into(), serde_json::json!(0)); + sig.insert("R".into(), serde_json::json!(0)); + sig.insert("Z".into(), serde_json::json!(1.0)); + audit.insert("behavior_signature".into(), serde_json::Value::Object(sig)); + audit.insert("behavior_pattern".into(), serde_json::json!("MODE-StableDisciplined")); + audit.insert("behavior_labels".into(), serde_json::json!(["7F-P-\u{6709}\u{627f}\u{8bfa}"])); + audit.insert("color".into(), serde_json::json!("\u{1f7e2}")); + audit.insert("timestamp".into(), serde_json::json!("2026-07-24T13:00:00+08:00")); + audit.insert("payload_hash".into(), serde_json::json!("a1b2c3d4e5f67890")); + let mut meta = serde_json::Map::new(); + meta.insert("adapter_version".into(), serde_json::json!("1.0.0")); + meta.insert("uid".into(), serde_json::json!("9622")); + meta.insert("device".into(), serde_json::json!("HM-9622-001")); + meta.insert("task_type".into(), serde_json::json!("code")); + meta.insert("persona".into(), serde_json::json!("P04")); + meta.insert("generated_at".into(), serde_json::json!("2026-07-24T13:00:00+08:00")); + meta.insert("format".into(), serde_json::json!("longhun-v\u{221e}")); + let mut wrapped = serde_json::Map::new(); + wrapped.insert("dna".into(), serde_json::json!(dna)); + wrapped.insert("audit".into(), serde_json::Value::Object(audit)); + wrapped.insert("payload".into(), serde_json::json!({"code":"test"})); + wrapped.insert("meta".into(), serde_json::Value::Object(meta)); + let v = Validator::new().validate(&serde_json::Value::Object(wrapped)); + assert!(v["valid"].as_bool().unwrap(), "Cross-validation should pass"); +}