Authoring Strategies
How to write your own AdaptationStrategy - the pattern, exhaustiveness, testing across all five levels, and design guidance.
A custom strategy is a pure object with a name, a describe(level), and a resolve(level) that returns your config. That's the whole contract - no registration, no base class. This page shows the pattern the built-ins use and what makes a strategy good.
The contract
import type { AdaptationStrategy, EnergyLevel } from '@kumbatio/energy-system'
interface AdaptationStrategy<TConfig> {
name: string
describe(level: EnergyLevel): string
resolve(level: EnergyLevel): TConfig
}Rules that keep strategies composable:
- Pure.
resolvecomputes a config from a level. No side effects, no reads of ambient state. - Total. Every one of the five levels must resolve - a level transition must never throw.
- Frozen. Return immutable configs so consumers can't mutate shared state.
The pattern: a config table per level
The built-ins all use the same shape - a Record<EnergyLevel, Config> checked exhaustively by the compiler:
import type { AdaptationStrategy, EnergyLevel } from '@kumbatio/energy-system'
import { getEnergyLevel } from '@kumbatio/energy-system'
export interface FormConfig {
readonly fieldsPerStep: number
readonly showOptionalFields: boolean
readonly inlineValidation: boolean
}
const FORM_CONFIGS = {
100: { fieldsPerStep: 12, showOptionalFields: true, inlineValidation: true },
75: { fieldsPerStep: 8, showOptionalFields: true, inlineValidation: true },
50: { fieldsPerStep: 5, showOptionalFields: false, inlineValidation: true },
25: { fieldsPerStep: 3, showOptionalFields: false, inlineValidation: false },
0: { fieldsPerStep: 1, showOptionalFields: false, inlineValidation: false },
} as const satisfies Readonly<Record<EnergyLevel, FormConfig>>
export const formStrategy: AdaptationStrategy<FormConfig> = {
name: 'form-density',
describe(level) {
const def = getEnergyLevel(level)
const config = FORM_CONFIGS[level]
return `${def.label}: ${config.fieldsPerStep} fields per step`
},
resolve(level) {
return FORM_CONFIGS[level]
},
}The satisfies Readonly<Record<EnergyLevel, FormConfig>> is the load-bearing part: if a sixth level ever appeared, or you forgot one, the compiler refuses.
Deriving from cognitive profiles
Instead of hand-tuning five rows, you can derive behavior from the level's CognitiveProfile - the description of what the brain can handle:
import { getEnergyLevel } from '@kumbatio/energy-system'
import type { AdaptationStrategy } from '@kumbatio/energy-system'
export const onboardingStrategy: AdaptationStrategy<{ stepsShown: number }> = {
name: 'onboarding-pace',
describe(level) {
return `${getEnergyLevel(level).label}: ${this.resolve(level).stepsShown} onboarding steps`
},
resolve(level) {
const { decisionCapacity } = getEnergyLevel(level).cognitiveProfile
switch (decisionCapacity) {
case 'high': return { stepsShown: 5 }
case 'moderate': return { stepsShown: 3 }
case 'low': return { stepsShown: 2 }
case 'minimal': return { stepsShown: 1 }
case 'none': return { stepsShown: 0 }
}
},
}Exhaustive switches over profile values get the same compiler protection as the table pattern.
Write a useful describe()
describe(level) is what settings screens and onboarding show users to explain why the app just changed. Make it a sentence a person at energy 25 can parse:
formStrategy.describe(25) // "Low: 3 fields per step"Using your strategy
No registration needed - it resolves anywhere the built-ins do:
// Core
const form = engine.resolve(formStrategy)// React
const form = useStrategy(formStrategy)For pure show/hide/mute behavior, don't write a strategy at all - a presence declaration is the smaller tool, and createPresenceStrategy lifts it into a strategy when you need one.
Test every level
Every level transition is an edge case. Test all five - the project's own contribution bar for core behavior:
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { ENERGY_LEVEL_VALUES } from '@kumbatio/energy-system'
import { formStrategy } from './form-strategy.js'
test('formStrategy resolves every level', () => {
for (const level of ENERGY_LEVEL_VALUES) {
const config = formStrategy.resolve(level)
assert.ok(config.fieldsPerStep >= 1)
assert.equal(typeof formStrategy.describe(level), 'string')
}
})Design guidance
Run your strategy through the same decision filter the products use:
- Does it reduce cognitive load at each level, or add to it?
- Does it improve agency or apply pressure? (Guide, don't lock -
taskComplexityStrategycaps what's surfaced, not what's allowed.) - Does it help at low energy, not just at peak?
- Does it work without judging? (
describe()states facts, not verdicts.)
Reusable strategies for common UI situations - forms, dashboards, notifications, onboarding - are the contribution the project wants most. If you build one that survives real use, open a PR.
