IndieK

IndieK archive format — specification, format version 3.0

This document is the normative specification of the IndieK archive format, format version 3.0. It is shipped inside the indiek Python package and, by ik manifest, at the root of every archive, next to MANIFEST.json and test_vectors.json. It contains everything needed to read, verify and write an archive without any IndieK software.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.

1. Overview

An IndieK archive stores a knowledge graph — nodes, directed edges between nodes, and tags on both — as a folder of plain files:

Every event is signed with Ed25519 by its author and linked to the previous event of its log by a hash, so that any change to the history is detectable. Nothing is ever edited in place: a revision is a new supersede event; the earlier record stays in the log.

Design goals, in order: the archive must remain verifiable for centuries (plain text, standard primitives, no dependency on any software, version control system or service); a single flipped bit must damage as little as possible and always be detected; the spec must be reimplementable from this document in a day.

The event logs and the blobs are the only source of truth. Aliases, indexes, caches and every other derived view MUST be rebuildable from them.

2. Notation

3. Archive layout and encoding

3.1 Archive root

Entry Content Required
events/ Base event log (§6, §7) for a non-empty archive
meta/events/ Meta event log (§6, §8) no
blobs/ Blob store, blobs/sha256/… (§10) as soon as an event references a blob
seals/ Seal log, seals/seals.ndjson (§12) no
MANIFEST.json Machine-readable layout and JSON Schemas (§17) no, informative
SPEC.md This document (§17) no, informative
test_vectors.json Test vectors (§18) no, informative

Readers MUST ignore any other entry. In particular, a verification cache is never part of an archive (§16): a .ik-cache/ folder, or any other file a reader might find, MUST NOT influence verification.

An archive without meta/ is valid on its own: base events never mention the meta log (§11). An archive without seals/ is valid too. A seal commits to both logs, so seals/ MUST NOT be kept without meta/ when the seals cover meta events (§12).

3.2 Log files and event order

A log is stored as NDJSON files under its root (events/ or meta/events/). Writers append each event, as one line terminated by LF (0x0A), to <log root>/YYYY/MM/DD/events.ndjson, where the date is the UTC date of the event’s header.timestamp.

To read a log, a reader MUST take every regular file under the log root whose name ends in ndjson, sort them by their path relative to the log root (lexicographic string order), and read each file top to bottom. The resulting sequence is the log; its first event is event number 1. The timestamps play no role in ordering.

A writer MUST NOT append an event to a file that sorts before the file holding the current last event (for example after the system clock moved back across midnight): the read order would no longer match the chain and the log would be invalid (Chain broken!).

3.3 Strict parsing of lines

Every line of every log (event logs and seal log) MUST be:

  1. valid UTF-8 — otherwise Invalid log line: not UTF-8;
  2. exactly one JSON value (RFC 8259) that is an object — otherwise Invalid log line: not a JSON object, or Invalid log line: <JSON error> for malformed JSON, including empty lines;
  3. free of duplicate keys in every object — otherwise Invalid log line: duplicate JSON key;
  4. free of non-integer numbers: no fraction, no exponent, no NaN, Infinity or -Infinity — otherwise Invalid log line: non-integer number …;
  5. free of integers outside [−(2^53−1), 2^53−1] = [−9007199254740991, 9007199254740991] — otherwise Invalid log line: integer … is outside ±(2^53−1);
  6. free of strings containing a lone surrogate (e.g. the escape "\ud800" not followed by a low surrogate): such a string is not a sequence of Unicode scalar values and has no canonical form — otherwise Invalid string: lone surrogate (not a Unicode scalar value).

A line that violates any rule makes the archive invalid. Whitespace between JSON tokens, including a trailing CR, is allowed and irrelevant: hashes and signatures are computed over canonical bytes of the parsed value (§4), never over the bytes on disk. Writers SHOULD write compact lines.

4. Canonical bytes

Hashes and signatures are computed over canonical bytes. The canonical form is RFC 8785 (JSON Canonicalization Scheme, JCS) restricted to:

Every key defined by this format is ASCII, and §3.3 rejects every other number, so every valid line has a canonical form. Within this subset the canonical bytes are:

In Python, for values in this subset, this is exactly json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8").

The restriction is deliberate: full RFC 8785 sorts non-ASCII keys by UTF-16 code units and serialises every number as an IEEE-754 double. A future format version that needs non-ASCII keys, fractions or larger integers MUST either adopt full RFC 8785 or define the extension explicitly.

Writers MUST NOT produce a value outside the subset (the reference implementation refuses with ArchiveRuleError).

Worked example — the canonical bytes of the unsigned event of test_vectors.json vector 1 (one line):

{"body":{"blob_hash":"sha256:71a0b54b349d9f8a95d6bb621386c3cb939b0a711dbf4e746e3031c38b4104f5","body_type":"node","event_type":"create_node","file_hash":null,"file_size":null,"last_verified":null,"media_type":"text/plain; charset=utf-8","node_id":"019a0000-0000-7000-8000-000000000001","path":null,"resource_type":"managed","title":"Gödel — incompleteness","uri":null},"header":{"author":"8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c","hash_alg":"sha256","prev_hash":null,"sig_alg":"ed25519","timestamp":"2026-10-17T12:00:00Z","version":"3.0"}}

5. Algorithms

Every hash and signature names its algorithm, so the format can outlive the algorithms it starts with (algorithm agility).

Kind Name Definition Encoding
hash sha256 SHA-256 (FIPS 180-4) sha256: + 64 hex
signature ed25519 PureEd25519 (RFC 8032), 32-byte public key ed25519: + 128 hex

These are the only algorithms of format 3.0. A reader MUST reject an event or seal naming any other algorithm (Unsupported algorithm …).

Public keys (header.author, seal key) are the 32-byte Ed25519 public key as 64 hex characters, without prefix. A signing identity is a 32-byte Ed25519 seed; it is never stored in an archive.

Blob digests (§10) are always sha256:; they do not follow hash_alg.

6. Events

6.1 Structure

Every line of an event log is an event:

{"header": {…}, "body": {…}, "signature": "ed25519:<128 hex>"}

6.2 Header (base and meta)

Field Type Rule
version string MUST be "3.0", else Unsupported format version …
hash_alg string MUST be a supported hash algorithm (§5)
sig_alg string MUST be a supported signature algorithm (§5)
prev_hash digest or null chain link (§6.5)
timestamp string RFC 3339 date-time with time zone, time of writing (e.g. 2026-10-17T12:00:00Z)
author string 64-hex Ed25519 public key of the signer

6.3 Meta header

Meta events carry two more header fields, the anchor (§11):

Field Type Rule
base_seq integer ≥ 0 number of base events the author had verified
base_head digest or null hash of base event number base_seq; null exactly when base_seq is 0

6.4 Signature

signature = <sig_alg>:<hex> of the Ed25519 signature, by the key in header.author, of canonical(event without its "signature" member) — that is, of {"body": …, "header": …}. The bytes are signed directly (no pre-hash). Because the header is signed, a signature is bound to its position in the chain, its time and its author: a signed event cannot be replayed elsewhere.

The prefix of signature MUST equal header.sig_alg; a missing or different prefix is Algorithm mismatch …. A signature that does not verify is Data tampered or bad author!.

6.5 Event hash and chain

The hash of an event under algorithm a is a: + hex of a(canonical(whole event, signature included)).

The hash of the last event of a log is its head, under that event’s own hash_alg. Two copies of a log are identical if and only if their heads are. Event hashes, under each event’s own hash_alg, are what anchors (§11), transcriptions (§9) and seals (§12) refer to.

7. Base network

7.1 Body types

The base body is discriminated by body_type:

body_type event_type Identifier fields Meaning
node create_node node_id create a node
edge create_edge edge_id, source_node, target_node create a directed edge from source_node to target_node
supersede_node supersede_node node_id replace the content of node node_id
supersede_edge supersede_edge edge_id, source_node, target_node replace the content of edge edge_id; endpoints MUST be unchanged
transcription transcription — attestation, §9

Identifiers are UUID strings (writers use UUIDv7, RFC 9562, lowercase, hyphenated). A node or edge keeps its identifier across supersedes.

7.2 Resource fields

Every node, edge and supersede body carries the resource fields below. Writers emit every field, with null when unused.

Field Type Meaning
resource_type "managed", "uri", "local_file" where the content lives
media_type media type or null one of the archival media types (§7.3)
blob_hash sha256:<64 hex> or null content in the blob store
uri string or null external reference, never fetched
path string or null path of a file on the writer’s machine
file_size integer ≥ 0 or null its size in bytes
file_hash sha256:<64 hex> or null its SHA-256
last_verified RFC 3339 date-time or null when size and hash were taken
title string or null human-readable title

Rules per resource type:

7.3 Archival media types

text/plain; charset=utf-8, text/markdown; charset=utf-8, image/tiff, image/png, image/jpeg, application/pdf; profile=pdfa.

8. Meta network

The meta network encodes tags as a graph of its own, so that anyone can annotate a base network without touching it.

body_type event_type Fields Meaning
tag create_node_tag or create_edge_tag node_id, name, description define a tag for nodes, or for edges
representation create_node_representation or create_edge_representation node_id, underlying_id a meta node standing for base node or edge underlying_id
edge set_node_tag or set_edge_tag edge_id, source_node, target_node assign tag source_node to the item represented by target_node
transcription transcription — attestation, §9

Tag names are free text and not unique. Tagging a base item therefore takes up to two meta events: a representation of the item (once per item) and a meta edge from the tag to that representation.

9. Transcriptions

A transcription event says: “I verified this log up to event number verified_count, whose hash is verified_head, under verified_hash_alg / verified_sig_alg, at verified_at.” Signed under current algorithms, it lets a chain of custody outlive the algorithms it started with: future authors re-attest old history with new primitives.

Body: body_type and event_type both "transcription", verified_head (digest), verified_count (integer ≥ 1), verified_hash_alg, verified_sig_alg (strings), verified_at (RFC 3339 date-time), note (string or null).

A transcription MAY appear in the base log or in the meta log and attests only the log it sits in. It is valid only if verified_count is at least 1 and smaller than its own event number, verified_head equals the hash of event number verified_count of the same log, and the prefix of verified_head equals verified_hash_alg; otherwise Transcription does not match chain. Any author may transcribe. A transcription creates no item and allocates no alias.

10. Blob store

A blob’s name is the hex SHA-256 of its bytes; it is stored at blobs/sha256/<hex[0:2]>/<hex[2:4]>/<hex>. Events reference it as blob_hash: "sha256:<hex>". Blobs are immutable and deduplicated: identical content is stored once.

For every event with a non-null blob_hash — in either log — the file MUST exist and its bytes MUST hash to its name; otherwise the archive is invalid: File corrupted or missing!. Text blobs are UTF-8.

A file under blobs/sha256/ that no event references is unreferenced (typically the leftover of an interrupted write, or a file added by hand). It is not part of the knowledge graph and does not make the archive invalid; readers SHOULD warn about it (§15).

11. Meta anchors

Meta commits to base; base never commits to meta. Every meta event records, in base_seq / base_head, the tip of the base log its author had verified when writing it. Readers verify the base log first, then check each meta event in order:

Base events have no anchor fields. The base can be shipped and verified without the meta log, and anyone can annotate a base with their own key.

12. Seal log

12.1 Seals

seals/seals.ndjson is an append-only log of seals, one per line, parsed with the rules of §3.3. A seal is a signed checkpoint of the whole archive:

Field Type Rule
type "seal"  
version "3.0" else Unsupported format version …
seq integer ≥ 1 MUST equal the seal’s position in the log, from 1
prev_seal_hash digest or null hash of the previous seal; null for the first seal and only there
base {"head", "count"} tip of the base log: head = hash of base event number count, null exactly when count is 0
meta {"head", "count"} tip of the meta log, same rule
blob_root sha256:<hex> Merkle root of the blobs the sealed events reference (§12.2)
time RFC 3339 date-time time of sealing
hash_alg, sig_alg strings supported algorithms (§5)
key 64 hex Ed25519 public key of the sealer
sig <sig_alg>:<hex> signature by key of canonical(seal without "sig")

The seal hash is hash_alg: + hex of hash_alg(canonical(whole seal)); prev_seal_hash is computed with the hash_alg of the seal holding it, and the prefixes of prev_seal_hash and sig MUST equal the seal’s algorithms (Algorithm mismatch …). The hash of the latest seal is one short string that commits to the entire archive at that moment; publishing it lets anyone check a copy (§16). Any key may seal.

12.2 blob_root

blob_root is the RFC 6962 Merkle Tree Hash, with SHA-256, over the set S of distinct blob digests referenced by blob_hash in base events 1…base.count and meta events 1…meta.count, sorted in ascending hex order:

12.3 Seal verification

Seals are checked after both logs, in order; for each seal:

  1. type is "seal", seq equals its position, prev_seal_hash links to the previous seal, sig verifies, and every field has the type above — otherwise Seal chain broken! (Unsupported format version …, Unsupported algorithm … and Algorithm mismatch … as for events);
  2. both tips exist, with those hashes, in the verified logs — otherwise Seal does not match logs! (a log rewritten or truncated after the seal, or meta/ missing);
  3. neither count is lower than in the previous seal — otherwise Seal goes back in log history;
  4. blob_root equals the root recomputed per §12.2 — otherwise Blob set does not match seal!.

Every seal is checked, not only the latest. Events appended after a seal do not affect it: a seal only speaks about the events up to its counts.

A seal cannot prove that it is the latest one. Freshness always needs an outside reference: a seal hash published somewhere the reader trusts.

13. Verification procedure

An archive is valid when all of the following hold:

  1. Every line of the base log, the meta log and the seal log obeys §3.3.
  2. The base log, read in order (§3.2), obeys for each event: version (§6.2), algorithms and prefixes (§5, §6.4, §6.5), chain link (§6.5), signature (§6.4), referenced blob (§10), transcription (§9).
  3. The meta log obeys the same rules plus the anchor rules (§11).
  4. The seal log obeys §12.3.
  5. Replaying both logs obeys the projection rules (§14).

The reference implementation checks the base log, then the meta log, then the seal log, then replays; within a log it reads line by line (parsing included) and runs each event’s checks in the order of item 2, the anchor coming right after the signature for meta events. It reports the first failure. An archive with several defects may legitimately be reported with a different message by another implementation; the normative part is validity.

14. Projection (replay) rules

Readers build the current state of the graph by replaying the logs in order.

Aliases. In the base log, each create_node or create_edge takes the next alias e1, e2, … (one counter for nodes and edges). In the meta log, each tag, representation and tag assignment takes the next alias m1, m2, …. Supersedes and transcriptions take none. Aliases are a convenience of the projection, not part of the format: they are never written in events.

Supersedes. A supersede_node / supersede_edge replaces the projected body of an existing item, which keeps its identifier and alias. On replay:

Tags. A tag assignment’s target_node MUST be a representation created earlier in the meta log, whose underlying_id is an item created earlier in the archive (writers only ever represent base nodes and edges); otherwise Tag assignment references an unknown representation. The assigned tags of a base item form a set: repeating an assignment changes nothing.

All projection failures are integrity errors: the archive is invalid.

15. Writer rules and warnings

These rules bind writers. Readers do not check them, so an archive written by other software that breaks them stays valid.

Readers SHOULD report, without failing:

16. Verification in practice (informative)

Checking every signature and re-hashing every blob on every open does not scale to large archives. A reader MAY skip some checks, but only on two grounds:

A reader MUST NOT skip any check because of a seal it merely found in the archive: any key may seal, so such a seal proves nothing about who checked what. Seals found in an archive are always checked, never trusted.

The reference implementation offers three levels: none (parse only; read-only), incremental (the default: skip only on the two grounds above) and full (check everything). Reading a blob always re-hashes it (File corrupted or missing! (blob <hex>)).

17. Informative files

ik manifest writes three files at the archive root:

None of them affects validity: they may be absent, and a reader finding them different from its own copies SHOULD only warn (they were probably written by another version of the software). This document never names a software version, so it changes only when the format or its wording does.

18. Test vectors

test_vectors.json lets an independent implementation prove itself. Its rules restate this specification in one line each. It contains:

The seeds are public test values; never use them as real keys.

19. Error messages

Messages of the reference implementation (IntegrityError), for cross-implementation reports. … stands for details.

Message Section
Invalid log line: not UTF-8 §3.3
Invalid log line: not a JSON object / Invalid log line: <JSON error> §3.3
Invalid log line: duplicate JSON key §3.3
Invalid log line: non-integer number … §3.3
Invalid log line: integer … is outside ±(2^53−1) §3.3
Invalid string: lone surrogate (not a Unicode scalar value) §3.3
Unsupported format version … §6.2, §12.1
Unsupported algorithm … §5
Algorithm mismatch: <field> uses '<alg>' prefix, header says '<alg>' (seals: seal says) §6.4, §6.5, §12.1
Chain broken! §6.5
Data tampered or bad author! §6.4
File corrupted or missing! §10
Meta anchor does not match base history §11
Meta anchor goes back in base history §11
Transcription does not match chain §9
Seal chain broken! §12.3
Seal does not match logs! §12.3
Seal goes back in log history §12.3
Blob set does not match seal! §12.3
Expected seal not found in seal log §16
Supersede event references unknown item <id> §14
Supersede event type does not match item <id> §14
Supersede by a non-author of item <id> §14
Supersede edge events cannot change endpoints §14
Tag assignment references an unknown representation §14

20. Conformance of the reference implementation

The indiek Python package implements this specification, with one known gap: on read it checks the fields that verification and replay use, but it does not yet validate every event against the schemas of §6–§9 (for example the per-resource-type field rules of §7.2, or the absence of unknown members). Such events are rejected when the reference implementation writes them, and an independent reader SHOULD reject them.

21. Versioning

This document specifies format 3.0, the first public version of the format. Every event and seal carries its format version; readers MUST reject versions they do not implement. Future versions will keep every archive written in a published version readable.

Appendix: outline of MANIFEST.json

ik manifest writes this document at the archive root (informative, §17). Every schema is a JSON Schema generated from the reference implementation; only LogTip is shown in full here.

{
  "format": "IndieK Public Archive",
  "format_version": "3.0",
  "description": "This MANIFEST.json documents the layout and schemas of an IndieK archive.  It is intended to allow any consumer to parse the archive independently of the original software, now or in the future.  SPEC.md, next to it, is the normative specification.",
  "spec": {
    "normative": "SPEC.md",
    "test_vectors": "test_vectors.json"
  },
  "authors": [
    {
      "public_key": "<64-hex public key of whoever ran ik manifest>",
      "algorithm": "Ed25519"
    }
  ],
  "authors_note": "Informative: the key of whoever ran 'ik manifest'.  Every event names its own author in header.author; validity never depends on this list.",
  "algorithms": {
    "hash": [
      "sha256"
    ],
    "signature": [
      "ed25519"
    ]
  },
  "archive_layout": {
    "description": "Entries of the archive root.  Only events/ is needed for a non-empty archive (plus blobs/ as soon as an event references a blob); every other entry is optional.  The event logs and the blobs are the only source of truth: indexes, aliases and caches are rebuildable views.",
    "entries": {
      "MANIFEST.json": "This file (informative, written by 'ik manifest').",
      "SPEC.md": "Normative, human-readable format specification (informative copy).",
      "test_vectors.json": "Test vectors of the format (informative copy).",
      "events/": "Base event log: the knowledge graph (nodes and edges).",
      "meta/events/": "Meta event log: tags and their assignments.",
      "seals/": "Seal log: signed checkpoints of the whole archive.",
      "blobs/": "Content-addressed blob store (blobs/sha256/...)."
    },
    "not_part_of_the_archive": "The verification cache lives in the reader's user cache directory, never in the archive; a .ik-cache/ folder, or any other entry not listed above, is ignored."
  },
  "blob_store": {
    "description": "Content-addressed blob store.  Every blob is immutable once written.",
    "root": "<archive_root>/blobs/sha256/",
    "path_pattern": "<root>/<hex[0:2]>/<hex[2:4]>/<sha256_hex>",
    "example": "blobs/sha256/ab/cd/abcd1234...ef",
    "notes": [
      "The filename is the full 64-character lowercase SHA-256 hex digest of the blob bytes.",
      "The two-level sharding (first 2 + next 2 hex chars) keeps directories small.",
      "To retrieve a blob referenced by 'sha256:XXXX' in an event, strip the 'sha256:' prefix and open blobs/sha256/<XXXX[0:2]>/<XXXX[2:4]>/<XXXX>.",
      "A referenced blob that is missing, or whose bytes hash to something else, makes the archive invalid."
    ]
  },
  "event_log": {
    "description": "Two append-only, hash-chained event logs stored as date-sharded NDJSON files: the base log (the knowledge graph) and the meta log (tags).",
    "base_event_log_root": "<archive_root>/events/",
    "meta_event_log_root": "<archive_root>/meta/events/",
    "path_pattern": "<log_root>/YYYY/MM/DD/events.ndjson (UTC date of the event)",
    "file_format": "NDJSON: one JSON object per line, UTF-8, each line terminated by LF",
    "ordering": "Sort every file whose name ends in 'ndjson' under the log root by path, lexicographically, then read each file top to bottom.  The result is the event sequence; event numbers start at 1.",
    "chain_integrity": "Each event's header.prev_hash is the '<hash_alg>:<hex>' digest, under that event's header.hash_alg, of the canonical bytes of the *entire* preceding event (signature included).  The first event of a log has prev_hash=null, and no other event may.  The hash of the last event is the head of the log.",
    "canonical_bytes": "RFC 8785 (JSON Canonicalization Scheme) restricted to ASCII object keys and integers in [-(2^53-1), 2^53-1] (9007199254740991); no other numbers are allowed.  Inside that subset it equals Python's json.dumps(obj, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False) encoded as UTF-8: no whitespace, keys sorted, non-ASCII characters kept as raw UTF-8.  Hashes and signatures are computed over the canonical bytes of the parsed line, never over the bytes on disk.  See SPEC.md and test_vectors.json.",
    "strict_parsing": "A log line is invalid if it is not UTF-8, is not one JSON object, repeats a key in any object, contains a non-integer number (fraction, exponent, NaN, Infinity) or an integer outside [-(2^53-1), 2^53-1], or holds a string with a lone surrogate.",
    "signature_algorithm": {
      "signing_curve": "Ed25519",
      "specification": "RFC 8032 (PureEd25519)",
      "what_is_signed": "The canonical bytes of the whole event without its signature field ({header, body}), signed directly (no pre-hash).  Signing the header binds each signature to its chain position, time and author.",
      "signature_encoding": "'ed25519:' followed by 128 lowercase hex characters",
      "public_key": "header.author: 64 lowercase hex characters (32 bytes)"
    },
    "meta_anchors": "Verify the base log first, keeping the hash of every base event.  Then every meta event's (base_seq, base_head) must name a base event that exists with exactly that hash (base_seq 0 with base_head null for an empty view), and base_seq must not decrease along the meta log.  Meta commits to base; base never commits to meta.",
    "algorithm_agility": "Every digest is written '<hash_alg>:<lowercase hex>' and every signature '<sig_alg>:<lowercase hex>'.  Each event names its algorithms in header.hash_alg and header.sig_alg; its prev_hash and signature prefixes MUST equal them.  Unknown algorithms or mismatched prefixes make the archive invalid.  Supported: hash sha256, signature ed25519.  Blob hashes are always 'sha256:'.  Transcription events let later authors re-attest a log under newer algorithms."
  },
  "seal_log": {
    "description": "Append-only log of signed checkpoints ('seals'), one JSON object per line.  The hash of the latest seal is the one string that commits to the whole archive: publish it, and check it with 'ik verify --expect'.",
    "path": "<archive_root>/seals/seals.ndjson",
    "file_format": "NDJSON, UTF-8, parsed with the same strict rules as event logs",
    "signing": "sig = 'ed25519:<hex>' Ed25519 signature by 'key' over the canonical bytes of the seal without its 'sig' field.  Seal hash = '<hash_alg>:<hex>' digest of the canonical bytes of the whole seal (sig included).",
    "chain": "seq is the 1-based position of the seal; prev_seal_hash is the hash of the previous seal, null for the first seal and only there.",
    "tips": "base and meta are {head, count}: head MUST be the hash of event number count of that verified log (null iff count is 0).  Counts MUST NOT decrease along the seal log.",
    "blob_root": "RFC 6962 Merkle Tree Hash, with SHA-256, over the ascending-sorted list of distinct blob digests referenced (blob_hash) by the first base.count base events and the first meta.count meta events.  Leaf = SHA-256(0x00 || 32 raw digest bytes); inner node = SHA-256(0x01 || left || right), pairing level by level and promoting an odd last node unchanged; no blobs = SHA-256 of the empty string.  Written 'sha256:<hex>'.  Every seal is checked.",
    "unreferenced_blobs": "Files under blobs/sha256/ that no event references (an interrupted write, or a file added by hand) are not covered by any seal.  Readers SHOULD warn about them; writers MUST NOT seal while any exist.  They are safe to delete.",
    "warnings": "A seal of an empty archive, or a seal committing to the same tips and blob_root as the previous seal, is valid but SHOULD be reported."
  },
  "verification": {
    "description": "Reader behaviour of the reference implementation (informative).  The validity of an archive is defined by SPEC.md alone.",
    "levels": {
      "none": "Parse only (debugging, recovery); the archive is opened read-only.",
      "incremental": "Recompute every hash link, anchor, transcription and seal; skip only the signature checks and blob re-hashing that the reader's own cache, or a seal hash the reader supplied, vouches for.",
      "full": "Check everything, then refresh the reader's cache."
    },
    "trust": "Checks may be skipped only on the basis of (a) the reader's own cache or (b) a seal hash obtained by the reader through a separate, trusted route.  A seal found in the archive is always checked, never trusted.",
    "cache": "The cache is local to the reader, non-authoritative and regenerable.  It lives outside the archive, in the user's cache directory, so that nothing shipped with an archive can make a reader skip a check."
  },
  "log_lines": {
    "events/": "#/schemas/EventModel",
    "meta/events/": "#/schemas/MetaEventModel",
    "seals/": "#/schemas/SealModel"
  },
  "schemas": {
    "Edge": "{ … JSON Schema … }",
    "EventHeader": "{ … JSON Schema … }",
    "EventModel": "{ … JSON Schema … }",
    "LogTip": {
      "description": "Tip of an event log as recorded in a seal: hash of event number\n``count`` (``None`` exactly when ``count`` is 0).",
      "properties": {
        "head": {
          "anyOf": [
            {
              "pattern": "^[a-z0-9-]+:[0-9a-f]+$",
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Head"
        },
        "count": {
          "minimum": 0,
          "title": "Count",
          "type": "integer"
        }
      },
      "required": [
        "head",
        "count"
      ],
      "title": "LogTip",
      "type": "object"
    },
    "MediaType": "{ … JSON Schema … }",
    "MetaEdge": "{ … JSON Schema … }",
    "MetaEventHeader": "{ … JSON Schema … }",
    "MetaEventModel": "{ … JSON Schema … }",
    "MetaEventType": "{ … JSON Schema … }",
    "MetaRepresentation": "{ … JSON Schema … }",
    "Node": "{ … JSON Schema … }",
    "ResourceType": "{ … JSON Schema … }",
    "SealModel": "{ … JSON Schema … }",
    "SupersedeEdge": "{ … JSON Schema … }",
    "SupersedeNode": "{ … JSON Schema … }",
    "Tag": "{ … JSON Schema … }",
    "Transcription": "{ … JSON Schema … }"
  }
}