Gå til hovedinnhold

Voice profile

A voice profile describes how a project's content should sound: its tone, its style measures and pattern rules, and worked examples of the voice being got right and wrong. Voice checks score content against it, and generation steps are steered by it. Word rules ("write this, not that") are terms; a voice file may carry them beside the voice under terms: (see Word rules).

It is the one serialization in this family written in YAML; the others are 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. kapi voice edit opens the stored profile as this YAML, validates what you save, and reads it back into the store as one recorded change.

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, naming a profile in the project's voice store or a pack. A recipe names no profile file.
  2. The profile's own name: a profile under profiles: that binds no voice: of its own is answered by the profile the store holds under its name, before the project default is; see Context.

Every rung answers out of the project's voice store. A voice.yaml in the checkout is what kapi store import reads; no command that resolves a profile opens one.

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

Fields​

FieldTypeNotes
namestringRequired.
idstringOptional identity.
descriptionstringOptional.
toneobjectThe tone profile (see Tone).
styleobjectStyle rules, including the prohibited and required patterns (see Style and patterns).
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.

A voice file may also hold a top-level terms: list of word rules beside these fields (see Word rules). The list belongs to the file, not to the profile, and importing the file moves it into the project's terms.

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.
failsbooltrue makes a match fail a check. Unset, a match reports. Optional.
advisorybooltrue makes a match report whatever fails says. Optional.
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: '\x{2014}'
description: "Em dashes read as machine-written; one per thousand words is the ceiling."
fails: true
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."
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_wordsint50The most words a sentence may hold. A sentence over it is a finding.
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.
failsboolfalseMakes a sentence over sentence_words, a comment over a length limit and a change over the density limit fail a check. Unset, the limits are style measures and every finding they raise reports.

A comment over a length limit is a 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 fails to load with an error naming the key, and so does a sentence_words written as a map.

style:
sentence_length: short
comments:
sentence_words: 50
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.

Word rules​

A voice profile holds no word list. Every word rule is a term, and a voice file may carry its rules beside the voice under a top-level terms: list, as the starter packs do. kapi store import and kapi voice import move the list into the project's terms and report how many rules they moved. Where a starter pack is bound as the voice, its terms apply beside the project's own, and a finding they raise names the pack (pack technical-docs).

A file that lists its words under vocabulary: (preferred_terms, forbidden_terms, competitor_terms) is converted as it is read: a forbidden term becomes a rule, a competitor term a rule with competitor: true, and a preferred term a preferred form, or an advisory rule when it names a different replacement. Abbreviations are dropped.

Each entry under terms: 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 carried by a voice file and a rule handed to a tool read identically.

A rule matches whole words. It never matches inside a placeholder's name, a URL or a bare domain such as docs.example.com or example.no, so a rule on docs leaves docs.example.com alone and still matches "the docs page".

FieldTypeNotes
termstringThe word or phrase to reject. A rule with a replacement and no term names a preferred form and rejects nothing.
replacementstringWhat to say instead. The translation tools skip a rule with no replacement unless do_not_translate is set, because "say this instead" needs a this; a check reads a bare term as "avoid this".
notestringWhy, shown with the finding. Optional.
advisorybooltrue makes a use of the term report without failing a check. Unset, a use fails.
competitorbooltrue marks the term as a competitor's name.
concept_idstringThe concept in the terms store and the graph this rule denotes. Empty for a rule written by hand.
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 terms expand asks a model for them in the term's language and writes them onto the term for review.
case_sensitiveboolWhether the term and its forms match in their own casing. Unset, a rule whose replacement is capitalised matches case-sensitively, as a product name does (term: QuickCast, replacement: Quickcast flags QuickCast and leaves a lower-case URL slug quickcast alone), and so does a rule whose term differs from its replacement only in case (term: Ripgrep, replacement: ripgrep); any other rule matches regardless of case. true or false overrides the default. term-check compares a source term with a replacement in another language, so it matches in the term's own casing only when this is true.
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.
terms:
- term: utilize
replacement: use
advisory: true
forms: [utilizes, utilized, utilizing]
scope: prose
- term: log in
replacement: sign in
- term: Acme Chat
replacement: messaging
competitor: true
- replacement: workspace
note: The name for a team's shared space

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 stored profile 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.
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.
channels:
child:
tone:
formality: casual
humor: light

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.

Resolution applies the locale override first, then the channel, then the persona. No override carries word rules: the terms bound at a point apply on every channel and to every persona.

Autonomy​

FieldTypeNotes
auto_promote_at_countintPromote a candidate rule into the project's terms 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 failing finding already drops a block below it, while a handful of findings that only report does not. The bar shapes roll-ups and dashboards; whether a check fails depends on its failing findings alone.

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

What the profile leaves out​

  • Scores and findings. Those are check results computed against the profile, stored with the content or the run, and never written back here.
  • The store it came from. A profile document is one profile; the voice store holds every profile a project has (voice.db for a standalone store; inside a project, the voice tables of the project store).
  • Locale-specific content. Overrides adjust voice, never copy. Translated strings belong in the deliverable.

See also​