From 3666d317a026769a6ae049e9936e0ccdd5073d1b Mon Sep 17 00:00:00 2001 From: johneapen80 Date: Sun, 26 Jul 2026 18:28:08 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20Ruby=20adapter=20=E2=80=94=20lh=5Fstand?= =?UTF-8?q?ard=5Fadapter=20for=20RubyGems=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby gem: DNAGenerator, AuditWrapper, Validator, LongHunAdapter. - Zero external dependencies (stdlib only: digest, json, time) - Minitest test suite (12 tests) - DNA format byte-compatible with Python reference --- adapters/ruby/lh-standard-adapter.gemspec | 16 +++ adapters/ruby/lib/lh_standard_adapter.rb | 45 +++++++++ .../lib/lh_standard_adapter/audit_wrapper.rb | 67 +++++++++++++ .../lib/lh_standard_adapter/dna_generator.rb | 76 +++++++++++++++ .../ruby/lib/lh_standard_adapter/validator.rb | 97 +++++++++++++++++++ adapters/ruby/test/test_adapter.rb | 96 ++++++++++++++++++ 6 files changed, 397 insertions(+) create mode 100644 adapters/ruby/lh-standard-adapter.gemspec create mode 100644 adapters/ruby/lib/lh_standard_adapter.rb create mode 100644 adapters/ruby/lib/lh_standard_adapter/audit_wrapper.rb create mode 100644 adapters/ruby/lib/lh_standard_adapter/dna_generator.rb create mode 100644 adapters/ruby/lib/lh_standard_adapter/validator.rb create mode 100644 adapters/ruby/test/test_adapter.rb diff --git a/adapters/ruby/lh-standard-adapter.gemspec b/adapters/ruby/lh-standard-adapter.gemspec new file mode 100644 index 0000000..4463593 --- /dev/null +++ b/adapters/ruby/lh-standard-adapter.gemspec @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +Gem::Specification.new do |spec| + spec.name = "lh-standard-adapter" + spec.version = "1.0.0" + spec.authors = ["LongHun Core"] + spec.summary = "LongHun Standard Adapter — v∞ DNA traceability + seven-factor behavioral audit for Ruby" + spec.description = "Ruby implementation of the LongHun standard adapter with DNA generation, audit wrapping, and validation." + spec.license = "CC-BY-NC-SA-4.0" + spec.required_ruby_version = ">= 2.7" + + spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE"] + spec.require_paths = ["lib"] + + spec.add_development_dependency "minitest", "~> 5.0" +end diff --git a/adapters/ruby/lib/lh_standard_adapter.rb b/adapters/ruby/lib/lh_standard_adapter.rb new file mode 100644 index 0000000..65379c2 --- /dev/null +++ b/adapters/ruby/lib/lh_standard_adapter.rb @@ -0,0 +1,45 @@ +require_relative "lh_standard_adapter/dna_generator" +require_relative "lh_standard_adapter/audit_wrapper" +require_relative "lh_standard_adapter/validator" + +module LhStandardAdapter + VERSION = "1.0.0" + + class LongHunAdapter + attr_reader :uid, :device, :locale + + def initialize(uid: "9622", device: "HM-9622-001", locale: "Asia/Shanghai") + @uid = uid; @device = device; @locale = locale + @dna_gen = DNAGenerator.new(uid: uid, device: device, locale: locale) + @audit = AuditWrapper.new(uid: uid) + @validator = Validator.new + end + + def wrap(data, task_type: "default", persona: "P04", action: "WRAP", version: nil) + dna = @dna_gen.generate(task_type: task_type, action: action, version: version) + audit = @audit.wrap(data, task_type: task_type, persona: persona) + { + dna: dna, audit: audit, payload: data, + meta: { + adapter_version: VERSION, uid: @uid, device: @device, + task_type: task_type, persona: persona, + generated_at: Time.now.utc.iso8601, format: "longhun-v∞" + } + } + end + + def validate(wrapped) + @validator.validate(wrapped) + end + + def schemas + { + dna_schema: { type: "string", description: "v∞ DNA traceability code" }, + audit_schema: { + type: "object", + required: %w[audit_version uid behavior_signature behavior_pattern behavior_labels color] + } + } + end + end +end diff --git a/adapters/ruby/lib/lh_standard_adapter/audit_wrapper.rb b/adapters/ruby/lib/lh_standard_adapter/audit_wrapper.rb new file mode 100644 index 0000000..645ff85 --- /dev/null +++ b/adapters/ruby/lib/lh_standard_adapter/audit_wrapper.rb @@ -0,0 +1,67 @@ +require "digest" +require "json" +require "time" + +module LhStandardAdapter + LABEL_MAP = { + "P" => {"HasPromise"=>"7F-P-有承诺","NoPromise"=>"7F-P-无承诺"}, + "F" => {"Fulfilled"=>"7F-F-已兑现","Unfulfilled"=>"7F-F-未兑现","Partial"=>"7F-F-部分兑现"}, + "E" => {"Willing"=>"7F-E-心甘情愿","Perfunctory"=>"7F-E-敷衍","Resentful"=>"7F-E-怨恨","Numb"=>"7F-E-麻木"}, + "A" => {"Self"=>"7F-A-自己","Partner"=>"7F-A-伴侣","Family"=>"7F-A-家庭","Outsider"=>"7F-A-外人","Public"=>"7F-A-公众"}, + "X" => {"OverExplain"=>"7F-X-过度解释","Silent"=>"7F-X-沉默","Genuine"=>"7F-X-真诚","Indifferent"=>"7F-X-冷漠"}, + "Y" => {"Changed"=>"7F-Y-改正","Resisted"=>"7F-Y-抗拒","Indifferent"=>"7F-Y-无视","NoResponse"=>"7F-Y-无响应"} + }.freeze + + class AuditWrapper + def initialize(uid: "9622") + @uid = uid + end + + def wrap(payload, task_type: "default", persona: "P04") + task_type = "default" if task_type.nil? || task_type.empty? + persona = "P04" if persona.nil? || persona.empty? + + sig = {"P"=>"HasPromise","F"=>"Fulfilled","T"=>0.0,"E"=>"Willing", + "C"=>0,"R"=>0,"A"=>"Self","X"=>"Genuine","Y"=>"NoResponse","Z"=>1.0} + pattern = classify(sig) + labels = make_labels(sig, pattern) + color = determine_color(pattern, sig["R"]) + payload_json = JSON.generate(payload) + payload_hash = Digest::SHA256.hexdigest(payload_json)[0,16] + + { + "audit_version"=>"v1.0","uid"=>"UID#{@uid}","persona"=>persona,"task_type"=>task_type, + "behavior_signature"=>sig,"behavior_pattern"=>pattern,"behavior_labels"=>labels, + "color"=>color,"timestamp"=>Time.now.utc.iso8601,"payload_hash"=>payload_hash + } + end + + private + + def classify(sig) + f, x, a, y, z = sig["F"], sig["X"], sig["A"], sig["Y"], sig["Z"] + return "MODE-DefensiveDefaulter" if f == "Unfulfilled" && x == "OverExplain" + return "MODE-ExternalTrustSpender" if f == "Fulfilled" && a == "Outsider" + return "MODE-InternalDestroyer" if f == "Unfulfilled" && y == "Indifferent" + return "MODE-Fluctuating" if z > 2.0 + "MODE-StableDisciplined" + end + + def make_labels(sig, pattern) + labels = [] + %w[P F E A X Y].each do |factor| + val = sig[factor] + labels << LABEL_MAP[factor][val] if LABEL_MAP[factor] && LABEL_MAP[factor][val] + end + labels << pattern + labels + end + + def determine_color(pattern, repeat) + return "🔴" if pattern == "MODE-InternalDestroyer" + return "🟡" if pattern == "MODE-Fluctuating" && repeat > 3 + return "🟡" if pattern == "MODE-DefensiveDefaulter" && repeat > 2 + "🟢" + end + end +end diff --git a/adapters/ruby/lib/lh_standard_adapter/dna_generator.rb b/adapters/ruby/lib/lh_standard_adapter/dna_generator.rb new file mode 100644 index 0000000..cfeb4b4 --- /dev/null +++ b/adapters/ruby/lib/lh_standard_adapter/dna_generator.rb @@ -0,0 +1,76 @@ +require "digest" +require "time" + +module LhStandardAdapter + TIAN_GAN = %w[Jia Yi Bing Ding Wu Ji Geng Xin Ren Gui].freeze + DI_ZHI = %w[Zi Chou Yin Mao Chen Si Wu Wei Shen You Xu Hai].freeze + SHI_CHEN = %w[ZiShi ChouShi YinShi MaoShi ChenShi SiShi WuShi WeiShi ShenShi YouShi XuShi HaiShi].freeze + + HEXAGRAMS = [ + {symbol:"䷀",en:"Qian",cn:"乾",domain:"governance"}, + {symbol:"䷁",en:"Kun",cn:"坤",domain:"archive"}, + {symbol:"䷂",en:"Zhun",cn:"屯",domain:"init"}, + {symbol:"䷃",en:"Meng",cn:"蒙",domain:"learn"}, + {symbol:"䷄",en:"Xu",cn:"需",domain:"async"}, + {symbol:"䷅",en:"Song",cn:"讼",domain:"legal"}, + {symbol:"䷜",en:"Kan",cn:"坎",domain:"engine"}, + {symbol:"䷝",en:"Li",cn:"离",domain:"audit"}, + {symbol:"䷲",en:"Zhen",cn:"震",domain:"security"}, + {symbol:"䷳",en:"Gen",cn:"艮",domain:"privacy"}, + {symbol:"䷸",en:"Xun",cn:"巽",domain:"deploy"}, + {symbol:"䷹",en:"Dui",cn:"兑",domain:"trust"}, + {symbol:"䷾",en:"JiJi",cn:"既济",domain:"complete"}, + {symbol:"䷿",en:"WeiJi",cn:"未济",domain:"progress"} + ].freeze + + TASK_HEXAGRAM_MAP = { + "default"=>"governance","code"=>"engine","deploy"=>"deploy","audit"=>"audit", + "security"=>"security","archive"=>"archive","init"=>"init","learn"=>"learn", + "legal"=>"legal","privacy"=>"privacy","trust"=>"trust","complete"=>"complete","progress"=>"progress" + }.freeze + + class DNAGenerator + CYCLE_YEAR = 1984 + CYCLE_MONTH = [2,4,6,8,10,0,2,4,6,8,10,0].freeze + + def initialize(uid: "9622", device: "HM-9622-001", locale: "Asia/Shanghai") + @uid, @device, @locale = uid, device, locale + end + + def generate(task_type: "default", action: "WRAP", version: nil) + task_type = "default" if task_type.nil? || task_type.empty? + action = "WRAP" if action.nil? || action.empty? + version = "V1.0" if version.nil? || version.empty? + + now = Time.now.utc + stem = compute_stem_branch(now) + hex = select_hexagram(task_type) + body = "ADAPTER-#{task_type.upcase}-#{action.upcase}-#{version}" + raw = "#{stem[:year]}#{stem[:month]}#{stem[:day]}#{stem[:shichen]}#{hex[:symbol]}#{hex[:en]}#{body}#{@device}#{now.iso8601}" + hash8 = Digest::SHA256.hexdigest(raw)[0,8] + "#LongHun⚡️#{stem[:year]}·#{stem[:month]}·#{stem[:day]}·#{stem[:shichen]}·#{hex[:symbol]}#{hex[:en]}-#{body}-#{hash8}" + end + + private + + def compute_stem_branch(dt) + y, m, doy, h = dt.year, dt.month, dt.yday, dt.hour + ys = (y - CYCLE_YEAR) % 10; yb = (y - CYCLE_YEAR) % 12 + mb = CYCLE_MONTH[(y - CYCLE_YEAR) % 10] + ms = (mb + m - 1) % 10; mbr = (m + 1) % 12 + ds = (y - 1900 + (y - 1900) / 4 + doy) % 10 + db = (y - 1900 + (y - 1900) / 4 + doy) % 12 + si = h / 2 + + { year: TIAN_GAN[ys] + DI_ZHI[yb], + month: TIAN_GAN[ms] + DI_ZHI[mbr], + day: TIAN_GAN[ds] + DI_ZHI[db], + shichen: SHI_CHEN[si] } + end + + def select_hexagram(task_type) + domain = TASK_HEXAGRAM_MAP[task_type] || "governance" + HEXAGRAMS.find { |h| h[:domain] == domain } || HEXAGRAMS.first + end + end +end diff --git a/adapters/ruby/lib/lh_standard_adapter/validator.rb b/adapters/ruby/lib/lh_standard_adapter/validator.rb new file mode 100644 index 0000000..d58b629 --- /dev/null +++ b/adapters/ruby/lib/lh_standard_adapter/validator.rb @@ -0,0 +1,97 @@ +module LhStandardAdapter + DNA_REGEX = /^#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})$/ + + REQUIRED_TOP = %w[dna audit payload meta].freeze + REQUIRED_AUDIT = %w[audit_version uid behavior_signature behavior_pattern behavior_labels color].freeze + REQUIRED_SIG = %w[P F T E C R A X Y Z].freeze + + VALID_P = %w[HasPromise NoPromise].freeze + VALID_F = %w[Fulfilled Unfulfilled Partial].freeze + VALID_E = %w[Willing Perfunctory Resentful Numb].freeze + VALID_A = %w[Self Partner Family Outsider Public].freeze + VALID_X = %w[OverExplain Silent Genuine Indifferent].freeze + VALID_Y = %w[Changed Resisted Indifferent NoResponse].freeze + VALID_COLORS = %w[🟢 🟡 🔴].freeze + VALID_PATTERNS = %w[MODE-DefensiveDefaulter MODE-ExternalTrustSpender MODE-InternalDestroyer MODE-Fluctuating MODE-StableDisciplined].freeze + + class Validator + attr_reader :errors, :warnings + + def initialize + @errors = []; @warnings = [] + end + + def validate(wrapped) + @errors.clear; @warnings.clear + unless wrapped.is_a?(Hash) && !wrapped.empty? + @errors << "Input not a non-empty Hash" + return result + end + + missing = REQUIRED_TOP - wrapped.keys + @errors << "Missing top keys: #{missing.join(',')}" unless missing.empty? + + dna = wrapped["dna"] || wrapped[:dna] || "" + if dna.empty? + @errors << "DNA empty" + elsif !DNA_REGEX.match?(dna) + @errors << "DNA regex fail: #{dna[0..59]}" + end + + audit = wrapped["audit"] || wrapped[:audit] || {} + if audit.is_a?(Hash) && !audit.empty? + validate_audit(audit) + else + @errors << "Audit not a Hash" + end + + result + end + + private + + def validate_audit(audit) + missing = REQUIRED_AUDIT - audit.keys.map(&:to_s) + @errors << "Missing audit keys: #{missing.join(',')}" unless missing.empty? + + sig = audit["behavior_signature"] || audit[:behavior_signature] || {} + if sig.is_a?(Hash) + missing_sig = REQUIRED_SIG - sig.keys.map(&:to_s) + @errors << "Missing sig keys: #{missing_sig.join(',')}" unless missing_sig.empty? + validate_sig_values(sig) + else + @errors << "sig not Hash" + end + + validate_optional(audit) + end + + def validate_sig_values(sig) + @warnings << "Invalid P: #{sig['P']||sig[:P]}" unless VALID_P.include?(sig["P"] || sig[:P]) + @warnings << "Invalid F: #{sig['F']||sig[:F]}" unless VALID_F.include?(sig["F"] || sig[:F]) + @warnings << "Invalid E: #{sig['E']||sig[:E]}" unless VALID_E.include?(sig["E"] || sig[:E]) + @warnings << "Invalid A: #{sig['A']||sig[:A]}" unless VALID_A.include?(sig["A"] || sig[:A]) + @warnings << "Invalid X: #{sig['X']||sig[:X]}" unless VALID_X.include?(sig["X"] || sig[:X]) + @warnings << "Invalid Y: #{sig['Y']||sig[:Y]}" unless VALID_Y.include?(sig["Y"] || sig[:Y]) + end + + def validate_optional(audit) + pattern = audit["behavior_pattern"] || audit[:behavior_pattern] || "" + @warnings << "Unknown pattern: #{pattern}" if !pattern.empty? && !VALID_PATTERNS.include?(pattern) + + color = audit["color"] || audit[:color] + @warnings << "Unknown color: #{color}" if color && !VALID_COLORS.include?(color) + + ph = audit["payload_hash"] || audit[:payload_hash] + @warnings << "Bad payload_hash: #{ph}" if ph && ph.length != 16 + end + + def result + valid = @errors.empty? + { + valid: valid, errors: @errors, warnings: @warnings, + summary: valid ? "✅ VALID — #{@warnings.length} warning(s)" : "❌ INVALID — #{@errors.length} error(s)" + } + end + end +end diff --git a/adapters/ruby/test/test_adapter.rb b/adapters/ruby/test/test_adapter.rb new file mode 100644 index 0000000..f051a06 --- /dev/null +++ b/adapters/ruby/test/test_adapter.rb @@ -0,0 +1,96 @@ +require "minitest/autorun" +require_relative "../lib/lh_standard_adapter" + +class TestDNAGenerator < Minitest::Test + def test_instance + g = LhStandardAdapter::DNAGenerator.new + assert_equal "9622", g.instance_variable_get(:@uid) + end + + def test_generate_prefix + g = LhStandardAdapter::DNAGenerator.new + dna = g.generate + assert dna.start_with?("#LongHun⚡️") + end + + def test_generate_hash8 + g = LhStandardAdapter::DNAGenerator.new + dna = g.generate + hash8 = dna.split("-").last + assert_match(/^[a-f0-9]{8}$/, hash8) + end + + def test_generate_unique + g = LhStandardAdapter::DNAGenerator.new + dnas = 10.times.map { |i| g.generate(task_type:"code", action:"GEN", version:"V#{i}") } + assert_equal 10, dnas.uniq.length + end +end + +class TestAuditWrapper < Minitest::Test + def test_wrap_version + w = LhStandardAdapter::AuditWrapper.new + r = w.wrap({"x"=>1}) + assert_equal "v1.0", r["audit_version"] + assert_equal "UID9622", r["uid"] + end + + def test_wrap_signature + w = LhStandardAdapter::AuditWrapper.new + sig = w.wrap({})["behavior_signature"] + %w[P F T E C R A X Y Z].each { |k| assert sig.key?(k), "missing #{k}" } + end + + def test_default_pattern + w = LhStandardAdapter::AuditWrapper.new + assert_equal "MODE-StableDisciplined", w.wrap({})["behavior_pattern"] + end +end + +class TestValidator < Minitest::Test + def test_nil_invalid + v = LhStandardAdapter::Validator.new + assert_equal false, v.validate(nil)[:valid] + end + + def test_valid_passes + a = LhStandardAdapter::LongHunAdapter.new + wrapped = a.wrap({"h"=>"w"}) + r = LhStandardAdapter::Validator.new.validate(wrapped) + assert_equal true, r[:valid], r[:summary] + end +end + +class TestLongHunAdapter < Minitest::Test + def test_instance + a = LhStandardAdapter::LongHunAdapter.new + assert_equal "9622", a.uid + assert_equal "HM-9622-001", a.device + end + + def test_wrap_structure + a = LhStandardAdapter::LongHunAdapter.new + r = a.wrap({"code"=>"test"}) + %w[dna audit payload meta].each { |k| assert r.key?(k.to_sym), "missing #{k}" } + end + + def test_wrap_preserves_payload + a = LhStandardAdapter::LongHunAdapter.new + data = {"code"=>"print('hello')"} + r = a.wrap(data) + assert_equal data, r[:payload] + end + + def test_self_validate + a = LhStandardAdapter::LongHunAdapter.new + r = a.validate(a.wrap({})) + assert_equal true, r[:valid], r[:summary] + end + + def test_get_schemas + a = LhStandardAdapter::LongHunAdapter.new + schemas = a.schemas + assert schemas.key?(:dna_schema) + assert schemas.key?(:audit_schema) + end +end