Add hook debug tooling and refine RT3 atlas
This commit is contained in:
parent
860d1aed90
commit
57bf0666e0
38 changed files with 14437 additions and 873 deletions
|
|
@ -6,4 +6,6 @@ license.workspace = true
|
|||
|
||||
[dependencies]
|
||||
rrt-model = { path = "../rrt-model" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
|
|
|
|||
|
|
@ -6,10 +6,34 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use rrt_model::{
|
||||
BINARY_SUMMARY_PATH, CANONICAL_EXE_PATH, CONTROL_LOOP_ATLAS_PATH, FUNCTION_MAP_PATH,
|
||||
REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS, load_binary_summary, load_function_map,
|
||||
REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS,
|
||||
finance::{FinanceOutcome, FinanceSnapshot},
|
||||
load_binary_summary, load_function_map,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
enum Command {
|
||||
Validate { repo_root: PathBuf },
|
||||
FinanceEval { snapshot_path: PathBuf },
|
||||
FinanceDiff { left_path: PathBuf, right_path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct FinanceDiffEntry {
|
||||
path: String,
|
||||
left: Value,
|
||||
right: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct FinanceDiffReport {
|
||||
matches: bool,
|
||||
difference_count: usize,
|
||||
differences: Vec<FinanceDiffEntry>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = real_main() {
|
||||
eprintln!("error: {err}");
|
||||
|
|
@ -18,22 +42,155 @@ fn main() {
|
|||
}
|
||||
|
||||
fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let repo_root = parse_repo_root()?;
|
||||
validate_required_files(&repo_root)?;
|
||||
validate_binary_summary(&repo_root)?;
|
||||
validate_function_map(&repo_root)?;
|
||||
validate_control_loop_atlas(&repo_root)?;
|
||||
println!("baseline validation passed");
|
||||
match parse_command()? {
|
||||
Command::Validate { repo_root } => {
|
||||
validate_required_files(&repo_root)?;
|
||||
validate_binary_summary(&repo_root)?;
|
||||
validate_function_map(&repo_root)?;
|
||||
validate_control_loop_atlas(&repo_root)?;
|
||||
println!("baseline validation passed");
|
||||
}
|
||||
Command::FinanceEval { snapshot_path } => {
|
||||
run_finance_eval(&snapshot_path)?;
|
||||
}
|
||||
Command::FinanceDiff {
|
||||
left_path,
|
||||
right_path,
|
||||
} => {
|
||||
run_finance_diff(&left_path, &right_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_repo_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
|
||||
let mut args = env::args().skip(1);
|
||||
match (args.next().as_deref(), args.next(), args.next()) {
|
||||
(None, None, None) => Ok(env::current_dir()?),
|
||||
(Some("validate"), None, None) => Ok(env::current_dir()?),
|
||||
(Some("validate"), Some(path), None) => Ok(PathBuf::from(path)),
|
||||
_ => Err("usage: rrt-cli [validate [repo-root]]".into()),
|
||||
match (args.next().as_deref(), args.next(), args.next(), args.next()) {
|
||||
(None, None, None, None) => Ok(Command::Validate {
|
||||
repo_root: env::current_dir()?,
|
||||
}),
|
||||
(Some("validate"), None, None, None) => Ok(Command::Validate {
|
||||
repo_root: env::current_dir()?,
|
||||
}),
|
||||
(Some("validate"), Some(path), None, None) => Ok(Command::Validate {
|
||||
repo_root: PathBuf::from(path),
|
||||
}),
|
||||
(Some("finance"), Some(subcommand), Some(path), None) if subcommand == "eval" => {
|
||||
Ok(Command::FinanceEval {
|
||||
snapshot_path: PathBuf::from(path),
|
||||
})
|
||||
}
|
||||
(Some("finance"), Some(subcommand), Some(left), Some(right)) if subcommand == "diff" => {
|
||||
Ok(Command::FinanceDiff {
|
||||
left_path: PathBuf::from(left),
|
||||
right_path: PathBuf::from(right),
|
||||
})
|
||||
}
|
||||
_ => Err(
|
||||
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json>]"
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_finance_eval(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let outcome = load_finance_outcome(snapshot_path)?;
|
||||
println!("{}", serde_json::to_string_pretty(&outcome)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_finance_diff(
|
||||
left_path: &Path,
|
||||
right_path: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let left = load_finance_outcome(left_path)?;
|
||||
let right = load_finance_outcome(right_path)?;
|
||||
let report = diff_finance_outcomes(&left, &right)?;
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_finance_outcome(path: &Path) -> Result<FinanceOutcome, Box<dyn std::error::Error>> {
|
||||
let text = fs::read_to_string(path)?;
|
||||
if let Ok(snapshot) = serde_json::from_str::<FinanceSnapshot>(&text) {
|
||||
return Ok(snapshot.evaluate());
|
||||
}
|
||||
if let Ok(outcome) = serde_json::from_str::<FinanceOutcome>(&text) {
|
||||
return Ok(outcome);
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"unable to parse {} as FinanceSnapshot or FinanceOutcome",
|
||||
path.display()
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
fn diff_finance_outcomes(
|
||||
left: &FinanceOutcome,
|
||||
right: &FinanceOutcome,
|
||||
) -> Result<FinanceDiffReport, Box<dyn std::error::Error>> {
|
||||
let left_value = serde_json::to_value(left)?;
|
||||
let right_value = serde_json::to_value(right)?;
|
||||
let mut differences = Vec::new();
|
||||
collect_json_differences("$", &left_value, &right_value, &mut differences);
|
||||
|
||||
Ok(FinanceDiffReport {
|
||||
matches: differences.is_empty(),
|
||||
difference_count: differences.len(),
|
||||
differences,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_json_differences(
|
||||
path: &str,
|
||||
left: &Value,
|
||||
right: &Value,
|
||||
differences: &mut Vec<FinanceDiffEntry>,
|
||||
) {
|
||||
match (left, right) {
|
||||
(Value::Object(left_map), Value::Object(right_map)) => {
|
||||
let mut keys = BTreeSet::new();
|
||||
keys.extend(left_map.keys().cloned());
|
||||
keys.extend(right_map.keys().cloned());
|
||||
|
||||
for key in keys {
|
||||
let next_path = format!("{path}.{key}");
|
||||
match (left_map.get(&key), right_map.get(&key)) {
|
||||
(Some(left_value), Some(right_value)) => {
|
||||
collect_json_differences(&next_path, left_value, right_value, differences);
|
||||
}
|
||||
(left_value, right_value) => differences.push(FinanceDiffEntry {
|
||||
path: next_path,
|
||||
left: left_value.cloned().unwrap_or(Value::Null),
|
||||
right: right_value.cloned().unwrap_or(Value::Null),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
(Value::Array(left_items), Value::Array(right_items)) => {
|
||||
let max_len = left_items.len().max(right_items.len());
|
||||
for index in 0..max_len {
|
||||
let next_path = format!("{path}[{index}]");
|
||||
match (left_items.get(index), right_items.get(index)) {
|
||||
(Some(left_value), Some(right_value)) => {
|
||||
collect_json_differences(&next_path, left_value, right_value, differences);
|
||||
}
|
||||
(left_value, right_value) => differences.push(FinanceDiffEntry {
|
||||
path: next_path,
|
||||
left: left_value.cloned().unwrap_or(Value::Null),
|
||||
right: right_value.cloned().unwrap_or(Value::Null),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ if left != right => differences.push(FinanceDiffEntry {
|
||||
path: path.to_string(),
|
||||
left: left.clone(),
|
||||
right: right.clone(),
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,3 +300,62 @@ fn sha256_file(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
|||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rrt_model::finance::{AnnualFinanceDecision, AnnualFinanceEvaluation, CompanyFinanceState, DebtRestructureSummary};
|
||||
|
||||
#[test]
|
||||
fn loads_snapshot_as_outcome() {
|
||||
let snapshot = FinanceSnapshot {
|
||||
policy: rrt_model::finance::AnnualFinancePolicy {
|
||||
dividends_allowed: false,
|
||||
..rrt_model::finance::AnnualFinancePolicy::default()
|
||||
},
|
||||
company: CompanyFinanceState::default(),
|
||||
};
|
||||
let path = write_temp_json("snapshot", &snapshot);
|
||||
|
||||
let outcome = load_finance_outcome(&path).expect("snapshot should load");
|
||||
assert_eq!(outcome.evaluation.decision, AnnualFinanceDecision::NoAction);
|
||||
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diffs_outcomes_recursively() {
|
||||
let left = FinanceOutcome {
|
||||
evaluation: AnnualFinanceEvaluation::no_action(),
|
||||
post_company: CompanyFinanceState::default(),
|
||||
};
|
||||
let mut right = left.clone();
|
||||
right.post_company.current_cash = 123;
|
||||
right.evaluation.debt_restructure = DebtRestructureSummary {
|
||||
retired_principal: 10,
|
||||
issued_principal: 20,
|
||||
};
|
||||
|
||||
let report = diff_finance_outcomes(&left, &right).expect("diff should succeed");
|
||||
assert!(!report.matches);
|
||||
assert!(report
|
||||
.differences
|
||||
.iter()
|
||||
.any(|entry| entry.path == "$.post_company.current_cash"));
|
||||
assert!(report
|
||||
.differences
|
||||
.iter()
|
||||
.any(|entry| entry.path == "$.evaluation.debt_restructure.retired_principal"));
|
||||
}
|
||||
|
||||
fn write_temp_json<T: Serialize>(stem: &str, value: &T) -> PathBuf {
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system time should be after epoch")
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("rrt-cli-{stem}-{nonce}.json"));
|
||||
let bytes = serde_json::to_vec_pretty(value).expect("json serialization should succeed");
|
||||
fs::write(&path, bytes).expect("temp json should be written");
|
||||
path
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue