Šķîþ ţö ḿàîñ çöñţéñţ

Ṽöîçé þŕöƒîļéš

Ŵĥéŕé ţéŕḿîñöļöĝý éñšüŕéš ýöü üšé ţĥé ŕîĝĥţ ŵöŕđš, à ṽöîçé þŕöƒîļé đéšçŕîƃéš ĥöŵ ýöü šàý ţĥéḿ: ţĥé þéŕšöñàļîţý, ƒöŕḿàļîţý, àñđ ŵŕîţîñĝ þàţţéŕñš ţĥàţ ḿàķé çöñţéñţ ŕéçöĝñîžàƃļé. neokapi çàþţüŕéš à ṽöîçé àš à ḿàçĥîñé-ŕéàđàƃļé þŕöƒîļé àñđ ŕüñš îţ àš öñé çĥéçķšéţ öṽéŕ ţĥé šàḿé çöñţéñţ-ṽéŕîƒîçàţîöñ éñĝîñé ţĥàţ þöŵéŕš ţéŕḿîñöļöĝý, đö-ñöţ-ţŕàñšļàţé, àñđ þļàçéĥöļđéŕ îñţéĝŕîţý: éṽéŕý çĥéçķéŕ éḿîţš ţĥé šàḿé ƒîñđîñĝš îñţö ţĥé šàḿé Block àññöţàţîöñ, šö ṽöîçé îš öñé çĥéçķ àḿöñĝ ḿàñý. Ţĥé Ĝö ļîƃŕàŕý ļîṽéš îñ core/profile/.

Üšéđ ţĥîš ŵàý, à ṽöîçé þŕöƒîļé ķééþš àñ ÀÎ àššîšţàñţ öñ-ṽöîçé ţĥé ŵàý à ţéšţ ķééþš çöđé çöŕŕéçţ: ļöàđ ţĥé þŕöƒîļé îñţö çöñţéẋţ (öŕ éẋþöšé îţ öṽéŕ ḾÇÞ) šö ĝéñéŕàţéđ çöþý îš öñ-ṽöîçé ƒŕöḿ ţĥé ƒîŕšţ đŕàƒţ, ţĥéñ çĥéçķ àñýţĥîñĝ ţĥàţ đŕîƒţš àñđ çàŕŕý ţĥé šàḿé ṽöîçé ţĥŕöüĝĥ éṽéŕý ţŕàñšļàţîöñ. Ţĥé ƒîñđîñĝš (ţĥé šþéçîƒîç ţéŕḿš àñđ ŕüļéš ţĥàţ ƃŕöķé) àŕé ţĥé šüƃšţàñçé; ţĥé 0–100 ŕöļļ-üþ îš à çöñṽéñîéñçé, ŕéļîàƃļé öñļý ŵĥéñ çàļîƃŕàţéđ àĝàîñšţ à ļàƃéļéđ šéţ.

Ṽöîçé þŕöƒîļéš ŵîţĥ ţĥé ÇĻÎ

Ţĥé kapi voice çöḿḿàñđ ĝŕöüþ ŵöŕķš àĝàîñšţ à þŕöƒîļé ƒŕöḿ à ƃüîļţ-îñ šţàŕţéŕ þàçķ (--pack), ţĥé ļöçàļ ṽöîçé šţöŕé (--profile), öŕ à šţàñđàļöñé ĝîţ-šĥàŕéàƃļé ÝÀḾĻ ƒîļé (--profile-file):

# Print the rendered guide (paste into an assistant, or pipe to a file)
kapi voice guide --pack friendly-dtc

# Score text: file argument, --input-text, or stdin. --min-score gates CI (exit 3).
# The default pass is rule-based and offline; --ai adds an LLM analysis of tone,
# style, and clarity.
kapi voice check --profile-file voice.yaml --min-score 80 release-notes.md

# Rewrite off-voice content: deterministic vocabulary substitution, no model.
# A rule with no replacement is reported under "skipped" rather than applied.
kapi voice rewrite --profile-file voice.yaml --input-text "Leverage our solution"

# Ask a model for the inflected forms of each vocabulary term, written as a diff
kapi voice expand --profile-file voice.yaml --language nb

# Manage profiles in the local store
kapi voice profiles

Ŵĥàţ ṽöîçé çĥéçķš àššéšš

Ţĥé đéƒàüļţ ṽöçàƃüļàŕý àñđ þàţţéŕñ çĥéçķš àššéšš éñçöđéđ ŕüļéš. kapi check --voice àđđš àđṽîšöŕý šîḿîļàŕîţý ţö þŕöƒîļé éẋàḿþļéš ţĥŕöüĝĥ ţĥé kapi-check þļüĝîñ; îţ đöéš ñöţ ŕéǫüéšţ àñ ĻĻḾ ĵüđĝḿéñţ. kapi voice check --ai àñđ ţĥé ŕàŵ voice-check ţööļ éẋþļîçîţļý ŕéǫüéšţ šéḿàñţîç ḿöđéļ ŕéṽîéŵ.

À çļéàñ ŕüļé ŕéšüļţ ḿéàñš ñö ƒîñđîñĝš îñ ţĥöšé ŕüļéš. Îţ đöéš ñöţ éšţàƃļîšĥ ţĥàţ à šéŕṽîçé đéšçŕîþţîöñ, þŕöḿîšé öŕ ƒàçţüàļ çļàîḿ îš çöŕŕéçţ. Ţĥé çàñöñîçàļ çĥéçķ ŕéþöŕţ îđéñţîƒîéš àñàļýšéš ţĥàţ ŕàñ, ŵéŕé ñöţ ŕéǫüéšţéđ öŕ çàññöţ ƃé àššéššéđ ƃý éẋàçţ ŕüļéš. Ŕéþöŕţš ŵîţĥöüţ éẋéçüţîöñ ḿéţàđàţà ĥàṽé üñŕéþöŕţéđ çöṽéŕàĝé. Àþþļîçàƃļé þŕöšé ĝüîđàñçé îš ŕéçöŕđéđ àš üñšüþþöŕţéđ ƃý đéţéŕḿîñîšţîç çĥéçķš; àñ éñçöđéđ þŕöĥîƃîţéđ éẋþŕéššîöñ çàñ þŕöđüçé à ŕüļé ƒîñđîñĝ.

Ŕéǫüéšţéđ àñàļýšîš ƒàîļüŕéš àŕé öþéŕàţîöñ éŕŕöŕš. Ḿîššîñĝ þŕöƒîļéš, üñàṽàîļàƃļé þļüĝîñš, þŕöṽîđéŕ éŕŕöŕš, ţîḿéöüţ àñđ çàñçéļļàţîöñ çàññöţ ýîéļđ à ñéŵ šüççéššƒüļ šçöŕé. Ţĥé ĻĻḾ ţööļ ṽàļîđàţéš šţŕüçţüŕéđ ƒîñđîñĝš ļöçàļļý: ḿàļƒöŕḿéđ ĴŠÖÑ, ḿîššîñĝ öŕ ñüļļ ƒîñđîñĝš, îñṽàļîđ ƒîéļđš àñđ îñṽàļîđ ƒîñđîñĝ ṽàļüéš àŕé éŕŕöŕš. Öñļý àñ éẋþļîçîţ éḿþţý ƒîñđîñĝš àŕŕàý îš à çöḿþļéţéđ çļéàñ ḿöđéļ ŕéšþöñšé. Éḿþţý çöñţéñţ šķîþš ḿöđéļ àñàļýšîš àñđ éḿîţš ñö šçöŕé.

Ṽöîçé þŕöƒîļéš

À þŕöƒîļé çàþţüŕéš ţöñé, šţýļé, àñđ ṽöçàƃüļàŕý àš ŕüļéš:

name: "Acme Corp"
description: "Professional yet approachable B2B SaaS voice"

tone:
personality: [knowledgeable, helpful, confident]
formality: neutral
emotion: warm
humor: light

style:
active_voice: true
sentence_length: medium
person_pov: second # "you" / "your"
contractions: sometimes

vocabulary:
preferred_terms:
- term: "workspace"
note: "Use instead of 'account' or 'organization'"
forbidden_terms:
- term: "leverage"
replacement: "use"
severity: minor
competitor_terms:
- term: "Slack"
replacement: "messaging platform"
severity: critical

examples:
- before: "Users can leverage the platform to achieve synergy."
after: "Your team can use the workspace to collaborate more effectively."
explanation: "Active voice, preferred terms, removed jargon"
category: style

Þŕöƒîļéš šüþþöŕţ ļöçàļé öṽéŕŕîđéš (é.ĝ. formal àñđ ţĥîŕđ-þéŕšöñ ÞÖṼ ƒöŕ ja), çĥàññéļ öṽéŕŕîđéš (é.ĝ. çàšüàļ, ƒŕéǫüéñţ ĥüḿöŕ ƒöŕ social_media) àñđ þéŕšöñà öṽéŕŕîđéš (àñ îñđîṽîđüàļ àüţĥöŕ'š ṽöîçé). Çĥàññéļ àñđ þéŕšöñà öṽéŕŕîđéš ŕéþļàçé ŵĥöļé Ţöñé/Šţýļé šéçţîöñš, àñđ ţĥéîŕ ṽöçàƃüļàŕý çàñ öñļý ţîĝĥţéñ ţĥé þŕöƒîļé'š; ļöçàļé öṽéŕŕîđéš ḿéŕĝé îñđîṽîđüàļ ƒîéļđš.

Ţĥŕéé ŕüļé ƒîéļđš đö ḿöšţ öƒ ţĥé ŵöŕķ ƃéýöñđ ţĥé éẋàḿþļé àƃöṽé:

  • À ṽöçàƃüļàŕý éñţŕý îš à ţéŕḿ ŕüļé: term, replacement, severity, àñđ öþţîöñàļļý forms (ţĥé îñƒļéçţîöñš ţĥé éẋàçţ ḿàţçĥéŕ àļšö ŕéçöĝñîšéš, ŵĥîçĥ kapi voice expand ƒîļļš îñ), case_sensitive, scope (prose, code, heading), do_not_translate, àñđ à concept_id ţýîñĝ ţĥé ŕüļé ţö à çöñçéþţ îñ ţĥé ţéŕḿš šţöŕé. Ţĥé šàḿé šĥàþé îš ŵĥàţ à ƒļöŵ šţéþ àççéþţš üñđéŕ term_rules: ƒöŕ term-check, translate, recycle, dnt-check àñđ pseudo-translate, šö à ŕüļé àüţĥöŕéđ îñ ţĥé þŕöƒîļé àñđ à ŕüļé ĥàñđéđ ţö à ţööļ ŕéàđ îđéñţîçàļļý.
  • À style.prohibited_patterns öŕ required_patterns éñţŕý îš à þàţţéŕñ: regex, description, severity, àñđ öþţîöñàļļý à rate (max ḿàţçĥéš þéŕ per_words, đéƒàüļţ 1,000) ţĥàţ ţüŕñš à ƃàñ îñţö à çéîļîñĝ, àñđ à scope.
  • min_score îš ţĥé çöḿþļîàñçé ƃàŕ à ƃļöçķ ḿüšţ ŕéàçĥ ţö çöüñţ àš çöḿþļîàñţ îñ ŕöļļ-üþš; üñšéţ, öñé çŕîţîçàļ ṽöçàƃüļàŕý ĥîţ àļŕéàđý đŕöþš à ƃļöçķ ƃéļöŵ îţ.

Éṽéŕý ƒîéļđ îš ļîšţéđ îñ ţĥé ṽöîçé þŕöƒîļé ŕéƒéŕéñçé.

Çöḿþļîàñçé šçöŕîñĝ

Çöḿþļîàñçé îš šçöŕéđ 0–100 àçŕöšš ƒîṽé đîḿéñšîöñš: Ţöñé, Šţýļé, Ṽöçàƃüļàŕý, Çļàŕîţý, àñđ öṽéŕàļļ ṽöîçé çöḿþļîàñçé. Éàçĥ ƒîñđîñĝ ŕéđüçéš ţĥé šçöŕé ƃý îţš šéṽéŕîţý ŵéîĝĥţ:

ŠéṽéŕîţýŴéîĝĥţÉẋàḿþļé
Neutral0Îñƒöŕḿàţîöñàļ ñöţé
Minor1Šļîĝĥţ ţöñé îñçöñšîšţéñçý
Major5Ŵŕöñĝ ţéŕḿ üšéđ
Critical25Çöḿþéţîţöŕ ţéŕḿ üšéđ

Šţàŕţéŕ þàçķš

Ƃüîļţ-îñ þàçķš þŕöṽîđé ŕéàđý-ţö-üšé šţàŕţîñĝ þöîñţš (professional-b2b, friendly-dtc, technical-docs, marketing-blog, àñđ customer-support), éàçĥ ŵîţĥ ţöñé šéţţîñĝš, šţýļé ŕüļéš, ṽöçàƃüļàŕý çöñšţŕàîñţš, àñđ ƃéƒöŕé/àƒţéŕ éẋàḿþļéš ţö çüšţöḿîžé.

Þîþéļîñé îñţéĝŕàţîöñ

Ţĥé voice-check ţööļ ŕüñš îñ ţĥé þîþéļîñé àļöñĝšîđé öţĥéŕ ţööļš:

recyclechanterm-checkchantranslateLLMchanvoice-checkLLMchanqaLLM

Îţ üšéš àñ ĻĻḾ ţö àñàļýžé çöñţéñţ àĝàîñšţ ţĥé þŕöƒîļé àñđ àţţàçĥéš çöḿþļîàñçé šçöŕéš àñđ ƒîñđîñĝš ţö éàçĥ Ƃļöçķ àš àññöţàţîöñš. Ţĥé ƒàšţéŕ, ŕüļé-ƃàšéđ voice-vocab-check ţööļ çĥéçķš ƒöŕƃîđđéñ àñđ çöḿþéţîţöŕ ţéŕḿš ŵîţĥöüţ ĻĻḾ çàļļš. Ṽöîçé ṽöçàƃüļàŕý àļšö ƒļöŵš ţĥŕöüĝĥ ţĥé öŕđîñàŕý ţéŕḿîñöļöĝý ţööļš: term-check àñđ dnt-check àŕé ţĥé ŕéĝîšţéŕéđ ţööļš, àñđ term-check ţàķéš ţĥé þŕöƒîļé'š ŕüļéš àš term_rules:, šö à ƒöŕƃîđđéñ öŕ çöḿþéţîţöŕ ţéŕḿ ƒîŕéš ţĥéŕé ţöö, àñđ ṽöîçé ĝüàŕđŕàîļš àñđ ţéŕḿîñöļöĝý šĥàŕé öñé éñƒöŕçéḿéñţ þàţĥ. Ţĥé term-lookup àñđ term-enforce šţàĝéš îñ terms/tool.go àŕé ļîƃŕàŕý çöđé ŕàţĥéŕ ţĥàñ ŕéçîþé ñàḿéš: ţĥé ŕüññéŕ àþþéñđš ţĥéḿ ƃéĥîñđ àñý ţööļ ŵĥöšé šçĥéḿà ŕéǫüîŕéš ţéŕḿš, àñđ ñéîţĥéŕ àþþéàŕš îñ kapi tools.

ḾÇÞ îñţéĝŕàţîöñ

ÀÎ àĝéñţš ŕéàçĥ ṽöîçé çĥéçķîñĝ ţĥŕöüĝĥ ţĥé kapi mcp šéŕṽéŕ:

{
"mcpServers": {
"kapi": {
"command": "kapi",
"args": ["mcp"]
}
}
}

Àĝéñţš çàñ šçöŕé çöñţéñţ ƒöŕ ṽöîçé çöḿþļîàñçé ŵîţĥ ţĥé voice_check ḾÇÞ ţööļ àñđ ŕéŵŕîţé öƒƒ-ṽöîçé çöþý ŵîţĥ voice_rewrite, ŵĥîçĥ ļîšţš üñđéŕ skipped ţĥé ţéŕḿš îţ ḿàţçĥéđ àñđ çöüļđ ñöţ ŕéþļàçé. Ţĥé ĝüîđé îţšéļƒ îš ŕéàđ ŕàţĥéŕ ţĥàñ çàļļéđ: kapi voice guide þŕîñţš îţ, àñđ ţĥé context://<path> ŕéšöüŕçé ŕéţüŕñš îţ ƒöŕ ţĥé þöîñţ à ƒîļé šîţš àţ, ŵîţĥ ţĥé ţéŕḿš ƃöüñđ ţĥéŕé. Šéŕṽéŕ đéþļöýḿéñţš çàñ éẋþöšé àñ ĤŢŢÞ ḾÇÞ éñđþöîñţ šö àĝéñţš çöñšüḿé þŕöƒîļéš àñđ šçöŕîñĝ ŵîţĥöüţ à ļöçàļ ÇĻÎ þŕöçéšš.

Ĝö ļîƃŕàŕý

Šţöŕé

type Store interface {
// Profile CRUD; scope is the storing host's partition key (empty for the local CLI)
CreateProfile(ctx context.Context, profile *VoiceProfile) error
GetProfile(ctx context.Context, id string) (*VoiceProfile, error)
UpdateProfile(ctx context.Context, profile *VoiceProfile) error
DeleteProfile(ctx context.Context, id string) error
ListProfiles(ctx context.Context, scope string) ([]*VoiceProfile, error)

// Version history and named tags
ListProfileVersions(ctx context.Context, profileID string) ([]*ProfileVersion, error)
GetProfileVersion(ctx context.Context, profileID string, version int) (*ProfileVersion, error)
GetProfileAtTag(ctx context.Context, profileID, tagName string) (*VoiceProfile, error)
CreateProfileTag(ctx context.Context, tag *ProfileTag) error
ListProfileTags(ctx context.Context, profileID string) ([]*ProfileTag, error)
DeleteProfileTag(ctx context.Context, profileID, tagName string) error

// Scores
StoreScore(ctx context.Context, score *StoredScore) error
GetScores(ctx context.Context, projectID string, locale model.LocaleID) ([]*StoredScore, error)
GetScoreTrends(ctx context.Context, projectID string, days int) ([]*ScoreTrend, error)
GetScoresByStream(ctx context.Context, projectID, stream string) ([]*StoredScore, error)

// Corrections and the candidate rules derived from them
StoreCorrection(ctx context.Context, correction *Correction) error
GetSuggestedRules(ctx context.Context, scope string, minCount int) ([]*SuggestedRule, error)
RecordRuleDecision(ctx context.Context, d *RuleDecision) error
GetRuleDecision(ctx context.Context, profileID, term string) (*RuleDecision, error)
ListRuleDecisions(ctx context.Context, profileID string) ([]*RuleDecision, error)

Close() error
}

StoredScore, ScoreTrend, àñđ ţĥé öţĥéŕ üñǫüàļîƒîéđ ţýþéš àŕé đéçļàŕéđ îñ ţĥé profile þàçķàĝé; model.LocaleID îš ţĥé ƂÇÞ-47 ļöçàļé ţýþé ƒŕöḿ github.com/neokapi/neokapi/core/model.

Ţĥé ƒŕàḿéŵöŕķ šĥîþš à ŠǪĻîţé ƃàçķéñđ (voice/sqlite.go) ƃüîļţ öñ ţĥé šĥàŕéđ core/storage ḿîĝŕàţîöñ šýšţéḿ, ŵîţĥ ĴŠÖÑ çöļüḿñš ƒöŕ ţĥé çöḿþļéẋ ţöñé/šţýļé/ṽöçàƃüļàŕý ƒîéļđš. Îñšîđé à þŕöĵéçţ ţĥé ṽöîçé ţàƃļéš ļîṽé îñ ţĥé šĥàŕéđ .kapi/work/store.db; à šţàñđàļöñé šţöŕé îš voice.db. Ţĥé îñţéŕƒàçé îš đéšîĝñéđ ƒöŕ éẋţéñšîöñ: šéŕṽéŕ đéþļöýḿéñţš çàñ àđđ à šçöþé-þàŕţîţîöñéđ ÞöšţĝŕéŠǪĻ ƃàçķéñđ.

Šçöŕîñĝ àñđ ŕéšöļüţîöñ

import "github.com/neokapi/neokapi/core/profile"

findings := []profile.VoiceFinding{
{Dimension: profile.DimensionVocabulary, Severity: profile.SeverityMajor,
Message: "Forbidden term: leverage", Suggestion: "use"},
{Dimension: profile.DimensionTone, Severity: profile.SeverityMinor,
Message: "Tone is too formal for this profile"},
}
score := profile.CalculateScore(findings) // score.Overall = 94 (100 - 5 - 1)

// ResolveProfile layers locale, then channel, then persona overrides on a base
// profile; a persona's vocabulary can only tighten the profile's
resolved := profile.ResolveProfile(base, "ja", "", "")

Þîþéļîñé ţööļš

import (
aitool "github.com/neokapi/neokapi/core/ai/tools"
"github.com/neokapi/neokapi/core/profile"
"github.com/neokapi/neokapi/core/tools"
)

// LLM-based: structured findings scored via CalculateScore, attached as a
// VoiceAnnotation plus voice-score / voice-findings properties
checkTool := aitool.NewVoiceCheckTool(llmProvider, profile)

// Rule-based: fast forbidden/competitor-term enforcement, no LLM calls
vocabTool := tools.NewVoiceVocabCheckTool(profile, terminology)

Šţàŕţéŕ þàçķš

import "github.com/neokapi/neokapi/core/profile/packs"

names, _ := packs.List() // the five built-in pack names
profile, _ := packs.Load("professional-b2b")
all, _ := packs.LoadAll()

Þàçķš àŕé ÝÀḾĻ ƒîļéš éḿƃéđđéđ ṽîà go:embed; éàçĥ ŕéţüŕñš à *profile.VoiceProfile ŕéàđý ţö üšé öŕ çüšţöḿîžé.

Çöñţéñţ ḿöđéļ îñţéĝŕàţîöñ

VoiceAnnotation îš à ŕéĝîšţéŕéđ þàýļöàđ (voice) šţöŕéđ àš à ƃļöçķ-šçöþéđ àññöţàţîöñ (Ƒ-02), ţĥé çöüñţéŕþàŕţ ţö þöšîţîöñàļ öṽéŕļàýš ļîķé term àñđ entity. Îţ îš ŕéàçĥéđ ţĥŕöüĝĥ ţĥé ƃļöçķ'š Anno/SetAnno ĥéļþéŕš àñđ ŕéĝîšţéŕéđ ƒöŕ ŵîŕé/šţöŕé ŕéĥýđŕàţîöñ ṽîà model.RegisterPayload:

type VoiceAnnotation struct {
ProfileID string `json:"profile_id"`
Score int `json:"score"` // 0-100 overall
Findings []VoiceFinding `json:"findings"`
Position model.Anchor `json:"position"`
}

func (a *VoiceAnnotation) AnnotationType() string { return "voice" }

Þŕöƒîļéš šéŕîàļîžé àš ƃöţĥ ĴŠÖÑ àñđ ÝÀḾĻ, šö ţĥéý çàñ ƃé àüţĥöŕéđ ƃý ĥàñđ öŕ çöñšţŕüçţéđ þŕöĝŕàḿḿàţîçàļļý àš à *profile.VoiceProfile.