catalogd parity helper: scrum-driven hardening

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) <noreply@anthropic.com>
This commit is contained in:
root 2026-05-03 04:29:38 -05:00
parent 2413c96817
commit 2222227c16

View File

@ -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<u8> {
(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<PathBuf> = None;
let mut key_path: Option<PathBuf> = 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 <path> --key <path>"));
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 <candidate_id>.audit.jsonl")
.unwrap_or_else(|| die("audit log path must end with <candidate_id>.audit.jsonl"))
.to_string();
// Stand up an in-memory object store, seed it with the audit log