ADR-0047 — KavoSettings groups request-cost ceilings under limits and lifts search to top level
Status: accepted
Context
KavoSettings mostly follows one convention for its optional subsystems: each lives at a top-level key, and false disables the subtree wholesale (cache, delete, realtime, arrayMutation). search, though, was buried at query.search — a scope discoverable only by already knowing the query block exists — while the request-cost ceilings that guard filter depth, IN-array length, and like-pattern length (query.maxFilterDepth/ maxInValues/maxLikePatternLength) sat in that same query block, and the two relation-include ceilings (relations.maxIncludeDepth/ maxIncludedNodes) sat in an unrelated block entirely — a caller tuning "how expensive can one request get" had to know to look in two places for five conceptually identical knobs. After ADR-0046 moved query.defaultSort out to defaults.sort, the query block held only ceilings and search, with no remaining reason to exist as its own scope.
Decision
KavoSettings drops the query block entirely. Its members move as follows:
searchmoves to a top-level key, keeping itsSearchSettings | falseshape andfalse-disables-the-subtree convention (the same onecache/delete/realtime/arrayMutationalready use).maxFilterDepth,maxInValues, andmaxLikePatternLengthjoin a newLimitsSettingsblock, alongsiderelations.maxIncludeDepthandrelations.maxIncludedNodes(renamedincludeDepth/includedNodes), which move out ofRelationSettings. Themaxprefix on each is dropped insidelimits— the block name already says these are ceilings — yieldinglimits.filterDepth,limits.inValues,limits.likePattern,limits.includeDepth,limits.includedNodes.RelationSettingsis{ edges }only after the move — inclusion permission (allowed.includable), inclusion defaults (defaults.include), and inclusion limits (limits.includeDepth/includedNodes) are now three separate blocks, none of themRelationSettings, which is left holding only per-relation loading tuning.pagination.maxLimitis the one ceiling that does not move tolimits. It stays inpaginationbecausemaxthere distinguishes it from the siblingdefaultLimit, and because it is genuinely coupled topagination.strategy(irrelevant understrategy: "none", a keyset ceiling under"cursor"/"since") in a way the other ceilings are not coupled to anything outsidelimits.
This is a pure restructuring: every ceiling keeps its default value and its enforcement point, and a breach still produces the same KAVO_QUERY_LIMIT_ EXCEEDED/400. Nothing here is behind a compatibility alias — old paths are simply gone (feat! with a BREAKING CHANGE: footer enumerating every path move).
Consequences
- A reviewer checking "is this a request-cost ceiling" now has one place to look (
limits) instead of two (query,relations), and "is search on" now has one place to look (search) instead of needing to know it hides underquery. validateSettings's error paths (limits.filterDepth,search.mode, etc.) and every doc comment, Swagger comment, and prose reference that named the old paths move with them — a reader following an error'spathfield into the schema finds the key at the name the error names.- A future settings key that caps some request cost has an obvious home (
limits) rather than a choice between inventing a new top-level key or overloading an unrelated block.