From 992e503299a1d4f91270845b34fd164732ac3036 Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:05:22 -0800 Subject: [PATCH 1/9] chore: scaffold production-hardening pass (round 4) From 38b9487ab16fb90c4674facf372c051d6ddaf2ad Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:12:10 -0800 Subject: [PATCH 2/9] style: apply rustfmt across crate and tests Establishes a formatting-clean baseline so 'cargo fmt --all -- --check' passes (it previously failed with many diffs). No behavior change. --- src/lib.rs | 859 ++++++++++++++++++++++++++++++++----------- tests/integration.rs | 405 ++++++++++++++++---- 2 files changed, 982 insertions(+), 282 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4c4764a..d4f4210 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,39 +31,39 @@ use core::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum Op { - Nop = 0x00, - Mov = 0x01, - Load = 0x02, + Nop = 0x00, + Mov = 0x01, + Load = 0x02, Store = 0x03, - Jmp = 0x04, - Jz = 0x05, - Jnz = 0x06, - Call = 0x07, - Iadd = 0x08, - Isub = 0x09, - Imul = 0x0A, - Idiv = 0x0B, - Imod = 0x0C, - Ineg = 0x0D, - Inc = 0x0E, - Dec = 0x0F, - Iand = 0x10, - Ior = 0x11, - Ixor = 0x12, - Inot = 0x13, - Ishl = 0x14, - Ishr = 0x15, - Push = 0x20, - Pop = 0x21, - Dup = 0x22, - Ret = 0x28, - Movi = 0x2B, - Cmp = 0x2D, - Je = 0x2E, - Jne = 0x2F, - Jsge = 0x30, - Jslt = 0x31, - Halt = 0x80, + Jmp = 0x04, + Jz = 0x05, + Jnz = 0x06, + Call = 0x07, + Iadd = 0x08, + Isub = 0x09, + Imul = 0x0A, + Idiv = 0x0B, + Imod = 0x0C, + Ineg = 0x0D, + Inc = 0x0E, + Dec = 0x0F, + Iand = 0x10, + Ior = 0x11, + Ixor = 0x12, + Inot = 0x13, + Ishl = 0x14, + Ishr = 0x15, + Push = 0x20, + Pop = 0x21, + Dup = 0x22, + Ret = 0x28, + Movi = 0x2B, + Cmp = 0x2D, + Je = 0x2E, + Jne = 0x2F, + Jsge = 0x30, + Jslt = 0x31, + Halt = 0x80, Yield = 0x81, Syscall = 0xF0, } @@ -71,18 +71,40 @@ pub enum Op { impl Op { pub fn from_byte(b: u8) -> Option { Some(match b { - 0x00 => Op::Nop, 0x01 => Op::Mov, 0x02 => Op::Load, - 0x03 => Op::Store, 0x04 => Op::Jmp, 0x05 => Op::Jz, - 0x06 => Op::Jnz, 0x07 => Op::Call, 0x08 => Op::Iadd, - 0x09 => Op::Isub, 0x0A => Op::Imul, 0x0B => Op::Idiv, - 0x0C => Op::Imod, 0x0D => Op::Ineg, 0x0E => Op::Inc, - 0x0F => Op::Dec, 0x10 => Op::Iand, 0x11 => Op::Ior, - 0x12 => Op::Ixor, 0x13 => Op::Inot, 0x14 => Op::Ishl, - 0x15 => Op::Ishr, 0x20 => Op::Push, 0x21 => Op::Pop, - 0x22 => Op::Dup, 0x28 => Op::Ret, 0x2B => Op::Movi, - 0x2D => Op::Cmp, 0x2E => Op::Je, 0x2F => Op::Jne, - 0x30 => Op::Jsge, 0x31 => Op::Jslt, - 0x80 => Op::Halt, 0x81 => Op::Yield, + 0x00 => Op::Nop, + 0x01 => Op::Mov, + 0x02 => Op::Load, + 0x03 => Op::Store, + 0x04 => Op::Jmp, + 0x05 => Op::Jz, + 0x06 => Op::Jnz, + 0x07 => Op::Call, + 0x08 => Op::Iadd, + 0x09 => Op::Isub, + 0x0A => Op::Imul, + 0x0B => Op::Idiv, + 0x0C => Op::Imod, + 0x0D => Op::Ineg, + 0x0E => Op::Inc, + 0x0F => Op::Dec, + 0x10 => Op::Iand, + 0x11 => Op::Ior, + 0x12 => Op::Ixor, + 0x13 => Op::Inot, + 0x14 => Op::Ishl, + 0x15 => Op::Ishr, + 0x20 => Op::Push, + 0x21 => Op::Pop, + 0x22 => Op::Dup, + 0x28 => Op::Ret, + 0x2B => Op::Movi, + 0x2D => Op::Cmp, + 0x2E => Op::Je, + 0x2F => Op::Jne, + 0x30 => Op::Jsge, + 0x31 => Op::Jslt, + 0x80 => Op::Halt, + 0x81 => Op::Yield, 0xF0 => Op::Syscall, _ => return None, }) @@ -94,33 +116,33 @@ impl Op { // ═══════════════════════════════════════════════════════════════════════════════ pub mod syscall { - pub const GET_INPUT_LEN: u32 = 1; - pub const GET_OUTPUT_LEN: u32 = 2; - pub const GET_INPUT_WORDS: u32 = 3; + pub const GET_INPUT_LEN: u32 = 1; + pub const GET_OUTPUT_LEN: u32 = 2; + pub const GET_INPUT_WORDS: u32 = 3; pub const GET_OUTPUT_WORDS: u32 = 4; - pub const GET_TOKEN_COUNT: u32 = 5; - pub const GET_REPETITION: u32 = 6; - pub const GET_CATEGORY: u32 = 7; - pub const SET_VIOLATION: u32 = 8; - pub const GET_BUDGET: u32 = 10; + pub const GET_TOKEN_COUNT: u32 = 5; + pub const GET_REPETITION: u32 = 6; + pub const GET_CATEGORY: u32 = 7; + pub const SET_VIOLATION: u32 = 8; + pub const GET_BUDGET: u32 = 10; pub const GET_UNIQUE_RATIO: u32 = 11; - pub const GET_ENTROPY: u32 = 12; - pub const GET_CALL_COUNT: u32 = 13; - pub const DECAY_BUDGET: u32 = 14; + pub const GET_ENTROPY: u32 = 12; + pub const GET_CALL_COUNT: u32 = 13; + pub const DECAY_BUDGET: u32 = 14; } /// Violation reason strings indexed by code. pub fn violation_reason(code: u32) -> &'static str { match code { - 1 => "Length budget exceeded", - 2 => "Excessive repetition detected", - 3 => "Category confinement violation", - 4 => "Information entropy violation", - 5 => "Information density below threshold", - 6 => "Scope discipline violation", - 7 => "Budget exhausted (decay cooldown)", + 1 => "Length budget exceeded", + 2 => "Excessive repetition detected", + 3 => "Category confinement violation", + 4 => "Information entropy violation", + 5 => "Information density below threshold", + 6 => "Scope discipline violation", + 7 => "Budget exhausted (decay cooldown)", 99 => "Custom conservation law violation", - _ => "Unknown conservation violation", + _ => "Unknown conservation violation", } } @@ -340,11 +362,12 @@ impl FluxVM { fn step(&mut self) -> Result { let opcode_byte = self.bytecode[self.pc]; - let op = Op::from_byte(opcode_byte) - .ok_or(VmError::InvalidOpcode(opcode_byte))?; + let op = Op::from_byte(opcode_byte).ok_or(VmError::InvalidOpcode(opcode_byte))?; match op { - Op::Nop => { self.pc += 1; } + Op::Nop => { + self.pc += 1; + } Op::Mov => { let (rd, rs) = self.decode_c(); let v = self.regs.get(rs as usize); @@ -385,30 +408,43 @@ impl FluxVM { } Op::Iadd => { let (rd, rs1, rs2) = self.decode_e(); - let v = self.regs.get(rs1 as usize).wrapping_add(self.regs.get(rs2 as usize)); + let v = self + .regs + .get(rs1 as usize) + .wrapping_add(self.regs.get(rs2 as usize)); self.regs.set(rd as usize, v); } Op::Isub => { let (rd, rs1, rs2) = self.decode_e(); - let v = self.regs.get(rs1 as usize).wrapping_sub(self.regs.get(rs2 as usize)); + let v = self + .regs + .get(rs1 as usize) + .wrapping_sub(self.regs.get(rs2 as usize)); self.regs.set(rd as usize, v); } Op::Imul => { let (rd, rs1, rs2) = self.decode_e(); - let v = self.regs.get(rs1 as usize).wrapping_mul(self.regs.get(rs2 as usize)); + let v = self + .regs + .get(rs1 as usize) + .wrapping_mul(self.regs.get(rs2 as usize)); self.regs.set(rd as usize, v); } Op::Idiv => { let (rd, rs1, rs2) = self.decode_e(); let d = self.regs.get(rs2 as usize); - if d == 0 { return Err(VmError::DivisionByZero); } + if d == 0 { + return Err(VmError::DivisionByZero); + } let v = self.regs.get(rs1 as usize) / d; self.regs.set(rd as usize, v); } Op::Imod => { let (rd, rs1, rs2) = self.decode_e(); let d = self.regs.get(rs2 as usize); - if d == 0 { return Err(VmError::DivisionByZero); } + if d == 0 { + return Err(VmError::DivisionByZero); + } let v = self.regs.get(rs1 as usize) % d; self.regs.set(rd as usize, v); } @@ -449,12 +485,18 @@ impl FluxVM { } Op::Ishl => { let (rd, rs1, rs2) = self.decode_e(); - let v = self.regs.get(rs1 as usize).wrapping_shl(self.regs.get(rs2 as usize)); + let v = self + .regs + .get(rs1 as usize) + .wrapping_shl(self.regs.get(rs2 as usize)); self.regs.set(rd as usize, v); } Op::Ishr => { let (rd, rs1, rs2) = self.decode_e(); - let v = self.regs.get(rs1 as usize).wrapping_shr(self.regs.get(rs2 as usize)); + let v = self + .regs + .get(rs1 as usize) + .wrapping_shr(self.regs.get(rs2 as usize)); self.regs.set(rd as usize, v); } Op::Push => { @@ -602,9 +644,13 @@ impl FluxVM { let w = words[i]; let mut c = 0u32; for j in 0..words.len() { - if words[j] == w { c += 1; } + if words[j] == w { + c += 1; + } + } + if c > max_count { + max_count = c; } - if c > max_count { max_count = c; } } self.regs.set(0, (max_count * 1000) / words.len() as u32); } @@ -620,7 +666,9 @@ impl FluxVM { let iw_set: Vec<&str> = iw.into_iter().collect(); let mut overlap = 0u32; for w in &ow { - if iw_set.contains(w) { overlap += 1; } + if iw_set.contains(w) { + overlap += 1; + } } let score = core::cmp::min(1000, (overlap * 1000) / ow.len() as u32); self.regs.set(0, score); @@ -645,7 +693,8 @@ impl FluxVM { unique.push(*w); } } - self.regs.set(0, ((unique.len() as u32) * 1000) / words.len() as u32); + self.regs + .set(0, ((unique.len() as u32) * 1000) / words.len() as u32); } } syscall::GET_ENTROPY => { @@ -659,7 +708,9 @@ impl FluxVM { let mut counted = Vec::new(); let mut ent = 0.0f32; for w in &words { - if counted.contains(w) { continue; } + if counted.contains(w) { + continue; + } counted.push(*w); let c = words.iter().filter(|&&x| x == *w).count() as f32; let p = c / total; @@ -689,11 +740,15 @@ impl Default for FluxVM { /// Simple log2 for f32 without external dependencies. fn log2_f32(x: f32) -> f32 { - if x <= 0.0 { return 0.0; } + if x <= 0.0 { + return 0.0; + } // Use bit manipulation: log2(x) = log(x) / log(2) // But we can't use std math in no_std... use a manual approximation. #[cfg(feature = "std")] - { x.ln() / core::f32::consts::LN_2 } + { + x.ln() / core::f32::consts::LN_2 + } #[cfg(not(feature = "std"))] { // Fast log2 approximation via bit manipulation @@ -701,7 +756,7 @@ fn log2_f32(x: f32) -> f32 { let exp = ((bits >> 23) & 0xFF) as i32 - 127; let mantissa_bits = (bits & 0x7FFFFF) | 0x800000; let m = mantissa_bits as f32 / 0x800000u32 as f32; // 1.0 to ~2.0 - // Linear interpolation: log2(1+f) ≈ f for the mantissa part + // Linear interpolation: log2(1+f) ≈ f for the mantissa part let frac = m - 1.0; (exp as f32) + frac - 0.5 * frac * frac // slightly better than linear } @@ -764,10 +819,12 @@ impl ConservationEnforcer { policy: policy_bytecode, budget, initial_budget: budget, - correction_template: correction_template.unwrap_or( - "⚠️ This response was blocked by a conservation law: {reason}. \ - Please try again with a more conserved response." - ).to_string(), + correction_template: correction_template + .unwrap_or( + "⚠️ This response was blocked by a conservation law: {reason}. \ + Please try again with a more conserved response.", + ) + .to_string(), call_count: 0, #[cfg(feature = "audit")] enable_audit: false, @@ -872,14 +929,24 @@ impl ConservationEnforcer { let input_hash = simple_hash(input_text); let output_hash = simple_hash(output_text); let timestamp = current_iso_timestamp(); - let violation_reason = result.violation.as_ref().map(|v| v.reason.as_str()).unwrap_or("null"); + let violation_reason = result + .violation + .as_ref() + .map(|v| v.reason.as_str()) + .unwrap_or("null"); let violation_code = result.violation.as_ref().map(|v| v.code).unwrap_or(0); let _ = writeln!( f, r#"{{"timestamp":"{}","input_hash":"{:016x}","output_hash":"{:016x}","allowed":{},"violation":{},"violation_code":{},"cycles":{},"remaining_budget":{},"call_count":{}}}"#, - timestamp, input_hash, output_hash, result.allowed, - format_json_string(violation_reason), violation_code, - result.cycles, self.budget, self.call_count + timestamp, + input_hash, + output_hash, + result.allowed, + format_json_string(violation_reason), + violation_code, + result.cycles, + self.budget, + self.call_count ); } } @@ -902,7 +969,9 @@ fn simple_hash(s: &str) -> u64 { #[cfg(feature = "audit")] fn current_iso_timestamp() -> String { use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); let secs = now.as_secs(); // Simple ISO timestamp approximation (UTC) let days = secs / 86400; @@ -921,7 +990,9 @@ fn days_to_date(days: u64) -> (u64, u64, u64) { let mut remaining = days; loop { let days_in_year = if is_leap(year) { 366 } else { 365 }; - if remaining < days_in_year { break; } + if remaining < days_in_year { + break; + } remaining -= days_in_year; year += 1; } @@ -932,7 +1003,9 @@ fn days_to_date(days: u64) -> (u64, u64, u64) { }; let mut month = 1u64; for &dim in &month_lengths { - if remaining < dim { break; } + if remaining < dim { + break; + } remaining -= dim; month += 1; } @@ -946,7 +1019,9 @@ fn is_leap(year: u64) -> bool { #[cfg(feature = "audit")] fn format_json_string(s: &str) -> String { - if s == "null" { return s.to_string(); } + if s == "null" { + return s.to_string(); + } format!("\"{}\"", s.replace('"', "\\\"")) } @@ -977,10 +1052,22 @@ pub mod assembler { impl Instr { fn new(op: u8, fmt: char) -> Self { Self { - op, fmt, - rd: 0, rs: 0, rs2: 0, imm: 0, - label: None, offset: 0, - size: match fmt { 'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 4, _ => 1 }, + op, + fmt, + rd: 0, + rs: 0, + rs2: 0, + imm: 0, + label: None, + offset: 0, + size: match fmt { + 'A' => 1, + 'B' => 2, + 'C' => 3, + 'D' => 4, + 'E' => 4, + _ => 1, + }, } } } @@ -991,14 +1078,18 @@ pub mod assembler { for (line_num, line) in source.lines().enumerate() { let text = strip_comments(line).trim().to_string(); - if text.is_empty() { continue; } + if text.is_empty() { + continue; + } // Label? let text = match parse_label(&text, &mut labels, &raw, line_num) { Ok(remaining) => remaining, Err(e) => return Err(e), }; - if text.is_empty() { continue; } + if text.is_empty() { + continue; + } let parts: Vec<&str> = text.splitn(2, char::is_whitespace).collect(); let mnem = parts[0].to_uppercase(); @@ -1016,7 +1107,8 @@ pub mod assembler { // CMP rd, rs let mut cmp_instr = Instr::new(Op::Cmp as u8, 'C'); - cmp_instr.rd = rd; cmp_instr.rs = rs; + cmp_instr.rd = rd; + cmp_instr.rs = rs; raw.push(cmp_instr); match mnem.as_str() { @@ -1087,7 +1179,12 @@ pub mod assembler { "XOR" | "IXOR" => (Op::Ixor as u8, 'E'), "SHL" | "ISHL" => (Op::Ishl as u8, 'E'), "SHR" | "ISHR" => (Op::Ishr as u8, 'E'), - _ => return Err(format!("Line {}: unknown instruction '{mnem}'", line_num + 1)), + _ => { + return Err(format!( + "Line {}: unknown instruction '{mnem}'", + line_num + 1 + )) + } }; let mut instr = Instr::new(op_byte, fmt); @@ -1110,8 +1207,13 @@ pub mod assembler { } else if ps.len() == 2 { instr.rd = parse_reg(ps[0], line_num)?; let v = ps[1]; - if !v.is_empty() && (v.as_bytes()[0].is_ascii_digit() || (v.starts_with('-') && v.len() > 1)) { - instr.imm = v.parse::().map_err(|_| format!("Line {}: bad immediate '{v}'", line_num + 1))?; + if !v.is_empty() + && (v.as_bytes()[0].is_ascii_digit() + || (v.starts_with('-') && v.len() > 1)) + { + instr.imm = v.parse::().map_err(|_| { + format!("Line {}: bad immediate '{v}'", line_num + 1) + })?; } else { instr.label = Some(v.to_string()); } @@ -1137,9 +1239,17 @@ pub mod assembler { } // Build label → byte-offset map - let mut label_bytes: std::collections::HashMap = std::collections::HashMap::new(); + let mut label_bytes: std::collections::HashMap = + std::collections::HashMap::new(); for (lbl, idx) in &labels { - label_bytes.insert(lbl.clone(), if *idx < raw.len() { raw[*idx].offset } else { offset }); + label_bytes.insert( + lbl.clone(), + if *idx < raw.len() { + raw[*idx].offset + } else { + offset + }, + ); } // Emit bytecode @@ -1149,11 +1259,16 @@ pub mod assembler { match instr.fmt { 'A' => {} 'B' => out.push(instr.rd), - 'C' => { out.push(instr.rd); out.push(instr.rs); } + 'C' => { + out.push(instr.rd); + out.push(instr.rs); + } 'D' => { out.push(instr.rd); if let Some(ref lbl) = instr.label { - let target = *label_bytes.get(lbl).ok_or_else(|| format!("Undefined label: '{lbl}'"))?; + let target = *label_bytes + .get(lbl) + .ok_or_else(|| format!("Undefined label: '{lbl}'"))?; let rel = target as i32 - (instr.offset + 4) as i32; out.push((rel & 0xFF) as u8); out.push(((rel >> 8) & 0xFF) as u8); @@ -1163,7 +1278,11 @@ pub mod assembler { out.push(((imm >> 8) & 0xFF) as u8); } } - 'E' => { out.push(instr.rd); out.push(instr.rs); out.push(instr.rs2); } + 'E' => { + out.push(instr.rd); + out.push(instr.rs); + out.push(instr.rs2); + } _ => {} } } @@ -1176,7 +1295,9 @@ pub mod assembler { let mut idx = line.len(); for marker in [';', '#'] { if let Some(pos) = line.find(marker) { - if pos < idx { idx = pos; } + if pos < idx { + idx = pos; + } } } &line[..idx] @@ -1189,11 +1310,15 @@ pub mod assembler { line_num: usize, ) -> Result<&'a str, String> { let text = text.trim(); - if text.is_empty() { return Ok(text); } + if text.is_empty() { + return Ok(text); + } // Check if text starts with identifier: let bytes = text.as_bytes(); - if bytes.is_empty() { return Ok(text); } + if bytes.is_empty() { + return Ok(text); + } let first = bytes[0]; if !(first.is_ascii_alphabetic() || first == b'_') { return Ok(text); @@ -1203,7 +1328,9 @@ pub mod assembler { if let Some(colon_pos) = text.find(':') { let label_part = &text[..colon_pos]; // Verify label is a valid identifier - let valid = label_part.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_'); + let valid = label_part + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'_'); if !valid { return Ok(text); } @@ -1224,9 +1351,14 @@ pub mod assembler { let tok = tok.trim().to_uppercase(); let tb = tok.as_bytes(); if tb.len() < 2 || tb[0] != b'R' || !tb[1..].iter().all(|b| b.is_ascii_digit()) { - return Err(format!("Line {}: expected register, got '{tok}'", line_num + 1)); + return Err(format!( + "Line {}: expected register, got '{tok}'", + line_num + 1 + )); } - let n: u8 = tok[1..].parse().map_err(|_| format!("Line {}: register out of range", line_num + 1))?; + let n: u8 = tok[1..] + .parse() + .map_err(|_| format!("Line {}: register out of range", line_num + 1))?; if n as usize >= super::NUM_REGISTERS { return Err(format!("Line {}: R{n} out of range", line_num + 1)); } @@ -1261,7 +1393,8 @@ block: MOVI R0, 1 HALT "# - )).expect("length_budget_policy assembly") + )) + .expect("length_budget_policy assembly") } /// Block outputs with excessive repetition. `max_ratio` is per-mille (300 = 30%). @@ -1282,7 +1415,8 @@ block: MOVI R0, 1 HALT "# - )).expect("repetition_policy assembly") + )) + .expect("repetition_policy assembly") } /// Block outputs that drift too far from input topic. `min_overlap` is per-mille. @@ -1303,7 +1437,8 @@ block: MOVI R0, 1 HALT "# - )).expect("category_policy assembly") + )) + .expect("category_policy assembly") } /// Block outputs with too-low Shannon entropy. `min_entropy` is entropy × 1000. @@ -1324,7 +1459,8 @@ block: MOVI R0, 1 HALT "# - )).expect("entropy_policy assembly") + )) + .expect("entropy_policy assembly") } /// Block outputs with low information density. @@ -1346,7 +1482,8 @@ block: MOVI R0, 1 HALT "# - )).expect("information_density_policy assembly") + )) + .expect("information_density_policy assembly") } /// Block outputs that drift outside the input's topic scope. @@ -1397,7 +1534,8 @@ block: MOVI R0, 1 HALT "# - )).expect("scope_discipline_policy assembly") + )) + .expect("scope_discipline_policy assembly") } /// Enforce budget decay over time — each call consumes budget. @@ -1428,7 +1566,8 @@ exhausted: MOVI R0, 1 HALT "# - )).expect("budget_decay_policy assembly") + )) + .expect("budget_decay_policy assembly") } /// Combined conservation policy: length + repetition + category + entropy + optional density + decay. @@ -1546,7 +1685,8 @@ block_decay: MOVI R0, 1 HALT "# - )).expect("combined_policy assembly") + )) + .expect("combined_policy assembly") } } @@ -1570,7 +1710,9 @@ pub mod audit { if let Some(parent) = p.parent() { let _ = fs::create_dir_all(parent); } - Self { path: path.to_string() } + Self { + path: path.to_string(), + } } pub fn log( @@ -1595,7 +1737,11 @@ pub mod audit { r#"{{"timestamp":"{timestamp}","input_hash":"{input_hash:016x}","output_hash":"{output_hash:016x}","allowed":{allowed},"violation":{violation},"violation_code":{violation_code},"cycles":{cycles},"remaining_budget":{remaining_budget},"call_count":{call_count}}}"#, ); - if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(&self.path) { + if let Ok(mut f) = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.path) + { let _ = writeln!(f, "{line}"); } } @@ -1613,7 +1759,9 @@ pub mod audit { if let Ok(content) = fs::read_to_string(&self.path) { for line in content.lines() { - if line.trim().is_empty() { continue; } + if line.trim().is_empty() { + continue; + } total += 1; if line.contains(r#""allowed":false"#) { blocked += 1; @@ -1625,9 +1773,17 @@ pub mod audit { AuditSummary { total_calls: total, total_blocked: blocked, - block_rate: if total > 0 { blocked as f64 / total as f64 } else { 0.0 }, + block_rate: if total > 0 { + blocked as f64 / total as f64 + } else { + 0.0 + }, total_cycles, - avg_cycles: if total > 0 { total_cycles as f64 / total as f64 } else { 0.0 }, + avg_cycles: if total > 0 { + total_cycles as f64 / total as f64 + } else { + 0.0 + }, } } @@ -1649,7 +1805,9 @@ pub mod audit { let pattern = format!(r#""{key}":"#); if let Some(pos) = line.find(&pattern) { let rest = &line[pos + pattern.len()..]; - let end = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len()); + let end = rest + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(rest.len()); rest[..end].parse().unwrap_or(0) } else { 0 @@ -1781,7 +1939,21 @@ mod tests { #[test] fn test_vm_add() { - let code = vec![Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 20, 0, Op::Iadd as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 20, + 0, + Op::Iadd as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 30); @@ -1789,7 +1961,21 @@ mod tests { #[test] fn test_vm_sub() { - let code = vec![Op::Movi as u8, 0, 50, 0, Op::Movi as u8, 1, 20, 0, Op::Isub as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 50, + 0, + Op::Movi as u8, + 1, + 20, + 0, + Op::Isub as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 30); @@ -1797,7 +1983,21 @@ mod tests { #[test] fn test_vm_mul() { - let code = vec![Op::Movi as u8, 0, 7, 0, Op::Movi as u8, 1, 6, 0, Op::Imul as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 7, + 0, + Op::Movi as u8, + 1, + 6, + 0, + Op::Imul as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 42); @@ -1805,7 +2005,21 @@ mod tests { #[test] fn test_vm_div() { - let code = vec![Op::Movi as u8, 0, 100, 0, Op::Movi as u8, 1, 5, 0, Op::Idiv as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 100, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Idiv as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 20); @@ -1813,7 +2027,21 @@ mod tests { #[test] fn test_vm_div_by_zero() { - let code = vec![Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 0, 0, Op::Idiv as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 0, + 0, + Op::Idiv as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); let result = vm.run(&code); assert_eq!(result, Err(VmError::DivisionByZero)); @@ -1821,7 +2049,21 @@ mod tests { #[test] fn test_vm_mod() { - let code = vec![Op::Movi as u8, 0, 17, 0, Op::Movi as u8, 1, 5, 0, Op::Imod as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 17, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Imod as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 2); @@ -1832,9 +2074,26 @@ mod tests { #[test] fn test_vm_je_taken() { let code = vec![ - Op::Movi as u8, 0, 5, 0, Op::Movi as u8, 1, 5, 0, - Op::Cmp as u8, 0, 1, Op::Je as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 5, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Je as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -1844,9 +2103,26 @@ mod tests { #[test] fn test_vm_jne_taken() { let code = vec![ - Op::Movi as u8, 0, 5, 0, Op::Movi as u8, 1, 3, 0, - Op::Cmp as u8, 0, 1, Op::Jne as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 5, + 0, + Op::Movi as u8, + 1, + 3, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jne as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -1856,9 +2132,26 @@ mod tests { #[test] fn test_vm_jsge_greater() { let code = vec![ - Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 5, 0, - Op::Cmp as u8, 0, 1, Op::Jsge as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jsge as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -1868,9 +2161,26 @@ mod tests { #[test] fn test_vm_jsge_less_should_not_jump() { let code = vec![ - Op::Movi as u8, 0, 3, 0, Op::Movi as u8, 1, 5, 0, - Op::Cmp as u8, 0, 1, Op::Jsge as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 3, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jsge as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -1880,9 +2190,26 @@ mod tests { #[test] fn test_vm_jslt_less() { let code = vec![ - Op::Movi as u8, 0, 3, 0, Op::Movi as u8, 1, 8, 0, - Op::Cmp as u8, 0, 1, Op::Jslt as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 3, + 0, + Op::Movi as u8, + 1, + 8, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jslt as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -1892,9 +2219,26 @@ mod tests { #[test] fn test_vm_jslt_greater_should_not_jump() { let code = vec![ - Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 3, 0, - Op::Cmp as u8, 0, 1, Op::Jslt as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 3, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jslt as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -1960,14 +2304,24 @@ mod tests { #[test] fn test_syscall_violation_flag() { let code = vec![ - Op::Movi as u8, 1, 2, 0, - Op::Movi as u8, 0, 8, 0, Op::Syscall as u8, + Op::Movi as u8, + 1, + 2, + 0, + Op::Movi as u8, + 0, + 8, + 0, + Op::Syscall as u8, Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert!(vm.violated()); - assert!(vm.violation_reason_str().to_lowercase().contains("repetition")); + assert!(vm + .violation_reason_str() + .to_lowercase() + .contains("repetition")); } // ── Stack tests ── @@ -1975,8 +2329,19 @@ mod tests { #[test] fn test_push_pop() { let code = vec![ - Op::Movi as u8, 0, 42, 0, Op::Push as u8, 0, - Op::Movi as u8, 0, 0, 0, Op::Pop as u8, 1, Op::Halt as u8, + Op::Movi as u8, + 0, + 42, + 0, + Op::Push as u8, + 0, + Op::Movi as u8, + 0, + 0, + 0, + Op::Pop as u8, + 1, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -1986,7 +2351,17 @@ mod tests { #[test] fn test_inc_dec() { let code = vec![ - Op::Movi as u8, 0, 5, 0, Op::Inc as u8, 0, Op::Inc as u8, 0, Op::Dec as u8, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 5, + 0, + Op::Inc as u8, + 0, + Op::Inc as u8, + 0, + Op::Dec as u8, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -2007,12 +2382,18 @@ mod tests { #[test] fn test_assemble_movi() { - assert_eq!(assemble("MOVI R0, 42").unwrap(), vec![Op::Movi as u8, 0, 42, 0]); + assert_eq!( + assemble("MOVI R0, 42").unwrap(), + vec![Op::Movi as u8, 0, 42, 0] + ); } #[test] fn test_assemble_add() { - assert_eq!(assemble("IADD R2, R0, R1").unwrap(), vec![Op::Iadd as u8, 2, 0, 1]); + assert_eq!( + assemble("IADD R2, R0, R1").unwrap(), + vec![Op::Iadd as u8, 2, 0, 1] + ); } #[test] @@ -2033,7 +2414,21 @@ mod tests { #[test] fn test_assemble_multiple() { let code = assemble("MOVI R0, 10\nMOVI R1, 20\nIADD R2, R0, R1\nHALT").unwrap(); - let expected = vec![Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 20, 0, Op::Iadd as u8, 2, 0, 1, Op::Halt as u8]; + let expected = vec![ + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 20, + 0, + Op::Iadd as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; assert_eq!(code, expected); } @@ -2059,9 +2454,7 @@ mod tests { #[test] fn test_assemble_je_label() { - let code = assemble( - "MOVI R0, 0\nCMP R0, R0\nJE done\nMOVI R0, 99\ndone:\nHALT" - ).unwrap(); + let code = assemble("MOVI R0, 0\nCMP R0, R0\nJE done\nMOVI R0, 99\ndone:\nHALT").unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(0), 0); @@ -2069,9 +2462,7 @@ mod tests { #[test] fn test_assemble_jmp_label() { - let code = assemble( - "MOVI R0, 1\nJMP skip\nMOVI R0, 99\nskip:\nMOVI R0, 42\nHALT" - ).unwrap(); + let code = assemble("MOVI R0, 1\nJMP skip\nMOVI R0, 99\nskip:\nMOVI R0, 42\nHALT").unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(0), 42); @@ -2082,8 +2473,9 @@ mod tests { #[test] fn test_jge_taken_greater() { let code = assemble( - "MOVI R0, 10\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 10\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -2092,8 +2484,9 @@ mod tests { #[test] fn test_jge_taken_equal() { let code = assemble( - "MOVI R0, 5\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 5\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -2102,8 +2495,9 @@ mod tests { #[test] fn test_jge_not_taken() { let code = assemble( - "MOVI R0, 3\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 99\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 3\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 99\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 99); @@ -2112,8 +2506,9 @@ mod tests { #[test] fn test_jlt_taken() { let code = assemble( - "MOVI R0, 3\nMOVI R1, 5\nJLT R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 3\nMOVI R1, 5\nJLT R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -2122,8 +2517,9 @@ mod tests { #[test] fn test_jgt_taken() { let code = assemble( - "MOVI R0, 10\nMOVI R1, 5\nJGT R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 10\nMOVI R1, 5\nJGT R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -2132,8 +2528,9 @@ mod tests { #[test] fn test_jgt_not_taken_equal() { let code = assemble( - "MOVI R0, 5\nMOVI R1, 5\nJGT R0, R1, hit\nMOVI R2, 99\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 5\nMOVI R1, 5\nJGT R0, R1, hit\nMOVI R2, 99\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 99); @@ -2142,8 +2539,9 @@ mod tests { #[test] fn test_jle_taken_equal() { let code = assemble( - "MOVI R0, 5\nMOVI R1, 5\nJLE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 5\nMOVI R1, 5\nJLE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -2152,8 +2550,9 @@ mod tests { #[test] fn test_jle_taken_less() { let code = assemble( - "MOVI R0, 3\nMOVI R1, 5\nJLE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT" - ).unwrap(); + "MOVI R0, 3\nMOVI R1, 5\nJLE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -2179,7 +2578,8 @@ mod tests { #[test] fn test_correction_template() { let mut e = ConservationEnforcer::with_options( - policies::length_budget_policy(3), 3, + policies::length_budget_policy(3), + 3, Some("🚫 Blocked: {reason}"), ); let r = e.enforce("Q", "This is a very long response that exceeds budget"); @@ -2202,7 +2602,12 @@ mod tests { let mut e = ConservationEnforcer::new(policies::repetition_policy(300), 1000); let r = e.enforce("Summarize", "the the the the the the the the the the"); assert!(!r.allowed); - assert!(r.violation.unwrap().reason.to_lowercase().contains("repetition")); + assert!(r + .violation + .unwrap() + .reason + .to_lowercase() + .contains("repetition")); } // ── Category policy tests ── @@ -2210,18 +2615,27 @@ mod tests { #[test] fn test_category_allows_on_topic() { let mut e = ConservationEnforcer::new(policies::category_policy(50), 1000); - let r = e.enforce("Python programming language", - "Python is a great programming language for beginners and experts alike"); + let r = e.enforce( + "Python programming language", + "Python is a great programming language for beginners and experts alike", + ); assert!(r.allowed); } #[test] fn test_category_blocks_off_topic() { let mut e = ConservationEnforcer::new(policies::category_policy(900), 1000); - let r = e.enforce("quantum physics particles", - "banana apple orange grape melon"); + let r = e.enforce( + "quantum physics particles", + "banana apple orange grape melon", + ); assert!(!r.allowed); - assert!(r.violation.unwrap().reason.to_lowercase().contains("category")); + assert!(r + .violation + .unwrap() + .reason + .to_lowercase() + .contains("category")); } // ── Entropy policy tests ── @@ -2229,18 +2643,24 @@ mod tests { #[test] fn test_entropy_allows_high() { let mut e = ConservationEnforcer::new(policies::entropy_policy(1000), 1000); - let r = e.enforce("List colors", - "red blue green yellow orange purple cyan magenta"); + let r = e.enforce( + "List colors", + "red blue green yellow orange purple cyan magenta", + ); assert!(r.allowed); } #[test] fn test_entropy_blocks_low() { let mut e = ConservationEnforcer::new(policies::entropy_policy(2500), 1000); - let r = e.enforce("Write a poem", - "go go go go go go go go go go"); + let r = e.enforce("Write a poem", "go go go go go go go go go go"); assert!(!r.allowed); - assert!(r.violation.unwrap().reason.to_lowercase().contains("entropy")); + assert!(r + .violation + .unwrap() + .reason + .to_lowercase() + .contains("entropy")); } // ── Combined policy tests ── @@ -2258,7 +2678,10 @@ mod tests { fn test_combined_blocks_on_length() { let policy = policies::combined_policy(3, 500, 0, 0, 0, false, 0); let mut e = ConservationEnforcer::new(policy, 3); - let r = e.enforce("Write a long essay about AI", &"Artificial intelligence is ".repeat(50)); + let r = e.enforce( + "Write a long essay about AI", + &"Artificial intelligence is ".repeat(50), + ); assert!(!r.allowed); assert!(r.violation.unwrap().reason.contains("Length")); } @@ -2267,10 +2690,17 @@ mod tests { fn test_combined_blocks_on_repetition() { let policy = policies::combined_policy(10000, 200, 0, 0, 0, false, 0); let mut e = ConservationEnforcer::new(policy, 10000); - let r = e.enforce("Describe a sunset", - "beautiful beautiful beautiful beautiful beautiful beautiful beautiful"); + let r = e.enforce( + "Describe a sunset", + "beautiful beautiful beautiful beautiful beautiful beautiful beautiful", + ); assert!(!r.allowed); - assert!(r.violation.unwrap().reason.to_lowercase().contains("repetition")); + assert!(r + .violation + .unwrap() + .reason + .to_lowercase() + .contains("repetition")); } // ── Enforcement result tests ── @@ -2342,18 +2772,24 @@ mod tests { #[test] fn test_density_allows_high() { let mut e = ConservationEnforcer::new(policies::information_density_policy(300), 1000); - let r = e.enforce("List colors", - "red blue green yellow orange purple cyan magenta violet turquoise"); + let r = e.enforce( + "List colors", + "red blue green yellow orange purple cyan magenta violet turquoise", + ); assert!(r.allowed); } #[test] fn test_density_blocks_low() { let mut e = ConservationEnforcer::new(policies::information_density_policy(500), 1000); - let r = e.enforce("Write a poem", - "go go go go go go go go go go"); + let r = e.enforce("Write a poem", "go go go go go go go go go go"); assert!(!r.allowed); - assert!(r.violation.unwrap().reason.to_lowercase().contains("density")); + assert!(r + .violation + .unwrap() + .reason + .to_lowercase() + .contains("density")); } #[test] @@ -2369,16 +2805,20 @@ mod tests { #[test] fn test_scope_allows_on_topic() { let mut e = ConservationEnforcer::new(policies::scope_discipline_policy(50, 10), 1000); - let r = e.enforce("Python programming language tutorial", - "Python is a great programming language for beginners"); + let r = e.enforce( + "Python programming language tutorial", + "Python is a great programming language for beginners", + ); assert!(r.allowed); } #[test] fn test_scope_blocks_off_topic() { let mut e = ConservationEnforcer::new(policies::scope_discipline_policy(500, 10), 1000); - let r = e.enforce("quantum physics particles energy", - "banana apple orange grape melon fruit"); + let r = e.enforce( + "quantum physics particles energy", + "banana apple orange grape melon fruit", + ); assert!(!r.allowed); assert!(r.violation.unwrap().reason.to_lowercase().contains("scope")); } @@ -2442,8 +2882,7 @@ mod tests { fn test_combined_with_density() { let policy = policies::combined_policy(10000, 500, 0, 0, 300, false, 0); let mut e = ConservationEnforcer::new(policy, 10000); - let r = e.enforce("Write something", - "blah blah blah blah blah blah blah"); + let r = e.enforce("Write something", "blah blah blah blah blah blah blah"); assert!(!r.allowed); } diff --git a/tests/integration.rs b/tests/integration.rs index f117974..28aa651 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -3,8 +3,7 @@ //! These mirror the Python test suite, ensuring behavioral parity. use conservation_enforcer::{ - assemble, ConservationEnforcer, EnforcementResult, FluxVM, Op, VmError, - policies, + assemble, policies, ConservationEnforcer, EnforcementResult, FluxVM, Op, VmError, }; // ═══════════════════════════════════════════════════════════════════════════════ @@ -13,7 +12,21 @@ use conservation_enforcer::{ #[test] fn vm_add() { - let code = vec![Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 20, 0, Op::Iadd as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 20, + 0, + Op::Iadd as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 30); @@ -21,7 +34,21 @@ fn vm_add() { #[test] fn vm_sub() { - let code = vec![Op::Movi as u8, 0, 50, 0, Op::Movi as u8, 1, 20, 0, Op::Isub as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 50, + 0, + Op::Movi as u8, + 1, + 20, + 0, + Op::Isub as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 30); @@ -29,7 +56,21 @@ fn vm_sub() { #[test] fn vm_mul() { - let code = vec![Op::Movi as u8, 0, 7, 0, Op::Movi as u8, 1, 6, 0, Op::Imul as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 7, + 0, + Op::Movi as u8, + 1, + 6, + 0, + Op::Imul as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 42); @@ -37,7 +78,21 @@ fn vm_mul() { #[test] fn vm_div() { - let code = vec![Op::Movi as u8, 0, 100, 0, Op::Movi as u8, 1, 5, 0, Op::Idiv as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 100, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Idiv as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 20); @@ -45,14 +100,42 @@ fn vm_div() { #[test] fn vm_div_by_zero() { - let code = vec![Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 0, 0, Op::Idiv as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 0, + 0, + Op::Idiv as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); assert_eq!(vm.run(&code), Err(VmError::DivisionByZero)); } #[test] fn vm_mod() { - let code = vec![Op::Movi as u8, 0, 17, 0, Op::Movi as u8, 1, 5, 0, Op::Imod as u8, 2, 0, 1, Op::Halt as u8]; + let code = vec![ + Op::Movi as u8, + 0, + 17, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Imod as u8, + 2, + 0, + 1, + Op::Halt as u8, + ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 2); @@ -65,9 +148,26 @@ fn vm_mod() { #[test] fn vm_je_taken() { let code = vec![ - Op::Movi as u8, 0, 5, 0, Op::Movi as u8, 1, 5, 0, - Op::Cmp as u8, 0, 1, Op::Je as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 5, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Je as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -77,9 +177,26 @@ fn vm_je_taken() { #[test] fn vm_jne_taken() { let code = vec![ - Op::Movi as u8, 0, 5, 0, Op::Movi as u8, 1, 3, 0, - Op::Cmp as u8, 0, 1, Op::Jne as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 5, + 0, + Op::Movi as u8, + 1, + 3, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jne as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -89,9 +206,26 @@ fn vm_jne_taken() { #[test] fn vm_jsge_greater() { let code = vec![ - Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 5, 0, - Op::Cmp as u8, 0, 1, Op::Jsge as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jsge as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -101,9 +235,26 @@ fn vm_jsge_greater() { #[test] fn vm_jsge_less_no_jump() { let code = vec![ - Op::Movi as u8, 0, 3, 0, Op::Movi as u8, 1, 5, 0, - Op::Cmp as u8, 0, 1, Op::Jsge as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 3, + 0, + Op::Movi as u8, + 1, + 5, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jsge as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -113,9 +264,26 @@ fn vm_jsge_less_no_jump() { #[test] fn vm_jslt_less() { let code = vec![ - Op::Movi as u8, 0, 3, 0, Op::Movi as u8, 1, 8, 0, - Op::Cmp as u8, 0, 1, Op::Jslt as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 3, + 0, + Op::Movi as u8, + 1, + 8, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jslt as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -125,9 +293,26 @@ fn vm_jslt_less() { #[test] fn vm_jslt_greater_no_jump() { let code = vec![ - Op::Movi as u8, 0, 10, 0, Op::Movi as u8, 1, 3, 0, - Op::Cmp as u8, 0, 1, Op::Jslt as u8, 0, 4, 0, - Op::Movi as u8, 0, 99, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 3, + 0, + Op::Cmp as u8, + 0, + 1, + Op::Jslt as u8, + 0, + 4, + 0, + Op::Movi as u8, + 0, + 99, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -195,14 +380,24 @@ fn syscall_unique_ratio() { #[test] fn syscall_violation_flag() { let code = vec![ - Op::Movi as u8, 1, 2, 0, - Op::Movi as u8, 0, 8, 0, Op::Syscall as u8, + Op::Movi as u8, + 1, + 2, + 0, + Op::Movi as u8, + 0, + 8, + 0, + Op::Syscall as u8, Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert!(vm.violated()); - assert!(vm.violation_reason_str().to_lowercase().contains("repetition")); + assert!(vm + .violation_reason_str() + .to_lowercase() + .contains("repetition")); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -212,8 +407,19 @@ fn syscall_violation_flag() { #[test] fn stack_push_pop() { let code = vec![ - Op::Movi as u8, 0, 42, 0, Op::Push as u8, 0, - Op::Movi as u8, 0, 0, 0, Op::Pop as u8, 1, Op::Halt as u8, + Op::Movi as u8, + 0, + 42, + 0, + Op::Push as u8, + 0, + Op::Movi as u8, + 0, + 0, + 0, + Op::Pop as u8, + 1, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -223,8 +429,17 @@ fn stack_push_pop() { #[test] fn stack_inc_dec() { let code = vec![ - Op::Movi as u8, 0, 5, 0, Op::Inc as u8, 0, - Op::Inc as u8, 0, Op::Dec as u8, 0, Op::Halt as u8, + Op::Movi as u8, + 0, + 5, + 0, + Op::Inc as u8, + 0, + Op::Inc as u8, + 0, + Op::Dec as u8, + 0, + Op::Halt as u8, ]; let mut vm = FluxVM::new(); vm.run(&code).unwrap(); @@ -253,7 +468,8 @@ fn length_blocks_long_output() { #[test] fn length_correction_message() { let mut e = ConservationEnforcer::with_options( - policies::length_budget_policy(3), 3, + policies::length_budget_policy(3), + 3, Some("🚫 Blocked: {reason}"), ); let result = e.enforce("Q", "This is a very long response that exceeds budget"); @@ -280,7 +496,12 @@ fn repetition_blocks_repetitive() { let mut e = ConservationEnforcer::new(policies::repetition_policy(300), 1000); let result = e.enforce("Summarize", "the the the the the the the the the the"); assert!(!result.allowed); - assert!(result.violation.unwrap().reason.to_lowercase().contains("repetition")); + assert!(result + .violation + .unwrap() + .reason + .to_lowercase() + .contains("repetition")); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -305,7 +526,12 @@ fn category_blocks_off_topic() { "banana apple orange grape melon", ); assert!(!result.allowed); - assert!(result.violation.unwrap().reason.to_lowercase().contains("category")); + assert!(result + .violation + .unwrap() + .reason + .to_lowercase() + .contains("category")); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -327,7 +553,12 @@ fn entropy_blocks_low() { let mut e = ConservationEnforcer::new(policies::entropy_policy(2500), 1000); let result = e.enforce("Write a poem", "go go go go go go go go go go"); assert!(!result.allowed); - assert!(result.violation.unwrap().reason.to_lowercase().contains("entropy")); + assert!(result + .violation + .unwrap() + .reason + .to_lowercase() + .contains("entropy")); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -366,7 +597,12 @@ fn combined_blocks_repetition() { "beautiful beautiful beautiful beautiful beautiful beautiful beautiful", ); assert!(!result.allowed); - assert!(result.violation.unwrap().reason.to_lowercase().contains("repetition")); + assert!(result + .violation + .unwrap() + .reason + .to_lowercase() + .contains("repetition")); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -386,12 +622,14 @@ fn density_allows_high() { #[test] fn density_blocks_low() { let mut e = ConservationEnforcer::new(policies::information_density_policy(500), 1000); - let result = e.enforce( - "Write a poem", - "go go go go go go go go go go", - ); + let result = e.enforce("Write a poem", "go go go go go go go go go go"); assert!(!result.allowed); - assert!(result.violation.unwrap().reason.to_lowercase().contains("density")); + assert!(result + .violation + .unwrap() + .reason + .to_lowercase() + .contains("density")); } #[test] @@ -424,7 +662,12 @@ fn scope_blocks_off_topic() { "banana apple orange grape melon fruit", ); assert!(!result.allowed); - assert!(result.violation.unwrap().reason.to_lowercase().contains("scope")); + assert!(result + .violation + .unwrap() + .reason + .to_lowercase() + .contains("scope")); } #[test] @@ -432,7 +675,12 @@ fn scope_blocks_excessive_expansion() { let mut e = ConservationEnforcer::new(policies::scope_discipline_policy(0, 10), 1000); let result = e.enforce("hi", &"hello ".repeat(100)); assert!(!result.allowed); - assert!(result.violation.unwrap().reason.to_lowercase().contains("scope")); + assert!(result + .violation + .unwrap() + .reason + .to_lowercase() + .contains("scope")); } #[test] @@ -488,10 +736,7 @@ fn decay_blocks_max_calls() { #[test] fn budget_syncs_after_decay() { - let mut e = ConservationEnforcer::new( - policies::budget_decay_policy(50, 5, 100), - 500, - ); + let mut e = ConservationEnforcer::new(policies::budget_decay_policy(50, 5, 100), 500); e.enforce("q", "a response here"); assert_eq!(e.remaining_budget(), 450); } @@ -586,9 +831,7 @@ fn enforce_with_llm_wraps_call() { #[test] fn asm_je_label() { - let code = assemble( - "MOVI R0, 0\nCMP R0, R0\nJE done\nMOVI R0, 99\ndone:\nHALT", - ).unwrap(); + let code = assemble("MOVI R0, 0\nCMP R0, R0\nJE done\nMOVI R0, 99\ndone:\nHALT").unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(0), 0); @@ -596,9 +839,7 @@ fn asm_je_label() { #[test] fn asm_jmp_label() { - let code = assemble( - "MOVI R0, 1\nJMP skip\nMOVI R0, 99\nskip:\nMOVI R0, 42\nHALT", - ).unwrap(); + let code = assemble("MOVI R0, 1\nJMP skip\nMOVI R0, 99\nskip:\nMOVI R0, 42\nHALT").unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(0), 42); @@ -608,7 +849,8 @@ fn asm_jmp_label() { fn asm_jge_taken_greater() { let code = assemble( "MOVI R0, 10\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -618,7 +860,8 @@ fn asm_jge_taken_greater() { fn asm_jge_taken_equal() { let code = assemble( "MOVI R0, 5\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -628,7 +871,8 @@ fn asm_jge_taken_equal() { fn asm_jge_not_taken() { let code = assemble( "MOVI R0, 3\nMOVI R1, 5\nJGE R0, R1, hit\nMOVI R2, 99\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 99); @@ -638,7 +882,8 @@ fn asm_jge_not_taken() { fn asm_jlt_taken() { let code = assemble( "MOVI R0, 3\nMOVI R1, 5\nJLT R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -648,7 +893,8 @@ fn asm_jlt_taken() { fn asm_jgt_taken() { let code = assemble( "MOVI R0, 10\nMOVI R1, 5\nJGT R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -658,7 +904,8 @@ fn asm_jgt_taken() { fn asm_jgt_not_taken_equal() { let code = assemble( "MOVI R0, 5\nMOVI R1, 5\nJGT R0, R1, hit\nMOVI R2, 99\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 99); @@ -668,7 +915,8 @@ fn asm_jgt_not_taken_equal() { fn asm_jle_taken_equal() { let code = assemble( "MOVI R0, 5\nMOVI R1, 5\nJLE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -678,7 +926,8 @@ fn asm_jle_taken_equal() { fn asm_jle_taken_less() { let code = assemble( "MOVI R0, 3\nMOVI R1, 5\nJLE R0, R1, hit\nMOVI R2, 0\nHALT\nhit:\nMOVI R2, 1\nHALT", - ).unwrap(); + ) + .unwrap(); let mut vm = FluxVM::new(); vm.run(&code).unwrap(); assert_eq!(vm.regs.get(2), 1); @@ -700,12 +949,18 @@ fn asm_nop() { #[test] fn asm_movi() { - assert_eq!(assemble("MOVI R0, 42").unwrap(), vec![Op::Movi as u8, 0, 42, 0]); + assert_eq!( + assemble("MOVI R0, 42").unwrap(), + vec![Op::Movi as u8, 0, 42, 0] + ); } #[test] fn asm_add() { - assert_eq!(assemble("IADD R2, R0, R1").unwrap(), vec![Op::Iadd as u8, 2, 0, 1]); + assert_eq!( + assemble("IADD R2, R0, R1").unwrap(), + vec![Op::Iadd as u8, 2, 0, 1] + ); } #[test] @@ -727,9 +982,18 @@ fn asm_cmp() { fn asm_multiple_instructions() { let code = assemble("MOVI R0, 10\nMOVI R1, 20\nIADD R2, R0, R1\nHALT").unwrap(); let expected = vec![ - Op::Movi as u8, 0, 10, 0, - Op::Movi as u8, 1, 20, 0, - Op::Iadd as u8, 2, 0, 1, + Op::Movi as u8, + 0, + 10, + 0, + Op::Movi as u8, + 1, + 20, + 0, + Op::Iadd as u8, + 2, + 0, + 1, Op::Halt as u8, ]; assert_eq!(code, expected); @@ -761,10 +1025,7 @@ fn asm_undefined_label() { fn combined_with_density() { let policy = policies::combined_policy(10000, 500, 0, 0, 300, false, 0); let mut e = ConservationEnforcer::new(policy, 10000); - let result = e.enforce( - "Write something", - "blah blah blah blah blah blah blah", - ); + let result = e.enforce("Write something", "blah blah blah blah blah blah blah"); assert!(!result.allowed); } From 60a34dfdcc70ae3cb5170eb4663628b90f553da8 Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:14:35 -0800 Subject: [PATCH 3/9] fix: length_budget_policy and combined_policy honor max_tokens length_budget_policy(max_tokens) and the length check inside combined_policy(...) compared the approximate output token count (GET_TOKEN_COUNT) against the enforcer's conservation budget (GET_BUDGET, syscall 10) instead of the configured max_tokens parameter. The parameter was completely unused (compiler warning), so the 'length budget' threshold always tracked the decay budget. The bug was masked because every existing test and README example passed identical values for max_tokens and the enforcer budget. Now both policies load max_tokens into R3 directly (MOVI R3, {max_tokens}) and compare the token count against it. Added two regression tests that decouple max_tokens from the budget: - length_threshold_is_max_tokens_not_budget: tiny max_tokens + huge budget still blocks an oversized output. - length_allows_under_max_tokens_even_when_budget_low: output above the budget but under max_tokens is allowed. Both fail against the previous implementation. --- src/lib.rs | 13 +++++++------ tests/integration.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d4f4210..2762c2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1374,15 +1374,18 @@ pub mod policies { use super::assemble; /// Enforce a maximum output length (approximate token count). + /// + /// `max_tokens` is the hard limit on the approximate token count of the + /// output (`len/4`). The output is blocked when the token count exceeds + /// `max_tokens`. Note that the value is encoded as a 16-bit FLUX + /// immediate, so it must fit in `0..=65535`. pub fn length_budget_policy(max_tokens: i32) -> Vec { assemble(&format!( r#" MOVI R0, 5 SYSCALL MOV R2, R0 - MOVI R0, 10 - SYSCALL - MOV R3, R0 + MOVI R3, {max_tokens} JGT R2, R3, block MOVI R0, 0 HALT @@ -1614,9 +1617,7 @@ exhausted: MOVI R0, 5 SYSCALL MOV R2, R0 - MOVI R0, 10 - SYSCALL - MOV R3, R0 + MOVI R3, {max_tokens} JGT R2, R3, block_length MOVI R0, 6 diff --git a/tests/integration.rs b/tests/integration.rs index 28aa651..b335ae8 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -477,6 +477,33 @@ fn length_correction_message() { assert!(result.output.contains("🚫 Blocked:")); } +// The threshold must be the `max_tokens` parameter, NOT the enforcer's +// conservation budget. Earlier the policy compared the token count against +// `GET_BUDGET`, so passing a small `max_tokens` with a large budget let +// oversized outputs through. These two cases pin the threshold to +// `max_tokens` regardless of the budget. + +#[test] +fn length_threshold_is_max_tokens_not_budget() { + // max_tokens = 3 (tiny), budget = 1_000_000 (huge). + // Output "word " * 100 -> 500 chars -> ~125 tokens, well above 3. + // Must be blocked even though the budget is enormous. + let mut e = ConservationEnforcer::new(policies::length_budget_policy(3), 1_000_000); + let result = e.enforce("Tell me everything", &"word ".repeat(100)); + assert!(!result.allowed); + assert!(result.violation.unwrap().reason.contains("Length budget")); +} + +#[test] +fn length_allows_under_max_tokens_even_when_budget_low() { + // max_tokens = 1000 (large), budget = 3 (tiny). + // Output of 24 chars -> ~6 tokens: above the budget of 3 but well under + // max_tokens of 1000. Must be allowed: the budget is not the length gate. + let mut e = ConservationEnforcer::new(policies::length_budget_policy(1000), 3); + let result = e.enforce("Q", "a short response here"); + assert!(result.allowed); +} + // ═══════════════════════════════════════════════════════════════════════════════ // Repetition Policy // ═══════════════════════════════════════════════════════════════════════════════ From 074d2e41fafe821f4c594622f2098d6ad3bbe80d Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:17:53 -0800 Subject: [PATCH 4/9] fix: compute per-mille syscall ratios in u64 to prevent overflow GET_REPETITION, GET_CATEGORY, and GET_UNIQUE_RATIO each compute a per-mille ratio as (count * 1000) / total using u32 arithmetic. When a single word appears more than ~4.29M times (count * 1000 > u32::MAX) the multiply overflows: a debug build panics and a release build silently wraps, producing a wrong (often tiny) ratio and thus a wrong enforcement decision. All three now accumulate counts in u64 before multiplying by 1000, casting back to u32 only for the register store (the ratio is bounded to 0..=1000 so it always fits). GET_REPETITION is also rewritten to count each distinct word once (iterator-based) rather than re-scanning the whole output for every token, which additionally makes the large-input case O(total) instead of O(total^2). This also clears the clippy needless_range_loop lint. Added syscall_ratios_no_overflow_on_large_input: feeds 4.5M identical tokens through GET_REPETITION and GET_UNIQUE_RATIO. Verified it panics with 'attempt to multiply with overflow' under the old u32 math. --- src/lib.rs | 39 +++++++++++++++++++-------------------- tests/integration.rs | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2762c2b..0708f28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -638,21 +638,23 @@ impl FluxVM { if words.is_empty() { self.regs.set(0, 0); } else { - let mut max_count = 0u32; - // simple word frequency - for i in 0..words.len() { - let w = words[i]; - let mut c = 0u32; - for j in 0..words.len() { - if words[j] == w { - c += 1; - } + let total = words.len() as u64; + let mut max_count = 0u64; + let mut seen: Vec<&str> = Vec::new(); + // Count the frequency of each distinct word once (the result is + // the maximum frequency). Arithmetic is done in u64 so that + // `count * 1000` cannot overflow for large inputs. + for w in &words { + if seen.contains(w) { + continue; } + seen.push(*w); + let c = words.iter().filter(|&&x| x == *w).count() as u64; if c > max_count { max_count = c; } } - self.regs.set(0, (max_count * 1000) / words.len() as u32); + self.regs.set(0, ((max_count * 1000) / total) as u32); } } syscall::GET_CATEGORY => { @@ -663,14 +665,10 @@ impl FluxVM { if ow.is_empty() { self.regs.set(0, 0); } else { - let iw_set: Vec<&str> = iw.into_iter().collect(); - let mut overlap = 0u32; - for w in &ow { - if iw_set.contains(w) { - overlap += 1; - } - } - let score = core::cmp::min(1000, (overlap * 1000) / ow.len() as u32); + let total = ow.len() as u64; + let overlap = ow.iter().filter(|w| iw.contains(*w)).count() as u64; + let raw = ((overlap * 1000) / total) as u32; + let score = core::cmp::min(1000, raw); self.regs.set(0, score); } } @@ -687,14 +685,15 @@ impl FluxVM { if words.is_empty() { self.regs.set(0, 1000); } else { - let mut unique = Vec::new(); + let total = words.len() as u64; + let mut unique: Vec<&str> = Vec::new(); for w in &words { if !unique.contains(w) { unique.push(*w); } } self.regs - .set(0, ((unique.len() as u32) * 1000) / words.len() as u32); + .set(0, (((unique.len() as u64) * 1000) / total) as u32); } } syscall::GET_ENTROPY => { diff --git a/tests/integration.rs b/tests/integration.rs index b335ae8..e3457f1 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -377,6 +377,29 @@ fn syscall_unique_ratio() { assert_eq!(vm.regs.get(0), 600); } +// The per-mille ratios are computed as `count * 1000 / total`. With u32 +// arithmetic, `count * 1000` overflows once a word appears more than +// ~4.29M times (panic in debug, wrap in release). The math must use a +// wider intermediate so large inputs cannot trigger undefined behaviour +// or silently wrong results. +#[test] +fn syscall_ratios_no_overflow_on_large_input() { + // 4.500.000 identical tokens: max_count * 1000 = 4.5e9 > u32::MAX. + let big = "x ".repeat(4_500_000); + let mut vm = FluxVM::new(); + vm.load_output(&big); + + // GET_REPETITION (syscall 6): all-same input -> 1000 per-mille. + let code = vec![Op::Movi as u8, 0, 6, 0, Op::Syscall as u8, Op::Halt as u8]; + vm.run(&code).unwrap(); + assert_eq!(vm.regs.get(0), 1000); + + // GET_UNIQUE_RATIO (syscall 11): 1 unique / 4.5M total -> 0 per-mille. + let code = vec![Op::Movi as u8, 0, 11, 0, Op::Syscall as u8, Op::Halt as u8]; + vm.run(&code).unwrap(); + assert_eq!(vm.regs.get(0), 0); +} + #[test] fn syscall_violation_flag() { let code = vec![ From d2af569752c04cdca422e08ac3030a2ba19d2317 Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:21:30 -0800 Subject: [PATCH 5/9] chore: clear all compiler and clippy warnings under default features cargo build/clippy were emitting warnings that would fail a strict CI gate (clippy --all-targets -D warnings previously errored). Fixed: - RegisterFile::set: drop the no-op 'val & 0xFFFFFFFF' mask and the if/else with identical arms; 'val as i32' already reinterprets the bits for the sign flag. - parse_label: use the line_num argument (it was unused) in the duplicate-label error message, and simplify the call site to '?'. - simple_hash: gate behind #[cfg(feature = "audit")] since it is only referenced from audit-gated code (was dead-code without the feature). - scope_discipline_policy: drop a useless format!() and the dummy 'let _ = i' by using a '_' loop binding. - is_leap (audit feature): use is_multiple_of instead of manual '%'. - tests/integration.rs: remove unused EnforcementResult import. No behavior change. --- src/lib.rs | 30 +++++++++++++----------------- tests/integration.rs | 4 +--- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0708f28..e52ca41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,13 +203,12 @@ impl RegisterFile { #[inline] pub fn set(&mut self, idx: usize, val: u32) { - let v = val & 0xFFFFFFFF; - self.r[idx] = v; - self.flag_zero = v == 0; - // interpret as signed - let signed = if v >= 0x80000000 { v as i32 } else { v as i32 }; - // Note: u32 → i32 wrap; in Rust `v as i32` does the wrap for us. - self.flag_sign = signed < 0; + self.r[idx] = val; + self.flag_zero = val == 0; + // Reinterpret the bits as signed to set the sign flag. `val as i32` + // performs the wrapping reinterpretation we want for both halves of + // the range, so no conditional is needed. + self.flag_sign = (val as i32) < 0; } } @@ -956,6 +955,7 @@ impl ConservationEnforcer { // ═══════════════════════════════════════════════════════════════════════════════ /// Simple FNV-1a hash for audit logging (privacy-preserving, non-cryptographic). +#[cfg(feature = "audit")] fn simple_hash(s: &str) -> u64 { let mut hash: u64 = 0xcbf29ce484222325; for byte in s.bytes() { @@ -1013,7 +1013,7 @@ fn days_to_date(days: u64) -> (u64, u64, u64) { #[cfg(feature = "audit")] fn is_leap(year: u64) -> bool { - (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400) } #[cfg(feature = "audit")] @@ -1082,10 +1082,7 @@ pub mod assembler { } // Label? - let text = match parse_label(&text, &mut labels, &raw, line_num) { - Ok(remaining) => remaining, - Err(e) => return Err(e), - }; + let text = parse_label(&text, &mut labels, &raw, line_num)?; if text.is_empty() { continue; } @@ -1336,7 +1333,7 @@ pub mod assembler { let label = label_part.to_string(); // Check for duplicates if labels.iter().any(|(l, _)| l == &label) { - return Err(format!("Duplicate label '{label}'")); + return Err(format!("Line {}: duplicate label '{label}'", line_num + 1)); } labels.push((label, raw.len())); let remaining = text[colon_pos + 1..].trim(); @@ -1494,12 +1491,11 @@ block: let mut add_lines = String::new(); if max_expansion >= 2 { add_lines.push_str("IADD R6, R4, R4\n"); // 2× - for i in 3..=max_expansion { - add_lines.push_str(&format!("IADD R6, R6, R4\n")); // i× - let _ = i; // suppress unused warning + for _ in 3..=max_expansion { + add_lines.push_str("IADD R6, R6, R4\n"); } } else { - // max_expansion = 1: just copy + // max_expansion <= 1: just copy add_lines.push_str("MOV R6, R4\n"); } diff --git a/tests/integration.rs b/tests/integration.rs index e3457f1..0f4b9c5 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2,9 +2,7 @@ //! //! These mirror the Python test suite, ensuring behavioral parity. -use conservation_enforcer::{ - assemble, policies, ConservationEnforcer, EnforcementResult, FluxVM, Op, VmError, -}; +use conservation_enforcer::{assemble, policies, ConservationEnforcer, FluxVM, Op, VmError}; // ═══════════════════════════════════════════════════════════════════════════════ // VM Arithmetic From e58bf4cdb3505f10d1d24624110319a08f915e5a Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:22:42 -0800 Subject: [PATCH 6/9] test: actually exercise the information-density threshold The density_boundary test claimed to check '1 unique / 2 total = 500 per-mille, exactly at threshold' but used the output 'hello world', which has 2 unique words out of 2 (ratio 1000) - well above the 500 threshold. The test therefore passed regardless of where the real boundary sat and could not detect an off-by-one (e.g. JLE vs JLT). Now uses 'go go' (1 unique / 2 total = 500, equal to the threshold, allowed because JLT is strict) and 'go go go' (1/3 = 333, just below the threshold, must be blocked) so both sides of the boundary are pinned. Updated in both the lib unit tests and integration tests. --- src/lib.rs | 11 ++++++++--- tests/integration.rs | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e52ca41..15301f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2791,9 +2791,14 @@ mod tests { #[test] fn test_density_boundary() { let mut e = ConservationEnforcer::new(policies::information_density_policy(500), 1000); - // 1 unique out of 2 total = 500 per-mille, exactly at threshold → passes (JLT is strict) - let r = e.enforce("test", "hello world"); - assert!(r.allowed); + // "go go" -> 1 unique out of 2 total = 500 per-mille, exactly at the + // threshold. JLT is strict (<), so equal-to-threshold is allowed. + let at = e.enforce("test", "go go"); + assert!(at.allowed); + // "go go go" -> 1 unique out of 3 total = 333 per-mille, just below the + // threshold, so it must be blocked. + let below = e.enforce("test", "go go go"); + assert!(!below.allowed); } // ── Scope discipline policy tests ── diff --git a/tests/integration.rs b/tests/integration.rs index 0f4b9c5..2383d76 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -682,10 +682,15 @@ fn density_blocks_low() { #[test] fn density_boundary() { - // 1 unique out of 2 total = 500 per-mille, exactly at threshold → passes (JLT is strict) let mut e = ConservationEnforcer::new(policies::information_density_policy(500), 1000); - let result = e.enforce("test", "hello world"); - assert!(result.allowed); + // "go go" -> 1 unique out of 2 total = 500 per-mille, exactly at the + // threshold. JLT is strict (<), so equal-to-threshold is allowed. + let at = e.enforce("test", "go go"); + assert!(at.allowed); + // "go go go" -> 1 unique out of 3 total = 333 per-mille, just below the + // threshold, so it must be blocked. + let below = e.enforce("test", "go go go"); + assert!(!below.allowed); } // ═══════════════════════════════════════════════════════════════════════════════ From b8e979e4f0c1b26e8e3572132b7ebc7f1ba98dc0 Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:24:53 -0800 Subject: [PATCH 7/9] fix: make AuditLog::read_all return the raw JSONL records AuditLog::read_all was declared to return Vec where JsonValue was an empty (uninhabited) enum in a private module. That made the method silently always return an empty vector - a stub dressed up as a working API. It now returns Vec of the non-empty JSONL records actually on disk (empty if the file is missing/unreadable), which is genuinely useful since the crate intentionally has no JSON dependency. The bogus serde_lite placeholder module is removed. Also silence the feature-gated clippy::too_many_arguments on AuditLog::log (it takes a flat audit-record argument list) so the audit feature passes 'clippy --all-features -D warnings'. Added a feature-gated test that logs two records and asserts read_all returns both with the expected fields (and agrees with summary()). --- src/lib.rs | 26 +++++++++++++++-------- tests/integration.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 15301f0..ce738d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1711,6 +1711,7 @@ pub mod audit { } } + #[allow(clippy::too_many_arguments)] pub fn log( &self, input_text: &str, @@ -1742,10 +1743,22 @@ pub mod audit { } } - pub fn read_all(&self) -> Vec { - // Lightweight: we don't depend on serde, so we parse manually - // For now, return raw lines - Vec::new() + /// Read all non-empty audit records as raw JSON Lines strings. + /// + /// This crate intentionally avoids a JSON dependency, so records are + /// returned as raw JSONL strings for the caller to parse or inspect. + /// Returns an empty vector if the audit file does not exist or cannot + /// be read. + pub fn read_all(&self) -> Vec { + match fs::read_to_string(&self.path) { + Ok(content) => content + .lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .map(str::to_string) + .collect(), + Err(_) => Vec::new(), + } } pub fn summary(&self) -> AuditSummary { @@ -1809,11 +1822,6 @@ pub mod audit { 0 } } - - // Minimal JSON value type placeholder - mod serde_lite { - pub enum JsonValue {} - } } // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/tests/integration.rs b/tests/integration.rs index 2383d76..da95d7d 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1090,3 +1090,53 @@ fn combined_with_decay() { let r2 = e.enforce("q", "a reasonable response here"); assert!(!r2.allowed); } + +// ═══════════════════════════════════════════════════════════════════════════════ +// Audit (feature-gated) +// ═══════════════════════════════════════════════════════════════════════════════ + +#[cfg(feature = "audit")] +#[test] +fn audit_log_read_all_returns_logged_lines() { + use conservation_enforcer::audit::AuditLog; + + let path = std::env::temp_dir().join(format!( + "conservation_enforcer_audit_{}.jsonl", + std::process::id() + )); + // Start from a clean file. + let log = AuditLog::new(path.to_str().unwrap()); + log.clear(); + assert!(log.read_all().is_empty()); + + log.log("question", "answer", true, None, 0, 12, 500, 1); + log.log( + "question", + "answer", + false, + Some("Length budget exceeded"), + 1, + 7, + 500, + 2, + ); + + let records = log.read_all(); + assert_eq!( + records.len(), + 2, + "read_all must return the two logged records" + ); + assert!(records[0].contains(r#""allowed":true"#)); + assert!(records[1].contains(r#""allowed":false"#)); + assert!(records[1].contains(r#""violation_code":1"#)); + + // summary() must agree with the raw records. + let s = log.summary(); + assert_eq!(s.total_calls, 2); + assert_eq!(s.total_blocked, 1); + + log.clear(); + assert!(log.read_all().is_empty()); + let _ = std::fs::remove_file(&path); +} From 8d85caa1f8afcd255286d9375d4e60c273e1787d Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:27:16 -0800 Subject: [PATCH 8/9] docs: correct README/Cargo.toml claims that didn't match the code Verified each public claim against the actual code and fixed the mismatches rather than overselling: - no_std: 'cargo build --no-default-features' fails (~90 errors; the code uses String/Vec/format!/HashMap). The README claimed 'No_std compatible - Works in embedded, WASM, and kernel contexts'. Replaced with an honest status marker: no_std/embedded is a stated goal, not a working configuration. The crate does build for wasm32-unknown-unknown with std, so that is now the advertised target. - Cargo.toml categories: dropped the false 'no-std' and 'embedded' crates.io categories (kept 'wasm', which is verified). - Test count: integration.rs has ~80 tests (not '95+'); total is 150+. Updated the badge and the Architecture section accordingly. - Cross-implementation claims: removed the unverified assertions that this is a 'line-by-line port of the Python v0.2.0', that 'the Python 95-test suite has been replicated', and that bytecode is 'binary-compatible' across implementations. None of these are checked by CI. Reworded as independent implementations of the same ISA with cross-compatibility as an unverified goal. --- Cargo.toml | 2 +- README.md | 35 ++++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 80aba40..98b3a4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT" description = "FLUX bytecode conservation-law enforcement for LLM outputs (Rust)" repository = "https://github.com/SuperInstance/conservation-enforcer-rs" keywords = ["flux", "conservation", "llm", "policy", "bytecode"] -categories = ["no-std", "embedded", "wasm"] +categories = ["wasm"] [lib] name = "conservation_enforcer" diff --git a/README.md b/README.md index 42e4359..54fad4c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Crates.io](https://img.shields.io/crates/v/si-conservation-enforcer) ![Rust](https://img.shields.io/badge/rust-stable-orange) -![Tests](https://img.shields.io/badge/tests-95%2B-brightgreen) +![Tests](https://img.shields.io/badge/tests-150%2B-brightgreen) ![License](https://img.shields.io/github/license/SuperInstance/conservation-enforcer-rs) **FLUX bytecode conservation-law enforcement for LLM outputs — Rust implementation.** @@ -21,11 +21,18 @@ The FLUX bytecode acts as a deterministic, auditable policy layer. **You can't l ## Why Rust? - **Zero-cost abstractions** — The entire VM, assembler, and enforcement layer adds negligible overhead -- **No_std compatible** — Works in embedded, WASM, and kernel contexts (use `default-features = false`) +- **WASM-ready** — Builds unmodified for `wasm32-unknown-unknown` with the default `std` feature - **No external dependencies** — The entire crate is self-contained - **Memory safe** — Rust's ownership model guarantees no UB in the policy VM - **Deterministic** — Same input + bytecode = same output, every time +> **`no_std` status:** The crate is annotated with `#![cfg_attr(not(feature = "std"), no_std)]` +> and has scaffolding for a `no_std`/embedded port, but building with +> `--no-default-features` does **not** currently compile (the implementation uses +> `String`/`Vec`/`format!` and `std::collections::HashMap`). Treat `no_std`/embedded +> support as a stated goal, not a working configuration. The `wasm32-unknown-unknown` +> target (with `std`) is supported. + ## Installation ```bash @@ -240,11 +247,15 @@ fn main() { | `audit` | ❌ | JSON Lines audit logging to files | | `metrics` | ❌ | Metrics collection and export | -For `no_std` environments: -```toml -[dependencies] -si-conservation-enforcer = { version = "0.1", default-features = false } -``` +> **Note:** A `no_std`/embedded build (`default-features = false`) is a stated +> goal but does **not** currently compile (see `no_std` status above). The +> snippet below is the intended usage once that port lands; it is not functional today. +> +> ```toml +> [dependencies] +> # Not yet working — requires the unfinished no_std port. +> si-conservation-enforcer = { version = "0.1", default-features = false } +> ``` ## Cross-Implementation @@ -256,7 +267,13 @@ Both implement the same specification. Choose based on your runtime. ### Detailed Comparison -This crate is a line-by-line port of the [Python conservation-enforcer](https://github.com/SuperInstance/conservation-enforcer) v0.2.0. The Python version's test suite (95 tests) has been replicated in Rust. The bytecode produced by the assembler is binary-compatible — you can assemble a policy in Python and execute it in Rust, and vice versa. +This crate is a Rust implementation of the same FLUX ISA and policy +semantics as the [Python conservation-enforcer](https://github.com/SuperInstance/conservation-enforcer). +It is **not** a verified line-by-line port: the two implementations are maintained +independently, this Rust suite is its own test suite (not a replication of the +Python suite), and cross-implementation bytecode compatibility is a design goal +that is **not** currently verified by CI or tests. Treat the table below as a +component mapping, not a parity guarantee. | Component | Python | Rust | |-----------|--------|------| @@ -273,7 +290,7 @@ src/ └── lib.rs Entire crate (VM, assembler, enforcer, policies, audit, metrics) tests/ -└── integration.rs Comprehensive test suite (95+ tests) +└── integration.rs Integration test suite (~80 tests; 150+ tests total with unit + doc tests) ``` ## Ecosystem From 694b4e9d5104c4564ed9311f1b4f18acbffa9dd0 Mon Sep 17 00:00:00 2001 From: PurplePincher Automation Date: Sun, 12 Jul 2026 09:28:17 -0800 Subject: [PATCH 9/9] ci: enforce fmt, clippy --all-targets --all-features, and wasm build The previous workflow ran 'cargo clippy -- -D warnings' (no --all-targets), so warnings in tests/integration.rs (e.g. the unused EnforcementResult import) were invisible to CI, and there was no formatting gate at all. That is how the formatting drift and the test-file unused import shipped. Strengthened the pipeline: - 'cargo fmt --all -- --check' step. - clippy now runs with '--all-targets --all-features -- -D warnings' so lib, unit tests, integration tests, and the audit/metrics features are all covered. - Tests run under both default features and --all-features (exercises the audit read_all test). - Added a dedicated wasm32-unknown-unknown build job to back up the README's WASM-ready claim. - fail-fast: false so a beta-only failure still surfaces stable results. --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e3abff..651c94d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,12 @@ name: CI on: [push, pull_request] + jobs: test: + name: Test & lint (${{ matrix.rust }}) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: rust: [stable, beta] steps: @@ -11,5 +14,24 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - - run: cargo test --verbose - - run: cargo clippy -- -D warnings + components: rustfmt, clippy + - name: Format check + run: cargo fmt --all -- --check + - name: Clippy (all targets, all features) + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Test (default features) + run: cargo test --verbose + - name: Test (all features) + run: cargo test --all-features --verbose + + wasm: + name: wasm32 build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + targets: wasm32-unknown-unknown + - name: Build for wasm32-unknown-unknown + run: cargo build --target wasm32-unknown-unknown --verbose