Skip to main content

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

FormatSuffixkindWhat it carries
Content bundle.kbf.jsonkapi-bundleExtracted blocks and their per-locale targets
Content memory.memory.jsonkapi-memoryMultilingual entries, entities, provenance
Terms.terms.jsonkapi-termsConcepts and their relations
Annotation sidecar.overlays.jsonlheader annotationTypeStand-off annotations anchored into runs
Overlay set.overlays.jsonkapi-overlay-setIn-progress workspace overlay layers
Project archive.kpzkapi-project, kapi-interchangeA whole project, or one interchange task
Project state.kapi-state.jsonkapi-project-stateReview and workflow decisions
Brand voice.yamlTone, 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/formatExt, 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