Ciolk · open record format

CiolkRecord v1

This is the format every sealed record uses. It is public so that a record can be verified by anyone, at any time, without Ciolk. Verify one now at /verify.

CiolkRecord v1 — the sealed record format

Spec identifier: ciolk-record/1

This document describes the format Ciolk uses to seal the history of a job:

what was found, what was shown to the customer, what the customer agreed to,

what was invoiced and what was paid. It is written so that an insurer's

engineer, an auditor, or a judge's clerk can verify a record in fifty years

without Ciolk existing. Everything needed is in this document and in the

files of an evidence export; nothing requires our servers.

1. What a record is

A record is a JSON document. Every record has the same envelope; the

subject differs by kind. Records are never edited or deleted. A correction

is a new record.


{
  "spec": "ciolk-record/1",
  "kind": "approval",
  "id": "sn_5b0c1d8e2f3a4b5c6d7e8f90",
  "chain": "quote:clx7…",
  "seq": 2,
  "prev": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "createdAt": "2026-03-04T19:16:02.113Z",
  "org": { "ref": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "name": "Northside Body Works" },
  "provenance": { "packId": "auto-body", "packVersion": "1.0.0", "priceBookVersion": 4, "modelVersion": "claude-sonnet-4-6", "renderer": "ciolk-record-html/1" },
  "evidence": [ { "id": "ev_…", "kind": "image", "mimeType": "image/jpeg", "sha256": "…64 hex…" } ],
  "subject": { "…": "see §4" }
}

Field by field:

  • spec — always ciolk-record/1 for this version. A future version changes this string; it never changes the meaning of a v1 record.
  • kind — one of assessment, quote_sent, supplement_sent, approval, invoice_issued, payment_allocated (§4).
  • id — the record's own name, sn_ plus 24 hexadecimal characters. Not sequential, not guessable.
  • chain — which history this record belongs to. quote:<id> names the original quote (amendments and supplements share their original's chain); invoice:<id> is used only for an invoice with no quote behind it.
  • seq — the record's position in its chain, counting from 0.
  • prev — the SHA-256 hash of the previous record in the chain, or null for the first record.
  • createdAt — when the record was sealed, ISO-8601, UTC.
  • org.ref — an opaque, stable identifier for the business (a SHA-256 of an internal id). org.name — its display name at the time.
  • provenance — the pack (domain content) and its version, the business's price-book version, the model version that produced any findings, and the renderer version of the human-readable copy.
  • evidence — every piece of evidence the record rests on, with its kind, MIME type and the SHA-256 of its bytes. An export includes the bytes when they were retained, named by that hash.
  • subject — what happened, per kind (§4).

2. Canonical form and the hash

A record's hash is the SHA-256 of its canonical text. The canonical text is

produced by these rules and nothing else:

  • Object keys are sorted by Unicode code point at every depth.
  • Arrays keep their order; order is meaningful.
  • Strings, numbers, booleans and null serialise exactly as JSON.stringify would (UTF-8, no whitespace).
  • A key whose value is undefined is omitted; JSON has no undefined.
  • Dates are ISO-8601 strings in UTC (the record is text; nothing is a Date object).
  • The record hash is SHA-256 of that canonical text, lower-case hex. Nothing inside the record names its own hash.
  • Each record's prev is the hash of the previous record in the same chain; the first record's prev is null.

Reference implementation (JavaScript, no dependencies):


function canonicalize(v) {
  if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
  if (Array.isArray(v)) return "[" + v.map(canonicalize).join(",") + "]";
  const parts = [];
  for (const k of Object.keys(v).sort()) {
    if (v[k] === undefined) continue;
    parts.push(JSON.stringify(k) + ":" + canonicalize(v[k]));
  }
  return "{" + parts.join(",") + "}";
}
// hash = sha256(canonicalize(record)), lower-case hex

3. The chain

Records of one job form a chain. Verifying a chain means, for each record in

seq order:

1. Recompute the hash from the record's canonical text and compare it to the hash the chain names for it.

2. Check that the record's prev equals the previous record's hash (and null for seq 0).

3. Check that seq values are contiguous from 0.

4. For each entry in evidence whose bytes are present, hash the bytes and compare.

If every check passes, no record was altered, removed, reordered or inserted

after sealing. A tampered record fails step 1; a removed record fails steps 2

and 3; a reordered record fails step 2.

Inside Ciolk, every record is additionally a leaf of a daily Merkle tree

whose root is chained day to day (the "Audit Shield" layer). That layer lets

Ciolk prove a record existed on a given day; it is not needed to verify a

record's content, which is why an export does not depend on it.

4. Subjects by kind

Every subject is plain JSON with no nested hashes of its own. Money is

integer cents. Quantities are hundredths (150 = 1.5).

`assessment` — what was found

  • assessmentId
  • summary — one sentence
  • findings[]id, key (the pack's finding key, e.g. collision.panel.dent), label, severity (minor | moderate | major | critical), region (where on the evidence, in the evidence's own coordinates, or null), basis (what supports the finding, or null when nothing does — a null basis is rendered as "flagged for review", never invented), reviewState, evidenceId, and proposedItems[] (desc, kind, qtyHundredths, unitCents, totalCents, source, confirmed).

`quote_sent` and `supplement_sent` — what was shown

  • quoteId, version, revisionKind (original | amendment | supplement), parentRevisionId, reason (for supplements)
  • view — exactly the customer-visible quote: orgName, clientName, lines[] (desc, qtyHundredths, unitCents, totalCents, kind), subtotalCents, taxCents, totalCents, depositPct, depositCents, currency, expiresAt, rendererVersion
  • viewHash — the SHA-256 of that view's canonical text. An approval names the same hash, which is how "what was agreed" is tied to "what was shown".

`approval` — what was agreed

  • approvalEventId, quoteId, version
  • party (customer | insurer | shop), role, method (portal | esign | verbal_logged | written)
  • scope"full", or { "lineIndexes": [...] } when only the changes of a supplement were approved
  • occurredAt, signedName
  • renderedSnapshotHash — the viewHash of what the approver was looking at, verified at the moment of approval. null only for approvals recorded before sealing existed.

`invoice_issued`

  • invoiceId, number, jobId, quoteId, lines[], subtotalCents, taxCents, totalCents, dueAt, termsDays

`payment_allocated` — what was paid

  • paymentId, allocationId, provider (stripe | manual), providerRef, amountCents, status, quoteId, invoiceId, payerId, lineIndex, receivedAt

5. An evidence export

A Ciolk evidence export is a zip with:

  • records/NNN-<kind>.record.json — the records, one file each, in chain order
  • records/NNN-<kind>.html — the same records rendered for a browser (informational)
  • chain.json{ spec, chain, exportedAt, links: [{ seq, kind, hash, prevHash, file }] }
  • evidence/<sha256>.<ext> — the evidence bytes that were retained
  • verify.mjs — a dependency-free Node.js script: node verify.mjs prints match or mismatch per record and exits 0 only when everything matches
  • verify.html — the same check in a browser, offline
  • SPEC.md — this document
  • 00-summary.pdf — everything in chronological order, for reading
  • README.txt

Anyone can verify an export. Only Ciolk can produce one — but a produced

export proves itself.

6. JSON Schema (envelope)


{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ciolk.com/evidence/spec/ciolk-record-1.json",
  "title": "CiolkRecord v1",
  "type": "object",
  "required": ["spec", "kind", "id", "chain", "seq", "prev", "createdAt", "org", "provenance", "evidence", "subject"],
  "additionalProperties": false,
  "properties": {
    "spec": { "const": "ciolk-record/1" },
    "kind": { "enum": ["assessment", "quote_sent", "supplement_sent", "approval", "invoice_issued", "payment_allocated"] },
    "id": { "type": "string", "minLength": 1 },
    "chain": { "type": "string", "pattern": "^(quote|invoice):[A-Za-z0-9_-]+$" },
    "seq": { "type": "integer", "minimum": 0 },
    "prev": { "type": ["string", "null"], "pattern": "^[0-9a-f]{64}$" },
    "createdAt": { "type": "string", "format": "date-time" },
    "org": { "type": "object", "required": ["ref", "name"], "properties": { "ref": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "name": { "type": "string" } } },
    "provenance": { "type": "object", "required": ["packId", "packVersion", "priceBookVersion", "modelVersion", "renderer"] },
    "evidence": { "type": "array", "items": { "type": "object", "required": ["id", "kind", "mimeType", "sha256"] } },
    "subject": { "type": "object" }
  }
}

7. Versioning

A v1 record is a v1 record forever. If the format changes, records carry a

new spec string and this document is joined by a v2 document; verifiers

must reject a spec they do not recognise rather than guess.