use std::path::{Path, PathBuf};
use std::process::ExitCode;
use deepstrike_core::runtime::chain_validator::{
ValidationReport, Verdict, validate_journal, validate_with_checkpoint,
};
const USAGE: &str = "Usage: ds-chain-validator [--journal <path>...] [--session-log <path>...] \
[--checkpoint <path>...] [--strict] [--format human|json]\n\
\x20 (at least one evidence plane is required)\n\
\n\
Validates a kernel journal prefix against P2 §5 rules C1–C5, marking\n\
degraded evidence per C7. With --session-log, the batch-3 cross-checks\n\
(C6/C8) join the SessionLog evidence plane against the journal. With\n\
--checkpoint, the batch-2 rule (C5) anchors each checkpoint against the\n\
journal: genesis/covered-head/tail digests (C5a) and the launch-token\n\
ledger (C5b).\n\
Exit codes: 0 = all green, 1 = violation proven, 2 = evidence insufficient.\n\
\n\
--journal <path> record source: directory of *.json records, a JSON\n\
\x20 object/array file, or a JSONL file (repeatable)\n\
--session-log <path> session-log source: a directory (each *.json/*.jsonl\n\
\x20 file is one append-ordered stream) or a single\n\
\x20 stream file — object/array/JSONL (repeatable)\n\
--checkpoint <path> checkpoint source: directory of *.json checkpoints,\n\
\x20 a JSON object/array file, or a JSONL file\n\
\x20 (repeatable; golden-fixture wrappers holding a\n\
\x20 nested `checkpoint` are unwrapped). Omit it and\n\
\x20 C5 is deferred, not red\n\
--strict with --checkpoint: re-plan the journal prefix from\n\
\x20 genesis through the covered step and require the\n\
\x20 re-derived state digest and launch-token ledger to\n\
\x20 match the checkpoint (costs one full fold)\n\
--format <mode> human (default) or json";
fn main() -> ExitCode {
let mut journals: Vec<PathBuf> = Vec::new();
let mut session_logs: Vec<PathBuf> = Vec::new();
let mut checkpoints: Vec<PathBuf> = Vec::new();
let mut strict = false;
let mut format = "human".to_string();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--journal" => {
let Some(path) = args.next() else {
return usage("--journal requires a path");
};
journals.push(PathBuf::from(path));
}
"--session-log" => {
let Some(path) = args.next() else {
return usage("--session-log requires a path");
};
session_logs.push(PathBuf::from(path));
}
"--checkpoint" => {
let Some(path) = args.next() else {
return usage("--checkpoint requires a path");
};
checkpoints.push(PathBuf::from(path));
}
"--strict" => strict = true,
"--format" => {
let Some(mode) = args.next() else {
return usage("--format requires human or json");
};
if mode != "human" && mode != "json" {
return usage("--format requires human or json");
}
format = mode;
}
"--help" | "-h" => {
println!("{USAGE}");
return ExitCode::from(0);
}
_ => return usage(&format!("unknown argument: {arg}")),
}
}
if journals.is_empty() && session_logs.is_empty() && checkpoints.is_empty() {
return usage("at least one --journal, --session-log, or --checkpoint path is required");
}
let mut blobs: Vec<Vec<u8>> = Vec::new();
for path in &journals {
match read_journal(path, &mut blobs) {
Ok(count) => eprintln!("read {count} record blob(s) from {}", path.display()),
Err(message) => {
eprintln!("ds-chain-validator: {message}");
return ExitCode::from(2);
}
}
}
let mut streams: Vec<Vec<Vec<u8>>> = Vec::new();
for path in &session_logs {
let streams_before = streams.len();
match read_session_log(path, &mut streams) {
Ok(count) => eprintln!(
"read {count} session event(s) in {} stream(s) from {}",
streams.len() - streams_before,
path.display()
),
Err(message) => {
eprintln!("ds-chain-validator: {message}");
return ExitCode::from(2);
}
}
}
let mut checkpoint_blobs: Vec<Vec<u8>> = Vec::new();
for path in &checkpoints {
match read_checkpoint(path, &mut checkpoint_blobs) {
Ok(count) => eprintln!("read {count} checkpoint blob(s) from {}", path.display()),
Err(message) => {
eprintln!("ds-chain-validator: {message}");
return ExitCode::from(2);
}
}
}
let report = if session_logs.is_empty() && checkpoints.is_empty() {
validate_journal(&blobs)
} else {
validate_with_checkpoint(&blobs, &streams, &checkpoint_blobs, strict)
};
match format.as_str() {
"json" => match serde_json::to_string_pretty(&report) {
Ok(json) => println!("{json}"),
Err(error) => {
eprintln!("ds-chain-validator: could not serialize the report: {error}");
return ExitCode::from(2);
}
},
_ => print_human(&report),
}
let code = report.exit_code();
ExitCode::from(u8::try_from(code).unwrap_or(2))
}
fn usage(message: &str) -> ExitCode {
eprintln!("ds-chain-validator: {message}\n\n{USAGE}");
ExitCode::from(64)
}
fn read_journal(path: &Path, blobs: &mut Vec<Vec<u8>>) -> Result<usize, String> {
let before = blobs.len();
if path.is_dir() {
let mut entries: Vec<PathBuf> = std::fs::read_dir(path)
.map_err(|error| format!("cannot read directory {}: {error}", path.display()))?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|entry| {
entry
.extension()
.is_some_and(|extension| extension == "json")
})
.collect();
entries.sort();
for entry in entries {
let bytes = std::fs::read(&entry)
.map_err(|error| format!("cannot read {}: {error}", entry.display()))?;
blobs.push(bytes);
}
return Ok(blobs.len() - before);
}
let bytes =
std::fs::read(path).map_err(|error| format!("cannot read {}: {error}", path.display()))?;
split_blob_file(&bytes, blobs);
Ok(blobs.len() - before)
}
fn read_session_log(path: &Path, streams: &mut Vec<Vec<Vec<u8>>>) -> Result<usize, String> {
let mut added = 0usize;
if path.is_dir() {
let mut entries: Vec<PathBuf> = std::fs::read_dir(path)
.map_err(|error| format!("cannot read directory {}: {error}", path.display()))?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|entry| {
entry
.extension()
.is_some_and(|extension| extension == "json" || extension == "jsonl")
})
.collect();
entries.sort();
for entry in entries {
let bytes = std::fs::read(&entry)
.map_err(|error| format!("cannot read {}: {error}", entry.display()))?;
let mut stream = Vec::new();
split_blob_file(&bytes, &mut stream);
added += stream.len();
streams.push(stream);
}
return Ok(added);
}
let bytes =
std::fs::read(path).map_err(|error| format!("cannot read {}: {error}", path.display()))?;
let mut stream = Vec::new();
split_blob_file(&bytes, &mut stream);
added += stream.len();
streams.push(stream);
Ok(added)
}
fn read_checkpoint(path: &Path, blobs: &mut Vec<Vec<u8>>) -> Result<usize, String> {
let before = blobs.len();
read_journal(path, blobs)?;
for blob in &mut blobs[before..] {
if let Ok(serde_json::Value::Object(map)) =
serde_json::from_slice::<serde_json::Value>(blob)
&& let Some(nested) = map.get("checkpoint")
&& nested.is_object()
{
*blob = serde_json::to_vec(nested).unwrap_or_default();
}
}
Ok(blobs.len() - before)
}
fn split_blob_file(bytes: &[u8], blobs: &mut Vec<Vec<u8>>) {
match serde_json::from_slice::<serde_json::Value>(bytes) {
Ok(serde_json::Value::Array(records)) => {
for record in records {
blobs.push(serde_json::to_vec(&record).unwrap_or_default());
}
}
Ok(_) => blobs.push(bytes.to_vec()),
Err(_) => {
for line in bytes.split(|byte| *byte == b'\n') {
let line = trim_ascii(line);
if !line.is_empty() {
blobs.push(line.to_vec());
}
}
}
}
}
fn trim_ascii(bytes: &[u8]) -> &[u8] {
let start = bytes
.iter()
.position(|byte| !byte.is_ascii_whitespace())
.unwrap_or(bytes.len());
let end = bytes
.iter()
.rposition(|byte| !byte.is_ascii_whitespace())
.map_or(start, |position| position + 1);
&bytes[start..end]
}
fn print_human(report: &ValidationReport) {
for segment in &report.segments {
println!("segment {} ({} hop(s))", segment.operation_id, segment.hops);
for rule in &segment.rules {
let label = match rule.verdict {
Verdict::Pass => "pass",
Verdict::Fail => "FAIL",
Verdict::Degraded => "degraded",
};
println!(" {} {label}: {}", rule.rule, rule.detail);
}
for hop in &segment.degraded_hops {
println!(
" degraded hop #{} (step {}): {}",
hop.ordinal,
hop.step_seq.map_or("?".to_string(), |seq| seq.to_string()),
hop.reason,
);
}
}
if report.unparseable_records > 0 {
println!(
"unparseable record blob(s): {} (evidence insufficient, not a violation)",
report.unparseable_records
);
}
if let Some(events) = report.session_events {
println!("session-log plane: {events} parseable event(s)");
}
if report.unparseable_events > 0 {
println!(
"unparseable session event blob(s): {} (evidence insufficient, not a violation)",
report.unparseable_events
);
}
if let Some(checkpoints) = report.checkpoints {
println!("checkpoint plane: {checkpoints} parseable checkpoint(s)");
}
if report.unparseable_checkpoints > 0 {
println!(
"unparseable checkpoint blob(s): {} (evidence insufficient, not a violation)",
report.unparseable_checkpoints
);
}
for rule in &report.cross_checks {
let label = match rule.verdict {
Verdict::Pass => "pass",
Verdict::Fail => "FAIL",
Verdict::Degraded => "degraded",
};
println!("cross-check {} {label}: {}", rule.rule, rule.detail);
}
for rule in &report.checkpoint_checks {
let label = match rule.verdict {
Verdict::Pass => "pass",
Verdict::Fail => "FAIL",
Verdict::Degraded => "degraded",
};
println!("checkpoint-check {} {label}: {}", rule.rule, rule.detail);
}
for deferred in &report.deferred {
println!("deferred: {deferred}");
}
let segments = report.segments.len();
let verdict = match report.exit_code() {
0 => "all green",
1 => "VIOLATION",
_ => "evidence insufficient",
};
println!(
"summary: {segments} segment(s), exit {} ({verdict})",
report.exit_code()
);
}