From 4b85acb40543f305e1fb091f89a2e1646263e26b Mon Sep 17 00:00:00 2001 From: wangye Date: Sat, 25 Jul 2026 08:59:44 +0800 Subject: [PATCH] feat: add TypeScript adapter for lh_standard_adapter [FaaFyfxR9WAQrL7FcAgEHJvztd8cVMxvjHRS55rw1nwH] - Full TypeScript port of DNAGenerator, AuditWrapper, and Validator - ESM + CommonJS dual exports via package.json - Type definitions exported for all public interfaces - 3 test suites with 20+ tests (DNA generation, audit wrapping, validation) - tsconfig with strict mode, declaration maps, and source maps --- adapters/typescript/README.md | 11 + adapters/typescript/package.json | 43 ++++ adapters/typescript/src/AuditWrapper.ts | 160 ++++++++++++++ adapters/typescript/src/DNAGenerator.ts | 174 +++++++++++++++ adapters/typescript/src/Validator.ts | 205 ++++++++++++++++++ adapters/typescript/src/index.ts | 17 ++ adapters/typescript/src/types.ts | 57 +++++ .../typescript/tests/AuditWrapper.test.ts | 49 +++++ .../typescript/tests/DNAGenerator.test.ts | 52 +++++ adapters/typescript/tests/Validator.test.ts | 75 +++++++ adapters/typescript/tsconfig.json | 21 ++ 11 files changed, 864 insertions(+) create mode 100644 adapters/typescript/README.md create mode 100644 adapters/typescript/package.json create mode 100644 adapters/typescript/src/AuditWrapper.ts create mode 100644 adapters/typescript/src/DNAGenerator.ts create mode 100644 adapters/typescript/src/Validator.ts create mode 100644 adapters/typescript/src/index.ts create mode 100644 adapters/typescript/src/types.ts create mode 100644 adapters/typescript/tests/AuditWrapper.test.ts create mode 100644 adapters/typescript/tests/DNAGenerator.test.ts create mode 100644 adapters/typescript/tests/Validator.test.ts create mode 100644 adapters/typescript/tsconfig.json diff --git a/adapters/typescript/README.md b/adapters/typescript/README.md new file mode 100644 index 0000000..1fc6d81 --- /dev/null +++ b/adapters/typescript/README.md @@ -0,0 +1,11 @@ +```bash +# Install dependencies +cd adapters/typescript +npm install + +# Build +npm run build + +# Run tests +npm test +``` \ No newline at end of file diff --git a/adapters/typescript/package.json b/adapters/typescript/package.json new file mode 100644 index 0000000..0c255ed --- /dev/null +++ b/adapters/typescript/package.json @@ -0,0 +1,43 @@ +{ + "name": "@uid9622/lh-standard-adapter", + "version": "1.0.0", + "description": "LongHun (龍魂) v∞ DNA Traceability Standard — TypeScript Adapter", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "scripts": { + "build": "tsc", + "test": "vitest run", + "test:watch": "vitest", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "longhun", + "dna-traceability", + "audit", + "龍魂" + ], + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.4.0", + "vitest": "^1.6.0", + "@types/node": "^20.0.0" + } +} \ No newline at end of file diff --git a/adapters/typescript/src/AuditWrapper.ts b/adapters/typescript/src/AuditWrapper.ts new file mode 100644 index 0000000..4674bd0 --- /dev/null +++ b/adapters/typescript/src/AuditWrapper.ts @@ -0,0 +1,160 @@ +/** + * Audit Wrapper — seven-factor behavioral audit metadata generation. + * + * DNA: #LongHun⚡️BingWu·GuiWei·JiaZi·ZiShi·䷝Li-AUDIT-WRAPPER-v1.0.0 + */ + +import { createHash } from 'node:crypto'; +import type { + AuditWrapperResult, + BehaviorPattern, + BehaviorSignature, +} from './types.js'; + +// ── Seven-Factor Value Sets ────────────────────────────────────────────────── + +const P_VALUES = ['HasPromise', 'NoPromise'] as const; +const F_VALUES = ['Fulfilled', 'Unfulfilled', 'Partial'] as const; +const E_VALUES = ['Willing', 'Perfunctory', 'Resentful', 'Numb'] as const; +const A_VALUES = ['Self', 'Partner', 'Family', 'Outsider', 'Public'] as const; +const X_VALUES = ['OverExplain', 'Silent', 'Genuine', 'Indifferent'] as const; +const Y_VALUES = ['Changed', 'Resisted', 'Indifferent', 'NoResponse'] as const; + +// ── Behavior Pattern Classification ────────────────────────────────────────── + +const PATTERNS: Record = { + 'MODE-DefensiveDefaulter': 'Promises fail + over-explains to deflect', + 'MODE-ExternalTrustSpender': 'Keeps promises to outsiders at inner-circle expense', + 'MODE-InternalDestroyer': 'Breaks promises with indifference, no correction', + 'MODE-Fluctuating': 'High volatility in commitment-to-fulfillment ratio', + 'MODE-StableDisciplined': 'Consistent, reliable execution', +}; + +// ── Factor → Label Mapping ─────────────────────────────────────────────────── + +const LABEL_MAP: Record> = { + 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-无响应' }, +}; + +// ── Wrapper ────────────────────────────────────────────────────────────────── + +export class AuditWrapper { + private readonly uid: string; + + constructor(uid = '9622') { + this.uid = uid; + } + + /** + * Generate audit wrapper with seven-factor signature. + * + * @param payload - Raw data to wrap + * @param taskType - Task category + * @param persona - Persona identifier + * @returns Audit metadata + */ + wrap(payload: unknown, taskType = 'default', persona = 'P04'): AuditWrapperResult { + const now = new Date(); + + // Default signature (StableDisciplined baseline) + const signature: BehaviorSignature = { + P: 'HasPromise', + F: 'Fulfilled', + T: 0.0, + E: 'Willing', + C: 0, + R: 0, + A: 'Self', + X: 'Genuine', + Y: 'NoResponse', + Z: 1.0, + }; + + const pattern = this.classify(signature); + const labels = this.makeLabels(signature, pattern); + const color = this.determineColor(pattern, signature.R); + + // Payload hash (not for crypto, for integrity check) + const payloadJson = JSON.stringify(payload, Object.keys(payload as object).sort()); + const payloadHash = createHash('sha256').update(payloadJson, 'utf-8').digest('hex').slice(0, 16); + + return { + audit_version: 'v1.0', + uid: `UID${this.uid}`, + persona, + task_type: taskType, + behavior_signature: signature, + behavior_pattern: pattern, + behavior_labels: labels, + color, + timestamp: now.toISOString(), + payload_hash: payloadHash, + }; + } + + /** + * Classify seven-factor signature into behavior pattern. + */ + private classify(sig: BehaviorSignature): BehaviorPattern { + const fVal = sig.F; + const xVal = sig.X; + const aVal = sig.A; + const yVal = sig.Y; + const zVal = sig.Z; + + if (fVal === 'Unfulfilled' && xVal === 'OverExplain') { + return 'MODE-DefensiveDefaulter'; + } + if (fVal === 'Fulfilled' && aVal === 'Outsider') { + return 'MODE-ExternalTrustSpender'; + } + if (fVal === 'Unfulfilled' && yVal === 'Indifferent') { + return 'MODE-InternalDestroyer'; + } + if (zVal > 2.0) { + return 'MODE-Fluctuating'; + } + return 'MODE-StableDisciplined'; + } + + /** + * Generate bilingual behavior labels from signature. + */ + private makeLabels(sig: BehaviorSignature, pattern: BehaviorPattern): string[] { + const labels: string[] = []; + for (const factor of ['P', 'F', 'E', 'A', 'X', 'Y'] as const) { + const val = sig[factor] as string; + if (LABEL_MAP[factor]?.[val]) { + labels.push(LABEL_MAP[factor][val]); + } + } + labels.push(pattern); + return labels; + } + + /** + * Determine three-color audit tag. + */ + private determineColor(pattern: BehaviorPattern, repeat: number): '🟢' | '🟡' | '🔴' { + if (pattern === 'MODE-InternalDestroyer') return '🔴'; + if (pattern === 'MODE-Fluctuating' && repeat > 3) return '🟡'; + if (pattern === 'MODE-DefensiveDefaulter' && repeat > 2) return '🟡'; + return '🟢'; + } +} + +// ── Convenience singleton ──────────────────────────────────────────────────── + +const defaultWrapper = new AuditWrapper(); + +/** + * Quick one-shot audit wrapper. + */ +export function auditWrap(payload: unknown, taskType = 'default', persona = 'P04'): AuditWrapperResult { + return defaultWrapper.wrap(payload, taskType, persona); +} \ No newline at end of file diff --git a/adapters/typescript/src/DNAGenerator.ts b/adapters/typescript/src/DNAGenerator.ts new file mode 100644 index 0000000..be4bb88 --- /dev/null +++ b/adapters/typescript/src/DNAGenerator.ts @@ -0,0 +1,174 @@ +/** + * DNA Generator — v∞ format traceability code generation. + * + * DNA: #LongHun⚡️BingWu·GuiWei·JiaZi·ZiShi·䷾JiJi-DNA-GENERATOR-v1.0.0 + */ + +import { createHash } from 'node:crypto'; +import type { Hexagram, StemBranch } from './types.js'; + +// ── Heavenly Stems and Earthly Branches ────────────────────────────────────── + +const TIAN_GAN = ['Jia', 'Yi', 'Bing', 'Ding', 'Wu', 'Ji', 'Geng', 'Xin', 'Ren', 'Gui']; +const DI_ZHI = ['Zi', 'Chou', 'Yin', 'Mao', 'Chen', 'Si', 'Wu', 'Wei', 'Shen', 'You', 'Xu', 'Hai']; +const SHI_CHEN = ['ZiShi', 'ChouShi', 'YinShi', 'MaoShi', 'ChenShi', 'SiShi', 'WuShi', 'WeiShi', 'ShenShi', 'YouShi', 'XuShi', 'HaiShi']; + +// ── I Ching Hexagrams ──────────────────────────────────────────────────────── + +const HEXAGRAMS: Hexagram[] = [ + { symbol: '䷀', enName: 'Qian', cnName: '乾', domain: 'governance' }, + { symbol: '䷁', enName: 'Kun', cnName: '坤', domain: 'archive' }, + { symbol: '䷂', enName: 'Zhun', cnName: '屯', domain: 'init' }, + { symbol: '䷃', enName: 'Meng', cnName: '蒙', domain: 'learn' }, + { symbol: '䷄', enName: 'Xu', cnName: '需', domain: 'async' }, + { symbol: '䷅', enName: 'Song', cnName: '讼', domain: 'legal' }, + { symbol: '䷜', enName: 'Kan', cnName: '坎', domain: 'engine' }, + { symbol: '䷝', enName: 'Li', cnName: '离', domain: 'audit' }, + { symbol: '䷲', enName: 'Zhen', cnName: '震', domain: 'security' }, + { symbol: '䷳', enName: 'Gen', cnName: '艮', domain: 'privacy' }, + { symbol: '䷸', enName: 'Xun', cnName: '巽', domain: 'deploy' }, + { symbol: '䷹', enName: 'Dui', cnName: '兑', domain: 'trust' }, + { symbol: '䷾', enName: 'JiJi', cnName: '既济', domain: 'complete' }, + { symbol: '䷿', enName: 'WeiJi', cnName: '未济', domain: 'progress' }, +]; + +// ── Task-to-hexagram domain mapping ────────────────────────────────────────── + +const TASK_HEXAGRAM_MAP: Record = { + 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', +}; + +// ── Month stem offsets ─────────────────────────────────────────────────────── + +const CYCLE_YEAR = 1984; // JiaZi year reference +const CYCLE_MONTH = [2, 4, 6, 8, 10, 0, 2, 4, 6, 8, 10, 0]; + +// ── Generator ──────────────────────────────────────────────────────────────── + +export class DNAGenerator { + private readonly uid: string; + private readonly device: string; + + constructor(uid = '9622', device = 'HM-9622-001') { + this.uid = uid; + this.device = device; + } + + /** + * Generate a full DNA traceability string. + * + * Format: #LongHun⚡️{StemBranch}·{Hexagram}-{ModulePath}-{Hash8} + * + * @param taskType - Task category (default, code, audit, etc.) + * @param action - Action type (WRAP, VALIDATE, etc.) + * @param version - Optional version string (default: V1.0) + * @returns DNA traceability code string + */ + generate(taskType = 'default', action = 'WRAP', version?: string): string { + const now = new Date(); + const stem = this.computeStemBranch(now); + const hexagram = this.selectHexagram(taskType); + const ver = version ?? 'V1.0'; + const body = `ADAPTER-${taskType.toUpperCase()}-${action.toUpperCase()}-${ver}`; + + const raw = `${stem.year}${stem.month}${stem.day}${stem.shichen}` + + `${hexagram.symbol}${hexagram.enName}${body}${this.device}${now.toISOString()}`; + + const hash8 = createHash('sha256').update(raw, 'utf-8').digest('hex').slice(0, 8); + + return `#LongHun⚡️${stem.year}·${stem.month}·${stem.day}·${stem.shichen}` + + `·${hexagram.symbol}${hexagram.enName}-${body}-${hash8}`; + } + + /** + * Compute Heavenly Stem + Earthly Branch for a given datetime. + */ + private computeStemBranch(dt: Date): StemBranch { + const year = dt.getFullYear(); + const month = dt.getMonth() + 1; // 1-indexed + const day = dt.getDate(); + const hour = dt.getHours(); + + // Year pillar + const yearStemIdx = (year - CYCLE_YEAR) % 10; + const yearBranchIdx = (year - CYCLE_YEAR) % 12; + + // Month pillar + const cycleIdx = (year - CYCLE_YEAR) % 10; + const monthStemBase = CYCLE_MONTH[cycleIdx]; + const monthStemIdx = (monthStemBase + (month - 1)) % 10; + const monthBranchIdx = (month + 1) % 12; + + // Day pillar (approximate — use a proper astronomical library for precision) + const dayOff = this.dayOffset(year, month, day); + const dayStemIdx = dayOff % 10; + const dayBranchIdx = dayOff % 12; + + // Shichen (2-hour period) + const shichenIdx = Math.floor(hour / 2); + + return { + year: TIAN_GAN[this.posMod(yearStemIdx, 10)] + DI_ZHI[this.posMod(yearBranchIdx, 12)], + month: TIAN_GAN[this.posMod(monthStemIdx, 10)] + DI_ZHI[this.posMod(monthBranchIdx, 12)], + day: TIAN_GAN[this.posMod(dayStemIdx, 10)] + DI_ZHI[this.posMod(dayBranchIdx, 12)], + shichen: SHI_CHEN[shichenIdx], + }; + } + + /** + * Approximate day offset from a reference date for stem-branch calculation. + */ + private dayOffset(year: number, month: number, day: number): number { + // Reference: 1900-01-01 is JiaZi day (day 0) + let total = 0; + for (let y = 1900; y < year; y++) { + total += this.isLeapYear(y) ? 366 : 365; + } + const daysInMonth = [31, this.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + for (let m = 0; m < month - 1; m++) { + total += daysInMonth[m]; + } + total += day - 1; + return total; + } + + private isLeapYear(y: number): boolean { + return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0); + } + + private posMod(n: number, m: number): number { + return ((n % m) + m) % m; + } + + /** + * Select I Ching hexagram based on task type. + */ + private selectHexagram(taskType: string): Hexagram { + const domain = TASK_HEXAGRAM_MAP[taskType] ?? 'governance'; + const candidates = HEXAGRAMS.filter(h => h.domain === domain); + return candidates.length > 0 ? candidates[0] : HEXAGRAMS[0]; // Default: Qian + } +} + +// ── Convenience singleton ──────────────────────────────────────────────────── + +const defaultGenerator = new DNAGenerator(); + +/** + * Quick one-shot DNA generation. + */ +export function generateDNA(taskType = 'default', action = 'WRAP', version?: string): string { + return defaultGenerator.generate(taskType, action, version); +} \ No newline at end of file diff --git a/adapters/typescript/src/Validator.ts b/adapters/typescript/src/Validator.ts new file mode 100644 index 0000000..18c4b82 --- /dev/null +++ b/adapters/typescript/src/Validator.ts @@ -0,0 +1,205 @@ +/** + * Validator — DNA and audit format validation. + * + * DNA: #LongHun⚡️BingWu·GuiWei·JiaZi·ZiShi·䷝Li-VALIDATOR-v1.0.0 + */ + +import type { + BehaviorPattern, + BehaviorSignature, + ValidationResult, +} from './types.js'; + +// ── DNA v∞ validation regex ────────────────────────────────────────────────── + +const DNA_REGEX = /^#LongHun⚡️([A-Z][a-zA-Z]+)·([A-Z][a-zA-Z]+)·([A-Z][a-zA-Z]+)·([A-Z][a-zA-Z]+)·([䷀-䷿][A-Za-z]+)-(.+)-([a-f0-9]{8})$/; + +// ── Validation constants ───────────────────────────────────────────────────── + +const REQUIRED_TOP_KEYS = new Set(['dna', 'audit', 'payload', 'meta']); +const REQUIRED_AUDIT_KEYS = new Set(['audit_version', 'uid', 'behavior_signature', 'behavior_pattern', 'behavior_labels', 'color']); +const REQUIRED_SIG_KEYS = new Set(['P', 'F', 'T', 'E', 'C', 'R', 'A', 'X', 'Y', 'Z']); +const VALID_COLORS = new Set(['🟢', '🟡', '🔴']); +const VALID_PATTERNS: Set = new Set([ + 'MODE-DefensiveDefaulter', + 'MODE-ExternalTrustSpender', + 'MODE-InternalDestroyer', + 'MODE-Fluctuating', + 'MODE-StableDisciplined', +]); +const VALID_P_VALUES = new Set(['HasPromise', 'NoPromise']); +const VALID_F_VALUES = new Set(['Fulfilled', 'Unfulfilled', 'Partial']); +const VALID_E_VALUES = new Set(['Willing', 'Perfunctory', 'Resentful', 'Numb']); +const VALID_A_VALUES = new Set(['Self', 'Partner', 'Family', 'Outsider', 'Public']); +const VALID_X_VALUES = new Set(['OverExplain', 'Silent', 'Genuine', 'Indifferent']); +const VALID_Y_VALUES = new Set(['Changed', 'Resisted', 'Indifferent', 'NoResponse']); + +// ── Validator ──────────────────────────────────────────────────────────────── + +export class Validator { + readonly errors: string[] = []; + readonly warnings: string[] = []; + + /** + * Validate a wrapped payload. + * + * @param wrapped - The wrapped payload object to validate + * @returns Validation result with errors and warnings + */ + validate(wrapped: Record): ValidationResult { + this.errors.length = 0; + this.warnings.length = 0; + + if (!wrapped || typeof wrapped !== 'object' || Object.keys(wrapped).length === 0) { + this.errors.push('Input is not a non-empty object'); + return this.result(); + } + + // 1. Top-level keys + const topKeys = new Set(Object.keys(wrapped)); + const missing = [...REQUIRED_TOP_KEYS].filter(k => !topKeys.has(k)); + if (missing.length > 0) { + this.errors.push(`Missing top-level keys: ${missing.join(', ')}`); + } + + // 2. DNA validation + const dna = wrapped.dna; + if (!dna || typeof dna !== 'string') { + this.errors.push('DNA field is empty or not a string'); + } else { + const match = DNA_REGEX.exec(dna); + if (!match) { + this.errors.push(`DNA does not match regex: ${dna.slice(0, 60)}...`); + } else { + const hash8 = match[7]; + if (hash8.length !== 8 || !/^[0-9a-f]{8}$/.test(hash8)) { + this.errors.push(`Invalid hash8: ${hash8}`); + } + } + } + + // 3. Audit validation + const audit = wrapped.audit; + if (!audit || typeof audit !== 'object') { + this.errors.push('Audit is not an object'); + } else { + this.validateAudit(audit as Record); + + // 4. UID consistency check + const meta = wrapped.meta as Record | undefined; + if (meta && typeof meta === 'object') { + const metaUid = meta.uid as string | undefined; + const auditUid = (audit as Record).uid as string | undefined; + if (metaUid && auditUid) { + const auditUidClean = (auditUid as string).replace('UID', ''); + if (metaUid !== auditUidClean) { + this.errors.push(`UID mismatch: meta.uid=${metaUid}, audit.uid=${auditUid}`); + } + } + } + } + + return this.result(); + } + + private validateAudit(audit: Record): void { + // Required keys + const auditKeys = new Set(Object.keys(audit)); + const missing = [...REQUIRED_AUDIT_KEYS].filter(k => !auditKeys.has(k)); + if (missing.length > 0) { + this.errors.push(`Missing audit keys: ${missing.join(', ')}`); + } + + // behavior_signature + const sig = audit.behavior_signature as Record | undefined; + if (!sig || typeof sig !== 'object') { + this.errors.push('behavior_signature is not an object'); + } else { + const sigKeys = new Set(Object.keys(sig)); + const missingSig = [...REQUIRED_SIG_KEYS].filter(k => !sigKeys.has(k)); + if (missingSig.length > 0) { + this.errors.push(`Missing signature keys: ${missingSig.join(', ')}`); + } else { + this.validateSigValues(sig as Record); + } + } + + // pattern + const pattern = audit.behavior_pattern as string; + if (pattern && !VALID_PATTERNS.has(pattern)) { + this.warnings.push(`Unknown behavior pattern: ${pattern}`); + } + + // color + const color = audit.color as string; + if (color && !VALID_COLORS.has(color)) { + this.warnings.push(`Unknown audit color: ${color}`); + } + + // payload_hash + const ph = audit.payload_hash as string | undefined; + if (ph && (ph.length !== 16 || !/^[0-9a-f]{16}$/.test(ph))) { + this.warnings.push(`Suspicious payload_hash: ${ph}`); + } + } + + private validateSigValues(sig: Record): void { + const checks: [unknown, Set | boolean, string][] = [ + [sig.P, VALID_P_VALUES, 'P'], + [sig.F, VALID_F_VALUES, 'F'], + [typeof sig.T === 'number', true, 'T (number)'], + [sig.E, VALID_E_VALUES, 'E'], + [typeof sig.C === 'number', true, 'C (number)'], + [Number.isInteger(sig.R) && (sig.R as number) >= 0, true, 'R (int >= 0)'], + [sig.A, VALID_A_VALUES, 'A'], + [sig.X, VALID_X_VALUES, 'X'], + [sig.Y, VALID_Y_VALUES, 'Y'], + [typeof sig.Z === 'number', true, 'Z (number)'], + ]; + + for (const [actual, expected, label] of checks) { + if (expected === true) { + if (!actual) { + this.warnings.push(`Invalid ${label}`); + } + } else if (expected instanceof Set) { + if (typeof actual === 'string' && !expected.has(actual)) { + this.warnings.push(`Invalid ${label}: '${actual}'`); + } + } + } + } + + private result(): ValidationResult { + const valid = this.errors.length === 0; + let summary: string; + if (valid) { + summary = `✅ VALID — ${this.warnings.length} warning(s)`; + if (this.warnings.length > 0) { + summary += ` (${this.warnings.slice(0, 2).join(', ')})`; + } + } else { + summary = `❌ INVALID — ${this.errors.length} error(s)`; + } + return { + valid, + errors: [...this.errors], + warnings: [...this.warnings], + summary, + }; + } +} + +// ── Convenience functions ──────────────────────────────────────────────────── + +/** + * Quick check: has required keys and valid DNA format? + */ +export function quickValidate(wrapped: Record): boolean { + if (!wrapped || typeof wrapped !== 'object') return false; + const keys = new Set(Object.keys(wrapped)); + if (!keys.has('dna') || !keys.has('audit')) return false; + const dna = wrapped.dna; + if (typeof dna !== 'string') return false; + return DNA_REGEX.test(dna); +} \ No newline at end of file diff --git a/adapters/typescript/src/index.ts b/adapters/typescript/src/index.ts new file mode 100644 index 0000000..ed561ac --- /dev/null +++ b/adapters/typescript/src/index.ts @@ -0,0 +1,17 @@ +/** + * LongHun (龍魂) v∞ DNA Traceability Standard — TypeScript Adapter + * + * Main entry point. Exports all public classes and utilities. + */ + +export { DNAGenerator, generateDNA } from './DNAGenerator.js'; +export { AuditWrapper, auditWrap } from './AuditWrapper.js'; +export { Validator, quickValidate } from './Validator.js'; +export type { + Hexagram, + StemBranch, + BehaviorSignature, + BehaviorPattern, + AuditWrapperResult, + ValidationResult, +} from './types.js'; \ No newline at end of file diff --git a/adapters/typescript/src/types.ts b/adapters/typescript/src/types.ts new file mode 100644 index 0000000..228f0d5 --- /dev/null +++ b/adapters/typescript/src/types.ts @@ -0,0 +1,57 @@ +/** + * Common types for the LongHun DNA Traceability Standard. + */ + +export interface Hexagram { + symbol: string; + enName: string; + cnName: string; + domain: string; +} + +export interface StemBranch { + year: string; + month: string; + day: string; + shichen: string; +} + +export interface BehaviorSignature { + P: string; + F: string; + T: number; + E: string; + C: number; + R: number; + A: string; + X: string; + Y: string; + Z: number; +} + +export type BehaviorPattern = + | 'MODE-DefensiveDefaulter' + | 'MODE-ExternalTrustSpender' + | 'MODE-InternalDestroyer' + | 'MODE-Fluctuating' + | 'MODE-StableDisciplined'; + +export interface AuditWrapperResult { + audit_version: string; + uid: string; + persona: string; + task_type: string; + behavior_signature: BehaviorSignature; + behavior_pattern: BehaviorPattern; + behavior_labels: string[]; + color: '🟢' | '🟡' | '🔴'; + timestamp: string; + payload_hash: string; +} + +export interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; + summary: string; +} \ No newline at end of file diff --git a/adapters/typescript/tests/AuditWrapper.test.ts b/adapters/typescript/tests/AuditWrapper.test.ts new file mode 100644 index 0000000..2053e1b --- /dev/null +++ b/adapters/typescript/tests/AuditWrapper.test.ts @@ -0,0 +1,49 @@ +/** + * Tests for the Audit Wrapper. + */ + +import { describe, it, expect } from 'vitest'; +import { AuditWrapper, auditWrap } from '../src/AuditWrapper.js'; + +describe('AuditWrapper', () => { + it('should wrap a payload with audit metadata', () => { + const result = auditWrap({ hello: 'world' }); + expect(result.audit_version).toBe('v1.0'); + expect(result.uid).toBe('UID9622'); + expect(result.behavior_pattern).toBe('MODE-StableDisciplined'); + expect(result.color).toBe('🟢'); + }); + + it('should include behavior labels', () => { + const result = auditWrap({ test: 'data' }); + expect(result.behavior_labels).toContain('MODE-StableDisciplined'); + expect(result.behavior_labels).toContain('7F-P-有承诺'); + expect(result.behavior_labels).toContain('7F-F-已兑现'); + }); + + it('should include a payload hash', () => { + const result = auditWrap({ data: 'test' }); + expect(result.payload_hash).toMatch(/^[a-f0-9]{16}$/); + }); + + it('should support custom personas', () => { + const result = auditWrap({}, 'default', 'P07'); + expect(result.persona).toBe('P07'); + }); + + it('should support custom task types', () => { + const result = auditWrap({}, 'security'); + expect(result.task_type).toBe('security'); + }); + + it('should generate different hashes for different payloads', () => { + const r1 = auditWrap({ a: 1 }); + const r2 = auditWrap({ a: 2 }); + expect(r1.payload_hash).not.toBe(r2.payload_hash); + }); + + it('should produce a timestamp in ISO format', () => { + const result = auditWrap({}); + expect(() => new Date(result.timestamp)).not.toThrow(); + }); +}); \ No newline at end of file diff --git a/adapters/typescript/tests/DNAGenerator.test.ts b/adapters/typescript/tests/DNAGenerator.test.ts new file mode 100644 index 0000000..daa3bc3 --- /dev/null +++ b/adapters/typescript/tests/DNAGenerator.test.ts @@ -0,0 +1,52 @@ +/** + * Tests for the DNA Generator. + */ + +import { describe, it, expect } from 'vitest'; +import { DNAGenerator, generateDNA } from '../src/DNAGenerator.js'; + +describe('DNAGenerator', () => { + it('should generate a valid DNA string', () => { + const dna = generateDNA(); + expect(dna).toMatch(/^#LongHun⚡️/); + expect(dna).toContain('ADAPTER-DEFAULT-WRAP-V1.0'); + expect(dna).toMatch(/-[a-f0-9]{8}$/); + }); + + it('should include hexagram in the correct position', () => { + const dna = generateDNA('audit', 'WRAP'); + // audit task maps to Li hexagram (䷝) + expect(dna).toContain('䷝'); + expect(dna).toContain('ADAPTER-AUDIT-WRAP-V1.0'); + }); + + it('should generate different hashes for different inputs', () => { + const dna1 = generateDNA('code', 'WRAP'); + const dna2 = generateDNA('audit', 'WRAP'); + expect(dna1).not.toBe(dna2); + }); + + it('should support custom task types', () => { + const dna = generateDNA('security', 'VALIDATE'); + expect(dna).toContain('ADAPTER-SECURITY-VALIDATE-V1.0'); + }); + + it('should use the default hexagram for unknown task types', () => { + const dna = generateDNA('unknown_type', 'WRAP'); + // Default is Qian (䷀) + expect(dna).toContain('䷀'); + }); + + it('should accept a custom version', () => { + const dna = generateDNA('default', 'WRAP', 'V2.0'); + expect(dna).toContain('ADAPTER-DEFAULT-WRAP-V2.0'); + }); + + it('should produce consistent results for the same input', () => { + const generator = new DNAGenerator('9622', 'HM-9622-001'); + const dna1 = generator.generate('default', 'WRAP', 'V1.0'); + const dna2 = generator.generate('default', 'WRAP', 'V1.0'); + // Different timestamps → different hash + expect(dna1).not.toBe(dna2); + }); +}); \ No newline at end of file diff --git a/adapters/typescript/tests/Validator.test.ts b/adapters/typescript/tests/Validator.test.ts new file mode 100644 index 0000000..de4d89f --- /dev/null +++ b/adapters/typescript/tests/Validator.test.ts @@ -0,0 +1,75 @@ +/** + * Tests for the Validator. + */ + +import { describe, it, expect } from 'vitest'; +import { Validator, quickValidate } from '../src/Validator.js'; +import { generateDNA } from '../src/DNAGenerator.js'; +import { auditWrap } from '../src/AuditWrapper.js'; + +describe('Validator', () => { + it('should validate a correctly wrapped payload', () => { + const dna = generateDNA('audit', 'WRAP'); + const audit = auditWrap({ data: 'test' }, 'audit'); + const wrapped = { + dna, + audit, + payload: { data: 'test' }, + meta: { uid: '9622', version: '1.0' }, + }; + const result = new Validator().validate(wrapped); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should reject an empty object', () => { + const result = new Validator().validate({}); + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should detect missing top-level keys', () => { + const result = new Validator().validate({ dna: 'test' }); + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('Missing top-level keys'))).toBe(true); + }); + + it('should detect invalid DNA format', () => { + const audit = auditWrap({}); + const wrapped = { + dna: 'invalid-dna-format', + audit, + payload: {}, + meta: { uid: '9622' }, + }; + const result = new Validator().validate(wrapped); + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('DNA does not match regex'))).toBe(true); + }); + + it('should detect UID mismatch', () => { + const dna = generateDNA(); + const audit = auditWrap({}); + const wrapped = { + dna, + audit, + payload: {}, + meta: { uid: '9999' }, // Different from audit's UID9622 + }; + const result = new Validator().validate(wrapped); + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('UID mismatch'))).toBe(true); + }); + + it('quickValidate should return true for valid structure', () => { + const dna = generateDNA(); + const audit = auditWrap({}); + const valid = quickValidate({ dna, audit, payload: {}, meta: {} }); + expect(valid).toBe(true); + }); + + it('quickValidate should return false for invalid structure', () => { + expect(quickValidate({})).toBe(false); + expect(quickValidate({ dna: 'bad' })).toBe(false); + }); +}); \ No newline at end of file diff --git a/adapters/typescript/tsconfig.json b/adapters/typescript/tsconfig.json new file mode 100644 index 0000000..fe19230 --- /dev/null +++ b/adapters/typescript/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "tests"] +} \ No newline at end of file