Project state (.kapi/state/*.jsonl)
Project state records where each block stands: what has been reviewed, what was approved and by whom, what status a target has reached. It is the portable representation of the project's block decisions.
Implemented in Go at
core/state.
Ledger storage and snapshots
A decision is appended to the project's decision ledger, which lives in the project store, and it is durable the moment it is recorded. Every local checkout reads the ledger and applies the decisions made on the source and translation it holds now: by their revisions where a decision records them, and by the source and target hashes where it was recorded before revisions.
The shards are the ledger in a form git can track: for each block, what was
decided about the source and the translation. kapi store import reads that
shape in, and the ledger moves between machines as operations, through a
context backend or a transfer file.
This has four consequences:
- The two directions agree. A snapshot writes the shards and an import reads them, so a clone that runs the import picks up what the shards carry.
- Reading a shard twice costs nothing. A line the ledger already holds is left alone, and a line older than what applies at its pairing is left out.
- A snapshot leaves unchanged shards untouched, which keeps a CI step from producing an empty commit.
- A server-backed project has a different sink for the same record:
kapi pushsends the decisions to the remote instead of writing shards.
One shard per document
The record is a directory of JSON Lines files, one per document scope, each line a single block:
.kapi/state/
├── d-app-messages.jsonl
└── d-guide-intro.jsonl
{"unit":"k1_…","scope":"d-app-messages","variant":{"locale":"nb"},"status":"established","targetHash":"…","revision":"r:…","basis":"r:…","contentHash":"…","contextHash":"…","decision":{"reviewState":"approved","at":"2026-08-04T09:12:44Z"},"updated":"2026-08-04T09:12:44Z"}
JSON Lines, because the record is appended to and diffed far more often than it is read whole: a line per block makes recording an approval a one-line diff instead of a rewrite of every byte. Sharded per document for the same reason at a larger grain: editing the documentation does not rewrite the shard holding the interface strings, and two branches touching unrelated documents do not conflict on sight.
Lines are sorted within a shard, so a file's bytes depend only on its contents. A shard whose last block is gone is pruned. A malformed line is an error: state is authoritative and is never silently discarded.
A shard's filename is derived from the document scope, never the scope itself. A scope is the document's resolved key, never its path (so renaming a file does not orphan its state), and deriving the filename keeps an opaque key from escaping the directory.
Why the shards are an artifact
The decisions themselves live in the project's store, where every checkout
reads them and a git checkout of another branch changes nothing about them.
The shards are what those decisions look like as reviewable text: a team that
keeps its approvals in a pull request reads them with kapi store import,
and the
shards sit beside the terms and the content memory they make claims about,
because all three are nodes of one context graph.
The overview states the general rule, and Moving context covers the four verbs that carry context between the store and files.
Block state
Each line carries one block's state. The record's schema identity is
kapi-state at schemaVersion 1.0; a shard is bare JSON Lines, so
the version rides the single-document serialization of the same records.
| Field | Type | Notes |
|---|---|---|
scope | string | The document the block belongs to, and the shard it is written into. Part of the record's identity: a block id is unique inside its document and nowhere wider. |
unit | string | Block identity: the block's content hash, the same key the block tables and overlays address it by. |
variant | object | The locale, and optional tone/channel, this state applies to. |
status | string | Target ladder position: draft → translated → established (approved). |
sourceStatus | string | Source ladder position: written → established (approved). |
origin | object | Provenance of the current target (engine/tool/reference). |
targetHash | string | Content hash of the translation this state blesses. |
revision | string | Revision of the translation this state blesses, inline codes included: the token kapi inspect prints. Absent on a record made before revisions, and on a source approval. |
basis | string | The decision's basis: the revision of the source the decision was made against. A record whose basis no longer matches the block's current source reads as stale. Absent on a record made before revisions. |
contentHash | string | The basis by text: the hash of the source wording the decision was made against. A record that carries no basis is graded by it. |
contextHash | string | The fingerprint of the governing context (voice, terms, point) in force when the block was produced; the staleness gate compares it against the context in force now. |
decision | object | The recorded workflow decision: reviewState (approved, rejected, …), by (empty for a person, ai/<model> for an autonomous AI approval, agent/<client> for an agent acting for a person), at, note, parked, assignee. |
aiReview | object | Last AI pre-review annotation. Optional. Its targetHash and revision name the translation it judged. |
updated | string | RFC 3339. |
Five fields need more explanation:
revision and targetHash name the translation without holding it. They
identify the content the review was made against, so an edit to the
translation makes the approval stale. The revision covers
the inline codes as well as the words, so a changed link counts as an edit. The
translation itself lives in the deliverable; this record only blesses it.
basis and contentHash bind the decision to its source. An approval is
about a pairing, this rendering of this source, so the record keeps the
revision of the source, and the hash of its wording, as the decision's basis.
Edit the source and the block reads as stale until someone decides the new
pairing; restore the source and the decision applies again. The content hash is
also what lets a block removed in one revision and restored in a later one come
back to its own history, with no re-translation.
A record made before revisions is read by its hashes. It carries no
revision or basis, applies while the text of both halves matches, and keeps
answering that way until the block is decided again. Once a decision with
revisions is recorded for the same text, the older record no longer answers.
contextHash binds the target to the governance it was produced under. The
staleness gate reads it: a target written under a voice profile or a terms
store that has since moved fails the gate, naming what moved, and kapi up
reproduces it under the context now in force.
aiReview is advisory and never a review outcome. It informs the review queue
with a score and findings. Where an AI pre-review justified an automatic
approval, that approval is recorded separately in decision, with by
carrying the ai/<model> identity, so a machine judgement and a machine
approval remain distinguishable in the record.
What the shards leave out
- The translations themselves. Only their hashes. The content lives in the deliverable or in a content bundle.
- Anything derivable. Coverage percentages and review queues are computed from these shards and the content.
- A decision this checkout does not hold. The shards are what this checkout holds now; the ledger keeps the rest, including the history of a block that has been decided more than once.
See also
- The project store: the context store that holds the ledger, and the projection that rebuilds.
- Overview: store, bundle, and exported artifact.
- Terms: the other artifact a reviewer reads.