Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions adapters/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
72 changes: 72 additions & 0 deletions adapters/rust/src/audit_wrapper.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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 "🟡" }
"🟢"
}
65 changes: 65 additions & 0 deletions adapters/rust/src/dna_generator.rs
Original file line number Diff line number Diff line change
@@ -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<Utc>) -> 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)
}
49 changes: 49 additions & 0 deletions adapters/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
66 changes: 66 additions & 0 deletions adapters/rust/src/validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use regex::Regex;
use serde_json::Value;

pub struct Validator { pub errors: Vec<String>, pub warnings: Vec<String> }

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<String, Value>) {
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") }
})
}
}
66 changes: 66 additions & 0 deletions adapters/rust/tests/tests.rs
Original file line number Diff line number Diff line change
@@ -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");
}