diff --git a/adapters/rust/Cargo.toml b/adapters/rust/Cargo.toml new file mode 100644 index 0000000..207229b --- /dev/null +++ b/adapters/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "lh-standard-adapter" +version = "1.0.0" +edition = "2021" +description = "LongHun Standard Adapter — v∞ DNA traceability + seven-factor behavioral audit for Rust" +license = "CC-BY-NC-SA-4.0" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +chrono = { version = "0.4", features = ["serde"] } diff --git a/adapters/rust/src/audit_wrapper.rs b/adapters/rust/src/audit_wrapper.rs new file mode 100644 index 0000000..3847f90 --- /dev/null +++ b/adapters/rust/src/audit_wrapper.rs @@ -0,0 +1,72 @@ +use sha2::{Sha256, Digest}; +use serde_json::Value; +use chrono::Utc; +use std::collections::HashMap; + +pub fn wrap_audit(uid: &str, payload: &Value, task_type: &str, persona: &str) -> Value { + let now = Utc::now(); + let sig = serde_json::json!({ + "P":"HasPromise","F":"Fulfilled","T":0.0,"E":"Willing", + "C":0,"R":0,"A":"Self","X":"Genuine","Y":"NoResponse","Z":1.0 + }); + let pattern = classify(&sig); + let labels = make_labels(&sig, pattern); + let color = determine_color(pattern, sig["R"].as_i64().unwrap_or(0) as i32); + let payload_str = serde_json::to_string(payload).unwrap_or_default(); + let hash = Sha256::digest(payload_str.as_bytes()); + let ph = format!("{:x}", hash); + + serde_json::json!({ + "audit_version":"v1.0", + "uid":format!("UID{}", uid), + "persona":persona, + "task_type":task_type, + "behavior_signature":sig, + "behavior_pattern":pattern, + "behavior_labels":labels, + "color":color, + "timestamp":now.to_rfc3339(), + "payload_hash":&ph[..16] + }) +} + +fn classify(sig: &Value) -> &'static str { + let f = sig["F"].as_str().unwrap_or(""); + let x = sig["X"].as_str().unwrap_or(""); + let a = sig["A"].as_str().unwrap_or(""); + let y = sig["Y"].as_str().unwrap_or(""); + let z = sig["Z"].as_f64().unwrap_or(0.0); + if f == "Unfulfilled" && x == "OverExplain" { return "MODE-DefensiveDefaulter" } + if f == "Fulfilled" && a == "Outsider" { return "MODE-ExternalTrustSpender" } + if f == "Unfulfilled" && y == "Indifferent" { return "MODE-InternalDestroyer" } + if z > 2.0 { return "MODE-Fluctuating" } + "MODE-StableDisciplined" +} + +fn make_labels(sig: &Value, pattern: &str) -> Vec { + let label_map: HashMap<&str, HashMap<&str, &str>> = HashMap::from([ + ("P", HashMap::from([("HasPromise","7F-P-有承诺"),("NoPromise","7F-P-无承诺")])), + ("F", HashMap::from([("Fulfilled","7F-F-已兑现"),("Unfulfilled","7F-F-未兑现"),("Partial","7F-F-部分兑现")])), + ("E", HashMap::from([("Willing","7F-E-心甘情愿"),("Perfunctory","7F-E-敷衍"),("Resentful","7F-E-怨恨"),("Numb","7F-E-麻木")])), + ("A", HashMap::from([("Self","7F-A-自己"),("Partner","7F-A-伴侣"),("Family","7F-A-家庭"),("Outsider","7F-A-外人"),("Public","7F-A-公众")])), + ("X", HashMap::from([("OverExplain","7F-X-过度解释"),("Silent","7F-X-沉默"),("Genuine","7F-X-真诚"),("Indifferent","7F-X-冷漠")])), + ("Y", HashMap::from([("Changed","7F-Y-改正"),("Resisted","7F-Y-抗拒"),("Indifferent","7F-Y-无视"),("NoResponse","7F-Y-无响应")])), + ]); + let mut labels = Vec::new(); + for factor in &["P","F","E","A","X","Y"] { + if let Some(val) = sig[factor].as_str() { + if let Some(fm) = label_map.get(factor) { + if let Some(lb) = fm.get(val) { labels.push(lb.to_string()) } + } + } + } + labels.push(pattern.to_string()); + labels +} + +fn determine_color(pattern: &str, repeat: i32) -> &'static str { + if pattern == "MODE-InternalDestroyer" { return "🔴" } + if pattern == "MODE-Fluctuating" && repeat > 3 { return "🟡" } + if pattern == "MODE-DefensiveDefaulter" && repeat > 2 { return "🟡" } + "🟢" +} diff --git a/adapters/rust/src/dna_generator.rs b/adapters/rust/src/dna_generator.rs new file mode 100644 index 0000000..83804c1 --- /dev/null +++ b/adapters/rust/src/dna_generator.rs @@ -0,0 +1,65 @@ +use chrono::{DateTime, Utc, Timelike, Datelike}; +use sha2::{Sha256, Digest}; +use std::collections::HashMap; + +const TIAN_GAN: [&str; 10] = ["Jia","Yi","Bing","Ding","Wu","Ji","Geng","Xin","Ren","Gui"]; +const DI_ZHI: [&str; 12] = ["Zi","Chou","Yin","Mao","Chen","Si","Wu","Wei","Shen","You","Xu","Hai"]; +const SHI_CHEN: [&str; 12] = ["ZiShi","ChouShi","YinShi","MaoShi","ChenShi","SiShi","WuShi","WeiShi","ShenShi","YouShi","XuShi","HaiShi"]; + +pub fn generate_dna(uid: &str, device: &str, task_type: &str, action: &str, version: &str) -> String { + let now = Utc::now(); + let stem = compute_stem_branch(now); + let hexagram = select_hexagram(task_type); + let version = if version.is_empty() { "V1.0" } else { version }; + let body = format!("ADAPTER-{}-{}-{}", task_type.to_uppercase(), action.to_uppercase(), version); + let raw = format!("{}{}{}{}{}{}{}{}{}", stem.year, stem.month, stem.day, stem.shichen, hexagram.0, hexagram.1, body, device, now.to_rfc3339()); + let hash = Sha256::digest(raw.as_bytes()); + let hash8 = format!("{:x}", hash); + format!("#LongHun⚡️{}·{}·{}·{}·{}{}-{}-{}", stem.year, stem.month, stem.day, stem.shichen, hexagram.0, hexagram.1, body, &hash8[..8]) +} + +struct StemBranch { year: String, month: String, day: String, shichen: String } + +fn compute_stem_branch(dt: DateTime) -> StemBranch { + let cycle_year: i32 = 1984; + let cycle_month: [i32; 12] = [2,4,6,8,10,0,2,4,6,8,10,0]; + let y = dt.year(); let m = dt.month() as i32; let doy = dt.ordinal() as i32; let h = dt.hour() as i32; + + let ys = ((y - cycle_year) % 10 + 10) % 10; + let yb = ((y - cycle_year) % 12 + 12) % 12; + + let mb = cycle_month[((y - cycle_year) % 10 + 10) as usize % 10]; + let ms = ((mb + m - 1) % 10 + 10) % 10; + let mbr = ((m + 1) % 12 + 12) % 12; + + let ds = ((y - 1900 + (y - 1900) / 4 + doy) % 10 + 10) % 10; + let db = ((y - 1900 + (y - 1900) / 4 + doy) % 12 + 12) % 12; + + let si = (h / 2) as usize; + + StemBranch { + year: format!("{}{}", TIAN_GAN[ys as usize], DI_ZHI[yb as usize]), + month: format!("{}{}", TIAN_GAN[ms as usize], DI_ZHI[mbr as usize]), + day: format!("{}{}", TIAN_GAN[ds as usize], DI_ZHI[db as usize]), + shichen: SHI_CHEN[si].to_string(), + } +} + +fn select_hexagram(task_type: &str) -> (&'static str, &'static str) { + let domain = 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", + }; + let hexagrams: [(&str,&str,&str); 14] = [ + ("䷀","Qian","governance"),("䷁","Kun","archive"),("䷂","Zhun","init"), + ("䷃","Meng","learn"),("䷄","Xu","async"),("䷅","Song","legal"), + ("䷜","Kan","engine"),("䷝","Li","audit"),("䷲","Zhen","security"), + ("䷳","Gen","privacy"),("䷸","Xun","deploy"),("䷹","Dui","trust"), + ("䷾","JiJi","complete"),("䷿","WeiJi","progress"), + ]; + for h in hexagrams.iter() { if h.2 == domain { return (h.0, h.1) } } + (hexagrams[0].0, hexagrams[0].1) +} diff --git a/adapters/rust/src/lib.rs b/adapters/rust/src/lib.rs new file mode 100644 index 0000000..8df07bf --- /dev/null +++ b/adapters/rust/src/lib.rs @@ -0,0 +1,49 @@ +pub mod dna_generator; +pub mod audit_wrapper; +pub mod validator; + +use serde_json::Value; +use chrono::Utc; + +pub struct LongHunAdapter { + pub uid: String, + pub device: String, + pub locale: String, +} + +impl LongHunAdapter { + pub fn new(uid: &str, device: &str, locale: &str) -> Self { + LongHunAdapter { + uid: if uid.is_empty() { "9622".into() } else { uid.into() }, + device: if device.is_empty() { "HM-9622-001".into() } else { device.into() }, + locale: if locale.is_empty() { "Asia/Shanghai".into() } else { locale.into() }, + } + } + + pub fn wrap(&self, data: Value, task_type: &str, persona: &str, action: &str, version: &str) -> Value { + let task_type = if task_type.is_empty() { "default" } else { task_type }; + let persona = if persona.is_empty() { "P04" } else { persona }; + let action = if action.is_empty() { "WRAP" } else { action }; + let version = if version.is_empty() { "V1.0" } else { version }; + let dna = dna_generator::generate_dna(&self.uid, &self.device, task_type, action, version); + let audit = audit_wrapper::wrap_audit(&self.uid, &data, task_type, persona); + serde_json::json!({ + "dna":dna,"audit":audit,"payload":data,"meta":{ + "adapter_version":"1.0.0","uid":self.uid,"device":self.device, + "task_type":task_type,"persona":persona, + "generated_at":Utc::now().to_rfc3339(),"format":"longhun-v∞" + } + }) + } + + pub fn validate(&self, wrapped: &Value) -> Value { + let mut v = validator::Validator::new(); + v.validate(wrapped) + } + + pub fn get_schemas(&self) -> (Value, Value) { + let dna = serde_json::json!({"type":"string","description":"v∞ DNA traceability code"}); + let audit = serde_json::json!({"type":"object","required":["audit_version","uid","behavior_signature","behavior_pattern","behavior_labels","color"]}); + (dna, audit) + } +} diff --git a/adapters/rust/src/validator.rs b/adapters/rust/src/validator.rs new file mode 100644 index 0000000..8d249e7 --- /dev/null +++ b/adapters/rust/src/validator.rs @@ -0,0 +1,66 @@ +use regex::Regex; +use serde_json::Value; + +pub struct Validator { pub errors: Vec, pub warnings: Vec } + +impl Validator { + pub fn new() -> Self { Validator { errors: vec![], warnings: vec![] } } + + pub fn validate(&mut self, wrapped: &Value) -> serde_json::Value { + self.errors.clear(); self.warnings.clear(); + if !wrapped.is_object() { + self.errors.push("Not an object".into()); + return self.result(); + } + let dna_regex = Regex::new(r"^#LongHun⚡️([A-Z][a-zA-Z]+)·([A-Z][a-zA-Z]+)·([A-Z][a-zA-Z]+)·([A-Z][a-zA-Z]+)·([\u{4DC0}-\u{4DFF}][A-Za-z]+)-(.+)-([a-f0-9]{8})$").unwrap(); + + let required_top = ["dna","audit","payload","meta"]; + for k in &required_top { if wrapped.get(k).is_none() { self.errors.push(format!("Missing top key: {}", k)) } } + + if let Some(dna) = wrapped["dna"].as_str() { + if dna.is_empty() { self.errors.push("DNA empty".into()) } + else if !dna_regex.is_match(dna) { self.errors.push(format!("DNA regex fail: {}", &dna[..dna.len().min(60)])) } + } else { self.errors.push("DNA missing".into()) } + + if let Some(audit) = wrapped["audit"].as_object() { + let required_audit = ["audit_version","uid","behavior_signature","behavior_pattern","behavior_labels","color"]; + for k in &required_audit { if audit.get(*k).is_none() { self.errors.push(format!("Missing audit key: {}", k)) } } + if let Some(sig) = audit["behavior_signature"].as_object() { + let req_sig = ["P","F","T","E","C","R","A","X","Y","Z"]; + for k in &req_sig { if sig.get(*k).is_none() { self.errors.push(format!("Missing sig key: {}", k)) } } + self.validate_sig_values(sig); + } else { self.errors.push("sig not object".into()) } + + if let Some(p) = audit["behavior_pattern"].as_str() { + let valid = ["MODE-DefensiveDefaulter","MODE-ExternalTrustSpender","MODE-InternalDestroyer","MODE-Fluctuating","MODE-StableDisciplined"]; + if !valid.contains(&p) { self.warnings.push(format!("Unknown pattern: {}", p)) } + } + } else { self.errors.push("Audit not object".into()) } + + self.result() + } + + fn validate_sig_values(&mut self, sig: &serde_json::Map) { + let valid_p: Vec<&str> = vec!["HasPromise","NoPromise"]; + let valid_f: Vec<&str> = vec!["Fulfilled","Unfulfilled","Partial"]; + let valid_e: Vec<&str> = vec!["Willing","Perfunctory","Resentful","Numb"]; + let valid_a: Vec<&str> = vec!["Self","Partner","Family","Outsider","Public"]; + let valid_x: Vec<&str> = vec!["OverExplain","Silent","Genuine","Indifferent"]; + let valid_y: Vec<&str> = vec!["Changed","Resisted","Indifferent","NoResponse"]; + + if let Some(v) = sig.get("P").and_then(|x| x.as_str()) { if !valid_p.contains(&v) { self.warnings.push(format!("Invalid P: {}", v)) } } + if let Some(v) = sig.get("F").and_then(|x| x.as_str()) { if !valid_f.contains(&v) { self.warnings.push(format!("Invalid F: {}", v)) } } + if let Some(v) = sig.get("E").and_then(|x| x.as_str()) { if !valid_e.contains(&v) { self.warnings.push(format!("Invalid E: {}", v)) } } + if let Some(v) = sig.get("A").and_then(|x| x.as_str()) { if !valid_a.contains(&v) { self.warnings.push(format!("Invalid A: {}", v)) } } + if let Some(v) = sig.get("X").and_then(|x| x.as_str()) { if !valid_x.contains(&v) { self.warnings.push(format!("Invalid X: {}", v)) } } + if let Some(v) = sig.get("Y").and_then(|x| x.as_str()) { if !valid_y.contains(&v) { self.warnings.push(format!("Invalid Y: {}", v)) } } + } + + fn result(&self) -> serde_json::Value { + let valid = self.errors.is_empty(); + serde_json::json!({ + "valid":valid,"errors":self.errors,"warnings":self.warnings, + "summary":if valid { format!("✅ VALID") } else { format!("❌ INVALID") } + }) + } +} diff --git a/adapters/rust/tests/tests.rs b/adapters/rust/tests/tests.rs new file mode 100644 index 0000000..cc0481a --- /dev/null +++ b/adapters/rust/tests/tests.rs @@ -0,0 +1,66 @@ +use lh_standard_adapter::LongHunAdapter; +use serde_json::json; + +#[test] +fn test_dna_generation() { + let adapter = LongHunAdapter::new("9622","HM-9622-001",""); + let wrapped = adapter.wrap(json!({"test":1}), "code", "P04", "WRAP", ""); + let dna = wrapped["dna"].as_str().unwrap(); + assert!(dna.starts_with("#LongHun⚡️")); + assert!(dna.len() > 40); +} + +#[test] +fn test_audit_wrap() { + let adapter = LongHunAdapter::new("9622","HM-9622-001",""); + let wrapped = adapter.wrap(json!({"x":1}), "", "", "", ""); + let audit = &wrapped["audit"]; + assert_eq!(audit["audit_version"], "v1.0"); + assert_eq!(audit["uid"], "UID9622"); +} + +#[test] +fn test_validate_own_wrap() { + let adapter = LongHunAdapter::new("9622","HM-9622-001",""); + let wrapped = adapter.wrap(json!({}), "", "", "", ""); + let result = adapter.validate(&wrapped); + assert_eq!(result["valid"], true); +} + +#[test] +fn test_invalid_null() { + let adapter = LongHunAdapter::new("","",""); + let result = adapter.validate(&json!(null)); + assert_eq!(result["valid"], false); +} + +#[test] +fn test_missing_dna() { + let adapter = LongHunAdapter::new("","",""); + let result = adapter.validate(&json!({"audit":{},"payload":{},"meta":{}})); + assert_eq!(result["valid"], false); +} + +#[test] +fn test_preserves_payload() { + let adapter = LongHunAdapter::new("","",""); + let data = json!({"code":"print('hello')"}); + let wrapped = adapter.wrap(data.clone(), "", "", "", ""); + assert_eq!(wrapped["payload"], data); +} + +#[test] +fn test_get_schemas() { + let adapter = LongHunAdapter::new("","",""); + let (dna, audit) = adapter.get_schemas(); + assert!(dna.is_object()); + assert!(audit.is_object()); +} + +#[test] +fn test_uid_consistency() { + let adapter = LongHunAdapter::new("8888","",""); + let wrapped = adapter.wrap(json!({}), "", "", "", ""); + assert_eq!(wrapped["meta"]["uid"], "8888"); + assert_eq!(wrapped["audit"]["uid"], "UID8888"); +}