Skip to main content

Context

Getting content right takes more than the words in the file. It takes knowing what those words are supposed to be: what this project calls a thing, which wording it has already approved, what it will not say. kapi keeps that knowledge as context, the standing record a person or an AI agent consults before writing and is checked against after.

Two questions describe the model:

  • Where does this content sit? Every piece of content has a place in a context space: the product it belongs to, the channel it ships on, and any further axis the project declares for itself, such as a brand.
  • What governs that place? A point in the space binds governance: a voice profile, a terms store, and the content memory of what has been said before.

Neither question mentions a language. A project that publishes in one language has both of them, and answering them is the ordinary case: keeping a repository's own writing consistent across the surfaces it ships on, whoever (or whatever) wrote the draft. A language is one more axis of the same space, and it is added last.

The context space

Content is written for a point rather than for a project. A repository that holds both a framework and the app built on it has two voices; a marketing page and an API reference have two registers. Two structural axes name the point:

AxisAnswersIn the recipe
productwhich thing this belongs toa key under profiles:
channelwhich surface it ships ona channel: on the collection, as product/channel

These two axes are structural: a key under profiles: is a product, the channels a profile lists are the channels that product ships on, and a collection names its point with one channel: reference. A project that sits at a single point declares no profiles at all, and the whole of it is governed by the project defaults.

kapi.yaml
profiles:
acme:
channels: [docs, app, landing]
voice: .kapi/voice.yaml

collections:
- name: acme-docs
channel: acme/docs
base: web
content:
- path: "docs/**/*.md"
- name: acme-landing
channel: acme/landing
content:
- path: "site/pages/**/*.mdx"

One product on three surfaces. A second product is a second key under profiles:, and its channels are its own: two products may both ship a docs channel without sharing anything but the word.

A collection's channel: is always written product/channel. A bare channel name is a load error, even when exactly one product declares it; the error names the qualified form to paste. A collection that binds no channel is governed by the project's default point.

Products and channels are slugs (lowercase, stable, never translated), so governance binds to an identifier that does not move when a term is reworded. A profile or a channel may carry a display concept, but resolution never looks at it. The project file reference covers the field shapes in full.

Declared axes

Beyond product and channel, a project can name the further dimensions its content varies along. These axes are declared, under defaults.coordinates, and every collection inherits them. A collection that sits elsewhere on one axis overrides that axis alone with its own coordinates: and keeps the rest. The most specific value wins, per axis.

kapi.yaml
defaults:
coordinates:
brand: acme

collections:
- name: acme-docs
channel: acme/docs
coordinates:
mode: how-to
content:
- path: "docs/**/*.md"

Two axes have a conventional spelling. brand is the brand a point sits under, coarser than product: a workspace has brands, a brand has products, a product ships channels. mode is the kind of document at the point, in the sense the Diátaxis scheme means: tutorial, how-to, reference or explanation. Correct style is a function of mode, so a profile applied flatly across a tutorial and an architecture note is wrong for one of them; a mode axis lets governance tell them apart. The coordinate map is open, and a project that splits its content some other way declares the axis it needs.

Product and channel are never written under coordinates:. They are derived from a collection's channel:, and a recipe that declares one there is rejected at load, so the point a recipe computes and the point it states cannot disagree.

What governs a point

Three stores hold the context, and all three are committed to git and versioned like code: the recipe binds them, and git diff reviews changes to them.

StoreHoldsBound by
voice profiletone, register, preferred and forbidden vocabularydefaults.voice or a profile's voice:
termsthe approved word for each concept, per languagedefaults.terms_source or a profile's termstore:
content memorysource/target pairs already produced and revieweddefaults.memory_source

A profile binds a voice and a vocabulary to its whole product; the collection's channel then refines the voice, selecting a channel-specific override inside the bound profile when one exists. A point that declares no override of its own falls back to the project default, the right answer for a voice that reads the same everywhere.

Two recipe keys sit beside these bindings. A collection with no target language can say so with source_only: true, so a run reads and checks it and writes nothing back; the project file reference covers it. And the wording rules a voice profile carries under vocabulary: have the same shape as the term_rules: a check or a translation step takes: one term, what to use instead, and how hard it bites. The voice profile reference lists the fields.

Where a profile's files live

The filesystem mirrors the recipe. The default governance sits flat in .kapi/; each profile keeps what it overrides in a directory of its own, named after its key under profiles::

.kapi/
├── voice.yaml # the project default
├── terms.json
├── memory/
└── profiles/
└── acme-labs/
├── voice.yaml # this product's voice, by convention
└── terms.json # this product's vocabulary, by convention

Only the differences belong there. A profile that does not override the vocabulary keeps no terms.json, and resolution falls through to the flat default. A profile directory is itself a binding: a profile that names neither voice: nor termstore: in the recipe is still answered by its own files, so a project that keeps its overrides here does not have to restate every one of them in kapi.yaml.

The content memory and the unit-state record stay at the top of .kapi/ rather than splitting by profile. A recycled wording and an approval are facts about a unit, true wherever that unit is governed from. See the project store for the full layout.

How a point resolves

Governance resolves from the broadest binding toward the most specific, and the most specific one that exists wins:

1project defaultdefaults.voice · the project's own terms2profileprofiles.<name>.voice / termstore:, else .kapi/profiles/<name>/3channelthe matching override inside the voice already selected

A collection's point resolves up the ladder; an unbound collection stops at rung 1.

The rungs behave differently:

  • Rung 2 selects a file. The profile's voice: wins as written; failing that, its conventional .kapi/profiles/<name>/voice.yaml answers; failing that, defaults.voice. Terms resolve the same way: termstore:, then .kapi/profiles/<name>/terms.json when the file is actually there, then the project's own store.
  • Rung 3 refines what rung 2 selected. A channel entry inside the bound voice profile replaces the tone, the style, or both, so a landing-page register is authored once beside the voice it varies rather than duplicated into a file per product-and-channel pair. A channel the profile has no entry for takes the base voice.

Vocabulary applies to every channel. The words a product will and will not use are the same on every surface it ships on, which is why rung 3 moves the register and leaves the term list alone.

Resolution follows the declarations in the recipe. Every failure (a channel no profile declares, a bare reference, a non-slug name, a voice binding naming two sources) is caught when the recipe loads, because the alternative is content governed by a plausible-looking wrong voice.

Governance can have a lifetime

A profile can declare from when until when it is the one in force. The bounds are a date or an RFC3339 instant, and the window is half-open: valid_from is inclusive, valid_to exclusive, so a valid_to date excludes that whole day.

kapi.yaml
profiles:
acme-relaunch:
channels: [landing]
voice: .kapi/profiles/acme-relaunch/voice.yaml
valid_from: 2026-09-01
valid_to: 2026-12-01

A profile outside its window stops governing. Resolution falls through to the next binding (the collection's channel, then the project default) exactly as if the expired one were not declared, so a seasonal voice retires itself on the date it says. The same instant governs a whole run, so a long convergence cannot cross a boundary halfway through.

The switch is always reported. The first time a run, a check or a push resolves through a lapsed profile it says so:

governance: profile "acme-relaunch" expired 2026-12-01; governing with the project default

kapi context search reads every bounded profile against the current time and labels it active, upcoming or expired, so a writer can see that the guidance they are about to follow has a start or an end date before it lapses.

The same half-open window scopes an individual term, which is what lets a vocabulary survive a rename: recognise the old name until March is a different instruction from never use it, and a context answer distinguishes them.

Two questions

Because the stores govern one point together, you do not ask them one at a time. You ask by location or by content, and kapi reads whichever stores hold the answer.

QuestionAskOver MCP
what applies here?kapi context <path>read context://<path>
what do we know about this?kapi context search <query>call context_search

What applies here

Point kapi context at a file and it answers for the place that file sits: the point it resolved to, the voice profile in force with its full guidance, the terms bound there, and the governance windows around them. It is one document, written to be read by a person or dropped into a model's context.

kapi context docs/guide.md

It resolves through the same seam a run, a check and a push resolve through, so the voice you read for a file is the voice a run applies to it, including a content item's own channel: and a profile whose window has closed. The answer is for the file's own content. An item that declares a file for its comments alone governs only those comments, so the answer comes from the next item that claims the file, or from the project's default point.

With no file in hand (an ad-hoc draft, a starter pack), name a profile instead:

kapi context --profile marketing

Add --json for the structured shape. Over MCP the two addresses are resources rather than tools, because reading what already applies at a place takes no arguments; context://docs/guide.md?format=json is the same document --json prints.

What do we know about this

The other question takes a word or a phrase and asks it of every store the project binds:

kapi context search "sign-in"
your question"sign-in"voice · terms · memoryevery bound storeone answergrouped by kind

One question reaches every store bound at the point.

The answer says what the project calls the thing, whether it is discouraged and what to say instead, and any wording already approved, grouped by kind and naming any store it could not reach. Asking before you write is cheaper than learning the same fact from a failing check afterwards.

Three details make the answer actionable. A discouraged term is reported with where it is actually used, so this word is retired becomes this word is retired and sits in thirty-four blocks. That count is read from the context graph kapi up writes when it extracts content, so it is as of the last run: a term added to the store since then shows no uses until the next kapi up, and the same number appears in the desktop explorer and on the platform's concept page. Approved wording that contains a term the same search found retired is flagged, so one answer never contradicts itself. And --locale narrows terms to one language, because a spelling can be retired in one and admitted in another. Add --json for the shape an assistant reads; it is the same shape the MCP tool returns.

Two directions around the writer

Context works on both sides of the moment content is written:

  • Before: the voice guide and the approved terms go into a writer's context (a person's, or an agent's), so a draft starts compliant rather than being corrected into compliance. The agent skill injects them automatically; kapi context <path> and context_search fetch them on demand.
  • After: kapi check scores the result block by block against the same stores, with an exit code, so the rules hold in the editing loop and again in CI with no assistant present.

Rules an agent proposes (a new forbidden term, a newly approved translation) land through kapi apply as typed change-set entries in the committed stores, so the review surface for the rules is git diff, exactly as for code.

One more axis: language

Adding target_languages to the recipe adds no governance axis. A translated block sits at the point its source sits at, and the same profile governs it: the voice bound at acme/landing is the voice its Norwegian landing page is written in, and the terms bound at acme/docs are the terms its German documentation uses.

What a language adds is an override inside the voice profile, next to the channel overrides: a locales: entry carries the formality, the point of view, the vocabulary and the examples that hold for that language only. So the thing you author for one language is the thing that governs every other, and the loop that keeps the languages in step is convergence.