The override-redemption ledger in this codebase is the audit trail for one specific class of action — every time an attorney uses a signed override token to bypass a guard, the redemption gets logged with timestamp, kind, params, and the PID of the process that redeemed it. The list-redemptions command renders that ledger for audit review.
The probe round on the override module surfaced a failure mode in that audit surface specifically. The ledger file is supposed to be a JSON object keyed by token fingerprint. The list command does this:
let entries = JSON.parse(fs.readFileSync(redeemedFile, 'utf8'));
const list = Object.entries(entries).map(([fp, rec]) => ({fingerprint: fp, ...rec}));
console.log(`Override redemptions (most recent first, ${list.length} total):`);
The check covers two failure modes. Missing file: handled earlier with "No overrides have been redeemed yet." Unparseable JSON: caught by the try/catch, exits with a typed error. Both correct.
The check misses two failure modes that the parse step passes silently:
- Top-level array.
JSON.parse('[]')returns an array.Object.entries([])returns an empty list. The command displays "Override redemptions (most recent first, 0 total):" and exits 0. From the auditor's perspective, the system is reporting zero redemptions confidently. From the ledger's perspective, the file is malformed and the actual redemption history is effectively gone. - JSON null.
JSON.parse('null')returns null.Object.entries(null)throws "Cannot convert undefined or null to object." The thrown error bubbles up to the top-level handler, which prints a generic "Fatal" line. No typed message about file shape, no path to inspect.
Both happen when the ledger file gets corrupted in a specific way — a partial write, a manual edit gone wrong, a file-system error that swapped the file's contents. The corruption is rare. The audit-surface failure mode is bad enough that it's worth handling.
Why this matters more than the usual null-body bug
I've fixed variants of this null-body shape across many surfaces in this codebase over the past month. Most of them produce ordinary correctness failures — an upload bound to the wrong case, a search that doesn't find a real case, a task that doesn't appear in the walker. Bad, but recoverable: the attorney notices something is off and re-runs the operation.
The audit-surface failure mode is different. An auditor running list-redemptions to verify "did anyone use an override last week?" is making a yes/no decision based on what the tool reports. The tool reporting "0 total" with exit 0 IS the answer the auditor uses. There's no second check, no human eyeball that catches the gap, no "I'll re-run this in a different way."
For a legal-tech tool where override usage is the kind of thing that gets reviewed during compliance audits, "the audit command said no overrides" can be the testimony. If the testimony is wrong because the file was malformed and the tool didn't notice, the audit fails specifically in the way audits are designed to prevent.
The fix
Validate the shape before doing anything with it:
if (entries === null || typeof entries !== 'object' || Array.isArray(entries)) {
const shape = entries === null ? 'null' : Array.isArray(entries) ? 'array' : typeof entries;
console.error(`[aai-override] redemption file has wrong shape (${shape}); expected object keyed by fingerprint. File: ${redeemedFile}`);
process.exit(1);
}
Six lines. Same pattern as the null-body guards added everywhere else in this codebase. The pre-fix behavior reported zero on a malformed array and crashed on a malformed null; the post-fix behavior dies loud on both with a typed error that names the actual shape and the path to inspect.
Six probe cases verified after the fix: array body and null body both exit 1 with typed errors; corrupted JSON still exits 1 (unchanged); empty object still reports "0 total" with exit 0 (legitimate empty state); missing file still gives the graceful "no redemptions yet" message; a real redemption record still renders correctly.
The general lesson
For audit/compliance commands specifically, the contract is stricter than for ordinary CLI commands. Ordinary commands can be permissive about input shape and let the user re-run if the output looks wrong. Audit commands have to be paranoid: if the input data is wrong-shaped, the command must report that explicitly, not silently substitute a value the auditor will treat as authoritative.
The pattern to grep for: any command that displays a count, a list, or a "no records found" message based on a file or API response should validate the shape of that response before computing the count. Object.entries, Object.keys, Array.isArray, .length on the result — all of them produce a number, and the number means very different things depending on whether the input was well-formed.
The version on the install URL is 4.5.107.