# Append-only means you correct out loud.

*31 July 2026 · the launch journal*

Every operational record contains mistakes. The design question is not how to prevent them — you cannot — but what the record looks like after one is fixed. A store that fixes mistakes with `UPDATE` has a second, quieter problem: after the fix, there is no evidence the mistake ever happened, which means there is no evidence about anything. If rows can be edited, any row might have been. The moment a dispute reaches the record, "we corrected it" and "we rewrote it" are indistinguishable — and that ambiguity is exactly what the counterparty's lawyer needs.

EPCIS ships the alternative in the core standard: **errorDeclaration**. A mistake is corrected by *declaring* it — a new event that marks the erroneous one, states a reason from the CBV's error-reason vocabulary, and links the corrective events forward. Nothing is edited. Nothing is deleted. The record stays trustworthy precisely because it visibly contains its own mistakes.

## The worked correction, captured for real

An operator books a shipment out of the wrong flow — the event says `shipping` when the goods were actually being received. Three events tell the whole story, and here is the capture transcript, run through the local gateway that ships in the package:

```
$ npx epcis.dev capture correction-doc.json
accepted	captureID=ab6d179a-4ac1-41dc-be26-c2ee731103b3	events=2
eventID	urn:uuid:5e900006-0000-4000-8000-000000000002
eventID	urn:uuid:5e900006-0000-4000-8000-000000000003
```

Event `…0001` is the original, wrong observation — `bizStep: shipping` — already in the store, untouched forever. Event `…0002` is the corrective event: the same EPC at the same time, with what actually happened (`receiving`). Event `…0003` is the declaration — a **new** event, with its own fresh `eventID`, that repeats the erroneous content and carries the declaration block:

```json
"errorDeclaration": {
  "declarationTime": "2026-07-19T12:15:00.000Z",
  "reason": "incorrect_data",
  "correctiveEventIDs": ["urn:uuid:5e900006-0000-4000-8000-000000000002"]
}
```

`reason` comes from CBV's standardized error-reason identifiers — `incorrect_data` when the observation was wrong, `did_not_occur` when the whole event was phantom. `correctiveEventIDs` points forward at the replacement. After the correction lands, all three events are present on every read: the corpus sequence fixture (`golden-corpus/sequences/correction-errordeclaration.json` in the package) asserts exactly that with three `present: true` reads.

## The hash fact that makes this airtight

The CBV 2.0 §8.9 event hash — the computed identity every event carries here — deliberately excludes three things: `eventID`, `recordTime`, and `errorDeclaration`. The hash identifies *the observation as originally captured*. Watch what that implies, with real output:

```
$ npx epcis.dev hash erroneous-original.json
hash	0	ni:///sha-256;649a245b6e626325c60ca71777cfb66f386efd6fcc722fab7c2fecdfc56cd0c6?ver=CBV2.0
$ npx epcis.dev hash declaration-event.json
hash	0	ni:///sha-256;649a245b6e626325c60ca71777cfb66f386efd6fcc722fab7c2fecdfc56cd0c6?ver=CBV2.0
```

The declaration event restates the erroneous observation and adds the declaration block — and hashes identically to the original, because the declaration is *about* the observation, not part of it. The declared event's identity is unchanged. That is the point: a correction never mints a new version of history. It adds testimony about the history that exists.

The same fact closes the back door. The EPCIS 1.2 idiom — re-submitting an existing `eventID` with an `errorDeclaration` bolted on, an in-place edit wearing a conformance costume — is deterministically **rejected** at this capture door. The append-only law here is checkable: any `(eventID, hash)` pair once read must be byte-identical on every future read. Since the hash ignores `errorDeclaration`, a same-`eventID` declaration would alias the original under the same identity with different bytes — so the gateway refuses it, and the refusal is a pinned corpus fixture (`sequences/errordeclaration-edit-attempt.json`). One `eventID`, one immutable row, forever. Declarations are inserts with fresh IDs, full stop.

## Read semantics: two views, one record

Append-only does not mean your operational dashboards display known-wrong events forever. It means the *record* keeps them while *views* decide. The query surface carries declaration-aware predicates from the standard's own parameter set — `EQ_errorReason`, `EQ_correctiveEventID`, and time-bounded reads over `declarationTime` — so the two consumers that matter each get their shape:

- **Operational reads** exclude declared-erroneous events and follow `correctiveEventIDs` to the replacement — the trace shows what actually happened.
- **Audit reads** include everything: the mistake, when it was declared, why, by what it was corrected, and the fact that the original bytes never moved.

Same table, two projections. The alternative — an "audit log" bolted beside an editable store — reproduces the original problem one layer up: now the log and the table can disagree, and you need a third thing to say which is right. An append-only record with declared errors has nothing to reconcile, because there is only one thing.

## Why a reviewer should care

When your security or architecture review asks "how are corrections handled," most vendors answer with process — who is allowed to edit, and how edits are logged. The structural answer is stronger: corrections are inserts; edits are impossible; the erroneous event, the declaration, and the correction are three content-addressed rows anyone can re-verify with `npx epcis.dev hash`. In a dispute, you do not attest that the record was handled properly. You hand over the record, and it demonstrates itself.

Validate the fixtures on your own machine — the corpus ships in the npm package, and the schema it validates against is sha256-pinned on [the conformance page](/conformance/):

```
npx epcis.dev validate node_modules/epcis.dev/golden-corpus/valid-standard/object-event-error-declaration.json
```

The computed-identity law this post leans on is the §8.9 event hash; the same identity is what makes capture idempotent under retries. The full local toolkit is on [run it yourself](/run-it-yourself/), and the business reading of an unrewritable record — evidence, not attestation — is [visibility.cloud](https://visibility.cloud)'s brief.

---

**Proof, not adjectives.** This build could not verify the gateway's test suite green, so no sentence on this page claims a passing run — the last verified run is recorded in the repository's own CI, not here. Live on this origin, no key: POST /translate, /validate, /hash. No conformance attestation has ever been issued. The dated ledger is /what-ships-today/.
