kapi serialization formats
These are the formats kapi writes for itself: the on-disk shapes it uses to move its own content, memory, terminology, annotations and decisions between commands, machines and people.
They are distinct from the formats kapi reads and writes on your behalf
(JSON catalogs, DOCX, Markdown, strings.xml and the rest), which are covered
in the format reference.
Three kinds of artifact
A file in this family plays one of three roles.
A projection is derived working state. .kapi/work/store.db holds what
kapi read out of the working tree: the block cache, the overlays a run wrote,
the skeleton and parse caches beside it. It is fast, indexed, mutable and
rebuildable from the content files.
A context store is authored state. The project's own store, in a workspace
outside the checkout, holds the content memory, the terms, the voice profiles
and the decision ledger. Every checkout of the project reads it, nothing
rebuilds it, and kapi store export is its backup. See
the project store.
A bundle is a portable serialization. One reviewable, hashable, diff-able file carrying the same content losslessly: a content bundle, a content-memory bundle, a terms bundle, a voice profile, a project block-state record, or a whole project archive. Bundles move: they are what you hand over, ship, or put in a pull request.
How bundles and the store relate
Bundles serialize authored context for exchange and review.
kapi terms export and kapi memory export write them, and
kapi store import <dir> reads
a directory of them. The context itself moves between machines as operations,
through a context backend or a transfer file.
.kapi/ is ignored by default; adjust its ignore rules if you choose it as the
snapshot directory.
The formats
| Format | Suffix | kind | What it carries |
|---|---|---|---|
| Content bundle | .kbf.json | kapi-bundle | Extracted blocks and their per-locale targets |
| Content memory | .memory.json | kapi-memory | Multilingual entries, entities, provenance |
| Terms | .terms.json | kapi-terms | Concepts and their relations |
| Annotation sidecar | .overlays.jsonl | header annotationType | Stand-off annotations anchored into runs |
| Overlay set | .overlays.json | kapi-overlay-set | In-progress workspace overlay layers |
| Project archive | .kpz | kapi-project, kapi-interchange | A whole project, or one interchange task |
| Project state | .jsonl (under .kapi/state/) | kapi-state | Review and workflow decisions |
| Voice profile | .yaml | (none) | Tone, style, patterns, examples |
Not sure which you want? Choosing a format.
The compound suffix convention
Every one of these files except the project archive and the voice profile is
JSON or JSON Lines, and each portable one keeps a marker segment ahead of the
serialization suffix: .kbf.json, .terms.json, .overlays.jsonl. The state
shards are the exception: they never travel alone, their names are derived
from document keys, and the directory holding them (.kapi/state/) already
says what they are.
The .json tail keeps jq, GitHub, editors and
syntax highlighting treating the file as what it is. The marker segment says
which JSON it is, so a directory listing stays readable and a format can be
recognised before parsing.
The compound suffix has one cost: the standard library's filepath.Ext reports .json for
every one of them. Extension-driven code goes through the
helpers in
core/format
(Ext, TrimExt, Stem, HasExt), which return the most specific match. A
naive filepath.Ext on en-US.kbf.json yields .json, and trimming that
leaves a stray .kbf behind.
One version contract, applied per format
Every format in the family carries a schemaVersion as MAJOR.MINOR and
follows the same forward-compatibility rule:
Reject an unknown major. Accept unknown minors of a known major.
Strict on major, best-effort on minor. A file from a future major version is refused with a message naming the version the build speaks.
The rule is the same for every format, but each format implements the check
separately, and the overlay set inside a project archive carries its own
schemaVersion, versioned independently of the enclosing package.
Two ways a file says what it is
A file identifies itself in two ways, which answer different questions:
kind: the magic string on the document root. Answers what schema is this?, works on content alone, and survives a rename. It is what sniffing uses.- The extension: answers what should I expect? before the file is opened, which is what lets a command route a path without reading it.
The two are checked independently, so a file that is not what it claims to be
is caught by whichever mechanism notices first: a wrong kind by the reader
that owns the envelope, a wrong extension by the command that had to choose a
reader before opening anything.
Import refuses a file it cannot identify
kapi memory import and kapi terms import resolve --format auto from the
extension, and they refuse to guess, because a wrong guess is silent. The
TMX reader handed a file with no <tu> element returns zero entries, and
the CSV reader skips every row it cannot turn into a concept, so a file the
importer never actually read reports Imported 0 entries and exits 0, which is
indistinguishable from an empty content memory.
An extension the command does not recognise is therefore an error, and one that
names the file, the extensions that are supported, and the --format flag that
settles the question:
cannot identify seed.dat: ".dat" is not a content-memory extension
(expected .memory.json, .tmx or .tmx.gz); pass --format (bundle, tmx) to say what it is
Export works the other way round. An output path names what the caller wants
written, and stdout carries no extension at all, so there auto falls back to
the interchange default.
See also
- Choosing a format: including when an interchange format is the right answer instead.
- Content model: the in-memory model these serialize.
- Project file: the
kapi.yamlrecipe that binds them.