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— alwaysciolk-record/1for this version. A future version changes this string; it never changes the meaning of a v1 record.kind— one ofassessment,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, ornullfor 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
previs the hash of the previous record in the same chain; the first record'sprevis 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
assessmentIdsummary— one sentencefindings[]—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, ornull),basis(what supports the finding, ornullwhen nothing does — anullbasis is rendered as "flagged for review", never invented),reviewState,evidenceId, andproposedItems[](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,rendererVersionviewHash— 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,versionparty(customer|insurer|shop),role,method(portal|esign|verbal_logged|written)scope—"full", or{ "lineIndexes": [...] }when only the changes of a supplement were approvedoccurredAt,signedNamerenderedSnapshotHash— theviewHashof what the approver was looking at, verified at the moment of approval.nullonly 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 orderrecords/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 retainedverify.mjs— a dependency-free Node.js script:node verify.mjsprints match or mismatch per record and exits 0 only when everything matchesverify.html— the same check in a browser, offlineSPEC.md— this document00-summary.pdf— everything in chronological order, for readingREADME.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.