{"$id":"https://resiliencechain.co.uk/schema/v1/event-hash-canonicalization","title":"Event Hash Canonicalization (Open Evidence Schema v1)","version":"1.0.0","hash_algorithm":"SHA-256","genesis_prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","field_order":["event_id","tenant_id","entity_type","entity_id","event_type","occurred_at","actor_user_id","source_channel","related_order_id","related_action_item_id","payload"],"separators":{"field_separator":"|","prev_hash_separator":"|"},"timestamp_format":{"standard":"ISO 8601","example":"2026-04-22T22:31:47.123Z","notes":"Serialise using the same method Node.js Date.prototype.toISOString() produces: UTC, millisecond precision, trailing Z."},"null_handling":{"rule":"Null-valued optional fields serialise as the empty string, NOT as the literal \"null\".","fields_affected":["actor_user_id","related_order_id","related_action_item_id"]},"canonical_json":{"name":"JCS-lite","rules":["Object keys MUST be sorted lexicographically (UTF-8 byte order) at every nesting depth.","Array element order MUST be preserved (arrays are meaningful).","Primitives serialise exactly as JSON.stringify would produce them.","No whitespace between tokens.","No trailing comma.","Null serialises as the literal token `null`."],"example":{"input":{"b":1,"a":{"y":2,"x":3}},"output":"{\"a\":{\"x\":3,\"y\":2},\"b\":1}"}},"hash_computation":{"pseudocode":["canonical = [","  event_id, tenant_id, entity_type, entity_id, event_type,","  occurred_at_iso_utc,","  actor_user_id || \"\",","  source_channel,","  related_order_id || \"\",","  related_action_item_id || \"\",","  canonical_json(payload)","].join(\"|\")","","event_hash = hex(sha256(prev_event_hash || \"|\" || canonical))","","For the genesis event (first event in the tenant's chain),","prev_event_hash = \"0\" * 64."]},"reference_implementations":[{"language":"TypeScript","url":"https://github.com/christie-emgee/resilience-chain/blob/master/src/lib/event-hash.ts"}],"verification_snippets":{"python":["import hashlib, json","","def canonical_json(v):","    if isinstance(v, dict):","        items = sorted(v.items(), key=lambda kv: kv[0])","        return \"{\" + \",\".join(json.dumps(k) + \":\" + canonical_json(vv) for k, vv in items) + \"}\"","    if isinstance(v, list):","        return \"[\" + \",\".join(canonical_json(x) for x in v) + \"]\"","    return json.dumps(v, separators=(\",\", \":\"))","","def compute_event_hash(prev_hash, row):","    canonical = \"|\".join([","        row[\"event_id\"], row[\"tenant_id\"], row[\"entity_type\"], row[\"entity_id\"],","        row[\"event_type\"], row[\"occurred_at\"],","        row.get(\"actor_user_id\") or \"\",","        row[\"source_channel\"],","        row.get(\"related_order_id\") or \"\",","        row.get(\"related_action_item_id\") or \"\",","        canonical_json(row.get(\"payload\", {})),","    ])","    return hashlib.sha256((prev_hash + \"|\" + canonical).encode()).hexdigest()"]}}