Kumbatio
Guides

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. resolve computes 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 - taskComplexityStrategy caps 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.