If you hash an audit record, you are not hashing your object. You are hashing the bytes a JSON reader will hand back.
Those two things drift apart quietly, and the drift is what breaks verification months later.
In our open-source project disclosurekit, the evidence log takes the strict route: append() normalizes the detail into plain JSON before anything gets hashed, and refuses values JSON cannot carry.
The mechanics are worth copying.
The normalizer honors toJSON exactly where JSON.stringify honors it. That one rule is why a Date lands in the log as the string it serializes to, instead of the empty object a naive canonical serializer would emit. Strings, booleans and null pass through. A number passes only when it is finite.
Everything else is a refusal, not a best-effort guess. A bigint, a function, a symbol, undefined, a non-finite number, a cycle: each one raises a typed error that names the path where the offending value sat, down to something like detail.applies[0].note. Arrays are mapped element by element with an indexed path, so the message points at the element and not at the array.
Objects are accepted only when their prototype is Object.prototype or null. A Map, or any class instance without toJSON, is refused rather than flattened into something that looks fine and hashes differently than it reads. Keys whose value is undefined are dropped, the way JSON drops them.
The part that matters operationally: the refusal happens before the previous entry is read and before the new entry is pushed. A rejected detail leaves nothing behind. The chain does not get a half-written link, and verify() still holds on the log you already have.
This is the trade-off we keep choosing in money-handling and compliance systems. A lossy write is worse than a failed write, because a failed write is visible at the call site, while a lossy one surfaces during an audit, when nobody can tell whether the data changed or the serializer did.
The general shape, we think: when a system derives a guarantee from bytes, it has to own the conversion to bytes, and own the right to say no.
If you run hash-chained or append-only logs, it is worth knowing which of the two you are doing: normalizing the payload before hashing, or trusting the serializer at the edge to stay stable.