Terms bundle (.terms.json)
A terms bundle is the portable, lossless serialization of a project's terminology: the concepts a project has agreed on, their terms per locale, and the relations between them.
A terms bundle is a committed, git-tracked file, like the content-memory
bundle the recipe binds. It is the review surface, the artifact a person reads
in a pull request when a term changes. The terms tables in the project
store (.kapi/work/store.db) are the gitignored index the
bundle compiles into, never the other way round.
Implemented in Go at
terms/ktb.
When kapi writes one
kapi terms export --format bundle: the explicit export.kapi pack: as theterms.jsonmember inside a.kpz.
kapi terms import accepts csv, tsv, json, tbx, or bundle. Only
bundle is lossless; the rest are covered under
what the other formats drop.
The lookup ladder
When a flow needs a project's terms and the recipe does not bind one explicitly, kapi looks for a conventional bundle in this order:
<project-root>/.kapi/terms.json<project-root>/terms.json
The context graph first. .kapi/ is where the rest of a project's
governance lives (the voice profile, the memory seeds, the unit-state record), and
terms are one node of that graph rather than a loose file beside it. It is
committed, so a bundle kept there appears in a diff, gets reviewed, and survives
a fresh clone. The root rung is for a project that would rather keep its terms at
the top level; both are committed, so the ordering is about where a reader
expects to look.
The recipe binding wins over both. Setting terms_source in kapi.yaml
names the file explicitly and skips the ladder entirely; it binds any path.
Content memory has no equivalent ladder
A project has one terms bundle, so a single conventional filename is
meaningful. Content memory is plural: this repository's own dogfood recipe
keeps a separate memory bundle per surface. A single well-known
memory.json at the project root would force every project into one bundle,
and a ladder that resolved to "whichever one is first" would silently pick the
wrong one.
So terms gets a ladder and content memory does not.
Envelope
{
"schemaVersion": "1.0",
"kind": "kapi-terms",
"created": "2026-07-26T09:12:04Z",
"generator": { "id": "kapi", "version": "1.2.3" },
"concepts": [],
"relations": []
}
| Field | Type | Notes |
|---|---|---|
schemaVersion | string | MAJOR.MINOR. Unknown major rejected; unknown minor of a known major accepted. |
kind | string | Always kapi-terms. |
created | string | RFC 3339. Optional. |
generator | object | {id, version}. Optional. |
concepts | array | The concepts. Always present. |
relations | array | Relations between concepts. Omitted entirely when there are none. |
concepts and relations reuse the terms store's own types directly rather
than a parallel wire struct, so the serialization cannot drift from the model
it serializes.
Determinism
Marshal produces byte-stable output: concepts and relations sorted by id,
terms within a concept sorted, timestamps normalized to UTC, HTML escaping off,
two-space indent, trailing newline. Two exports of the same store are the same
bytes, so a bundle diffs cleanly and can be hashed as a unit, which is what
lets it sit inside a .kpz under
the package's Merkle root.
What the other formats drop
kapi terms export also emits csv, json, and tbx (tsv is accepted on
import only). All three are lossy, and the JSON one is the easiest to mistake
for the bundle because both are JSON:
--format jsonwrites{name, version, concepts}and nothing else. It carries no relations at all, and its envelope has nokindmarker, so it cannot be sniffed, and a reader cannot tell which schema it is holding. It is a convenience export for spreadsheets and scripts.--format tbxis the industry interchange format. It has no representation for the cross-links that make a kapi concept useful inside kapi, most notably the entity cross-references content memory uses.--format csvis flat: one row per term, no concept structure and no relations.
Reach for bundle whenever the file is staying inside kapi.
What is deliberately not in it
- The compiled SQLite tables. The terms tables in
.kapi/work/store.dbare derived from the bundle and rebuilt on import. - Match state. Which blocks a term was found in is a check result rather
than a property of the terminology;
kapi terms occurrencesreads it from the project's block cache.
See also
- Overview: store, bundle, and committed state.
- Content memory: the peer bundle, and why it has no ladder.
- Choosing a format: native versus interchange.