From 2222227c1636d7fc9da3313bc0fd3b3eb8a7955a Mon Sep 17 00:00:00 2001 From: root Date: Sun, 3 May 2026 04:29:38 -0500 Subject: [PATCH] catalogd parity helper: scrum-driven hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per 2026-05-03 step_7_8_retention_and_parity scrum (opus). 5 findings, 0 convergent — but two real fixes shipped: 1. WARN parity_subject_audit.rs:argv — replace .expect() panics with stderr+exit(2). The parity script captures stdout for byte-compare; a Rust panic backtrace lands in stdout (script merges 2>&1) and reads as a parity break instead of a usage error. Added die() helper that mirrors the Go side's error-exit pattern. 2. INFO parity_subject_audit.rs:5 — doc comment hardcoded the absolute path /home/profit/golangLAKEHOUSE/... Replaced with repo-relative reference. INFO findings on retention_sweep argv style + --as-of report path overwrite were noted but not actioned (style only / acceptable for the forecast use case). The major scrum-surfaced bug (Go json.Marshal HTML-escaping <>& while serde_json keeps them literal) is fixed on the Go side in parallel commit. Rust side here is correct as-is — serde_json::to_vec doesn't HTML-escape by default, so no change needed in canonical_json. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../catalogd/src/bin/parity_subject_audit.rs | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/crates/catalogd/src/bin/parity_subject_audit.rs b/crates/catalogd/src/bin/parity_subject_audit.rs index a867d52..f0d8afe 100644 --- a/crates/catalogd/src/bin/parity_subject_audit.rs +++ b/crates/catalogd/src/bin/parity_subject_audit.rs @@ -2,9 +2,11 @@ //! //! Specification: docs/specs/SUBJECT_MANIFESTS_ON_CATALOGD.md §5 Step 8. //! -//! This binary is consumed by scripts/cutover/parity/subject_audit_parity.sh -//! (which lives in /home/profit/golangLAKEHOUSE/scripts/cutover/parity/). -//! Its Go counterpart is at golangLAKEHOUSE/scripts/cutover/parity/subject_audit_helper/main.go. +//! This binary is consumed by the Go-repo's +//! scripts/cutover/parity/subject_audit_parity.sh. +//! Go counterpart: scripts/cutover/parity/subject_audit_helper/main.go +//! (in the Go repo). Both helpers are kept in lockstep by the parity +//! script's byte-equality assertions — change one, change both. //! //! Both helpers MUST produce byte-identical output for the same inputs. //! Divergence here is a parity break — a SubjectManifest written by Rust @@ -88,6 +90,15 @@ fn deterministic_key() -> Vec { (0u8..32).collect() } +/// Print to stderr + exit(2). Never returns. Used in place of `.expect()` +/// on argv / file reads so failures produce a clean usage error rather +/// than a Rust panic backtrace that the parity script would mistake +/// for a parity break. +fn die(msg: &str) -> ! { + eprintln!("error: {msg}"); + std::process::exit(2); +} + #[derive(Serialize)] struct KnownAnswerOut { mode: &'static str, @@ -141,6 +152,10 @@ async fn main() { let mut audit_path: Option = None; let mut key_path: Option = None; + // Argv parsing fails via stderr+exit, never via panic. The parity + // script captures stdout for byte-comparison; a Rust panic backtrace + // would land in stdout (the script merges 2>&1) and read as a parity + // break instead of a usage error. (Caught 2026-05-03 opus scrum WARN.) let mut i = 1; while i < argv.len() { match argv[i].as_str() { @@ -149,13 +164,13 @@ async fn main() { i += 1; } "--verify" => { - audit_path = Some(PathBuf::from( - argv.get(i + 1).expect("--verify needs path"), - )); + let p = argv.get(i + 1).unwrap_or_else(|| die("--verify needs a path")); + audit_path = Some(PathBuf::from(p)); i += 2; } "--key" => { - key_path = Some(PathBuf::from(argv.get(i + 1).expect("--key needs path"))); + let p = argv.get(i + 1).unwrap_or_else(|| die("--key needs a path")); + key_path = Some(PathBuf::from(p)); i += 2; } "-h" | "--help" => { @@ -175,15 +190,15 @@ async fn main() { return; } - let audit_path = audit_path.expect("need --known-answer OR --verify ... --key ..."); - let key_path = key_path.expect("--verify also needs --key"); + let audit_path = audit_path.unwrap_or_else(|| die("need --known-answer OR --verify --key ")); + let key_path = key_path.unwrap_or_else(|| die("--verify also needs --key")); - let key = std::fs::read(&key_path).expect("read key file"); + let key = std::fs::read(&key_path).unwrap_or_else(|e| die(&format!("read key file: {e}"))); let candidate_id = audit_path .file_name() .and_then(|s| s.to_str()) .and_then(|s| s.strip_suffix(".audit.jsonl")) - .expect("audit log path must end with .audit.jsonl") + .unwrap_or_else(|| die("audit log path must end with .audit.jsonl")) .to_string(); // Stand up an in-memory object store, seed it with the audit log