Gå til hovedinnhold

Voice profile

A voice profile describes how a project's content should sound: its tone, its style rules, the vocabulary it prefers and forbids, and worked examples of the voice being got right and wrong. Voice checks score content against it, and generation steps are steered by it.

It is the one serialization in this family that is YAML rather than JSON.

Implemented in Go at core/profile.

Why YAML

Every other format here is machine-produced: exported from a store, packed by a command, canonicalized for hashing. A voice profile is the opposite: it is authored and edited by a person, and it is mostly prose. Tone descriptions, style rules, and the worked examples are multi-line English text.

YAML carries multi-line prose without escaping it, allows comments explaining why a rule exists, and diffs a reworded paragraph legibly. JSON does none of those well. The type also carries JSON tags, so the same profile serializes to JSON when it moves over an API or into a store; the YAML is the authoring form.

Resolution ladder

When a command needs a profile and no --profile, --profile-file, or --pack flag is given, kapi resolves one in this order:

  1. The recipe binding: defaults.voice in kapi.yaml, which may name a profile_file, a pack, or a profile in the local voice store. A profile under profiles: binds its own voice: for the product it governs, and its conventional .kapi/profiles/<name>/voice.yaml answers before the project default does; see Context.
  2. <project-root>/.kapi/voice.yaml: the profile's home in the committed context graph, beside the terms and the content memory.
  3. <project-root>/voice.yaml

The binding wins. Rungs 2 and 3 are conventions that make kapi voice check DRAFT.md work flag-free inside a project; a project that has bound a profile never reaches them.

--profile, --profile-file, and --pack are mutually exclusive; passing more than one is an error rather than a silent precedence.

Fields

FieldTypeNotes
namestringRequired.
idstringOptional identity.
descriptionstringOptional.
toneobjectThe tone profile (see Tone).
styleobjectStyle rules, including the prohibited and required patterns (see Style and patterns).
vocabularyobjectPreferred, forbidden, and competitor terms, each a list of term rules (see Vocabulary and term rules).
examplesarrayWorked examples of the voice (see Examples).
localesmapPer-locale overrides, keyed by locale id (see Overrides).
channelsmapPer-channel overrides.
personasmapPer-persona overrides.
autonomyobjectHow far automated steps may act on the profile (see Autonomy).
min_scoreintThe compliance bar, 0–100 (see The compliance bar).
version, version_noteint, stringProfile revision and a note describing it.
created_at, updated_at, created_bytimestamp, timestamp, stringProvenance.
workspace_idstringSet when the profile came from a workspace.

Tone

FieldTypeNotes
personalitystring[]Adjectives, e.g. [friendly, knowledgeable, direct].
formalitystringcasual, neutral, formal, or technical.
emotionstringwarm, neutral, or authoritative.
humorstringnone, light, or frequent.
guidelinesstringFree prose the guide renders and the model reads. Optional.

Style and patterns

FieldTypeNotes
active_voiceboolPrefer the active voice.
sentence_lengthstringshort, medium, or varied.
person_povstringfirst_plural, second, or third.
contractionsstringalways, sometimes, or never.
prohibited_patternsPattern[]Regex rules whose match is a finding.
required_patternsPattern[]Regex rules whose absence is a finding.
commentsobjectWord limits for the comments in source files (see Comment limits). Optional.

Patterns

Each entry under prohibited_patterns and required_patterns is a Pattern:

FieldTypeNotes
regexstringThe pattern, in Go regular-expression syntax. Required.
descriptionstringWhat the rule is for; shown with the finding and rendered into the guide.
severitystringminor, major, or critical. Decides the score penalty and whether the finding fails a strict check.
rateobjectA density ceiling instead of a ban: max matches allowed per per_words words (per_words defaults to 1,000). Under the ceiling nothing is reported; over it, every match is. A max of 0 is rejected at validation. Optional.
scopestringWhere the rule applies: unset means everywhere; prose skips fenced blocks and inline code; code applies only inside them; heading applies only to headings. Optional.
not_afterstringA regular expression tested against the text before each match; a match it matches there is not a violation. Anchor it with $. Optional, and only for prohibited_patterns.
style:
prohibited_patterns:
- regex: "—"
description: "Em dashes read as machine-written; one per thousand words is the ceiling."
severity: minor
rate: { max: 1, per_words: 1000 }
scope: prose

The allowance a rate grants is rounded up and never below max, so a 300-word document under a "one per 1,000 words" rule allows one.

Go regular expressions have no lookbehind, so a rule whose meaning turns on the words before a match states them with not_after. The past-habitual "used to" describes what code did before a change, while "is used to" and "an id, used to flag" describe what it does:

style:
prohibited_patterns:
- regex: '(?i)\bused to\b'
not_after: '(?i)(?:(?:\b(?:is|are|was|were|be|been|being|get|gets|got|isn''t|aren''t|wasn''t|weren''t)|[''’]s)\s+(?:\w+\s+)?|[^\w\s)\]"''\x60’”]\s*|(?:^|\s)["“‘''\x60]|^\s*)$'
description: "A comment describes what the code does, not what it used to do."
severity: major
scope: prose

Comment limits

style.comments holds the comments in source files to word limits. A profile that sets it asks kapi check for the comment limits at every point it governs, and each limit left out takes its default, so comments: {} applies them all. A limit counts words, and a code span, a reference or a link in a comment is not a word.

FieldTypeDefaultNotes
sentence_words.minorint50A sentence over this many words is a minor finding.
sentence_words.majorint70A sentence over this many words is a major finding. Must be above minor.
comment_wordsint100The most words a comment that documents no declaration may hold.
doc_wordsint150The most words a declaration's doc comment may hold.
package_doc_wordsint300The most words the doc comment of a package or module may hold.
density.rationumber1The most comment lines a change may add for each code line it adds.
density.min_comment_linesint8How many comment lines a change adds before density.ratio applies.

A comment over a length limit is a major finding, and so is a change over the density limit. Density is measured in a check scoped to a diff: it counts the lines the change adds to each file, a comment line being one that holds a comment and nothing else and a code line one that holds anything outside a comment. The package or module doc comment, a directive and a blank line count as neither, so a new package's doc comment is never dense. A check over whole files reports density as unsupported. Each limit is a positive number: a profile with a zero or negative limit, or a minor sentence limit that is not below the major one, fails to load with an error naming the key.

style:
sentence_length: short
comments:
sentence_words: { minor: 50, major: 70 }
doc_words: 150

sentence_length stays guidance for a writer or a model, and the limits are what a check enforces. A channel or persona that supplies its own style drops the profile's comment limits unless it restates them, and validation warns about it.

Vocabulary and term rules

FieldTypeNotes
preferred_termsTermRule[]The spelling to use; a rule's replacement is the preferred form and its term the one to avoid.
forbidden_termsTermRule[]Words the voice never uses, with what to say instead.
competitor_termsTermRule[]Names of other products, flagged wherever they appear.
abbreviationsmapAbbreviation to expansion, for the guide.

Term rules

Each entry in those three lists is a TermRule. The same shape is what a flow step or defaults.tools accepts under term_rules: for term-check, translate, recycle, dnt-check and pseudo-translate, so a rule authored in a profile and a rule handed to a tool read identically.

FieldTypeNotes
termstringThe word or phrase the rule is about. Required.
replacementstringWhat to say instead. A rule with no replacement is skipped by the tools unless do_not_translate is set, because "say this instead" needs a this.
notestringWhy, shown with the finding. Optional.
severitystringminor, major, or critical. minor warns; everything else, including unset, fails, because rules resolved from a terms store carry no severity and must not be silently downgraded.
concept_idstringThe concept in the terms store and the graph this rule denotes. Set when a rule is promoted from a concept-backed correction; empty for a standalone profile.
do_not_translateboolThe term must survive verbatim into every target. Gives a bare term with no replacement its meaning.
formsstring[]The other surface shapes the term takes (inflections, declensions, conjugations), matched alongside term. Matching is exact, so the forms are declared rather than derived; kapi voice expand asks a model for them in the profile's language and writes them here as a diff.
case_sensitiveboolMatch the term and its forms in their own casing. Off by default; on for a rule whose whole content is capitalisation (term: Ripgrep, replacement: ripgrep).
replacement_formsstring[]Other forms of replacement in the language it is written in, such as the plural. term-check accepts a translation that contains any of them.
acceptedobject[]Further wordings that satisfy the rule, each with text and optional forms: the admitted and approved terms a concept carries beside its preferred one. replacement stays the wording a translation is asked to use.
scopestringThe same values a pattern's scope takes. Empty means everywhere.
vocabulary:
forbidden_terms:
- term: utilize
replacement: use
severity: minor
forms: [utilizes, utilized, utilizing]
scope: prose
preferred_terms:
- term: log in
replacement: sign in
concept_id: c-sign-in

Examples

FieldTypeNotes
beforestringThe wording to avoid.
afterstringThe wording in this voice.
explanationstringWhy the second is better. Optional.
categorystringtone, style, or vocabulary. Optional.

Overrides

locales, channels, and personas each hold partial overrides layered onto the base profile at resolution time. A profile can therefore say "this is the voice" once and then vary it (terser in a push notification, more formal in de-DE, warmer for a particular persona) without duplicating the whole document per variant.

Resolution applies the requested locale, channel, and persona to produce one effective profile; the file itself stays the single authored source.

A locale override (locales.<locale>) carries:

FieldTypeNotes
formality, humor, person_povstringReplace the base tone or style value for this language.
cultural_notesstringGuidance that holds for this language only.
vocabulary_overridesTermRule[]Rules that hold for this language only.
example_overridesVoiceExample[]Examples in this language.

A channel override (channels.<channel>) adjusts the voice on one channel. A collection's channel selects the matching entry.

FieldTypeNotes
tone, styleobjectReplace the resolved tone or style wholesale.
vocabularyobjectWritten like the profile's own vocabulary. Its forbidden and competitor terms are added to the resolved ones, and an abbreviation applies only where none is defined. A preferred term is dropped where an earlier rule already governs the term.
channels:
child:
vocabulary:
forbidden_terms:
- term: authenticate
replacement: sign in
severity: major

A persona override (personas.<name>) layers an individual author's voice inside the profile's guardrails:

FieldTypeNotes
tone, styleobjectReplace the resolved tone or style, as a channel override does.
preferred_termsTermRule[]Terms the author leans on. A preferred term is dropped where an earlier rule already governs the term.
avoided_termsTermRule[]Terms the author steers clear of; added to the forbidden set.

Resolution applies the locale override first, then the channel, then the persona. A channel and a persona can only tighten vocabulary. Every rule an earlier layer set keeps applying at its own severity, and a later preferred term that names a term an earlier layer forbids, lists as a competitor, or already words its own way is dropped. The merge order enforces this, whoever wrote the override, and kapi voice validate warns about each preferred term it drops.

Autonomy

FieldTypeNotes
auto_promote_at_countintPromote a candidate rule into the profile automatically once this many corrections stand behind it, with no human review. 0 (the default) keeps every promotion manual.

The compliance bar

min_score is the minimum voice-compliance score a block must reach to count as compliant in roll-ups. When a profile does not set one, the default of 80 applies: a bar calibrated so that a single critical vocabulary hit already drops a block below it, while a handful of minor issues does not.

A profile that cannot be read resolves to the default bar, so roll-ups over historical scores stay comparable.

What is deliberately not in it

  • Scores and findings. Those are check results computed against the profile, stored with the content or the run, and never written back here.
  • The compiled voice store. The store is derived (voice.db for a standalone store; inside a project, the voice tables of the shared .kapi/work/store.db); the profile is the source.
  • Locale-specific content. Overrides adjust voice, never copy. Translated strings belong in the deliverable.

See also