Add config key
How to add a new key to KavoSettings — schema, default, merge semantics, validation, and docs — through the one layered precedence chain (global → entity → operation → per-call). Use when a change needs a new configurable behavior rather than a hardcoded constant.From its SKILL.md
npx -y skills add kavo-labs/kavo --skill add-config-keyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.3 KB, as published. Nobody here has run it
Adding a config key
Kavo has one configuration mechanism: KavoSettings, merged through a
single precedence chain (docs/internals/architecture/08-configuration.md):
built-in defaults → global (createKavo) → entity (createCrud)
→ operation (operations.<id>) → per-call (KavoCallOptions)
The schema-extensibility rule (doc 08 §1) is normative: feature work adds
keys to this schema — it never adds a second config mechanism. If you find
yourself threading a new constructor parameter or a separate options object
through the engine, stop; it belongs in KavoSettings.
Where the pieces live
BUILT_IN_DEFAULTS(packages/core/src/config/defaults.ts) — add the key with its default value. Every key needs one; there is no "unset" state a resolved config can observe.- The settings type — extend the
KavoSettingsinterface/schema so the new key is typed at every scope (global defaults, entity config,operations.<id>overrides,KavoCallOptions.settings). mergeSettings(packages/core/src/config/merge-settings.ts) — confirm the new key's shape matches the existing merge semantics rather than inventing a new one:- scalars and object-as-value keys: nearer scope replaces farther scope, key by key;
falsedisables an inheritable feature where the schema allows it (follow thesoftDelete/operations.<id>pattern) — a nearer object re-enables it;- arrays replace wholesale, never merge element-wise;
undefinedat any scope is skipped, not treated as an explicit override.
validateSettings— add bootstrap validation that fails with aConfigurationExceptionnaming the entity, the key path, and the offending value (doc 08 §4's message shape is the bar:Invalid configuration for entity 'User' at 'pagination.maxLimit': ...). Validation runs once at bootstrap (resolveEntityConfig), not per request — the resolved config is deep-frozen afterward, so this is the only chance to catch a bad value before it's baked in.describeResolvedConfig— check whether the new key should surface inkavo.describe(entityName)'s debug dump; most settings keys should.
Naming
Config keys are camelCase, and booleans are phrased positively —
exposeInternals, never hideInternals (CLAUDE.md Conventions). A boolean
key that reads as a double negative when combined with false-to-disable
semantics is a naming bug, not just a style nit.
Tests
Per the write-tests skill, at minimum:
- the new key's default value takes effect with nothing overriding it;
- each scope in the precedence chain actually overrides the one before it (a test that only checks global-vs-default misses entity/operation/ per-call regressions);
false-disables-then-nearer-re-enables, if the key supports that;- bootstrap validation rejects an invalid value with
ConfigurationExceptionand the right code, naming the entity and key path; - if the key is per-call (
KavoCallOptions.settings), that it's merged as a parameter for that request only and never mutates the frozen resolved config for subsequent calls.
Finish with pnpm check.
Docs
Update docs/internals/architecture/08-configuration.md's key table (§1) —
it's the single source of truth for what's configurable, and a key missing
from it is invisible to the next reader. If the key represents a new
load-bearing precedent (not just a parameter on an existing mechanism),
see the add-adr skill.
Allowlist-style keys are a different mechanism — don't force them in here
EntityConfig.allowlists (filterable/sortable/selectable/includable/
searchable/creatable/updatable) is declared on EntityConfig directly,
not on KavoSettings — there is no global default, no per-operation
override, and none of the merge machinery above applies. A key belongs here
instead of in KavoSettings when its job is to name a subset of the
entity's own fields or relations that a request may touch, rather than to
tune a behavior. Issue #259 (creatable/updatable, narrowing the writable
projection for createOne/updateOne/patchOne) is a worked example of
adding one:
- The raw selector type (
packages/core/src/config/entity-config.ts) — an array-or-{ exclude }shape typed against the right path depth.QueryFieldSelector<Entity>(FieldPath<Entity>, depth 3) for a key that can name a dotted relation path; a depth-1FieldPath<Entity, 1>selector (seeWritableFieldSelector) for a key that only ever grants one field or relation segment, the way a write body does. Add the key toQueryAllowlists, documented with its default posture (does it default to "every own column", likefilterable, or opt-in likeincludable?) and its narrowing/precedence relationship to any nearby DTO-based override. - The resolved type (
resolved-entity-config.ts) — add the frozen, always-array field toResolvedQueryAllowlists. resolveAllowlists(resolve-entity-config.ts) — compute the key's base set (what it means unconfigured) fromEntityMetadata, and resolve the configured selector against that base withresolveFieldSelector(generic over the path type, so it serves both depth-3 and depth-1 selectors already). If the key can never legally name a computed field (ascreatable/updatablecan't — a computed field has no column to write, ADR-0019), reject one at bootstrap with aConfigurationException, the same wayfilterable/sortable/searchablealready reject one.- Where the resolved list actually gates something — an allowlist key is
inert until some consumer reads it.
creatable/updatableare read inDefaultDeserializer.deserialize(per call, offcontext.config.allowlists, keyed bycontext.operation) — find or add the analogous read site for a new key, and decide its DTO precedence: does a registered DTO with a runtime shape still win outright (theselectable-vs-dto.itemprecedent, ADR-0026), or does the new key gate something no DTO already governs? - The core barrel (
index.ts) — a new selector type is a new public type; add it to the explicit list (ADR-0010), and totests/barrel.spec.ts's manifest. - Docs —
docs/features/allowlists.md(the adopter-facing guide) anddocs/reference/config-keys.md's## allowliststable, not08-configuration.md— that document isKavoSettingsonly.
Tests follow the same shape as write-tests describes for a KavoSettings
key, minus the per-call/operation-override cases that don't apply here: the
default derivation, an explicit array used verbatim, the { exclude } form
resolved against the base (not "every column"), the computed-field rejection
if applicable, and — for a key with a consumer — that the consumer actually
narrows behavior and that any unconditional exclusion it must respect (an id,
a soft-delete marker) survives even when the list names it explicitly.