{"record":{"id":"5733c565598bea4b","repo":"Hmbown/CodeWhale","slug":"late-usage-ledger-has-an-unsupported-or-unbounded-shape","errorCode":null,"errorMessage":"late usage ledger has an unsupported or unbounded shape","messagePattern":"late usage ledger has an unsupported or unbounded shape","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/session_manager.rs","lineNumber":1526,"sourceCode":"        if u64::try_from(raw.len()).unwrap_or(u64::MAX) > MAX_LATE_USAGE_LEDGER_BYTES {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\n                    \"late usage ledger {} exceeds its size bound\",\n                    path.display()\n                ),\n            ));\n        }\n        let ledger: LateUsageLedger = serde_json::from_slice(&raw)\n            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;\n        if ledger.schema_version != CURRENT_LATE_USAGE_SCHEMA_VERSION\n            || ledger.records.len() > MAX_LATE_USAGE_RECORDS_PER_SESSION\n            || ledger.records.iter().any(|record| {\n                !is_sha256_fingerprint(&record.source_fingerprint)\n                    || !is_sha256_fingerprint(&record.turn_fingerprint)\n            })\n        {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                \"late usage ledger has an unsupported or unbounded shape\",\n            ));\n        }\n        Ok(ledger)\n    }\n\n    fn with_session_write_admission<T>(\n        &self,\n        session_id: &str,\n        write: impl FnOnce() -> io::Result<T>,\n    ) -> io::Result<Option<T>> {\n        let (path, lock_path) = self.ensure_late_usage_paths(session_id)?;\n        let lock_file = open_private_lock_file(&lock_path)?;\n        let mut lock = fd_lock::RwLock::new(lock_file);\n        let _guard = lock.write()?;\n        if Self::late_usage_is_deleted(&path)? {\n            return Ok(None);","sourceCodeStart":1508,"sourceCodeEnd":1544,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/session_manager.rs#L1508-L1544","documentation":"The ledger parsed as JSON but its shape is rejected: schema_version does not equal CURRENT_LATE_USAGE_SCHEMA_VERSION, records exceed MAX_LATE_USAGE_RECORDS_PER_SESSION, or any record's source_fingerprint/turn_fingerprint is not a valid SHA-256 fingerprint. This is a semantic validation error (InvalidData) guarding against unbounded or future-format ledgers.","triggerScenarios":"Loading a ledger whose schema_version differs from the current constant, a ledger with more records than the per-session cap, or records whose fingerprint strings are not 64-char SHA-256 hex values.","commonSituations":"App upgraded/downgraded so schema_version changed; a runaway loop appended thousands of records before the cap existed; fingerprints were written as empty strings, prefixed hashes, or different hash encodings by external tooling.","solutions":["Check the ledger's schema_version against CURRENT_LATE_USAGE_SCHEMA_VERSION in session_manager.rs and migrate or regenerate the file to the current version.","Trim records to the per-session cap; keep the most recent MAX_LATE_USAGE_RECORDS_PER_SESSION entries.","Fix fingerprint fields to be valid SHA-256 hex (64 lowercase hex chars) — recompute them from the source/turn content if they were altered.","Delete the out-of-shape ledger so the session starts with a fresh, empty ledger."],"exampleFix":"// before (rejected record)\n{ \"source_fingerprint\": \"abc123\", \"turn_fingerprint\": \"def456\" }\n// after (full SHA-256 hex)\n{ \"source_fingerprint\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\", \"turn_fingerprint\": \"60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752\" }","handlingStrategy":"validation","validationCode":"let v: serde_json::Value = serde_json::from_slice(&raw)?;\nlet schema_ok = v[\"schema_version\"].as_u64() == Some(CURRENT_LATE_USAGE_SCHEMA_VERSION as u64);\nlet records_ok = v[\"records\"].as_array().map_or(false, |r| r.len() <= MAX_LATE_USAGE_RECORDS_PER_SESSION);\nlet fp_ok = |s: &str| s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit());\nif !(schema_ok && records_ok) { /* migrate, trim, or reject before loading */ }","typeGuard":"fn is_sha256_fingerprint(s: &str) -> bool {\n    s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F'))\n}","tryCatchPattern":"match manager.load_late_usage(&session_id) {\n    Ok(l) => l,\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {\n        // shape mismatch: archive the file, start a fresh ledger or run a schema migration\n        archive_and_reset(&ledger_path);\n        LateUsageLedger::default()\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Keep schema_version in sync when upgrading; run a migration step on first load after a version bump.","Enforce the record cap at append time so the file never exceeds MAX_LATE_USAGE_RECORDS_PER_SESSION.","Always emit fingerprints via a single hex-encoding helper rather than ad-hoc formatting."],"tags":["schema","validation","sha256","version-mismatch","session-state"],"backgroundTag":"schema-validation-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}