1use clap::{Parser, Subcommand};
3use hikmah_kernel::decision::{evaluate, Criterion, DecisionFrame, DecisionOption};
4use hikmah_kernel::hook::rules_verdict;
5use hikmah_kernel::policy::KernelPolicy;
6use hikmah_kernel::recall::RecallQuery;
7use hikmah_kernel::trace::{Trace, TraceKind};
8use hikmah_kernel::MemoryStore;
9use serde_json::{json, Value};
10use std::collections::BTreeMap;
11use std::error::Error;
12use std::fs::{self, OpenOptions};
13use std::io::{self, BufRead, Write};
14use std::path::{Path, PathBuf};
15use std::process::{Command, Stdio};
16use std::time::Instant;
17
18type Result<T> = std::result::Result<T, Box<dyn Error>>;
19const CLOCK: u64 = 1_700_000_000_000;
20
21#[derive(Parser)]
22struct Args {
23 #[command(subcommand)]
24 command: Mode,
25}
26
27#[derive(Subcommand)]
28enum Mode {
29 Scale {
30 #[arg(long)]
31 dir: PathBuf,
32 #[arg(long, default_value_t = 1000)]
33 records: usize,
34 #[arg(long, default_value_t = 30)]
35 queries: usize,
36 },
37 Decisions {
38 #[arg(long, default_value_t = 10000)]
39 cases: usize,
40 #[arg(long, default_value_t = 20260926)]
41 seed: u64,
42 },
43 Recovery {
44 #[arg(long)]
45 dir: PathBuf,
46 },
47 #[command(hide = true)]
49 CommittedChild {
50 #[arg(long)]
51 dir: PathBuf,
52 },
53 Gate,
55}
56
57fn ms(start: Instant) -> f64 {
58 start.elapsed().as_secs_f64() * 1000.0
59}
60
61fn quantiles(mut values: Vec<f64>) -> Value {
62 values.sort_by(f64::total_cmp);
63 let q = |p: f64| {
64 if values.is_empty() {
65 return None;
66 }
67 let x = (values.len() - 1) as f64 * p;
68 let lo = x.floor() as usize;
69 let hi = x.ceil() as usize;
70 Some(values[lo] + (values[hi] - values[lo]) * x.fract())
71 };
72 json!({"n": values.len(), "p50": q(0.5), "p95": q(0.95), "p99": q(0.99)})
73}
74
75fn rss() -> Value {
76 let status = fs::read_to_string("/proc/self/status").unwrap_or_default();
77 let field = |key: &str| {
78 status.lines().find_map(|line| {
79 line.strip_prefix(key)
80 .and_then(|s| s.split_whitespace().next())
81 .and_then(|n| n.parse::<u64>().ok())
82 })
83 };
84 json!({"rss_kib": field("VmRSS:"), "process_peak_rss_kib": field("VmHWM:")})
85}
86
87fn token(mut n: usize) -> String {
88 let mut out = String::from("event");
89 for _ in 0..8 {
90 out.push((b'a' + (n % 26) as u8) as char);
91 n /= 26;
92 }
93 out
94}
95
96fn trace(i: usize) -> Trace {
97 let mut t = Trace::new(
98 TraceKind::Observation,
99 format!("{} deployment observation", token(i)),
100 "synthetic-kernel-benchmark",
101 );
102 t.id = format!("sample-{i}");
103 t.created_at_ms = CLOCK;
104 t.provenance.observed_at_ms = CLOCK;
105 t
106}
107
108fn fresh(dir: &Path) -> Result<PathBuf> {
109 fs::create_dir(dir)?;
111 Ok(dir.join("memory.jsonl"))
112}
113
114fn scale(dir: &Path, records: usize, queries: usize) -> Result<Value> {
115 if records == 0 || queries == 0 {
116 return Err("records and queries must be positive".into());
117 }
118 let path = fresh(dir)?;
119 let mut store = MemoryStore::open(&path, KernelPolicy::default())?;
120 let single_count = records.min(50);
121 let batched = records - single_count;
122 let mut batches = Vec::new();
123 let seed_started = Instant::now();
124 for begin in (0..batched).step_by(1000) {
125 let traces = (begin..(begin + 1000).min(batched)).map(trace).collect();
126 let start = Instant::now();
127 store.remember_many(traces)?;
128 batches.push(ms(start));
129 }
130 let mut appends = Vec::new();
131 for i in batched..records {
132 let t = trace(i);
133 let start = Instant::now();
134 store.remember(t)?;
135 appends.push(ms(start));
136 }
137 let seed_ms = ms(seed_started);
138 let memory_after_seed = rss();
139 let mut recall_ms = Vec::new();
140 let mut hits = 0;
141 let mut returned = 0;
142 let mut result_ids = Vec::new();
143 std::hint::black_box(store.recall(&RecallQuery::new(token(0))));
145 for i in 0..queries {
146 let selected = i * records / queries;
147 let mut query = RecallQuery::new(token(selected));
148 query.now_ms = CLOCK;
149 query.limit = 1;
150 let start = Instant::now();
151 let found = store.recall(&query);
152 recall_ms.push(ms(start));
153 returned += found.len();
154 hits += usize::from(
155 found
156 .first()
157 .is_some_and(|r| r.trace.id == format!("sample-{selected}")),
158 );
159 result_ids.push(found.iter().map(|r| r.trace.id.clone()).collect::<Vec<_>>());
160 }
161 let start = Instant::now();
162 store.verify()?;
163 let verify_ms = ms(start);
164 let before = store.head();
165 drop(store);
166 let start = Instant::now();
167 let store = MemoryStore::open_existing(&path, KernelPolicy::default())?;
168 let replay_ms = ms(start);
169 store.verify()?;
170 let retained = store
171 .all()
172 .filter(|e| {
173 e.trace.provenance.source == "synthetic-kernel-benchmark"
174 && e.trace.provenance.observed_at_ms == CLOCK
175 && !e.trace.provenance.verified
176 })
177 .count();
178 let mut replay_matches = 0;
179 for (i, expected) in result_ids.iter().enumerate() {
180 let mut query = RecallQuery::new(token(i * records / queries));
181 query.now_ms = CLOCK;
182 query.limit = 1;
183 let ids: Vec<_> = store
184 .recall(&query)
185 .into_iter()
186 .map(|r| r.trace.id)
187 .collect();
188 replay_matches += usize::from(ids == *expected);
189 }
190 let head_same = serde_json::to_value(before)? == serde_json::to_value(store.head())?;
191 let ok = store.record_count() == records
192 && retained == records
193 && hits == queries
194 && replay_matches == queries
195 && head_same;
196 Ok(json!({
197 "kind": "synthetic_scale", "ok": ok, "records": records, "queries": queries,
198 "workload": "unclaimed observation traces, unique exact lexical cues, fixed clock",
199 "seed_ms": seed_ms, "batch_size": 1000, "batched_records": batched,
200 "batch_append_ms": quantiles(batches), "single_append_ms": quantiles(appends),
201 "recall_ms": quantiles(recall_ms), "verify_ms": verify_ms, "replay_ms": replay_ms,
202 "exact_cue_hits": hits, "returned": returned, "recall_at_1": hits as f64 / queries as f64,
203 "provenance_retained": retained, "replayed_records": store.record_count(),
204 "recall_replay_matches": replay_matches, "head_unchanged": head_same,
205 "ledger_bytes": fs::metadata(path)?.len(), "memory_after_seed": memory_after_seed,
206 "memory_after_replay": rss(), "policy": store.policy(),
207 "limits": ["Batched seeding does not measure per-record durable append latency.",
208 "Warm filesystem cache; reopening in the same process is not a cold-start measurement.",
209 "No structured claims in this workload; conflict-heavy stores need a separate workload.",
210 "Synthetic exact-cue quality is not semantic or real-world retrieval accuracy.",
211 "RSS is process-wide Linux /proc data; unavailable platforms report null."]
212 }))
213}
214
215struct Rng(u64);
217impl Rng {
218 fn next(&mut self) -> u64 {
219 self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15);
220 let mut x = self.0;
221 x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
222 x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
223 x ^ (x >> 31)
224 }
225 fn unit(&mut self) -> f64 {
226 (self.next() % 10001) as f64 / 10000.0
227 }
228}
229
230fn option(name: &str, a: f64, b: f64) -> DecisionOption {
231 DecisionOption {
232 name: name.into(),
233 description: None,
234 scores: BTreeMap::from([("a".into(), a), ("b".into(), b)]),
235 model_scores: BTreeMap::new(),
236 evidence_confidence: 1.0,
237 hard_blocks: vec![],
238 reversible: false,
239 }
240}
241
242fn decisions(cases: usize, seed: u64) -> Result<Value> {
243 if cases < 6 {
244 return Err("at least 6 cases required to exercise every family".into());
245 }
246 let mut rng = Rng(seed);
247 let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
248 let mut failures = Vec::new();
249 let mut violations = 0;
250 let start = Instant::now();
251 for i in 0..cases {
252 let mut a = option("a", rng.unit(), rng.unit());
253 let mut b = option("b", rng.unit(), rng.unit());
254 let family;
255 let mut expected: Option<Option<String>> = None;
256 match i % 6 {
257 0 => {
258 family = "high_score_hard_block";
259 a.scores.values_mut().for_each(|s| *s = 1.0);
260 a.hard_blocks.push("no authorization".into());
261 expected = Some(Some("b".into()));
262 }
263 1 => {
264 family = "all_blocked";
265 a.hard_blocks.push("privacy".into());
266 b.hard_blocks.push("consent".into());
267 expected = Some(None);
268 }
269 2 => {
270 family = "complete_score_oracle";
271 let sum_a: f64 = a.scores.values().sum();
272 let sum_b: f64 = b.scores.values().sum();
273 expected = Some(Some(if sum_a >= sum_b { "a" } else { "b" }.into()));
274 }
275 3 => {
276 family = "unknowns_and_model_estimates";
277 a.scores.clear();
278 a.model_scores.insert("a".into(), rng.unit());
279 }
280 4 => {
281 family = "reversibility_band";
282 let top = 0.5 + rng.unit() * 0.4;
283 let gap = if rng.next().is_multiple_of(2) {
284 0.01
285 } else {
286 0.03
287 };
288 a.scores.values_mut().for_each(|s| *s = top);
289 a.evidence_confidence = 0.1;
290 b.scores.values_mut().for_each(|s| *s = top - gap);
291 b.reversible = true;
292 expected = Some(Some(if gap < 0.02 { "b" } else { "a" }.into()));
293 }
294 _ => {
295 family = "invalid_numeric_input";
296 a.scores.insert("a".into(), 1.01 + rng.unit());
297 }
298 }
299 *counts.entry(family).or_default() += 1;
300 let frame = DecisionFrame {
301 question: "synthetic decision".into(),
302 criteria: ["a", "b"]
303 .into_iter()
304 .map(|id| Criterion {
305 id: id.into(),
306 weight: 1.0,
307 description: id.into(),
308 })
309 .collect(),
310 options: vec![a, b],
311 };
312 let actual = evaluate(&frame);
313 let mut reasons = Vec::new();
314 if family == "invalid_numeric_input" {
315 if actual.is_ok() {
316 reasons.push("invalid input admitted");
317 }
318 } else if let Ok(answer) = &actual {
319 if let Some(expected) = expected {
320 if answer.recommended != expected {
321 reasons.push("wrong recommendation");
322 }
323 }
324 if let Some(name) = &answer.recommended {
325 if frame
326 .options
327 .iter()
328 .any(|o| &o.name == name && !o.hard_blocks.is_empty())
329 {
330 reasons.push("hard-blocked option selected");
331 }
332 }
333 if family == "unknowns_and_model_estimates" {
334 let ranked = answer.ranking.iter().find(|o| o.name == "a").unwrap();
335 if ranked.coverage != 0.0
336 || ranked.evidence_interval != [0.0, 1.0]
337 || ranked.missing_criteria != ["b"]
338 || answer.decisive
339 {
340 reasons.push("unknown evidence hidden or model guess counted as evidence");
341 }
342 }
343 let mut permuted = frame.clone();
344 permuted.options.reverse();
345 permuted.question = "irrelevant wording changed".into();
346 let other = evaluate(&permuted)?;
347 if answer.recommended != other.recommended
348 || serde_json::to_value(&answer.ranking)? != serde_json::to_value(&other.ranking)?
349 {
350 reasons.push("order or irrelevant text changed result");
351 }
352 } else {
353 reasons.push("valid frame rejected");
354 }
355 if !reasons.is_empty() {
356 violations += 1;
357 if failures.len() < 20 {
358 failures.push(
359 json!({"case_index": i, "family": family, "reasons": reasons,
360 "frame": frame, "actual": actual.as_ref().ok()}),
361 );
362 }
363 }
364 }
365 Ok(
366 json!({"kind": "synthetic_decision_invariants", "seed": seed, "rng": "splitmix64",
367 "cases": cases, "families": counts, "violations": violations,
368 "violation_rate": violations as f64 / cases as f64, "ok": violations == 0,
369 "elapsed_ms": ms(start), "counterexamples": failures,
370 "limits": ["Generated families are not real decision-quality labels or a proof over all inputs.",
371 "Metamorphic checks keep criteria fixed; weight perturbations require separate expectations."]}),
372 )
373}
374
375fn committed_child(dir: &Path) -> Result<()> {
376 let path = fresh(dir)?;
377 let mut store = MemoryStore::open(path, KernelPolicy::default())?;
378 store.remember_many((0..50).map(trace).collect())?;
379 println!("committed:50");
380 io::stdout().flush()?;
381 loop {
383 std::thread::park();
384 }
385}
386
387fn recovery(dir: &Path) -> Result<Value> {
388 fs::create_dir(dir)?;
389 let child_dir = dir.join("committed");
390 let mut child = Command::new(std::env::current_exe()?)
391 .args(["committed-child", "--dir"])
392 .arg(&child_dir)
393 .stdin(Stdio::null())
394 .stdout(Stdio::piped())
395 .spawn()?;
396 let mut ack = String::new();
397 let read_result = io::BufReader::new(child.stdout.take().unwrap()).read_line(&mut ack);
398 let kill_result = child.kill();
399 let wait_result = child.wait();
400 read_result?;
401 kill_result?;
402 wait_result?;
403 if ack.trim() != "committed:50" {
404 return Err("child did not acknowledge commit".into());
405 }
406 let path = child_dir.join("memory.jsonl");
407 let start = Instant::now();
408 let store = MemoryStore::open_existing(&path, KernelPolicy::default())?;
409 store.verify()?;
410 let restart_ms = ms(start);
411 let acknowledged_survived =
412 store.record_count() == 50 && (0..50).all(|i| store.get(&format!("sample-{i}")).is_some());
413 let head_path = store.head_path();
414 drop(store);
415 let original = fs::read(&path)?;
416 let original_head = fs::read(head_path)?;
417 let copied_store = |name: &str, bytes: &[u8]| -> Result<PathBuf> {
418 let target = fresh(&dir.join(name))?;
419 let store = MemoryStore::open(&target, KernelPolicy::default())?;
420 let target_head = store.head_path();
421 drop(store);
422 fs::write(&target, bytes)?;
423 fs::write(target_head, &original_head)?;
424 Ok(target)
425 };
426 let torn = copied_store("torn", &original)?;
427 OpenOptions::new()
428 .append(true)
429 .open(&torn)?
430 .write_all(b"{\"seq\":")?;
431 let mut store = MemoryStore::open_existing(&torn, KernelPolicy::default())?;
432 store.remember(trace(50))?;
433 store.verify()?;
434 let torn_repaired = store.record_count() == 51;
435 let mut edited = String::from_utf8(original.clone())?;
436 edited = edited.replacen("deployment observation", "deployment fabrication", 1);
437 let tampered = copied_store("edited", edited.as_bytes())?;
438 let rejected = |path: &Path| match MemoryStore::open_existing(path, KernelPolicy::default()) {
439 Err(_) => true,
440 Ok(store) => store.verify().is_err(),
441 };
442 let edit_detected = rejected(&tampered);
443 let last = original[..original.len() - 1]
444 .iter()
445 .rposition(|b| *b == b'\n')
446 .unwrap()
447 + 1;
448 let truncated = copied_store("truncated", &original[..last])?;
449 let truncation_detected = rejected(&truncated);
450 Ok(
451 json!({"kind": "synthetic_recovery", "acknowledged_records": 50,
452 "acknowledged_survived": acknowledged_survived, "restart_ms": restart_ms,
453 "torn_tail_repaired": torn_repaired, "edit_detected": edit_detected,
454 "truncation_detected": truncation_detected,
455 "ok": acknowledged_survived && torn_repaired && edit_detected && truncation_detected,
456 "limits": ["Process killed after an acknowledged commit, not during fsync or head replacement.",
457 "Torn tail, edit and truncation are injected into separate copies.",
458 "This does not simulate power loss or an attacker rewriting both ledger and head."]}),
459 )
460}
461
462fn gate() -> Result<()> {
463 let mut out = io::BufWriter::new(io::stdout().lock());
464 std::hint::black_box(rules_verdict("Done. TODO: tests"));
466 for line in io::stdin().lock().lines() {
467 let input: Value = serde_json::from_str(&line?)?;
468 let id = input["id"].as_str().ok_or("id must be a string")?;
469 let message = input["message"]
470 .as_str()
471 .ok_or("message must be a string")?;
472 let start = Instant::now();
473 let blocked = rules_verdict(message);
474 writeln!(
475 out,
476 "{}",
477 json!({"id": id, "blocked": blocked, "ms": ms(start)})
478 )?;
479 }
480 out.flush()?;
481 Ok(())
482}
483
484fn main() -> Result<()> {
485 let result = match Args::parse().command {
486 Mode::Scale {
487 dir,
488 records,
489 queries,
490 } => scale(&dir, records, queries)?,
491 Mode::Decisions { cases, seed } => decisions(cases, seed)?,
492 Mode::Recovery { dir } => recovery(&dir)?,
493 Mode::CommittedChild { dir } => return committed_child(&dir),
494 Mode::Gate => return gate(),
495 };
496 println!("{}", serde_json::to_string_pretty(&result)?);
497 if result["ok"] != true {
498 std::process::exit(1);
499 }
500 Ok(())
501}