Third P4 sub-unit: serialize/parse for the witness hash chain so
audit bundles can be written to disk and replayed.
Wire shape (one record per line, alphabetical field order locked):
{"kind":"...","payload_hex":"...","prev_hash":"...","seq":N,
"this_hash":"...","timestamp_unix_s":N}
Why alphabetical field order: auditors archive whole bundles and
hash them. A rebuild that reordered fields would silently
invalidate every archival hash — locking the order is what makes
the JSONL stable across compiler / serde-json upgrades.
Why hex everywhere: human-greppable, monospace-friendly, no base64
ambiguity, no Vec<u8> JSON-array ugliness. Same convention as
ADR-101's `binary_sha256`.
Critically, `from_jsonl_line` RE-VERIFIES `this_hash` against
the canonical bytes derived from the parsed fields. A tampered
bundle fires `WitnessParseError::HashMismatch` BEFORE the event
loads — the parser is itself an auditor.
New surfaces:
* `WitnessHash::from_hex` (with structured length/parse errors)
* `WitnessEvent::to_jsonl_line`, `from_jsonl_line`
* `WitnessParseError` enum: Json | MissingField | WrongType |
HashLength | HashHex | PayloadHex | PayloadLength | HashMismatch
* private `hex_encode` / `hex_decode` helpers (no `hex` crate dep)
10 new tests:
* jsonl round-trip preserves all fields
* jsonl line has no embedded \n / \r (one record per line)
* jsonl field order is alphabetical (byte-stable archival)
* parser rejects tampered payload via HashMismatch
* parser rejects non-hex characters in hash
* parser rejects missing field
* hex encode/decode round-trip across empty / single byte / 0xff /
UTF-8 / arbitrary bytes
* hex decode rejects odd-length input
* WitnessHash::from_hex round-trip
* WitnessHash::from_hex rejects wrong length
44/44 cog tests green (34 → 44).
ADR-116 P4 row enumerates 4 sub-units now: ✅ mDNS record-builder,
✅ witness chain primitive, ✅ witness JSONL persistence,
⏳ responder + embedded broker + Ed25519 signing.
Co-Authored-By: claude-flow <ruv@ruv.net>