Gå til hovedinnhold

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 push sends 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
one line of a shard
{"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.

FieldTypeNotes
scopestringThe 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.
unitstringBlock identity: the block's content hash, the same key the block tables and overlays address it by.
variantobjectThe locale, and optional tone/channel, this state applies to.
statusstringTarget ladder position: draft → translated → established (approved).
sourceStatusstringSource ladder position: written → established (approved).
originobjectProvenance of the current target (engine/tool/reference).
targetHashstringContent hash of the translation this state blesses.
revisionstringRevision 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.
basisstringThe 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.
contentHashstringThe basis by text: the hash of the source wording the decision was made against. A record that carries no basis is graded by it.
contextHashstringThe 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.
decisionobjectThe 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.
aiReviewobjectLast AI pre-review annotation. Optional. Its targetHash and revision name the translation it judged.
updatedstringRFC 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.