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. This section is about kapi's own file
family.
Three kinds of artifact
The most useful distinction in this family is not between one format and another. It is between three roles a file can play.
A store is working state. SQLite databases under the project's .kapi/
directory — the content-memory store (tm.db), the terms store
(termbase.db), block and skeleton caches. They are fast, indexed, mutable,
and rebuildable. .kapi/ is gitignored, so nothing in it is a source of
truth.
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, or a whole project archive. Bundles move: they are what you commit, hand over, or ship.
Committed state is an authored or decided record. A terms bundle kept as the project terminology, a brand voice profile, a project state file. These are the review surface — the files a person reads in a pull request.
The rule this produces
Artifacts that must be reviewed live outside .kapi/, and the conventional
lookup for them prefers the project root. The terms bundle resolves
<root>/terms.json before <root>/.kapi/terms.json; the project state file
defaults to .kapi-state.json at the root — a sibling of the state
directory, not a file inside it. Neither would survive a fresh clone, or ever
appear in a diff, if it lived in .kapi/.
If a person needs to review it, it does not belong in the gitignored cache.
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 | .kapi-state.json | kapi-project-state | Review and workflow decisions |
| Brand voice | .yaml | — | Tone, style, vocabulary, 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 keeps a marker segment ahead of the
serialization suffix — .kbf.json, .terms.json, .overlays.jsonl.
Both halves earn their place. 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.
One cost follows: 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 — not open-ended forward compatibility. A file from a future major version is refused with a message naming the version the build speaks, rather than parsed into something half-understood.
The rule is uniform; the code is not. 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
Identification is deliberately split, and the two mechanisms 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 identifies the file, or says that it cannot
kapi memory import and kapi terms import resolve --format auto from the
extension, and they refuse to guess. Guessing looks harmless and is not. The
TMX reader handed a file with no translation units 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 expresses what the caller
wants written rather than what an unread file already is, 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.