# Contributing (/docs/energy-system/contributing) Contributions of every size are welcome - an issue describing where the model fails you is as valuable as a PR. This page summarizes [CONTRIBUTING.md](https://github.com/kumbatio/energy-system/blob/main/CONTRIBUTING.md); the repository version is canonical. ## Ground rules [#ground-rules] This project is built by and for people with variable cognitive capacity. That shapes how it works: * **No pressure, no pace-shaming.** Reviews happen when capacity allows - yours and the maintainers'. A PR sitting for a week is normal, not neglect. * **Small contributions are first-class.** A typo fix, one adaptation strategy, one clarified sentence in the docs - all genuinely useful. * **Say the hard thing kindly.** Direct technical criticism is welcome; judgment of people never is. * **Disappearing is allowed.** If you start something and life happens, no explanation owed. Someone else can pick it up, or it waits. ## What's needed most [#whats-needed-most] 1. **Adaptation strategies** - reusable `(level) → config` patterns for common UI situations: forms, dashboards, notifications, onboarding. Start with the [authoring guide](/docs/energy-system/guides/authoring-strategies). 2. **Real-world reports** - you tried the SDK in an actual app: what worked, what fought you. 3. **Docs written for energy `25`** - if a doc page needed peak capacity to understand, that's a bug; file it. 4. **Model criticism** - places where the [5-level model](/docs/energy-system/concepts/levels) oversimplifies, with the scenario that breaks it. ## Process [#process] 1. **Bugs, ideas, criticism:** open an issue. Templates keep it short - the minimum viable issue is two sentences. 2. **Code:** fork → branch → PR. For anything bigger than a fix, open an issue first so nobody burns energy on a direction that won't merge. 3. **Every level transition is an edge case.** PRs touching core behavior need tests across all five levels. ## Development setup [#development-setup] ```bash pnpm install pnpm test pnpm build ``` Before opening a PR, `pnpm run validate` runs the full local pipeline: format check, lint, typecheck, tests, and a pack dry-run. ## The decision filter [#the-decision-filter] Every feature must pass the same filter the Kumbatio products do: * Does it reduce cognitive load or add to it? * Does it improve agency or apply pressure? * Does it help at low energy, not just at peak? * Does it work without judging? If a proposal fails most of these, it won't merge regardless of technical quality - worth knowing before you build it. ## Conduct [#conduct] Be the kind of contributor this project exists for others to have. Harassment, shame-framing, and gatekeeping get one warning, then removal. Contact: [hello@kumbat.io](mailto:hello@kumbat.io). # Getting Started (/docs/energy-system/getting-started) Install the package, create an engine, resolve a strategy. That's the whole loop - everything else in the SDK builds on it. ## Install [#install] ```bash pnpm add @kumbatio/energy-system ``` ```bash npm install @kumbatio/energy-system ``` ```bash yarn add @kumbatio/energy-system ``` ```bash bun add @kumbatio/energy-system ``` The core has zero runtime dependencies. React (`>=19.2`) is an optional peer dependency, only needed if you import `@kumbatio/energy-system/react`. The floor is 19.2 rather than 19 because the React entry point uses ``, which landed in 19.2. ## First engine [#first-engine] ```ts import { createEnergyEngine, uiVisibilityStrategy, notificationStrategy, } from '@kumbatio/energy-system' import { localStoragePersistence } from '@kumbatio/energy-system/persistence' const engine = createEnergyEngine({ initialLevel: 75, persistence: localStoragePersistence(), }) // The user says "I'm at steady capacity now" engine.setLevel(50) // Resolve behavior from that state const ui = engine.resolve(uiVisibilityStrategy) const notifications = engine.resolve(notificationStrategy) ui.sidebar // true at 50 notifications.priorityThreshold // 'high' at 50 - only high+ gets through ``` `setLevel()` updates subscribers synchronously; persistence runs in the background with retry. Call `await engine.flush()` when a workflow must wait for durable storage. ## What you get [#what-you-get] The package ships five entry points: | Entry point | What's in it | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `.` (core) | Engine, levels, strategies, presence, focus sessions, notification gate, deferral, metrics, compatibility helpers. Runs in any JS/TS runtime. | | `./react` | `EnergyProvider`, hooks, ``, ``. | | `./dom` | `applyEnergyLevel`, `readEnergyLevel`, `observeEnergyLevel` - data attributes + CSS variables for any framework (or none). | | `./persistence` | `localStoragePersistence`, `memoryPersistence`. | | `./css` | Reference stylesheet implementing the UI visibility strategy and presence attributes. | ## Pick your path [#pick-your-path] Framework-agnostic engine: levels, subscriptions, strategies. Provider, hooks, gating components. Data attributes and CSS variables, no framework required. The reference stylesheet and CSS-only presence gating. # Introduction (/docs/energy-system) `@kumbatio/energy-system` is a framework-agnostic TypeScript SDK for building **energy-aware applications** - software that adapts to the user's current cognitive capacity instead of assuming a constant one. The user declares their energy level; your app resolves behavior from it. No biometrics, no inference from typing speed. Self-reported, explicit state that any layer of your app can read. ## The model: energy ≠ time [#the-model-energy--time] Most software assumes equal capacity across a day. Real cognitive energy is variable and non-linear. Three metrics tell the story: * **Work Hours (wh)** - total time present * **Productive Hours (ph)** - the focused subset of that time * **Stuff Done (sd)** - measurable output The constraint is always `ph ≤ wh`. Extending time does not linearly increase productive output - forcing 160wh doesn't produce 160ph, it produces 80–100ph stretched across more calendar, with worse quality from fatigue and context switching. The goal isn't working more or less. It's working when you're actually capable of producing results - and having software that cooperates. Read the full argument in [Energy over Time](/docs/kumbatio/thesis). ## The five levels [#the-five-levels] Energy is modeled as five discrete levels, not a slider. Fewer choices mean less decision fatigue (critical exactly when energy is low), and clear boundaries make adaptation rules predictable. | Level | Key | What it means | | ----: | -------- | ---------------------------------------------------------- | | `100` | `peak` | High capacity. Planning, complex decisions, creative work. | | `75` | `active` | Good capacity. Focused execution, problem-solving. | | `50` | `steady` | Moderate capacity. Routine tasks, familiar work. | | `25` | `low` | Limited capacity. Simple tasks, review, light work. | | `0` | `rest` | Recovery. Consumption only - reading, reflecting. | Each level carries a [cognitive profile](/docs/energy-system/concepts/levels) - what kind of decisions, focus, and interruptions the brain can handle there - so strategies adapt to capability, not just to a number. ## What the SDK gives you [#what-the-sdk-gives-you] * **A core engine** - immutable, revisioned energy state with subscriptions, persistence, and deterministic cross-context reconciliation * **Strategies** - pure `(level) => config` mappings; seven built-ins cover UI visibility, notifications, task complexity, interaction forgiveness, deferral ordering, agent autonomy, and inbound-demand admission * **Presence annotation** - declare which levels a component belongs to (`defineEnergyPresence`, ``, or plain `data-energy-min` attributes) * **A behavioral runtime** - a notification gate that *enforces* the notification config (defer, never drop) and focus sessions that always end on time * **Adapters** - React provider and hooks, a DOM adapter, a reference stylesheet, and pluggable persistence ## When to use it [#when-to-use-it] Use the Energy System when your app has anything worth adapting: chrome that could get out of the way, notifications that could wait, tasks that could be re-ordered, or destructive actions that deserve a bigger undo window at low capacity. It works anywhere TypeScript runs - web, desktop, CLI. The core has zero dependencies; React is an optional peer. This package is in production in three apps: [kumbat.io](https://kumbat.io) runs its entire interface on it - move the energy control on the site and watch the UI adapt - [Anasa](/docs/kumbatio/ecosystem/anasa) (public alpha) builds its adaptive writing workspace on it, and [Meltemi](https://meltemi.app), an email client from entro314 labs built outside the Kumbatio product line, runs its notification deferral and focus sessions on it. Two further entro314 labs apps are integrated pre-release: Entromail (webmail) and Equidock (desktop canvas). See [Production Patterns](/docs/energy-system/guides/production-patterns) for what real integrations look like. ## Where to go next [#where-to-go-next] Install the package and run your first engine in two minutes. Create an engine, set levels, subscribe, resolve strategies. Provider, hooks, and the EnergyGate component. The adaptation model and the built-in strategies. *** *energy-system supports self-management and workflow adaptation. It is not a medical device, diagnosis tool, or treatment.* # Roadmap (/docs/energy-system/roadmap) `energy-system` is developed milestone by milestone, with no dates attached. The project practices what it argues: output is a function of capacity, not calendar. Milestones are ordered; the top unfinished one is what's being worked on now. The canonical, always-current version lives in [ROADMAP.md](https://github.com/kumbatio/energy-system/blob/main/ROADMAP.md) in the repository. ## Shipped [#shipped] **v0.0.x - Foundations** The 5-level model with immutable, revisioned state; the framework-agnostic engine with strategy resolution; the first three built-in strategies (UI visibility, notifications, task complexity); the DOM adapter; React provider, hooks, and headless components; localStorage/memory persistence with external observation; deterministic clocks for testing; derived metrics; legacy-level compatibility mapping; published to npm. **M1 - Identity and polish (v0.1)** - complete Renamed and published as `@kumbatio/energy-system` (old name deprecated), an API review pass for naming consistency and exhaustive level handling, and CI running typecheck, lint, and tests on every PR. **v0.4 - Presence annotation and behavioral runtime** - complete Patterns studied in a field ADHD app (an email client that shipped focus mode, universal snooze, and notification batching) and reimplemented as first-class, tested primitives - including guarantees against the two failure modes observed in the wild: suppressed reminders destroyed instead of deferred, and focus suppression that never auto-expired. This shipped [presence annotation](/docs/energy-system/concepts/presence), the [notification gate](/docs/energy-system/concepts/notification-gate) with its defer-not-drop guarantee, [focus sessions](/docs/energy-system/concepts/focus-sessions) with auto-expiry, [deferral presets](/docs/energy-system/concepts/deferral) with energy-aware ordering, the interaction forgiveness strategy, and `prefers-reduced-motion` handling in the stylesheet. **M3 - Reference integration** - complete [kumbat.io](https://kumbat.io) migrated from its inline energy provider to this package and remains the living integration test. Since then the SDK has shipped in two real apps: [Anasa](/docs/kumbatio/ecosystem/anasa) (Kumbatio's writing workspace, public alpha - custom persistence, energy-gated AI surfaces, notification filtering) and [Meltemi](https://meltemi.app) (an email client in private beta from entro314 labs, built outside the Kumbatio product line - notification gate, focus sessions, deferral, and interaction forgiveness, integrated without the React adapter). The case study of which adaptation strategies survived contact with real use is written: [Production Patterns](/docs/energy-system/guides/production-patterns). **v0.6 - Autonomy and inbound demand** - complete The recipient's capacity applied to the queue that ignores it. Every triage system in general use is organised around properties of the message; this asks the same question about the receiver. Two additions: the [autonomy strategy](/docs/api/core/strategies#autonomystrategy), which narrows what automation may do unattended as energy falls - the mirror of interaction forgiveness, since forgiveness protects against the user's mistakes and autonomy against the agent's - and [inbound demand admission](/docs/api/core/demand), a pure policy deciding whether an arrival that asks something of you reaches you now, is acknowledged and queued, or is queued in silence. Policy only, and deliberately so: the effects an acknowledgment implies leave the process and cannot be made transactional by an in-process runtime, so the orchestration stays with the consuming app until a second consumer proves what the shared machinery actually is. That is the same order the v0.4 primitives arrived in - shipped in a real app first, generalised second. **v1.0 - Specification, conformance, and the API freeze** - complete The model is now specified independently of this implementation, so an implementation in another language is an implementation of the same model rather than a port of this one. [SPEC.md](/docs/energy-system/guides/spec-and-conformance) is the normative, language-independent definition; `spec/energy-state.schema.json` is the interchange format for sharing one person's state across processes; `conformance.json` is 252 vectors plus every strategy table, generated on each build and shipped in the package, so the vectors cannot drift from the behavior they describe. The freeze covers more than type signatures: a shipped strategy table's **values** are API, and so is the reconciliation rule - now exported as `isPreferredEnergyState` rather than buried in the engine, because it is the hardest part of the model to reimplement correctly. The accessibility review landed `prefers-contrast: more` and `forced-colors: active` handling, an honest statement of where the resting opacities stand against WCAG 1.4.11, and the requirements written into the spec so they bind ports too. Coverage now runs all 20 level transitions in both directions plus the model's directional invariants - protection never decreases and automation never gains discretion as capacity falls. **v2.0 - Corrections to the freeze** - complete Four correctness bugs found in 1.0 after it was frozen, and shipped as a major because for this package behavior is API even when no type signature moves. The notification gate classified an intent once, when it was published, and then delivered an open batch window under whatever policy happened to be in force later - so something admitted at Steady could arrive mid-focus-session, and something batched at Steady could surface at Rest with every channel disabled. It now re-judges what it is holding whenever energy or suppression changes, in both directions, and `flush()` overrides the wait rather than the policy. External state is validated against the published schema exactly, so a state carrying unknown properties is rejected rather than silently trimmed - two implementations can no longer exchange a state and disagree about what they exchanged. A configured `originId` no longer corrupts the unproduced sentinel. And `api-surface.json` was missing `EnergyEngine.resolve()`, because the declaration parser did not recognise generic members: a method absent from the freeze is a method nobody notices removing. The most uncomfortable of the four was in the guard itself. `pnpm test` ran the full build first, so the drift check compared `conformance.json` against a copy it had just written - it could not fail, whatever was committed. Both generators now take `--check`, generation belongs to `build`, and the suite verifies rather than regenerates. The React peer range also moved to `>=19.2.0`: the React entry point imports ``, so the previous `>=19` advertised a compatibility that throws on first render. ## In progress [#in-progress] **M2 - Documentation for real adoption** * Docs readable at energy `25`: short pages, one concept each, optional depth (this site is that effort) * An example gallery: navbar, dashboard, form, and notification patterns at each level * An [adaptation strategy authoring guide](/docs/energy-system/guides/authoring-strategies) ## Upcoming [#upcoming] **M5 - Beyond the current adapters** Web-component and vanilla examples, plus an additional framework adapter chosen by adopter demand - open an issue to vote. A first non-JavaScript implementation belongs here too, when a real consumer needs one. The [spec and vectors](/docs/energy-system/guides/spec-and-conformance) are what make that a small job rather than a fork, and the bet is that the first genuine demand is server-side rather than another UI framework - that is where the energy models which did *not* adopt this one already live. ## Continuous work [#continuous-work] * Issues and PRs from adopters take priority over roadmap order when they unblock a real shipped use. * Research translation: mapping the model against cognitive load and occupational health literature, and correcting the model where it oversimplifies. ## How to influence this [#how-to-influence-this] Open an issue. Adopters shipping real features get the loudest voice; sponsors get roadmap *input*, never veto - the [decision filter](/docs/kumbatio/decision-filter) outranks money. # The decision filter (/docs/kumbatio/decision-filter) Before publishing content, adding a feature, or shipping a product under the Kumbatio name, we ask six questions. If the answer to most of them is no, it's not a Kumbatio product or piece of content - no matter how interesting it is to build. ## The questions [#the-questions] 1. **Does this reduce cognitive load or add to it?** Every feature carries a processing cost. If the value doesn't clearly outweigh the added load, it doesn't ship. 2. **Does this improve the user's agency or apply pressure?** Tools should expand what people can choose to do, not push them toward what the tool wants. Nudges that guilt, deadlines that shame, and defaults that trap all fail this test. 3. **Does this help someone at low energy, not just at peak?** It's easy to build features that shine when the user is at `100`. The harder and more important question is what the feature does for someone at `25`. 4. **Does this assume one default brain, or design for variability?** If a feature only works for users with stable attention and predictable schedules, it recreates the problem Kumbatio exists to solve. 5. **Is this honest about what it does and doesn't do?** No vague promises, no clinical overreach, no capability we can't actually deliver. If we can't describe it plainly, we don't ship it. 6. **Would this feel supportive to someone having a genuinely difficult day?** The final gut check. Not "is this useful in the abstract" - would it feel like an embrace or like another demand? ## Why a filter instead of a roadmap [#why-a-filter-instead-of-a-roadmap] The Kumbatio products are deliberately different from each other - different metaphors, different UI patterns, different user moments. That flexibility is a feature, not a weakness. What holds the umbrella together isn't uniformity. It's shared intent: * Human-first over productivity theater * Clarity over complexity * Adaptation over rigid workflows * Supportive language over shame-based language * Practical usefulness over trend features The filter is how that intent gets applied to concrete decisions. A product can look nothing like its siblings and still be unmistakably Kumbatio - because it passed the same questions. ## What the filter has ruled out [#what-the-filter-has-ruled-out] The filter isn't decorative. It's why Kumbatio products have no streaks that punish breaks, no engagement-maximizing notifications, no productivity scores, and no gamification designed around loss aversion. Each of those is standard in the category. Each fails at least three of the six questions. # What is Kumbatio (/docs/kumbatio) Kumbatio builds open-source software that responds to the capacity a person says they have. Most software assumes stable attention, steady energy, and predictable working hours. Many people cannot meet that assumption every day, including people with ADHD, autism, depression, chronic fatigue, and anyone whose cognitive capacity fluctuates. ## The name [#the-name] **kumbatio** /kum·BAH·tee·oh/ - Swahili, noun: *embrace*. The name describes the intended relationship between the person and the software. The person reports what they can handle; the software responds without turning that answer into a score. ## The thesis in one line [#the-thesis-in-one-line] **Energy > Time.** Hours present do not equal productive output. Cognitive capacity changes across a day, week, and life. Kumbatio gives software a way to reduce complexity and interruptions when the person reports lower capacity. Read the full argument in [Energy over Time](/docs/kumbatio/thesis). ## Who this is for [#who-this-is-for] * **Neurodivergent people** - ADHD, depression, autism, anxiety, dyslexia, and overlapping presentations. * **Knowledge workers** who experience inconsistency, burnout, or difficulty sustaining output under standard conditions. * **Teams and managers** who want to work with people more effectively instead of pushing on hours. * **Developers** integrating energy-awareness into their own products via [`energy-system`](/docs/kumbatio/ecosystem/energy-system). What they have in common is a need for software that does not assume the same capacity every day. ## Scope and boundaries [#scope-and-boundaries] Kumbatio is a software ecosystem built from lived experience with depression and ADHD. That experience explains the problems the model prioritizes. The implementation and its limits remain open to inspection and criticism. Kumbatio provides workflow and self-management tools. It is not medical care, diagnosis, treatment, or a replacement for professional mental health support. ## Where to go next [#where-to-go-next] The core thesis: why time-based assumptions fail and what the wh/ph/sd model measures instead. Not a marketing label - five concrete design commitments. The questions every feature and product must answer before it ships. energy-system, Anasa, MPath, and Nami - what's live and what's coming. *** *Kumbatio products support self-management, workflow, and cognitive energy awareness. They are not medical diagnosis tools or treatment, and are not a replacement for professional mental health support.* # What neuroinclusive means (/docs/kumbatio/neuroinclusive) Neuroinclusive is not a marketing label. In the context of Kumbatio it means five specific, checkable things.
## Designed for variable states [#1-designed-for-variable-states] Products work at energy level `0` (rest) and `100` (peak), not just the middle. Most software is implicitly designed for a user at 75–100. Kumbatio products are designed across the whole range - including the states where most software becomes unusable.
## Cognitive load is taken seriously [#2-cognitive-load-is-taken-seriously] Features are added only if they reduce load or provide clear agency. Features that add complexity without proportional value don't belong - no matter how impressive they'd look on a landing page.
## No shame-based mechanics [#3-no-shame-based-mechanics] No streaks that punish breaks. No notifications designed to guilt. No productivity scores that imply failure. Variable energy is not a moral failure, and inconsistency is not laziness. That assumption is never questioned, hedged, or softened - it is the premise.
## Flexible structure, not rigid workflow [#4-flexible-structure-not-rigid-workflow] People with executive dysfunction need structure they can modify, not structures that collapse when they miss a step. A system that punishes a skipped day is worse than no system at all.
## The worst-day case is a design requirement [#5-the-worst-day-case-is-a-design-requirement] If it doesn't work when the user has `25` energy, it doesn't work well enough. The low-energy state isn't an edge case to handle gracefully - it's a primary design target.
## How language fits in [#how-language-fits-in] Neuroinclusive design extends to how we write. Mental health language requires care: * Describe conditions accurately - ADHD, depression, anxiety, autism. No invented euphemisms. * Never conflate neurodivergence with being broken or deficient. * Never use recovery or treatment language for what is workflow software. * Write for low cognitive load: short paragraphs, clear headers, key points front-loaded. Copy should be readable at `25` energy, not only at `100`. Kumbatio deliberately avoids clinical overreach. You will not see phrases like "treat your ADHD" or "manage your symptoms" anywhere in these products. That's workflow software pretending to be medicine, and it's dishonest. ## Where this comes from [#where-this-comes-from] Kumbatio was built from personal experience with depression and ADHD. That context explains why the model exists and which problems it prioritizes. Lived experience does not replace research or evaluation, so the model remains open to correction where it oversimplifies. These commitments are enforced through [the decision filter](/docs/kumbatio/decision-filter) - the questions every feature answers before it ships. *** *Kumbatio products support self-management, workflow, and cognitive energy awareness. They are not medical diagnosis tools or treatment, and are not a replacement for professional mental health support.* # Energy over Time (/docs/kumbatio/thesis) Work is usually planned in hours, but hours do not tell us how much cognitive capacity is available inside them. More hours do not guarantee more output. When time and capacity pull in different directions, capacity determines what work is possible. Seventy-five focused hours can produce more than a hundred and sixty depleted ones. ## Why time-based assumptions fail [#why-time-based-assumptions-fail] Most systems - calendars, project plans, workplace norms - are designed around one default brain: consistent attention, stable energy, predictable working hours. That assumption does not fit many people. Cognitive capacity changes across a day, a week, and a life. For people with ADHD, depression, autism, anxiety, or chronic fatigue, that variability can be larger or less predictable, but it is not unique to any one diagnosis. Software that ignores this leaves people to compensate manually: muting interruptions, simplifying the task, hiding overload, or pretending their capacity is steady. That extra work consumes capacity too. ## The model: wh, ph, sd [#the-model-wh-ph-sd] The argument is specific, not vague. Three variables: * **Work hours (`wh`)** - total hours present or clocked. * **Productive hours (`ph`)** - the subset of those hours where focused output actually happens. * **Stuff done (`sd`)** - measurable output: tasks shipped, docs written, decisions made. The constraint is always: ``` ph ≤ wh ``` You cannot have more productive hours than work hours, but you can have fewer. Extending `wh` does not linearly increase `ph`, and `ph` is what produces `sd`. Adding hours to a depleted person adds time; it may not add useful output. The practical conclusion is to plan work around the capacity available, not hours alone. ## Energy as a first-class variable [#energy-as-a-first-class-variable] If capacity is what matters, software should treat it as real state - not something the user silently absorbs. Kumbatio models cognitive energy on a five-level scale: * **`100` peak** - full capacity. Complex work, dense interfaces, everything available. * **`75` active** - engaged and capable. Normal operation. * **`50` steady** - functional but conserving. Reduce noise, surface what matters. * **`25` low** - limited capacity. Essentials only, maximum forgiveness. * **`0` rest** - not working. The system should get out of the way entirely. Energy here means cognitive capacity - not physical energy, not enthusiasm. It's self-reported: you tell the system what you can handle, and the system adapts complexity, notification load, and interaction patterns to match. This is what [`energy-system`](/docs/kumbatio/ecosystem/energy-system) implements as an SDK, and what every Kumbatio product builds on. ## Beyond work [#beyond-work] The same principle applies past the workday. Learning, socializing, and self-care all run on the same variable capacity. A system that respects energy at work but assumes infinite capacity everywhere else has missed the point. Energy > Time is the design premise shared by every Kumbatio product. It is a position to test in practice, not a measured law of human performance. *** *Kumbatio products support self-management, workflow, and cognitive energy awareness. They are not medical diagnosis tools or treatment, and are not a replacement for professional mental health support.* # CSS (/docs/api/css) ```ts import '@kumbatio/energy-system/css' ``` `energy.css` is the reference CSS for the [UI visibility strategy](/docs/api/core/strategies#uivisibilitystrategy). It is **one** way to consume energy state - apps can also use the JS strategy API directly and apply styles however they want. The per-level custom property values in the stylesheet mirror `uiVisibilityStrategy`; [`applyEnergyLevel`](/docs/api/dom#applyenergylevel) sets the same properties as inline styles, which take precedence but resolve to identical values, so the JS and CSS-only paths always agree. To activate it, set `data-energy-level` on your root element (body or a container) - manually, via `applyEnergyLevel`, or via `EnergyProvider` with `applyToDOM`. ## Classes [#classes] Apply these classes to your components: | Class | Meaning | | -------------------- | -------------------------------------------------------------------- | | `.energy-chrome` | Any chrome element (titlebar, toolbar, status bar) - fades per level | | `.energy-sidebar` | Sidebar container - hidden at low levels | | `.energy-tab-bar` | Tab bar container - hidden at low levels | | `.energy-status-bar` | Status bar - hidden at low levels | | `.energy-toolbar` | Editor/app toolbar - hidden only at rest | | `.energy-content` | Main content area - constrained and scaled at low levels | ## Custom properties [#custom-properties] Declared on `[data-energy-level]` with these base values, then overridden per level: | Property | Base (`100`) | `75` | `50` | `25` | `0` | | ------------------------------- | ------------ | ------ | ------ | ------ | ------ | | `--energy-chrome-opacity` | `1` | `0.7` | `0.4` | `0.1` | `0.05` | | `--energy-chrome-opacity-hover` | `1` | `1` | `1` | `1` | `0.8` | | `--energy-content-max-width` | `none` | `none` | `90ch` | `80ch` | `75ch` | | `--energy-content-font-scale` | `1` | `1` | `1` | `1.05` | `1.1` | | `--energy-muted-opacity` | `0.5` | `0.5` | `0.5` | `0.5` | `0.5` | ## Per-level rules [#per-level-rules] ### Level 100 - Peak [#level-100---peak] No rules beyond the base custom properties: all chrome fully visible. ### Level 75 - Active [#level-75---active] * `.energy-chrome` fades to `var(--energy-chrome-opacity, 0.7)` with a `0.3s ease` opacity transition; restores to `var(--energy-chrome-opacity-hover, 1)` on hover. ### Level 50 - Steady [#level-50---steady] * `.energy-chrome` fades to `var(--energy-chrome-opacity, 0.4)` (same transition and hover restore). * `.energy-content` gets `max-width: var(--energy-content-max-width, 90ch)` and centered `margin-inline: auto`. ### Level 25 - Low [#level-25---low] * `.energy-sidebar`, `.energy-tab-bar`, `.energy-status-bar` are `display: none`. * `.energy-chrome` fades to `var(--energy-chrome-opacity, 0.1)` (same transition and hover restore). * `.energy-content` gets `max-width: var(--energy-content-max-width, 80ch)`, `margin-inline: auto`, `padding: 2rem`, and `font-size: calc(1em * var(--energy-content-font-scale, 1.05))`. ### Level 0 - Rest [#level-0---rest] * `.energy-sidebar`, `.energy-tab-bar`, `.energy-status-bar`, `.energy-toolbar` are `display: none !important`. * `.energy-chrome` fades to `var(--energy-chrome-opacity, 0.05)`; hover restores only to `var(--energy-chrome-opacity-hover, 0.8)`. * `.energy-content` gets `max-width: var(--energy-content-max-width, 75ch)`, `margin-inline: auto`, `padding: 2rem`, `line-height: 1.4`, `font-size: calc(1em * var(--energy-content-font-scale, 1.1))`, and `cursor: default !important` on itself and all descendants (the read-only cursor). ## Presence gating attributes [#presence-gating-attributes] The CSS-only path of the [presence API](/docs/api/core/presence). Annotate any element with the energy range it belongs to: ```html
AI chat - needs 75+ energy
Recovery hint - low energy only
``` The element hides automatically (`display: none`) whenever the root's `data-energy-level` falls outside its declared range: * `data-energy-min="N"` - hidden when the current level is **below** `N` (mirrors `presenceAtOrAbove(N)`) * `data-energy-max="N"` - hidden when the current level is **above** `N` (mirrors `presenceAtOrBelow(N)`) The rules enumerate every level/threshold comparison explicitly, so both paths agree by definition. ## Resolved-presence hooks [#resolved-presence-hooks] Stamp `data-energy-presence` with the value from `resolveEnergyPresence()` / `useEnergyPresence()` when you want CSS to handle the muted treatment: | Selector | Effect | | --------------------------------- | ------------------------------------------- | | `[data-energy-presence='muted']` | `opacity: var(--energy-muted-opacity, 0.5)` | | `[data-energy-presence='hidden']` | `display: none` | ## Reduced motion [#reduced-motion] Presence changes are layout changes; the stylesheet keeps them instant when the user (or OS) asked for reduced motion: ```css @media (prefers-reduced-motion: reduce) { [data-energy-level] .energy-chrome { transition: none; } } ``` Apps animating presence transitions themselves should honour the same preference. # DOM (/docs/api/dom) ```ts import { applyEnergyLevel, readEnergyLevel, observeEnergyLevel } from '@kumbatio/energy-system/dom' ``` Framework-free DOM projection of the energy level. All three functions take an optional `root` element; when omitted they use `document.body` and throw `Error('Energy DOM APIs require a browser document or an explicit root element')` outside a browser document. ## applyEnergyLevel [#applyenergylevel] ```ts function applyEnergyLevel(level: EnergyLevel, root?: HTMLElement): void ``` Apply an energy level to a root element. Sets the `data-energy-level` attribute and four CSS custom properties derived from [`uiVisibilityStrategy`](/docs/api/core/strategies#uivisibilitystrategy): | Custom property | Source field | | ------------------------------- | -------------------- | | `--energy-chrome-opacity` | `chromeOpacity` | | `--energy-chrome-opacity-hover` | `chromeOpacityHover` | | `--energy-content-max-width` | `contentMaxWidth` | | `--energy-content-font-scale` | `contentFontScale` | Throws for an invalid level. Pairs with the [reference stylesheet](/docs/api/css), whose per-level custom property values mirror the same strategy - the JS and CSS-only paths always agree. ```ts applyEnergyLevel(25) // ``` ## readEnergyLevel [#readenergylevel] ```ts function readEnergyLevel(root?: HTMLElement): EnergyLevel ``` Read the current energy level from a root element's `data-energy-level` attribute. Returns `100` if no valid level is set. ## observeEnergyLevel [#observeenergylevel] ```ts function observeEnergyLevel(callback: EnergyChangeListener, root?: HTMLElement): () => void ``` Observe energy level changes on a root element via `MutationObserver` (watching the `data-energy-level` attribute). Calls back with `EnergyState` values whose timestamp is the observation time and whose source is `'inferred'`. Only fires when the level actually changed. Returns a cleanup function that disconnects the observer. ```ts const stop = observeEnergyLevel((state, prev) => { console.log(`DOM level ${prev.level} -> ${state.level}`) }) // later stop() ``` When using the React entry with `applyToDOM` (the default), `EnergyProvider` calls `applyEnergyLevel` for you on `document.body`. Use `observeEnergyLevel` to react to that projection from non-React code. # API Reference (/docs/api) `@kumbatio/energy-system` is a framework-agnostic TypeScript library for energy-aware application behavior. This section has two halves, and they are kept honest in different ways: Hand-written pages, grouped by topic, with worked examples and the reasoning behind each API. Every type table on these pages is compiled from the installed package's own declarations. Every public export, with its exact signature and doc comment, generated from the type declarations of the version this site has installed. Complete by construction. Deliberately no version number is written on this page. The generated reference states the version it was built from, and it is the only place that can state it without going stale. ## Requirements [#requirements] The package is ESM-only (`"type": "module"`) and requires Node.js `>=24`. It has one optional peer dependency: `react >= 19.2` and `@types/react >= 19.2`, needed only for the `/react` entry point, which imports `` - added in React 19.2. Earlier React majors throw on first render rather than degrading, which is why the range is a hard floor rather than a recommendation. ## Entry points [#entry-points] | Import path | Contents | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@kumbatio/energy-system` | Core: engine, types, level definitions, built-in strategies, presence, focus sessions, notification gate, deferral, inbound-demand policy, metrics, external-level compatibility | | `@kumbatio/energy-system/react` | `EnergyProvider`, hooks, and headless components (`EnergyGate`, `EnergyIndicator`) | | `@kumbatio/energy-system/dom` | Direct DOM projection: `applyEnergyLevel`, `readEnergyLevel`, `observeEnergyLevel` | | `@kumbatio/energy-system/persistence` | Persistence adapters: `localStoragePersistence`, `memoryPersistence` | | `@kumbatio/energy-system/css` | Reference stylesheet (`energy.css`) implementing the UI visibility strategy in pure CSS | Three more subpaths are published as data rather than code, and exist so that consumers and other implementations can check themselves against this one: `./api-surface.json` (the exact public surface), `./conformance.json` (the level table and the values every built-in strategy resolves to), and `./spec/energy-state.schema.json` with `./spec/conformance.schema.json`. See [Spec and conformance](/docs/energy-system/guides/spec-and-conformance). ## Explained pages [#explained-pages] Everything exported from the package root, from createEnergyEngine to the notification gate. EnergyProvider, all hooks, and the EnergyGate and EnergyIndicator components. Apply and observe energy levels on DOM elements without a framework. Built-in EnergyPersistence adapters for localStorage and memory. Every class, custom property, and data-attribute selector in the reference stylesheet. ## Core pages [#core-pages] createEnergyEngine - options, state access, subscriptions, persistence flushing, disposal. EnergyLevel, EnergyState, CognitiveProfile, AdaptationStrategy, EnergyPersistence, and friends. The five level definitions and the pure level functions. The built-in adaptation strategies and their config types. Declare which energy levels a UI element belongs to. Focus sessions and the runtime notification gate. Not-now presets and energy-aware deferral resolution. The inbound-demand admission policy and its acknowledgments. Derived metrics and bridges for non-native level models. # Persistence (/docs/api/persistence) ```ts import { localStoragePersistence, memoryPersistence } from '@kumbatio/energy-system/persistence' ``` Two built-in implementations of the [`EnergyPersistence`](/docs/api/core/types#energypersistence) contract. Pass them to [`createEnergyEngine`](/docs/api/core/engine#createenergyengine) or `EnergyProvider`. ## localStoragePersistence [#localstoragepersistence] ```ts function localStoragePersistence(key?: string): EnergyPersistence ``` localStorage-based persistence adapter. Stores the full `EnergyState` as JSON under `key`. Behavior: * **`load()`** - returns `null` when `localStorage` is unavailable, the key is empty, the JSON is malformed, or any field fails validation (level, source, timestamp, revision, origin). Valid data is rebuilt through `createEnergyState`. * **`save(state)`** - writes `JSON.stringify(state)`. Failures (quota exceeded, missing `localStorage`) reject with `Error("Failed to save energy state to localStorage key ''", { cause })` rather than being swallowed, so the engine's persistence queue can observe the failure and retry with backoff. * **`observe(onState)`** - listens to the window `storage` event, so state changes from **other tabs** propagate into this engine (same-tab writes do not fire `storage`). Events for other keys or storage areas are ignored; invalid payloads are dropped. Returns an unsubscribe function; in environments without `addEventListener`/`localStorage` it is a no-op. ```ts import { createEnergyEngine } from '@kumbatio/energy-system' import { localStoragePersistence } from '@kumbatio/energy-system/persistence' const engine = createEnergyEngine({ persistence: localStoragePersistence('my-app:energy'), }) ``` ## memoryPersistence [#memorypersistence] ```ts function memoryPersistence(initial?: EnergyState): EnergyPersistence ``` In-memory persistence adapter. Useful for tests, SSR, or ephemeral sessions. Behavior: * **`load()`** - resolves the stored state, or `null` when never saved and no `initial` was given. * **`save(state)`** - stores a validated copy and synchronously notifies all `observe` listeners. * **`observe(onState)`** - registers a listener that fires on every `save`. Returns an unsubscribe function. This makes two engines sharing one `memoryPersistence` instance converge, which is handy for simulating cross-context sync in tests. ```ts import { createEnergyEngine, createEnergyState } from '@kumbatio/energy-system' import { memoryPersistence } from '@kumbatio/energy-system/persistence' const store = memoryPersistence(createEnergyState(50, 'scheduled')) const engine = createEnergyEngine({ persistence: store }) await engine.hydrate() engine.getState().level // 50 ``` # React (/docs/api/react) ```ts import { EnergyProvider, useEnergyLevel, EnergyGate } from '@kumbatio/energy-system/react' ``` Requires the optional peer dependency `react >= 19.2`. All hooks must be used inside an `EnergyProvider`; they throw `Error('Energy hooks must be used within an EnergyProvider')` otherwise. State reads use `useSyncExternalStore`, so they are concurrent-rendering safe. ## EnergyProvider [#energyprovider] ```tsx function EnergyProvider(props: EnergyProviderProps): React.ReactElement ``` Provides an [`EnergyEngine`](/docs/api/core/engine) to the tree. Either pass a pre-created `engine`, or let the provider create and own one (it disposes its internal engine on unmount, and recreates it correctly under React StrictMode). ```tsx import { EnergyProvider } from '@kumbatio/energy-system/react' import { localStoragePersistence } from '@kumbatio/energy-system/persistence' export function App({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### EnergyProviderProps [#energyproviderprops] /> ## Hooks [#hooks] ### useEnergyState [#useenergystate] ```ts function useEnergyState(): EnergyState ``` Get the full energy state (level + timestamp + source + revision + origin). ### useEnergyLevel [#useenergylevel] ```ts function useEnergyLevel(): [EnergyLevel, (level: EnergyLevel, source?: EnergySource) => void] ``` Read the current energy level and a setter (source defaults to `'manual'`). ```tsx const [level, setLevel] = useEnergyLevel() ``` ### useEnergyLevelCycler [#useenergylevelcycler] ```ts function useEnergyLevelCycler(): () => void ``` Returns a stable function that cycles to the next energy level (`100 -> 75 -> 50 -> 25 -> 0 -> 100`). ### useStrategy [#usestrategy] ```ts function useStrategy(strategy: AdaptationStrategy): T ``` Resolve a [strategy](/docs/api/core/strategies) against the current energy level. Memoized on `[strategy, level]`. ```tsx import { uiVisibilityStrategy } from '@kumbatio/energy-system' import { useStrategy } from '@kumbatio/energy-system/react' const ui = useStrategy(uiVisibilityStrategy) if (!ui.sidebar) return null ``` ### useEnergyGate [#useenergygate] ```ts function useEnergyGate(minLevel: EnergyLevel): boolean ``` Returns `true` if the current energy level meets or exceeds the given minimum. ### useEnergyPresence [#useenergypresence] ```ts function useEnergyPresence(presence: EnergyPresenceMap): EnergyPresence ``` Resolve a [presence map](/docs/api/core/presence) against the current energy level. Memoized on `[presence, level]` - keep the map referentially stable (module scope or `useMemo`). ## EnergyGate [#energygate] ```tsx function EnergyGate(props: EnergyGateProps): React.ReactNode ``` Declarative energy gating for a subtree. Headless: renders no wrapper element of its own. When the resolved presence is `'hidden'`, `fallback` takes over visually. Hidden subtrees keep their state by default. Energy moves up and down, so a gate that destroyed its children would throw away a half-written message every time capacity dipped. Hiding goes through React 19.2's ``: component state, DOM and scroll position survive, effects are torn down while hidden and re-run on reveal, and hidden content is not server-rendered. Set `whenHidden="unmount"` for subtrees whose cost is worth reclaiming instead - media, canvases, live connections. ```tsx // Hide the AI chat at 50 and below: // Full presence map, muted state styled by the child: {(presence) => } ``` ### EnergyGateProps [#energygateprops] Exactly one gating form must be used: a full `presence` map, `min` (optionally with `max` for band gating), or `max` alone. Passing none throws `Error('EnergyGate requires a presence map or min/max level')`. /> ### EnergyHiddenBehavior [#energyhiddenbehavior] ```ts type EnergyHiddenBehavior = 'preserve' | 'unmount' ``` The `whenHidden` vocabulary, exported for props that pass the choice through. ## EnergyIndicator [#energyindicator] ```tsx function EnergyIndicator(props: EnergyIndicatorProps): React.ReactNode ``` Headless energy indicator - bring your own UI. Takes a single render-prop child. ```tsx {({ level, label, cycle }) => ( )} ``` ### EnergyIndicatorProps [#energyindicatorprops] /> ### EnergyIndicatorRenderProps [#energyindicatorrenderprops] /> # Core (/docs/energy-system/quickstart/core) The core engine is a small stateful coordinator: it holds the current `EnergyState`, notifies subscribers on change, and resolves strategies against the current level. It runs in any JS/TS runtime. ## Create an engine [#create-an-engine] ```ts import { createEnergyEngine } from '@kumbatio/energy-system' const engine = createEnergyEngine({ initialLevel: 75 }) engine.getState() // { level: 75, timestamp: ..., source: 'manual', revision: 0, origin: '...' } ``` All options are optional: ## Set and cycle levels [#set-and-cycle-levels] ```ts engine.setLevel(50) // source defaults to 'manual' engine.setLevel(25, 'scheduled') // or 'inferred' engine.cycleLevel() // 100 → 75 → 50 → 25 → 0 → 100 ``` `setLevel()` is synchronous for in-memory subscribers. If a persistence adapter is configured, saving happens in the background with bounded exponential backoff. ## Subscribe to changes [#subscribe-to-changes] ```ts const unsubscribe = engine.subscribe((state, prev) => { console.log(`Energy: ${prev.level} -> ${state.level}`) }) // later unsubscribe() ``` Listeners receive both the new and previous state. A listener that throws is logged and does not break the others. ## Resolve strategies [#resolve-strategies] A strategy is a pure `(level) => config` mapping. `engine.resolve()` applies it to the current level: ```ts import { uiVisibilityStrategy, taskComplexityStrategy, } from '@kumbatio/energy-system' const ui = engine.resolve(uiVisibilityStrategy) if (!ui.sidebar) hideSidebar() const tasks = engine.resolve(taskComplexityStrategy) tasks.maxComplexity // 'routine' at level 50 ``` See [Strategies](/docs/energy-system/concepts/strategies) for the built-ins and [Authoring strategies](/docs/energy-system/guides/authoring-strategies) to write your own. ## Persistence and cleanup [#persistence-and-cleanup] ```ts import { localStoragePersistence } from '@kumbatio/energy-system/persistence' const engine = createEnergyEngine({ initialLevel: 75, persistence: localStoragePersistence(), }) engine.setLevel(50) await engine.flush() // wait for durable storage (optional) engine.dispose() // release subscriptions and observation resources ``` `flush()` rejects if the engine is disposed, or if the unchanged initial state can't be safely written because the hydration read failed - the engine never overwrites storage it couldn't read. Details in [Persistence](/docs/energy-system/concepts/persistence). ## Deterministic testing [#deterministic-testing] Inject a clock so tests control time: ```ts let now = 0 const engine = createEnergyEngine({ initialLevel: 100, clock: () => now, originId: 'test-engine', }) engine.setLevel(50) now += 60_000 engine.setLevel(25) ``` Full engine reference: [/docs/api/core](/docs/api/core). # CSS (/docs/energy-system/quickstart/css) The package ships a reference stylesheet that implements the UI visibility strategy in pure CSS. Set `data-energy-level` on a root element (the [DOM adapter](/docs/energy-system/quickstart/dom) or React provider does this for you) and annotated elements adapt automatically. ## Import [#import] ```ts import '@kumbatio/energy-system/css' ``` The stylesheet is one way to consume energy state, not the only one. You can instead resolve `uiVisibilityStrategy` in JS and style however you want - both paths use the same values, so they always agree. ## Chrome classes [#chrome-classes] Apply these classes to your layout; the stylesheet progressively de-emphasizes and hides them as the level drops: | Class | Use for | | -------------------- | ---------------------------------------------------------------------------------------- | | `.energy-chrome` | Any chrome element (titlebar, toolbar, status bar) - fades with level, restores on hover | | `.energy-sidebar` | Sidebar container - hidden at 25 and 0 | | `.energy-tab-bar` | Tab bar - hidden at 25 and 0 | | `.energy-status-bar` | Status bar - hidden at 25 and 0 | | `.energy-toolbar` | App/editor toolbar - hidden at 0 | | `.energy-content` | Main content area - narrows and scales up type at lower levels | ```html
…at 10% opacity, 100% on hover…
…80ch wide, slightly larger type…
``` ## Presence attributes: data-energy-min / data-energy-max [#presence-attributes-data-energy-min--data-energy-max] Annotate any element with the energy range it belongs to. No JS needed - the stylesheet hides it whenever the root's `data-energy-level` falls outside the range: ```html
AI chat - needs 75+ energy
Recovery hint - low energy only
``` * `data-energy-min="75"` - hidden whenever the current level is **below** 75 * `data-energy-max="25"` - hidden whenever the current level is **above** 25 This is the CSS mirror of `presenceAtOrAbove` / `presenceAtOrBelow` - the two paths agree by definition. See [Presence](/docs/energy-system/concepts/presence). ## Resolved-presence hooks: data-energy-presence [#resolved-presence-hooks-data-energy-presence] When you resolve presence in JS (`resolveEnergyPresence`, `useEnergyPresence`) and want CSS to handle the treatment, stamp the result on the element: ```html
De-emphasized at 50% opacity
Not rendered
``` The muted opacity is controlled by `--energy-muted-opacity` (default `0.5`). ## Custom properties [#custom-properties] The stylesheet defines these per level, mirroring `uiVisibilityStrategy` - use them in your own rules: | Property | 100 | 75 | 50 | 25 | 0 | | ------------------------------- | ---- | ---- | ---- | ---- | ---- | | `--energy-chrome-opacity` | 1 | 0.7 | 0.4 | 0.1 | 0.05 | | `--energy-chrome-opacity-hover` | 1 | 1 | 1 | 1 | 0.8 | | `--energy-content-max-width` | none | none | 90ch | 80ch | 75ch | | `--energy-content-font-scale` | 1 | 1 | 1 | 1.05 | 1.1 | ```css .my-panel { opacity: var(--energy-chrome-opacity, 1); } ``` ## Reduced motion [#reduced-motion] The stylesheet honours `prefers-reduced-motion: reduce` for its own opacity transitions. If your app animates presence changes, honour the same preference. # DOM (/docs/energy-system/quickstart/dom) The DOM adapter projects energy state onto an element as a `data-energy-level` attribute plus CSS custom properties. Any framework - or plain HTML and CSS - can react to it. Import from `@kumbatio/energy-system/dom`. ## Apply a level [#apply-a-level] ```ts import { applyEnergyLevel } from '@kumbatio/energy-system/dom' applyEnergyLevel(50) ``` This sets, on `document.body` (or a root you pass): * `data-energy-level="50"` * `--energy-chrome-opacity`, `--energy-chrome-opacity-hover` * `--energy-content-max-width`, `--energy-content-font-scale` The variable values come from `uiVisibilityStrategy`, so the JS and [CSS-only](/docs/energy-system/quickstart/css) paths always agree. ```ts // Scope to a specific element instead of body applyEnergyLevel(25, document.querySelector('#workspace')!) ``` An invalid level throws; outside a browser you must pass an explicit root. ## Read the current level [#read-the-current-level] ```ts import { readEnergyLevel } from '@kumbatio/energy-system/dom' const level = readEnergyLevel() // EnergyLevel; 100 when nothing valid is set ``` ## Observe changes [#observe-changes] `observeEnergyLevel` watches the attribute with a `MutationObserver` and calls back with `(state, prev)`: ```ts import { observeEnergyLevel } from '@kumbatio/energy-system/dom' const cleanup = observeEnergyLevel((state, prev) => { console.log(`Energy: ${prev.level} -> ${state.level}`) }) // later cleanup() ``` Observed states are synthesized at observation time: their `source` is `'inferred'` and their `timestamp` is when the mutation was seen. Use this to react to changes made by another script, devtools, or a different part of the page - not as the authoritative state history (that's the engine's job). ## Wiring the engine to the DOM [#wiring-the-engine-to-the-dom] Bridge the [core engine](/docs/energy-system/quickstart/core) to the DOM in one subscription: ```ts import { createEnergyEngine } from '@kumbatio/energy-system' import { applyEnergyLevel } from '@kumbatio/energy-system/dom' const engine = createEnergyEngine({ initialLevel: 75 }) applyEnergyLevel(engine.getState().level) engine.subscribe((state) => { applyEnergyLevel(state.level) }) ``` In React, `` does exactly this for you. ## Data attributes at a glance [#data-attributes-at-a-glance] | Attribute | Set by | Meaning | | ---------------------- | ------------------------------ | --------------------------------------------- | | `data-energy-level` | `applyEnergyLevel` | Current level on the root element | | `data-energy-min` | you, in markup | Element hides when the level drops below this | | `data-energy-max` | you, in markup | Element hides when the level rises above this | | `data-energy-presence` | you, from JS-resolved presence | Hooks for `muted`/`hidden` styling | The `min`/`max`/`presence` attributes are handled by the reference stylesheet - see the [CSS quickstart](/docs/energy-system/quickstart/css). Full DOM reference: [/docs/api/dom](/docs/api/dom). # React (/docs/energy-system/quickstart/react) The React layer wraps the core engine in a provider and exposes hooks that re-render exactly when energy state changes. Import from `@kumbatio/energy-system/react` (requires React 19.2+). ## Provider [#provider] Wrap your app once. The provider creates its own engine, or accepts one you created: ```tsx import { EnergyProvider } from '@kumbatio/energy-system/react' import { localStoragePersistence } from '@kumbatio/energy-system/persistence' export function App() { return ( ) } ``` With `applyToDOM` left on, the provider keeps `document.body` stamped for the [CSS path](/docs/energy-system/quickstart/css) automatically. ## Hooks [#hooks] ```tsx import { useEnergyState, useEnergyLevel, useEnergyLevelCycler, useStrategy, useEnergyGate, useEnergyPresence, } from '@kumbatio/energy-system/react' import { uiVisibilityStrategy, presenceAtOrAbove } from '@kumbatio/energy-system' function Screen() { const state = useEnergyState() // full EnergyState (level, timestamp, source, ...) const [level, setLevel] = useEnergyLevel() // tuple: level + setter const cycle = useEnergyLevelCycler() // () => void, 100 → 75 → ... → 0 → 100 const ui = useStrategy(uiVisibilityStrategy) // resolved config for current level const canDoComplexWork = useEnergyGate(75) // boolean: level >= 75 const aiChat = useEnergyPresence(presenceAtOrAbove(75)) // 'visible' | 'muted' | 'hidden' return (
{ui.sidebar && } {canDoComplexWork && }
) } ``` | Hook | Returns | | -------------------------------- | --------------------------------------------------------- | | `useEnergyState()` | The full `EnergyState` | | `useEnergyLevel()` | `[level, setLevel]` - setter takes `(level, source?)` | | `useEnergyLevelCycler()` | A stable function that cycles to the next level | | `useStrategy(strategy)` | The strategy's config, memoized per level | | `useEnergyGate(minLevel)` | `true` when the current level meets or exceeds `minLevel` | | `useEnergyPresence(presenceMap)` | The resolved `EnergyPresence` for the current level | All hooks must be used inside an `EnergyProvider` - they throw otherwise. ## EnergyGate [#energygate] Declarative gating for a subtree. Headless - it renders no wrapper element: ```tsx import { EnergyGate } from '@kumbatio/energy-system/react' // Shorthand: needs at least 75 energy, hidden below // Low-energy-only affordance // Full presence map + fallback; function children receive the resolved // presence so 'muted' can style itself }> {(presence) => } ``` `min` and `max` together create a band (visible only inside the range). See [Presence](/docs/energy-system/concepts/presence) for how presence maps work. ## EnergyIndicator [#energyindicator] A headless render-prop component for building your own energy control - battery, gauge, emoji, whatever: ```tsx import { EnergyIndicator } from '@kumbatio/energy-system/react' {({ level, label, description, cycle, setLevel, levels }) => ( )} ``` The render props also include `state`, `definition`, and `cognitiveProfile` for richer indicators. Full React reference: [/docs/api/react](/docs/api/react). # Authoring Strategies (/docs/energy-system/guides/authoring-strategies) 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 [#the-contract] ```ts import type { AdaptationStrategy, EnergyLevel } from '@kumbatio/energy-system' interface AdaptationStrategy { 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-pattern-a-config-table-per-level] The built-ins all use the same shape - a `Record` checked exhaustively by the compiler: ```ts 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> export const formStrategy: AdaptationStrategy = { 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>` is the load-bearing part: if a sixth level ever appeared, or you forgot one, the compiler refuses. ## Deriving from cognitive profiles [#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: ```ts 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() [#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: ```ts formStrategy.describe(25) // "Low: 3 fields per step" ``` ## Using your strategy [#using-your-strategy] No registration needed - it resolves anywhere the built-ins do: ```ts // Core const form = engine.resolve(formStrategy) ``` ```tsx // React const form = useStrategy(formStrategy) ``` For pure show/hide/mute behavior, don't write a strategy at all - a [presence declaration](/docs/energy-system/concepts/presence) is the smaller tool, and `createPresenceStrategy` lifts it into a strategy when you need one. ## Test every level [#test-every-level] Every level transition is an edge case. Test all five - the project's own contribution bar for core behavior: ```ts 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 [#design-guidance] Run your strategy through the same [decision filter](/docs/kumbatio/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](/docs/energy-system/contributing). If you build one that survives real use, open a PR. # Migrating from Legacy Scales (/docs/energy-system/guides/migration) The package model is fixed to `100 | 75 | 50 | 25 | 0`. If your existing app uses a different discrete scale - say a legacy `100 | 66 | 33 | 0` - the compatibility helpers let you migrate incrementally: keep reading old persisted values and keep your old control UX while internally applying native levels. ## Build the bridge [#build-the-bridge] ```ts import { createExternalLevelCompatibility } from '@kumbatio/energy-system' const legacy = createExternalLevelCompatibility({ levels: [100, 66, 33, 0] as const, // your scale, in cycle order toEnergyLevel: { 100: 100, 66: 50, 33: 25, 0: 0, }, fallbackLevel: 100, // used when input is unknown }) ``` The options are validated at creation: levels must be unique finite numbers, every level needs a mapping, every mapping must be a valid native level, and `fallbackLevel` must be in `levels`. Bad configuration fails immediately, not on first use. For the old `0 | 33 | 66 | 100` scale specifically: `66 → 75` and `33 → 25` or `50` are also defensible mappings, depending on what cognitive load your old levels actually meant. Pick the mapping that preserves *intent*, not the arithmetic midpoint. ## What the bridge gives you [#what-the-bridge-gives-you] ```ts // Read legacy persisted values → native level legacy.toEnergyLevel(66) // 50 legacy.toEnergyLevel(70) // 50 - unknown values snap to the nearest legacy level first // Native level → nearest legacy value (for UIs still rendering the old scale) legacy.fromEnergyLevel(50) // 66 // Keep the old control's cycle order legacy.cycleExternalLevel(66) // 33 // ...while internally applying native levels legacy.cycleMappedEnergyLevel(33) // cycles 33 → 0, then maps: 0 ``` ## Recommended migration sequence [#recommended-migration-sequence] **Read through the bridge.** Wherever legacy values enter (persisted state, URL params, API), convert with `legacy.toEnergyLevel(...)` before touching the engine. **Write native levels.** New writes persist native values (`100 | 75 | 50 | 25 | 0`) via the engine - never write legacy values back. **Switch UI controls to native cycling.** Replace `legacy.cycleMappedEnergyLevel` with the package's `cycleEnergyLevel` (or `engine.cycleLevel()`). **Remove the bridge.** Once persisted data is fully normalized, delete the compatibility mapping. ```ts import { cycleEnergyLevel } from '@kumbatio/energy-system' // End state - no compatibility layer left const next = cycleEnergyLevel(engine.getState().level) ``` ## Lower-level helpers [#lower-level-helpers] The bridge is built from generic utilities you can use directly for one-off conversions: ```ts import { cycleDiscreteLevel, mapToNearestDiscreteLevel, mapToNearestEnergyLevel, } from '@kumbatio/energy-system' cycleDiscreteLevel(66, [100, 66, 33, 0], 100) // 33 - cycle any discrete list mapToNearestDiscreteLevel(70, [100, 66, 33, 0], 100) // 66 - snap to nearest mapToNearestEnergyLevel(60) // 50 - snap any number to a native level ``` `mapToNearestEnergyLevel` is also useful for ingesting continuous values (a 0–100 slider, an imported metric) into the discrete model. # Production Patterns (/docs/energy-system/guides/production-patterns) Four real applications run on `energy-system` today: **[Anasa](/docs/kumbatio/ecosystem/anasa)** (a local-first writing workspace, in public alpha), **[Meltemi](https://meltemi.app)** (an email client in private beta from [entro314 labs](https://github.com/entro314-labs), the studio behind Kumbatio - built outside the Kumbatio product line), **Entromail** (a webmail platform from the same studio, pre-release), and **[kumbat.io](https://kumbat.io)** itself. This page collects the patterns that survived contact with real use - including the ones the SDK's own docs didn't anticipate. Every pattern here is shipped code, not a proposal. ## Two ways to hold the engine [#two-ways-to-hold-the-engine] The SDK doesn't care whether you use its React layer. Both of these are in production: **Provider tree (Anasa).** The documented path: create the engine, hand it to `EnergyProvider`, let `applyToDOM` stamp the document, read state through `useEnergyState` and `useStrategy`. **Module singleton (Meltemi).** No provider at all. The engine lives in a plain module, and React binds to it with `useSyncExternalStore` over `engine.subscribe`: ```ts // energy.ts - module scope, imported by anything export const energyEngine = createEnergyEngine({ initialLevel: 100, persistence: localStoragePersistence('myapp:energy'), originId: stableOriginId(), }) ``` ```ts // React binding - the whole adapter export function useEnergyState() { return useSyncExternalStore( (cb) => energyEngine.subscribe(cb), () => energyEngine.getState(), ) } ``` The singleton pattern matters when your state layer (a Zustand store, a service worker, non-React code) needs the engine as much as your components do. Meltemi's store reads energy at six mutation sites and never touches React context to do it. ## Custom persistence and write identity [#custom-persistence-and-write-identity] `localStoragePersistence` is a starting point, not a ceiling. Anasa implements the `EnergyPersistence` interface - `load`, `save`, `observe` - against its own settings service, so energy state lives with the rest of the app's preferences and syncs the same way. The part that bites: **preserve `revision` and `origin` on the round trip.** The engine uses them to tell its own echoes apart from genuinely external writes. If your persistence layer strips or re-mints them, every save observed back looks like a newer external write, and the engine persists forever in a loop. Pass the state through untouched. Two related disciplines from Anasa's integration: * **Mint one stable `originId` per install** (e.g. `myapp:app:`, cached in storage) and pass it to `createEnergyEngine`. Synthesized states - defaults you construct outside the engine - get their own deterministic origin so they're distinguishable. * **Call `engine.flush()` on exit paths, with a bound.** Anasa races `flush()` against a 3-second timeout on shutdown so a stuck retry can't wedge the quit. Durability is worth waiting for; hanging is not. ## Extend strategies by layering, not forking [#extend-strategies-by-layering-not-forking] Both apps needed behavior the built-in strategies don't model, and both solved it the same way: resolve the built-in, then layer app-specific config on top. Anasa's UI-visibility resolver starts from `uiVisibilityStrategy.resolve(level)` and adds its own surfaces - a right panel, and an AI-availability tier (`'none' | 'minimal' | 'full'`). Meltemi maps levels onto its own `MailScope` (`all | priority | committed | readonly`) that drives inbox filtering. Neither app forked a strategy; the package supplies the level semantics, the product supplies the domain. The same shape works for the lookup strategies: Meltemi's snooze dialog preselects a preset by reading `deferralStrategy.resolve(level).defaultPresetId` and mapping it onto its own preset list, and its undo-send window comes straight from `interactionForgivenessStrategy`. See [Authoring Strategies](/docs/energy-system/guides/authoring-strategies) for building your own from scratch. ## Read level copy from the package, once [#read-level-copy-from-the-package-once] Entromail wraps `getEnergyLevel` in a single `describeEnergy(level)` helper returning `{ label, description }`, and every surface that names a level goes through it - the battery control, the hint bar, the settings summary, the keyboard-shortcut toast. Nothing hardcodes "Steady" or writes its own gloss. That matters because level prose is **product copy, not semver-covered API** (the changelog says so explicitly, and the Rest description has already been rewritten once). An app that inlines the wording gets a silent drift between its five surfaces on the next upgrade; an app that reads the table gets the correction for free. Where the copy has to be app-specific, derive it - Entromail's hint bar composes the package label with its own scope description rather than replacing it. ## Defer, don't drop - end to end [#defer-dont-drop---end-to-end] Meltemi is the first full production test of the [notification gate](/docs/energy-system/concepts/notification-gate). Native mail-arrival events are published into a gate from `createNotificationGate`; at low energy or during a focus session (via `createFocusSessionController`) they're held, and when energy climbs they're released as **one** summarized catch-up banner - with sound only for immediate deliveries, never for catch-ups. Nothing is lost; nothing interrupts. If you adopt one runtime piece of the SDK, make it this one. It's the difference between "quiet mode" and an inbox you can trust while resting. ## Gate the AI you didn't ask for, not the AI you did [#gate-the-ai-you-didnt-ask-for-not-the-ai-you-did] Both apps converged on the same rule independently: energy level gates *passive* AI - panels that appear on their own - but never *invoked* AI. * Meltemi wraps its reading-pane AI panel in a presence check built from `presenceAtOrAbove(75)`; the command palette and agent console stay available at every level. * Anasa's AI surfaces scale `full` at 50 and above, `minimal` at 25, `none` at 0 - but explicitly-triggered assistance still answers. Low energy means less unsolicited stimulus, not fewer capabilities. That's the [decision filter](/docs/kumbatio/decision-filter) applied to AI. ## The escape-hatch invariant [#the-escape-hatch-invariant] At low levels, dim and de-emphasize - never remove the way out. In practice: * Meltemi dims peripheral chrome to an opacity floor of `0.4` (restored on hover), and the energy battery control is **excluded from dimming** - the control that raises the level must never fade with it. * At level `0`, Meltemi deliberately shows the *full* thread list read-only instead of filtering it: hiding rows would strand threads someone already had open. Rest reads what's already there. Whatever your app dims at `25` and `0`, the level control itself is exempt. This invariant started as an Anasa lesson and transferred to Meltemi unchanged. ## Upgrades should be boring [#upgrades-should-be-boring] Meltemi integrated at `0.4.0` and rode every minor release to `1.0` with **zero changes** to its integration code: additive surface, stable semantics for the five levels. `2.0` was the exception, and it is the more useful story. No type signature moved in it - it is a major because for this package a shipped strategy table's *values* are API, and two of its four fixes changed runtime behavior. An upgrade that the compiler waves through can still change what your app does, which is why the semver contract here covers behavior and why [`conformance.json`](/docs/energy-system/guides/spec-and-conformance) ships the tables: you can diff what your users will actually experience, not just what still typechecks. Concretely, upgrading from `1.x` breaks you only if you pinned `react` below 19.2 (which never worked with the React entry point), persisted state carrying properties outside the published schema, or depended on a batched notification being delivered after suppression started. See the [upgrade guide](/docs/energy-system/guides/upgrading). ## What apps build on top [#what-apps-build-on-top] The SDK deliberately stops at state and strategy resolution. Anasa shows what the layer above can look like: it records energy readings into its own durable store, derives hour-of-day and day-of-week patterns on-device, and offers a suggested level with a stated confidence and reason. The engine doesn't predict - apps that know their users can. Shipped something on `energy-system`? Tell us at [hello@kumbat.io](mailto:hello@kumbat.io) - adopters shipping real features get the loudest voice in the [roadmap](/docs/energy-system/roadmap). # Spec and conformance (/docs/energy-system/guides/spec-and-conformance) `@kumbatio/energy-system` is the reference implementation of the energy model. It is not the definition. The definition is two files that ship inside the package: | File | What it is | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SPEC.md` | The normative model, in RFC 2119 language. Levels, state, reconciliation, the strategy contract, autonomy, inbound demand, the runtime invariants, the accessibility requirements. | | `spec/energy-state.schema.json` | The interchange format for a single state. Validation is exact, including `additionalProperties: false` - a state carrying unknown keys is rejected rather than trimmed, so two implementations cannot exchange one and disagree about what they exchanged. | | `spec/conformance.schema.json` | The schema for the vectors themselves, so a consumer can validate the file before trusting it. | | `conformance.json` | The model as data. 252 vectors plus every strategy table, generated from the built library on each build and validated against its own schema before it is written. | Anything that implements the spec - in Swift, Kotlin, Rust, Python, Go, or another JavaScript library - is an implementation of the same model. States produced by one can be read by another. ## Why a spec instead of ports [#why-a-spec-instead-of-ports] The claim the model makes is that **capacity is first-class application state**: as real as the current user or the current document, and as deserving of a stable representation. That only pays off if one person's energy state can be shared by everything they use - a mail client, a writing tool, a coordination app, a phone. Sharing state across processes and languages is an interchange problem, and interchange wants a specification, not a set of ports. Five hand-written ports of the same tables drift within two releases. On the day they disagree, "one energy state across an ecosystem" quietly stops being true, and nothing fails loudly enough for anyone to notice. A spec plus vectors makes disagreement a test failure instead. It also makes the trust claim checkable. "The adaptive logic is MIT-licensed, read it" is worth something; "and here is how to prove your implementation agrees with it" is worth more. ## Using the vectors [#using-the-vectors] The vectors ship in the package and are importable directly: ```ts import conformance from '@kumbatio/energy-system/conformance.json' with { type: 'json' } ``` From another language, read the file out of the installed package or vendor a copy pinned to a version. Each section is a flat array or map of inputs and expected outputs: | Section | Covers | | ------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `levels` | The five definitions, including the cognitive profile. | | `cycle` | `100 → 75 → 50 → 25 → 0 → 100`. | | `strategies` | Every built-in strategy's config at every level. | | `presence` | `presenceAtOrAbove` / `presenceAtOrBelow` maps. | | `decisions.notification` | 30 rows: level × priority × suppressed → outcome. | | `decisions.demand` | 180 rows: level × tier × obligation × confidence → outcome. | | `deferral` | Six reference instants × five presets, chosen to hit every branch (weekend crossing, before/after the evening hour). | | `reconciliation` | The ordering rule, each pair asserted in both directions. | | `metrics` | Derived metrics per level at a fixed instant. | | `externalLevelMapping` | Rounding from an external percentage, including both midpoints. | A conformance run is a loop: ```ts for (const vector of conformance.decisions.demand) { const outcome = myResolveDemandOutcome( myAdmissionStrategy(vector.level), myAutonomyStrategy(vector.level), vector, ) assertDeepEqual(outcome, vector.outcome) } ``` This package's own `test/conformance.test.ts` does exactly this and is a reasonable file to copy the shape of. If you build a drift guard like this one, make sure it cannot pass by construction. The version here originally regenerated the artifact and then compared the result to itself, which meant it could not fail whatever was committed. Generation now belongs to the build, both generators take `--check`, and the check asserts that a `--check` run left the file untouched. ### Two things to watch [#two-things-to-watch] **Time zone.** The deferral presets compute in local time on purpose - "tomorrow morning" means the user's morning, not UTC's. The vectors are therefore generated under `TZ=UTC`, the generator refuses to run in any other zone, and a run replaying them must do the same. **Boundary confidences.** The demand vectors sample confidence at exactly `0.6`, `0.7`, `0.8`, `0.9` and `1` because those are the shipped autonomy thresholds. An implementation that writes `>` where the spec says `>=` passes every other row and fails these. ## What the vectors do not cover [#what-the-vectors-do-not-cover] Vectors describe pure functions: tables, and functions of their arguments alone. The stateful guarantees cannot be expressed that way, and they are the requirements most often got wrong - each one is in the spec because it was observed failing in a shipped product. * **Notifications defer, never drop.** Anything undeliverable now is held and released when the level rises, suppression lifts, or the gate is disposed. A gate being torn down must surface what it still holds. * **Suppression windows expire on their own**, as an emitted event rather than a condition to poll, with suppression lifted *before* the end event - otherwise the window swallows its own completion notice. * **Persisted state round-trips verbatim.** Storing only the level and rebuilding the rest produces a fresh `timestamp` and `origin` on every read, which reads as a new write to the reconciliation rule and makes contexts fight. These are normative in `SPEC.md` §9 and checked by this package's suite. A port needs its own tests for them. ## Accessibility is part of conformance [#accessibility-is-part-of-conformance] Adaptive interfaces fail people in ways static ones do not, so `SPEC.md` §10 states the requirements rather than leaving them to taste. The short version: * Hiding must remove from the accessibility tree, not just from view. * Focus must survive a level change - never left on a removed node. * Faded chrome must reveal on `:focus-within`, not only on `:hover`. * Level transitions must be announceable. * Reduced motion must be honored. * Contrast values must be overridable, and should rise under `prefers-contrast: more`. The reference stylesheet is honest about where it stands: at Low and Rest the resting chrome opacity does **not** meet WCAG 1.4.11 for non-text contrast. That is a deliberate design default for a receding interface, not a conformance claim - which is why every value is a custom property, and why the stylesheet raises them under `prefers-contrast: more` and drops opacity entirely under `forced-colors: active`. ## Versioning [#versioning] The vectors carry the reference implementation's version. Within a major version, existing vectors do not change meaning; sections and vectors may be added. **A shipped table's values are API.** Changing what `notificationStrategy` returns at level 50 changes how every consumer behaves, so it is a major-version change - and a change to the specification, not only to the library. The same holds for the reconciliation rule, which two implementations must agree on to share state at all. Prose returned by `describe()` is explicitly not covered. Wording is a product and localisation decision. ## Should you write a port? [#should-you-write-a-port] Probably not yet, and the honest reason is that a port needs a consumer that cannot use JavaScript. Every current consumer runs a web renderer, Tauri included. The likelier first demand is server-side rather than another UI framework - energy policy enforced where the data lives, for the kind of team and coordination tools that grew their own database-backed energy models instead of adopting this one. If that is you, the spec and the vectors are what make it a small job, and an issue describing the use is more useful than a pull request adding an adapter nobody asked for. # Upgrading (/docs/energy-system/guides/upgrading) Semver for this package covers more than type signatures. **A shipped strategy table's values are API**: changing what `notificationStrategy` returns at level `50` changes how every consumer behaves, so it is a major-version change - and a change to the [spec](/docs/energy-system/guides/spec-and-conformance), not only to this library. The reconciliation rule is covered the same way, because two implementations must agree on it to share state at all. Prose returned by `describe()` and the level descriptions are *not* covered. Wording is a product decision. The practical consequence: **an upgrade the compiler waves through can still change what your app does.** Type-checking a major is necessary and not sufficient. Diff the tables in [`conformance.json`](/docs/energy-system/guides/spec-and-conformance) - it ships in the package, one file per version - to see what your users will actually experience. ## 1.x to 2.0 [#1x-to-20] No type signature changed in `2.0`. It is a major because two of its four fixes changed runtime behavior, and one narrowed an install range. ### Does it break you? [#does-it-break-you] Three questions. If all three are "no", the upgrade is a version bump. **Are `react` or `@types/react` pinned below 19.2?** The React entry point imports ``, added in React 19.2. The old `>=19` peer range advertised a compatibility that throws on first render under 19.0 and 19.1 - so this narrowing documents reality rather than creating a break. Raise the pin, or stop importing `@kumbatio/energy-system/react`. **Do you persist or exchange states carrying properties outside the published schema?** Through `1.x`, unknown properties were silently trimmed to fit. They are now rejected, because trimming let two implementations exchange a state and disagree about what they had exchanged. If you were stashing your own fields alongside an `EnergyState`, move them out of the object - `createEnergyState()` will now throw rather than quietly drop them. Fractional timestamps are rejected for the same reason: [`spec/energy-state.schema.json`](/docs/energy-system/guides/spec-and-conformance) never allowed them. **Do you depend on a batched notification being delivered after suppression started, or after energy fell below its threshold?** A notification used to be classified once, when published, and an open batch window was then delivered under whatever policy happened to be in force later. So an intent admitted at Steady could arrive in the middle of a focus session, and one batched at Steady could surface at Rest with every channel disabled. The gate now re-judges everything it is holding whenever energy or suppression changes. Batched intents the current policy no longer admits move to the deferred queue and are released when something admits them. Nothing is dropped - the defer-not-drop guarantee is unchanged - but **delivery timing moved**, and it moved deliberately. `flush()` also no longer bypasses active suppression; it overrides the *wait*, not the *policy*. ### Also in 2.0 [#also-in-20] * A configured `originId` no longer corrupts the unproduced sentinel. Construction used to stamp the configured producer identity onto the untouched default state, so `isUnproducedState()` returned `false` for it and `getEnergyMetrics()` reported an age measured from the epoch. The sentinel is always `origin: "0-initial"`; your configured identity owns the first state the engine actually produces. * `api-surface.json` now includes `EnergyEngine.resolve()`. The declaration parser did not recognise generic members, so a public method was missing from the frozen surface - and a method absent from the freeze is a method nobody notices removing. * `2.0.1` changed the `Rest` level description from "Depleted." to "Recovery." - product copy, not API. "Depleted" describes damage; the level copy is supposed to say what a person *can* do at a level, not what they cannot. ## Upgrading within a major [#upgrading-within-a-major] Minors and patches are additive. Meltemi integrated at `0.4.0` and rode every minor release to `1.0` without touching its integration code, which is the bar the project holds itself to. To verify an upgrade yourself rather than taking that on trust: ```bash # The exact public surface of each version, as data npm view @kumbatio/energy-system@1.0.0 dist.tarball ``` Both `api-surface.json` and `conformance.json` ship inside the package and carry the version of the release they belong to, so you can diff two installs directly - surface against surface, table against table. (Through `2.0.4` both files were published with the *previous* release's version stamp; `2.0.5` fixed that.) ## Migrating from a different level scale [#migrating-from-a-different-level-scale] Version upgrades are not the same problem as adopting the five-level model when your app already has its own. For that, see [Migrating from legacy scales](/docs/energy-system/guides/migration). # Deferral (/docs/energy-system/concepts/deferral) Deferring an item is an energy statement: it declares insufficient capacity for it right now and names when it should resurface. The SDK models this with pure presets - `(now) => Date` functions - plus a strategy that orders them by energy level so the one-tap default matches capacity. ## The presets [#the-presets] `createDeferralPresets` builds the standard set. Times are computed in **local time** - "tomorrow morning" means the user's morning: ```ts import { createDeferralPresets } from '@kumbatio/energy-system' const presets = createDeferralPresets({ morningHour: 9, eveningHour: 18 }) // both options optional; defaults are 9 and 18 ``` | Preset id | Label | Resurfaces | | ------------------ | ----------------------- | --------------------------------------------------- | | `in-1-hour` | In 1 hour | now + 60 minutes | | `this-evening` | This evening (18:00) | today at `eveningHour`, or tomorrow if already past | | `tomorrow-morning` | Tomorrow morning (9:00) | tomorrow at `morningHour` | | `next-workday` | Next workday (9:00) | next non-weekend day at `morningHour` | | `next-monday` | Next Monday (9:00) | next Monday at `morningHour` | The stable ids are exported as `DEFERRAL_PRESET_IDS` so configs and strategies can reference them without magic strings: ```ts import { DEFERRAL_PRESET_IDS } from '@kumbatio/energy-system' DEFERRAL_PRESET_IDS.tomorrowMorning // 'tomorrow-morning' ``` ## Resolving a deferral [#resolving-a-deferral] `resolveDeferral` maps a preset id to a resurface timestamp (epoch ms). Unknown ids return `null` - the caller decides whether that's an error: ```ts import { resolveDeferral } from '@kumbatio/energy-system' const resurfaceAt = resolveDeferral(presets, 'tomorrow-morning') // number (epoch ms), or null for an unknown id // Deterministic in tests: pass the reference moment resolveDeferral(presets, 'next-monday', new Date('2026-07-21T10:00:00')) ``` Storing and resurfacing the item at that timestamp is your app's job - the SDK computes *when*, deliberately staying out of *where* your items live. ## Energy-aware ordering [#energy-aware-ordering] `deferralStrategy` resolves the presentation order and the one-tap default for the current level: ```ts import { deferralStrategy } from '@kumbatio/energy-system' const { defaultPresetId, orderedPresetIds } = engine.resolve(deferralStrategy) ``` | Level | Default | Reasoning | | ----: | ---------------- | ----------------------------------------------- | | `100` | in 1 hour | High capacity - short deferrals are realistic | | `75` | in 1 hour | | | `50` | this evening | Push past the current stretch | | `25` | tomorrow morning | Resurface when capacity has plausibly recovered | | `0` | tomorrow morning | Not in an hour - you're resting | `orderedPresetIds` lists all five, most-prominent first, so a snooze menu can render them in capacity-appropriate order. ## Putting it together [#putting-it-together] ```ts import { createDeferralPresets, deferralStrategy, resolveDeferral, } from '@kumbatio/energy-system' const presets = createDeferralPresets() function snooze(itemId: string) { const { defaultPresetId } = engine.resolve(deferralStrategy) const resurfaceAt = resolveDeferral(presets, defaultPresetId) if (resurfaceAt !== null) { saveSnooze(itemId, resurfaceAt) } } ``` Deferral presets compute times; they hold no state. If you also route the item's *notification* through the [gate](/docs/energy-system/concepts/notification-gate), the defer-not-drop guarantee applies when it resurfaces at a low-energy moment. # Energy State (/docs/energy-system/concepts/energy-state) `EnergyState` is a frozen, point-in-time snapshot of cognitive capacity. Every field exists so that two contexts (tabs, workers, devices) looking at the same storage can agree on which state wins - deterministically. ## The shape [#the-shape] ```ts interface EnergyState { readonly level: EnergyLevel // 0 | 25 | 50 | 75 | 100 readonly timestamp: number // when it was set (epoch ms) readonly source: EnergySource // how it was determined readonly revision: number // logical sequence for same-timestamp writes readonly origin: string // stable identity of the producing engine } ``` States are immutable - the engine never mutates one, it replaces it. `createEnergyState()` validates every field and freezes the result. ## EnergySource [#energysource] `source` records *how* the level was determined, not just what it is: | Source | Meaning | Reconciliation priority | | ------------- | ----------------------------------------------- | ----------------------- | | `'manual'` | The user set it themselves | Highest | | `'scheduled'` | An automation applied a pre-set curve | Middle | | `'inferred'` | Derived from observation (e.g. the DOM adapter) | Lowest | The priority ordering encodes a principle: a human's explicit statement about their own capacity outranks anything the software guessed. ## Why revision and origin exist [#why-revision-and-origin-exist] Two writes can share a wall-clock timestamp - a fast double-tap, or a deterministic test clock that doesn't advance. `revision` is a logical counter that breaks the tie: local writes advance it when the clock does not advance. `origin` is a stable per-engine identity (a UUID by default) that breaks the tie when even revisions match, so every context converges on the same winner without coordination. ## Reconciliation ordering [#reconciliation-ordering] When the engine receives an external state (from hydration or a persistence `observe` callback), it accepts it only if the candidate is *preferred* over the current state. Preference is checked in strict order: 1. **Timestamp** - later wins 2. **Revision** - higher wins 3. **Source priority** - `manual` beats `scheduled` beats `inferred` 4. **Origin** - lexicographically greater wins (arbitrary but deterministic) Every context applies the same rules, so concurrent writers always converge. External states are validated strictly before reconciliation: a legal level and source, a finite non-negative timestamp, a non-negative safe-integer revision, and a non-empty origin. Invalid records are **ignored**, never repaired - a corrupted record must not be promoted into a more authoritative state. ## Clock-skew protection [#clock-skew-protection] A context with a badly wrong clock could stamp states far in the future and win reconciliation until real time catches up. The engine rejects hydrated or observed states stamped more than `maxFutureSkewMs` ahead of the local clock (default 5 minutes). Pass `Number.POSITIVE_INFINITY` to accept any finite timestamp. ```ts const engine = createEnergyEngine({ persistence: localStoragePersistence(), maxFutureSkewMs: 60_000, // tolerate at most 1 minute of future skew }) ``` ## Derived metrics [#derived-metrics] `getEnergyMetrics(state, now?)` computes app-agnostic guidance from a state snapshot - no separate logging system required: ```ts import { getEnergyMetrics } from '@kumbatio/energy-system' const metrics = getEnergyMetrics(engine.getState()) metrics.stateAgeMinutes // staleness: "you set this 3 hours ago" metrics.expectedProductivityWindowMinutes // 120 / 90 / 45 / 25 / 0 by level metrics.suggestedBreakIntervalMinutes // 0 at rest - rest is already a break metrics.recommendedTaskComplexity // from the level's cognitive profile metrics.sustainable // true only for 25/50/75 - peak depletes, rest recovers metrics.recoveryHintMinutes // present at 50/25/0 only ``` [Focus sessions](/docs/energy-system/concepts/focus-sessions) use these metrics for their energy-derived defaults. ## Related [#related] * [Levels](/docs/energy-system/concepts/levels) - what each of the five values means * [Persistence](/docs/energy-system/concepts/persistence) - where states are stored and how hydration works # Focus Sessions (/docs/energy-system/concepts/focus-sessions) A focus session is a temporary "one thing at a time" commitment layered on top of the energy model - it suppresses interruptions for a bounded duration, surfaces break nudges, and **always ends on time**. It is not an energy level; it's a window. ## Two invariants, by construction [#two-invariants-by-construction] Both come from field evidence (a shipped ADHD email client got both wrong, and both were user-hostile): 1. **Sessions auto-expire.** Expiry is an emitted event, never a predicate your app must remember to poll - suppression can never outlive the session. 2. **Suppression lifts *before* the end event fires.** An end-of-session notification can never be swallowed by the session's own suppression. ## Basic usage [#basic-usage] ```ts import { createEnergyEngine, createNotificationGate, createFocusSessionController, } from '@kumbatio/energy-system' const engine = createEnergyEngine({ initialLevel: 75 }) const gate = createNotificationGate(engine, { onDeliver({ notifications }) { showToasts(notifications) }, }) const focus = createFocusSessionController({ engine, gate }) focus.subscribe((event, session) => { if (event === 'break') showBreakNudge() if (event === 'end') showSessionSummary(session) }) focus.start() // duration and break cadence default from the current energy level ``` While a session runs, the controller holds the gate's suppression flag; on stop, end, or dispose it releases it. The controller owns the flag for the session's lifetime, so it can never be left stuck on. ## Energy-derived defaults [#energy-derived-defaults] With an `engine` supplied, `start()` derives its defaults from the current level: * **Duration** - the level's expected productivity window: 120 / 90 / 45 / 25 minutes at `100/75/50/25`. At `0` the window is 0, so an explicit session falls back to 25 minutes rather than expiring instantly. * **Break cadence** - the task-complexity guidance: every 45 minutes at `50`, every 25 at `25`, no nudges at `100`, `75`, or `0`. Without an engine: 25 minutes, no breaks. Override either explicitly: ```ts focus.start({ durationMinutes: 50, breakEveryMinutes: 20 }) ``` A break nudge that would land at or after the session end is skipped - the end event already tells the user to step away. ## Lifecycle events [#lifecycle-events] ```ts type FocusSessionEvent = 'start' | 'break' | 'end' | 'stop' ``` | Event | When | | ------- | ---------------------------------------------------------------------- | | `start` | A session began (starting while one is active stops the old one first) | | `break` | A break nudge is due - recurring while the session runs | | `end` | The session reached `endsAt` and auto-expired | | `stop` | The session was ended manually before `endsAt` | Every listener receives the session snapshot: `{ startedAt, endsAt, breakIntervalMs }`. ## Inspecting a session [#inspecting-a-session] ```ts focus.getSession() // FocusSession | null focus.remainingMs() // 0 when idle import { sessionRemainingMs, isSessionExpired } from '@kumbatio/energy-system' sessionRemainingMs(session) // pure helpers, take an optional `now` isSessionExpired(session) ``` ## Controller options [#controller-options] `dispose()` stops any active session first (releasing suppression and emitting `stop`), then tears down listeners. ## Related [#related] * [Notification gate](/docs/energy-system/concepts/notification-gate) - what suppression actually does to notifications * [Energy state](/docs/energy-system/concepts/energy-state) - the metrics behind the defaults # Levels (/docs/energy-system/concepts/levels) The energy model has exactly five discrete levels: `100 | 75 | 50 | 25 | 0`. Discrete rather than continuous because fewer choices mean less decision fatigue - critical exactly when energy is already low - and because clear boundaries make adaptation rules predictable. ## The five levels [#the-five-levels] | Value | Key | Label | Description | | ----: | -------- | ------ | ---------------------------------------------------------- | | `100` | `peak` | Peak | High capacity. Planning, complex decisions, creative work. | | `75` | `active` | Active | Good capacity. Focused execution, problem-solving. | | `50` | `steady` | Steady | Moderate capacity. Routine tasks, familiar work. | | `25` | `low` | Low | Limited capacity. Simple tasks, review, light work. | | `0` | `rest` | Rest | Recovery. Consumption only - reading, reflecting. | Cycle order is descending, wrapping at the bottom: `100 → 75 → 50 → 25 → 0 → 100`. ## Cognitive profiles [#cognitive-profiles] Each level carries a `CognitiveProfile` describing what the brain can handle there - not what the sidebar should do. Strategies read this to make informed decisions, and different apps can adapt differently from the same profile. ```ts interface CognitiveProfile { readonly decisionCapacity: 'high' | 'moderate' | 'low' | 'minimal' | 'none' readonly focusDuration: 'extended' | 'moderate' | 'short' | 'minimal' | 'none' readonly taskComplexity: 'complex' | 'moderate' | 'routine' | 'simple' | 'consumption' readonly interruptionTolerance: 'high' | 'moderate' | 'low' | 'minimal' | 'none' } ``` | Level | Decisions | Focus | Task complexity | Interruptions | | ----: | --------- | -------- | --------------- | ------------- | | `100` | high | extended | complex | high | | `75` | moderate | moderate | moderate | moderate | | `50` | low | short | routine | low | | `25` | minimal | minimal | simple | minimal | | `0` | none | none | consumption | none | ## Reading level definitions [#reading-level-definitions] ```ts import { getEnergyLevels, getEnergyLevel, cycleEnergyLevel, isEnergyLevel } from '@kumbatio/energy-system' getEnergyLevels() // all five definitions, ordered 100 → 0 getEnergyLevel(50) // { value: 50, key: 'steady', label: 'Steady', description, cognitiveProfile } cycleEnergyLevel(25) // 0 isEnergyLevel(66) // false - runtime validation for untrusted input ``` Definitions are deeply frozen. Use `label` and `description` directly in your UI instead of re-inventing level names. ## Can I customize the levels? [#can-i-customize-the-levels] The scale itself is fixed - the values, keys, labels, and cognitive profiles are part of the package contract and cannot be redefined. This is deliberate: a shared, stable vocabulary is what lets strategies, presence maps, CSS attributes, and different apps interoperate. What you *can* customize: * **Behavior per level** - write your own [adaptation strategy](/docs/energy-system/guides/authoring-strategies); the built-ins are defaults, not mandates. * **Presentation** - the headless `EnergyIndicator` and the level definitions' `label`/`description` let you render levels however fits your product. * **External scales** - if your existing app uses a different discrete scale (say `100 | 66 | 33 | 0`), bridge it with [`createExternalLevelCompatibility`](/docs/energy-system/guides/migration) instead of forking the model. If the five-level model oversimplifies a real scenario for you, that's exactly the kind of report the project wants - see [Contributing](/docs/energy-system/contributing). # Notification Gate (/docs/energy-system/concepts/notification-gate) The notification gate turns `NotificationConfig` from guidance into enforcement. Your app publishes notification *intents* through the gate; the gate resolves the current level's config and decides whether each intent is delivered now, batched, or deferred. The design rule is inherited from field evidence (a shipped scheduler destroyed reminders that came due while suppressed): **the gate never silently drops a notification.** Anything not deliverable now is deferred and released when energy rises, suppression lifts, or the gate is disposed. ## Basic usage [#basic-usage] ```ts import { createEnergyEngine, createNotificationGate } from '@kumbatio/energy-system' const engine = createEnergyEngine({ initialLevel: 75 }) const gate = createNotificationGate(engine, { onDeliver({ notifications, reason, channels, level }) { if (channels.visual) showToast(notifications, reason) if (channels.sound) playChime() }, }) gate.publish({ priority: 'high', payload: { title: 'Build finished' } }) // returns 'delivered' | 'batched' | 'deferred' ``` At level `75` this delivers immediately. At `50` it enters a 5-minute batch. At `0` everything is deferred - held, not lost. ## How each publish is decided [#how-each-publish-is-decided] Priorities are `'normal' | 'high' | 'critical'` (default `'normal'`). The decision, in order: 1. **Suppressed?** (e.g. a focus session is running) → `deferred` 2. **Below the level's priority threshold?** → `deferred` 3. **Level has a batch interval?** → `batched`, delivered together when the window closes 4. Otherwise → `delivered` immediately The pure decision function is exported so you can unit-test your notification policy without constructing a gate: ```ts import { resolveNotificationOutcome, notificationStrategy } from '@kumbatio/energy-system' const config = notificationStrategy.resolve(25) resolveNotificationOutcome(config, 'critical', false) // 'batched' resolveNotificationOutcome(config, 'normal', false) // 'deferred' ``` ## Defer, not drop [#defer-not-drop] Deferred intents are re-evaluated on every energy change. The moment the new level's config (or lifted suppression) admits them, they're **released immediately** - not re-batched, because they already waited once. Delivery `reason` tells you which path an intent took: | Reason | Meaning | | ------------- | ------------------------------------------------------------- | | `'immediate'` | Admitted at publish time | | `'batch'` | A batch window closed | | `'released'` | Previously deferred, now admitted - or flushed out on dispose | `dispose()` delivers everything still held as a final `'released'` delivery before going inert. An intent that entered the gate always exits through `onDeliver`. ## Channels [#channels] Each delivery carries the channels the active config permits - `{ visual, sound, vibration }` - resolved at *delivery* time, not publish time. Your `onDeliver` decides how to render within those constraints. ## Gate API [#gate-api] ```ts gate.publish({ priority: 'critical', payload }) // PublishOutcome gate.setSuppressed(true) // focus sessions call this for you gate.isSuppressed() gate.pendingCount() // { batched: number, deferred: number } gate.flush() // deliver the open batch now + release eligible deferred gate.dispose() // final 'released' delivery, then inert ``` Pair the gate with a [focus session controller](/docs/energy-system/concepts/focus-sessions): the controller suppresses the gate on session start and is guaranteed to release it on stop, end, or dispose. # Persistence (/docs/energy-system/concepts/persistence) Persistence is an interface, not an assumption. The engine works entirely in memory by default; give it an adapter and it hydrates on creation, saves in the background with retry, and can observe external writes for cross-context sync. ## The contract [#the-contract] ```ts interface EnergyPersistence { load(): Promise save(state: EnergyState): Promise // Optional: subscribe to externally persisted updates (cross-tab, worker, ...) observe?(onState: (state: EnergyState) => void): () => void } ``` Adapters persist the **full** `EnergyState` - level, timestamp, source, revision, origin - because reconciliation needs all of it. A `save` that fails should reject (not swallow) so the engine's retry queue can observe the failure. Platform-specific adapters (SQLite for desktop, IndexedDB, server sync) belong in consuming apps - the contract is deliberately small enough to implement in a few lines. ## Built-in adapters [#built-in-adapters] Import from `@kumbatio/energy-system/persistence`: ```ts import { localStoragePersistence, memoryPersistence } from '@kumbatio/energy-system/persistence' ``` ### localStoragePersistence(key?) [#localstoragepersistencekey] Stores the state as JSON under `key` (default `'energy-state'`). Its `observe` hooks the `storage` event, so two tabs sharing the key converge automatically - the engine reconciles whichever state [wins deterministically](/docs/energy-system/concepts/energy-state). In environments without `localStorage`, `load` returns `null` and `observe` is a no-op; `save` rejects so the engine retries rather than silently losing state. ### memoryPersistence(initial?) [#memorypersistenceinitial] In-memory storage for tests, SSR, or ephemeral sessions. Its `observe` notifies on every `save`, which makes it handy for wiring two engines together in tests. ## Engine behavior with persistence [#engine-behavior-with-persistence] ```ts const engine = createEnergyEngine({ initialLevel: 75, persistence: localStoragePersistence(), onPersistenceError(error, state) { telemetry.report(error) // observe failures; the engine still retries }, }) ``` * **Hydration is automatic.** On creation the engine calls `load()` and reconciles the stored state against the in-memory one. Invalid or too-far-future records are ignored, never repaired. * **Saves are background.** `setLevel()` notifies subscribers synchronously; persistence runs behind it with bounded exponential backoff (250ms doubling up to 30s), so a failing store - quota exceeded, say - is not hammered forever. * **External observation is wired for you.** If the adapter implements `observe`, the engine subscribes and reconciles incoming states through the same deterministic ordering as hydration. ## flush() - waiting for durability [#flush---waiting-for-durability] `setLevel` doesn't wait for storage. When a workflow must not report completion until the state is durable, flush: ```ts engine.setLevel(50) await engine.flush() ``` `flush()` resolves once the current state version is durably persisted. Two failure modes reject instead of lying: * **Disposed engine** - flushing after `dispose()` rejects. * **Unreadable storage at startup** - an initial `flush()` (before any level change) waits for hydration first, and rejects if that hydration *read* failed. The engine will not overwrite storage it couldn't read with a default state. Without a persistence adapter, `flush()` resolves immediately. ## dispose() - releasing resources [#dispose---releasing-resources] ```ts engine.dispose() ``` Dispose releases the persistence observation, cancels retry timers, clears subscribers, and rejects any pending `flush()` waiters. A disposed engine is inert: `setLevel`/`cycleLevel` become no-ops and no further persistence is scheduled. In React, the `EnergyProvider` disposes the engine it created on unmount. ## Writing your own adapter [#writing-your-own-adapter] ```ts import type { EnergyPersistence, EnergyState } from '@kumbatio/energy-system' export function myStorePersistence(store: MyStore): EnergyPersistence { return { async load() { const record = await store.get('energy-state') return record ?? null // return null, not a fabricated state }, async save(state: EnergyState) { await store.set('energy-state', state) // reject on failure - the engine retries }, observe(onState) { const stop = store.onChange('energy-state', onState) return stop }, } } ``` Validate untrusted records before returning them from `load()` - or return them as-is and rely on the engine, which strictly validates and ignores anything invalid. Never "repair" a corrupt record into a newer-looking state; that could win reconciliation it shouldn't. # Presence (/docs/energy-system/concepts/presence) Presence answers one question per element: *at which energy levels does this belong on screen?* The declaration is a plain typed object - one presence value per level - so the same annotation can drive React rendering, engine resolution, or CSS. ## The three presence values [#the-three-presence-values] ```ts type EnergyPresence = 'visible' | 'muted' | 'hidden' ``` * `visible` - rendered normally * `muted` - rendered but de-emphasized (reduced opacity, secondary styling) * `hidden` - not rendered at all A complete declaration is an `EnergyPresenceMap`: a frozen record with an entry for all five levels. ## Declaring presence [#declaring-presence] `defineEnergyPresence` builds a complete map from a partial spec; unlisted levels fall back to `default` (`'visible'` when omitted): ```ts import { defineEnergyPresence } from '@kumbatio/energy-system' // Hide the AI chat at 50 and below, mute it at 75 const aiChatPresence = defineEnergyPresence({ default: 'visible', 75: 'muted', 50: 'hidden', 25: 'hidden', 0: 'hidden', }) ``` ### Shorthands [#shorthands] Most declarations are threshold-shaped, so two helpers cover them: ```ts import { presenceAtOrAbove, presenceAtOrBelow } from '@kumbatio/energy-system' const composerTools = presenceAtOrAbove(50) // hidden at 25 and 0 const aiSidebar = presenceAtOrAbove(75, 'muted') // muted (not hidden) below 75 const recoveryHint = presenceAtOrBelow(25) // low-energy-only affordance ``` The second argument sets what the element becomes *outside* its range (`'hidden'` by default). ## Resolving presence [#resolving-presence] ```ts import { resolveEnergyPresence, isPresenceVisible } from '@kumbatio/energy-system' resolveEnergyPresence(aiChatPresence, 50) // 'hidden' isPresenceVisible('muted') // true - only 'hidden' is false ``` ## Lifting into a strategy [#lifting-into-a-strategy] `createPresenceStrategy` turns a presence map into a regular `AdaptationStrategy`, so it resolves through the engine like any built-in. The map is validated once at creation, so `resolve()` can never fail later: ```ts import { createPresenceStrategy } from '@kumbatio/energy-system' const aiChat = createPresenceStrategy('ai-chat', aiChatPresence) engine.resolve(aiChat) // 'visible' | 'muted' | 'hidden' ``` ## The same annotation in every layer [#the-same-annotation-in-every-layer] ```tsx import { EnergyGate, useEnergyPresence } from '@kumbatio/energy-system/react' // Component form }> {(presence) => } // Hook form const presence = useEnergyPresence(aiChatPresence) ``` ```html
AI chat - needs 75+ energy
Recovery hint - low energy only
De-emphasized
```
```ts const strategy = createPresenceStrategy('ai-chat', aiChatPresence) engine.subscribe(() => { panel.hidden = engine.resolve(strategy) === 'hidden' }) ```
The CSS attribute path and the JS helpers enumerate the same comparisons, so they agree by definition. ## Validation [#validation] `isEnergyPresence(value)` type-guards untrusted input. `defineEnergyPresence` and the shorthands throw on invalid levels or presence values rather than producing a partial map. ## Related [#related] * [React quickstart](/docs/energy-system/quickstart/react) - `` with `min`/`max` band gating * [CSS quickstart](/docs/energy-system/quickstart/css) - the attribute rules in detail # Strategies (/docs/energy-system/concepts/strategies) A strategy is a pure mapping from energy level to behavior configuration. The SDK doesn't dictate what happens at each level - it provides the state, and strategies decide what to do with it. ## The model [#the-model] ```ts interface AdaptationStrategy { name: string describe(level: EnergyLevel): string // human-readable summary resolve(level: EnergyLevel): TConfig // compute the config for a level } ``` Strategies are pure functions with no side effects, so they're trivially composable - an app resolves several at once: ```ts const ui = engine.resolve(uiVisibilityStrategy) const notifications = engine.resolve(notificationStrategy) ``` In React, `useStrategy(strategy)` resolves against the current level and re-renders on change. To write your own, see [Authoring strategies](/docs/energy-system/guides/authoring-strategies). ## The built-ins [#the-built-ins] ### uiVisibilityStrategy [#uivisibilitystrategy] Progressively simplifies interface chrome as energy drops. Resolves to a `UIVisibilityConfig`: | Level | sidebar | tabBar | statusBar | toolbar | chrome opacity | content width | font scale | | ----: | :-----: | :----: | :-------: | :-----: | :------------: | :-----------: | :--------: | | `100` | ✓ | ✓ | ✓ | ✓ | 1 | none | 1 | | `75` | ✓ | ✓ | ✓ | ✓ | 0.7 | none | 1 | | `50` | ✓ | ✓ | ✓ | ✓ | 0.4 | 90ch | 1 | | `25` | - | - | - | ✓ | 0.1 | 80ch | 1.05 | | `0` | - | - | - | - | 0.05 | 75ch | 1.1 | At `0` the config also sets `readOnlyCursor: true` - rest mode is for consuming, not producing. The [reference stylesheet](/docs/energy-system/quickstart/css) implements this same table in CSS. ### notificationStrategy [#notificationstrategy] Reduces interruptions as energy drops. Resolves to a `NotificationConfig`: | Level | visual | sound | vibration | batch interval | priority threshold | | ----: | :----: | :---: | :-------: | :------------: | :----------------: | | `100` | ✓ | ✓ | ✓ | immediate | all | | `75` | ✓ | ✓ | - | immediate | all | | `50` | ✓ | - | - | 5 min | high | | `25` | ✓ | - | - | 15 min | critical | | `0` | - | - | - | - | none | This config is *guidance* on its own. The [notification gate](/docs/energy-system/concepts/notification-gate) is the runtime that enforces it - including the guarantee that nothing below the threshold is dropped, only deferred. ### taskComplexityStrategy [#taskcomplexitystrategy] Caps the complexity of work worth surfacing, and suggests break cadence: | Level | maxComplexity | suggestBreaks | break interval | | ----: | :-----------: | :-----------: | :------------: | | `100` | complex | - | - | | `75` | moderate | - | - | | `50` | routine | ✓ | 45 min | | `25` | simple | ✓ | 25 min | | `0` | consumption | - | - | Rest gets no break suggestions on purpose: prompting someone at `0` to take a break from resting is noise. ### interactionForgivenessStrategy [#interactionforgivenessstrategy] Lower energy means slower error detection, so forgiveness scales *inversely* with capacity - longer undo windows, confirmation on destructive actions, more frequent autosave: | Level | undo window | confirm destructive | autosave every | | ----: | :---------: | :-----------------: | :------------: | | `100` | 5s | - | 60s | | `75` | 8s | - | 45s | | `50` | 10s | ✓ | 30s | | `25` | 15s | ✓ | 20s | | `0` | 20s | ✓ | 15s | ### deferralStrategy [#deferralstrategy] Orders the ["not now" presets](/docs/energy-system/concepts/deferral) by level so a one-tap defer matches capacity. Resolves to `{ orderedPresetIds, defaultPresetId }`: | Level | default deferral | | ----: | ---------------- | | `100` | in 1 hour | | `75` | in 1 hour | | `50` | this evening | | `25` | tomorrow morning | | `0` | tomorrow morning | At low energy the default is "tomorrow morning", not "in 1 hour" - items should resurface when capacity has plausibly recovered. ### autonomyStrategy [#autonomystrategy] How much latitude automation has to act without asking. The mirror of `interactionForgivenessStrategy`: forgiveness protects against the *user's* mistakes at low energy, autonomy against the *agent's*. Resolves to an `AutonomyConfig`: | Level | confidence threshold | generated content | max unattended steps | | ----: | :------------------: | :---------------: | :------------------: | | `100` | 0.6 | ✓ | 8 | | `75` | 0.7 | ✓ | 5 | | `50` | 0.8 | ✓ | 3 | | `25` | 0.9 | templates only | 1 | | `0` | 1.0 | templates only | 1 | What narrows as energy falls is *discretion*, not action. At rest the automation may still take a single, certain, template-only step - the system acts on the user's behalf precisely when they are least able to supervise it, so the worst day is the wrong day for it to improvise. ### demandAdmissionStrategy [#demandadmissionstrategy] Who gets through to the user, and what happens to everyone else. Resolves to a `DemandAdmissionConfig`; the full model, including acknowledgments, lives in [Inbound demand](/docs/api/core/demand). | Level | originator threshold | acknowledge the rest | detail | | ----: | :------------------: | :------------------: | :-----: | | `100` | all | - | full | | `75` | known | ✓ | full | | `50` | exempt | ✓ | full | | `25` | exempt | ✓ | brief | | `0` | exempt | ✓ | minimal | Nothing is dropped at any level. What changes is whether demand reaches the inbox now or is acknowledged and queued - and how much the acknowledgment says. `createPresenceStrategy` is an eighth export in this family, but it is a *factory*: you hand it a presence map and it returns a strategy for that map. See [Presence](/docs/energy-system/concepts/presence). ## describe() - explaining behavior to users [#describe---explaining-behavior-to-users] Every strategy can explain itself at any level, which is useful for settings screens and onboarding: ```ts notificationStrategy.describe(50) // "Steady: Only high priority, batched every 5min" ``` Full strategy reference: [/docs/api/core](/docs/api/core). # Anasa (/docs/kumbatio/ecosystem/anasa) Anasa is the **creation layer** of the Kumbatio ecosystem: a local-first Markdown writing and thinking workspace designed to accommodate variable cognitive load. Anasa is in **public alpha** - you can download and use it today at [anasa.md](https://anasa.md). It's an alpha: features are real and shipping, but the product is still changing fast. This page describes what exists, not a finished product. ## The problem it addresses [#the-problem-it-addresses] Writing and thinking tools assume you arrive with focus. Dense interfaces, infinite options, and blank-page pressure all cost cognitive energy before you've written a word. On a low-capacity day, most knowledge workspaces are unusable - not because the features are bad, but because the tool assumes a brain at `100`. Anasa is built for the full range: a workspace that's rich when you have capacity and quiet when you don't. ## What exists today [#what-exists-today] Anasa is a desktop app (macOS, Windows, Linux) built on [`energy-system`](/docs/kumbatio/ecosystem/energy-system). The alpha ships: * **Local-first Markdown vaults** - your notes are plain files on your machine, with writing, connected notes, tasks, and daily notes on top. * **An energy-aware interface.** The whole shell adapts to your reported energy level: the energy battery is a first-class control, AI surfaces scale down as energy drops (full above `50`, minimal at `25`, gone at `0`), notifications are filtered by level, and task suggestions respect a complexity ceiling. * **End-to-end encrypted vault sync** - two-way whole-vault sync with a key derived on-device; conflicts are preserved as copies, never silently merged. * **Optional AI, local options included** - AI assistance is opt-in, with local models supported alongside cloud providers. Anasa is also where the energy model gets tested against real use: it records energy readings over time on-device and derives hour-of-day and day-of-week patterns, so the app can suggest a level with a stated confidence instead of guessing. The integration patterns it proved out are documented in the SDK's [production patterns guide](/docs/energy-system/guides/production-patterns). ## Where it sits in the ecosystem [#where-it-sits-in-the-ecosystem] | | | | -------- | ------------------------------------------- | | Layer | Creation | | Built on | `energy-system` | | Status | Public alpha - [anasa.md](https://anasa.md) | Anasa covers the *thinking and making* moment. [MPath](/docs/kumbatio/ecosystem/mpath) covers coordinating work, and [Nami](/docs/kumbatio/ecosystem/nami) covers support when executive function is low. # energy-system (/docs/kumbatio/ecosystem/energy-system) `energy-system` is the foundation of the Kumbatio ecosystem. It's a framework-agnostic TypeScript library for building **energy-aware applications** - and it's live, open source, and installable today. ```bash pnpm add @kumbatio/energy-system ``` This page is the ecosystem overview. For installation, API reference, strategies, and integration guides, see the full [energy-system documentation](/docs/energy-system). ## What it does [#what-it-does] Instead of adapting software to clock time, `energy-system` adapts behavior to current cognitive capacity. It models energy as explicit, self-reported application state - the five-level scale (`100 | 75 | 50 | 25 | 0`) described in [the thesis](/docs/kumbatio/thesis) - and resolves behavior strategies from that state. An application built with it can adjust complexity, notification load, and interaction patterns based on what a person can realistically handle right now. ## What ships in the box [#what-ships-in-the-box] * A framework-agnostic core engine with a rich, immutable state object * A strategy system mapping energy level to behavior config - UI visibility, notification filtering, task complexity guidance, interaction forgiveness, deferral ordering, agent autonomy, and inbound-demand admission * Presence annotation: declare which energy levels a component or view belongs to * Focus sessions: time-boxed suppression windows with auto-expiry and break nudges * A notification gate that enforces threshold, batching, and defer-not-drop at runtime * A DOM adapter, React provider and hooks, persistence adapters, and deterministic clocks for testing ## Why infrastructure first [#why-infrastructure-first] Kumbatio's thesis is that energy-awareness shouldn't be a feature of one app - it should be a capability any software can have. Shipping the model as an open-source SDK means: * **The apps share one model.** Anasa and Nami run on the same engine, so an energy level means the same thing in both. MPath's own model predates the SDK and is planned to bridge to it; the point of publishing the model as a package - and as a [spec with conformance vectors](/docs/energy-system/guides/spec-and-conformance) - is that bridging is a defined job rather than a guess. * **Anyone can build with it.** Developers outside Kumbatio can make their own products energy-aware without adopting anything else from the ecosystem. The first proof: [Meltemi](https://meltemi.app), an email client from [entro314 labs](https://github.com/entro314-labs) (the studio behind Kumbatio) built outside the Kumbatio product line, runs its notification deferral, focus sessions, and inbox scoping on this SDK - and nothing else from the ecosystem. * **The model is inspectable.** The claims Kumbatio makes about adaptation aren't marketing - they're readable code. ## In production [#in-production] The SDK isn't just installable - it's shipped: * **[Anasa](/docs/kumbatio/ecosystem/anasa)** - Kumbatio's writing workspace, in public alpha, runs its entire adaptive shell on the engine: custom persistence, energy-gated AI surfaces, notification filtering, and task-complexity guidance. * **[Meltemi](https://meltemi.app)** - an email client in private beta from entro314 labs, built outside the Kumbatio product line. Energy level drives inbox scope, undo-send windows, UI density, and a defer-not-drop notification gate. The widest use of the API surface anywhere: presence gating, deferral presets, autonomy limits, and inbound demand admission. * **[Nami](/docs/kumbatio/ecosystem/nami)** - Kumbatio's AI cognitive-support copilot, in development. Built on the React entry point: a provider at the root, with the level driving assistant copy, planning output, and how much the assistant may do unattended. * **[kumbat.io](https://kumbat.io)** - the marketing site itself adapts to the energy level you set on it. Two more entro314 labs applications are integrated and still pre-release: * **Entromail** - a webmail platform. The dashboard consumes the engine wholesale: level-derived thread density, snooze ordering, hint copy from `describeEnergy`, and interaction forgiveness across the reading surfaces. * **Equidock** - a desktop canvas app. Uses the engine and a custom SQLite persistence adapter for the dial itself; the strategy tables are not wired up yet. The patterns these integrations proved out are written up in the [production patterns guide](/docs/energy-system/guides/production-patterns). ## Where to go next [#where-to-go-next] Installation, quick starts, the strategy system, React integration, and API reference. Why the SDK models energy instead of time - the wh/ph/sd argument. # The ecosystem (/docs/kumbatio/ecosystem) Kumbatio currently has four products at different stages. They share the same five-level model of self-reported cognitive capacity. The products do different jobs and use different interfaces. They share the underlying energy model and the same [design principles](/docs/kumbatio/decision-filter). ## Current status [#current-status] Honest snapshot: **`energy-system` is live** - an open-source SDK you can install today. **Anasa is in public alpha** - you can download it at [anasa.md](https://anasa.md). **MPath and Nami are in development** and currently waitlist-only. | Product | Role | Layer | Runs on the SDK | Status | | --------------- | ----------------------------------------- | -------------- | --------------- | ---------------- | | `energy-system` | SDK for energy-aware application behavior | Infrastructure | - | **Live** | | Anasa | Writing and thinking workspace | Creation | Yes | **Public alpha** | | MPath | Project and work coordination | Coordination | Not yet | Waitlist | | Nami | AI cognitive support copilot | Support | Yes | Waitlist | ## The products [#the-products] The infrastructure layer. An open-source TypeScript SDK that models cognitive energy as application state. Live and installable today. The creation layer. A local-first writing and thinking workspace with an energy-aware interface. In public alpha at anasa.md. The coordination layer. Project and work coordination that adapts to actual human capacity. In development, waitlist open. The support layer. An AI cognitive support copilot for when executive function is low. In development, waitlist open. ## How they fit together [#how-they-fit-together] `energy-system` is the foundation - it defines the five-level energy model and the strategy system for adapting behavior to capacity. Anasa and Nami are built on it directly; MPath runs its own server-side energy model today and is planned to bridge to the SDK rather than replace its model with it. Each covers a different need: * **Create** with Anasa when you need to think and write. * **Coordinate** with MPath when you need to plan and track work - alone or with a team. * **Get support** from Nami when executive function is low and you need help getting through the day. One shared state can therefore mean the same thing in each product, even though the products respond to it differently. If you're a developer, you don't have to wait for the apps - you can build energy-awareness into your own product with the SDK today, the way [Meltemi](https://meltemi.app) - an email client from [entro314 labs](https://github.com/entro314-labs), the studio behind Kumbatio, built outside the Kumbatio product line - already has. Start with the [energy-system documentation](/docs/energy-system), and see the [production patterns guide](/docs/energy-system/guides/production-patterns) for how real apps integrate it. # MPath (/docs/kumbatio/ecosystem/mpath) MPath is the **coordination layer** of the Kumbatio ecosystem: project and work coordination that adapts to actual human capacity. MPath has not been released. It's in active development, and the waitlist is open at [kumbat.io](https://kumbat.io/waitlist?product=mpath). Everything on this page describes what's being built, not what you can use today. ## The problem it addresses [#the-problem-it-addresses] Project tools plan in time: deadlines, sprints, hours estimated and hours logged. But time isn't what produces output - [productive capacity is](/docs/kumbatio/thesis). A plan that assumes every hour is equal breaks the moment a real person has a low week, and then the tool makes it worse: red overdue markers, slipping burndown charts, pressure mechanics. MPath is being built to coordinate work around what people can actually do - useful for individuals, and especially for mixed-neurotype teams where capacity varies across people as well as across days. ## What's being built [#whats-being-built] MPath is an energy-aware coordination system with multi-level planning. It uses mountain metaphors for its planning hierarchy - strategy down to individual tasks - the "Mountain Path" that gives the product its name. Unlike the other products, MPath does **not** currently run on [`energy-system`](/docs/kumbatio/ecosystem/energy-system). Its energy model is server-side and predates the SDK: a numeric level plus a qualitative reading, logged per check-in and stored in Postgres, because coordination needs history and cross-user aggregation that a client-side engine does not provide. Adopting the SDK here means bridging the two through [`createExternalLevelCompatibility`](/docs/api/core/metrics-and-compat) rather than swapping one for the other - product work, not a mechanical migration. It is planned, not done. The model is well developed; the implementation is still being built and aligned. Because it isn't released, we won't present the design as a finished feature list. When MPath ships, this page will document what it actually does. ## Where it sits in the ecosystem [#where-it-sits-in-the-ecosystem] | | | | ------------ | ------------------------------------------------------- | | Layer | Coordination | | Energy model | Own server-side model; `energy-system` adoption planned | | Status | In development, waitlist open | MPath covers the *planning and tracking* moment. [Anasa](/docs/kumbatio/ecosystem/anasa) covers creating, and [Nami](/docs/kumbatio/ecosystem/nami) covers support when executive function is low. # Nami (/docs/kumbatio/ecosystem/nami) Nami is the **support layer** of the Kumbatio ecosystem: an AI cognitive support copilot for when executive function is low. Nami has not been released. It's in active development, and the waitlist is open at [kumbat.io](https://kumbat.io/waitlist?product=nami). Everything on this page describes what's being built, not what you can use today. ## The problem it addresses [#the-problem-it-addresses] Executive dysfunction is the gap between knowing what to do and being able to start doing it. On a hard day, planning a task, remembering context, or even choosing what to eat can be genuinely out of reach - and most AI assistants make it worse by demanding well-formed prompts and returning walls of options. Nami is being built as a copilot for exactly those moments: support that meets you at low capacity instead of assuming you arrive articulate and organized. ## What's being built [#whats-being-built] Nami is a neurodivergent-focused AI assistant built on [`energy-system`](/docs/kumbatio/ecosystem/energy-system), with a calm, capacity-aware UX. The SDK is not a veneer here: the energy level drives the assistant's copy, the shape of its planning output, and what it is allowed to do unattended, all resolved from the shared model rather than re-invented. The direction includes conversational support, planning help, energy tracking, recovery support, and memory that carries context so you don't have to re-explain yourself on a bad day. Because it isn't released, we won't present the design as a finished feature list. When Nami ships, this page will document what it actually does. Nami is workflow and self-management support. It is not therapy, not a crisis service, and not a substitute for professional mental health care. That boundary is a design principle, not fine print. ## Where it sits in the ecosystem [#where-it-sits-in-the-ecosystem] | | | | -------- | ----------------------------- | | Layer | Support | | Built on | `energy-system` | | Status | In development, waitlist open | Nami covers the *getting through the day* moment. [Anasa](/docs/kumbatio/ecosystem/anasa) covers creating, and [MPath](/docs/kumbatio/ecosystem/mpath) covers coordinating work. *** *Kumbatio products support self-management, workflow, and cognitive energy awareness. They are not medical diagnosis tools or treatment, and are not a replacement for professional mental health support.* # Deferral (/docs/api/core/deferral) Deferral ("snooze") is the "not now" primitive. Deferring an item is an energy statement: it declares insufficient capacity for it right now and names when it should resurface. Presets are pure `(now: Date) => Date` functions; the energy-aware strategy orders them so the default suggestion matches current capacity - low energy means longer deferrals, because items should resurface when capacity has plausibly recovered, not in an hour. ## DEFERRAL\_PRESET\_IDS [#deferral_preset_ids] ```ts const DEFERRAL_PRESET_IDS: Readonly<{ inOneHour: 'in-1-hour' thisEvening: 'this-evening' tomorrowMorning: 'tomorrow-morning' nextWorkday: 'next-workday' nextMonday: 'next-monday' }> ``` Stable preset ids, exported so configs/strategies can reference them. ## DeferralPreset [#deferralpreset] A named deferral option. /> ## createDeferralPresets [#createdeferralpresets] ```ts function createDeferralPresets(options?: DeferralPresetOptions): readonly DeferralPreset[] ``` Build the standard deferral presets. Times are computed in **local time** - "tomorrow morning" means the user's morning. Throws for hours outside the integer range 0-23. ### DeferralPresetOptions [#deferralpresetoptions] /> ### The five presets [#the-five-presets] | Id | Label | Resurface time | | ------------------ | ----------------------- | ----------------------------------------------------------------------------- | | `in-1-hour` | In 1 hour | `now` + 60 minutes | | `this-evening` | This evening (18:00) | Today at `eveningHour`; if that has already passed, tomorrow at `eveningHour` | | `tomorrow-morning` | Tomorrow morning (9:00) | Tomorrow at `morningHour` | | `next-workday` | Next workday (9:00) | Next non-weekend day at `morningHour` | | `next-monday` | Next Monday (9:00) | The coming Monday at `morningHour` (a full week ahead when today is Monday) | ```ts import { createDeferralPresets, resolveDeferral, DEFERRAL_PRESET_IDS } from '@kumbatio/energy-system' const presets = createDeferralPresets({ morningHour: 8 }) const resurfaceAt = resolveDeferral(presets, DEFERRAL_PRESET_IDS.tomorrowMorning) // epoch ms for tomorrow 08:00 local time ``` ## resolveDeferral [#resolvedeferral] ```ts function resolveDeferral( presets: readonly DeferralPreset[], presetId: string, now?: Date, // default: new Date() ): number | null ``` Resolve a preset id to a resurface timestamp (epoch ms). Returns `null` for an unknown id - callers decide whether that is an error. ## deferralStrategy [#deferralstrategy] ```ts const deferralStrategy: AdaptationStrategy ``` The energy-aware ordering strategy (`name: 'deferral'`). Resolve it directly or through the engine like any [built-in strategy](/docs/api/core/strategies). ### DeferralConfig [#deferralconfig] /> ### Values per level [#values-per-level] | Level | orderedPresetIds | defaultPresetId | | ----- | ------------------------------------------------------------------------------ | ------------------ | | `100` | `in-1-hour`, `this-evening`, `tomorrow-morning`, `next-workday`, `next-monday` | `in-1-hour` | | `75` | `in-1-hour`, `this-evening`, `tomorrow-morning`, `next-workday`, `next-monday` | `in-1-hour` | | `50` | `this-evening`, `tomorrow-morning`, `in-1-hour`, `next-workday`, `next-monday` | `this-evening` | | `25` | `tomorrow-morning`, `next-workday`, `this-evening`, `next-monday`, `in-1-hour` | `tomorrow-morning` | | `0` | `tomorrow-morning`, `next-monday`, `next-workday`, `this-evening`, `in-1-hour` | `tomorrow-morning` | # Inbound demand (/docs/api/core/demand) **Inbound demand** is anything arriving from outside that asks for the user's attention or action: an email, a document comment, a review request, a task assignment, a collaboration invite. Every triage system in general use is organised around properties of the *message* - who sent it, how urgent it claims to be, what category it fits. None is organised around the state of the *recipient*, which is the thing that actually decides whether an arrival is a small task or a crushing weight. This is that variable, applied to the queue. ## What this module is, and is not [#what-this-module-is-and-is-not] It is policy, and it is pure. [`demandAdmissionStrategy`](#demandadmissionstrategy) resolves the rules for a level; [`resolveDemandOutcome`](#resolvedemandoutcome) applies them to one arrival and returns a decision. It performs no effects. Acknowledging an originator means sending mail, posting a comment, or updating a status chip depending on the app, and those are irreversible in ways an in-process runtime cannot make transactional - unlike the [notification gate](/docs/api/core/sessions-and-gates#createnotificationgate), whose defer-never-drop guarantee is enforceable precisely because nothing it touches leaves the process. The orchestration, with its ordering, retries, and deduplication, belongs to the consuming app. The practical shape of that ordering: **capture first, acknowledge second**. A capture is the reversible half, so a failed acknowledgment can roll it back. An acknowledgment cannot be unsent. ## resolveDemandOutcome [#resolvedemandoutcome] ```ts function resolveDemandOutcome( config: DemandAdmissionConfig, autonomy: AutonomyConfig, demand: DemandInput, ): DemandOutcome ``` The pure gating decision - the counterpart of [`resolveNotificationOutcome`](/docs/api/core/sessions-and-gates#resolvenotificationoutcome), and testable the same way, with no wiring. Both configs are required because the two questions are genuinely separate: `config` says what the level's policy wants done, [`autonomy`](/docs/api/core/strategies#autonomystrategy) says how much of it may happen without the user watching. ```ts import { autonomyStrategy, deferralStrategy, demandAdmissionStrategy, resolveDemandOutcome, } from '@kumbatio/energy-system' const outcome = resolveDemandOutcome( engine.resolve(demandAdmissionStrategy), engine.resolve(autonomyStrategy), { originatorTier: 'unknown', bearsObligation: true, confidence: 0.9 }, ) switch (outcome.admission) { case 'live': return inbox.deliver(item) case 'acknowledge': // One act, never two. Capture first - it is the half you can take back. await tasks.capture(item, engine.resolve(deferralStrategy).defaultPresetId) return replies.acknowledge(item, outcome.acknowledgment) case 'silent': return tasks.capture(item, engine.resolve(deferralStrategy).defaultPresetId) } ``` Throws for an invalid `originatorTier`, or a `confidence` outside `0–1`. ### DemandInput [#demandinput] /> ### DemandOutcome [#demandoutcome] /> Outcomes are frozen. ### DemandAcknowledgment [#demandacknowledgment] /> The two axes are independent on purpose. Length comes from the level's admission config; whether the wording may be composed at all comes from autonomy. Collapsing them would make `brief` unreachable. ## Types [#types] ### OriginatorTier [#originatortier] ```ts type OriginatorTier = 'exempt' | 'known' | 'unknown' ``` * `exempt` - the inner circle. Always admitted live, at every level, and never acknowledged by machine. * `known` - an established correspondent. * `unknown` - no established relationship. Tier assignment is the app's job: a screener approval, a contacts list, an org chart. The policy only consumes the tier. The exempt rule is also what defuses the gaming risk. An originator who learns that an acknowledgment means "deprioritised" and escalates through another channel only succeeds if their escalation is one the user cannot ignore - which is what makes them exempt in the first place. ### DemandAdmission [#demandadmission] ```ts type DemandAdmission = 'live' | 'acknowledge' | 'silent' ``` * `live` - reaches the user now, untouched by this policy. * `acknowledge` - the originator is acknowledged and the obligation is captured for later. The two are one act: an acknowledgment without a capture is a promise nobody kept, a capture without an acknowledgment leaves the originator in silence. * `silent` - captured for later with no acknowledgment. ### AcknowledgmentDetail [#acknowledgmentdetail] ```ts type AcknowledgmentDetail = 'full' | 'brief' | 'minimal' ``` How much the acknowledgment may say. Every variant should report system state and never intent: "received and queued, current response horizon is early next week" is a fact, while "I'll get back to you soon" is a promise the user's Tuesday self has to keep. Take the horizon from [`deferralStrategy`](/docs/api/core/deferral#deferralstrategy) so the queue and the acknowledgment cannot disagree. ### DemandOutcomeReason [#demandoutcomereason] ```ts type DemandOutcomeReason = | 'exempt-originator' | 'tier-admitted' | 'no-obligation' | 'acknowledgment-disabled' | 'below-confidence' | 'acknowledged' ``` ### isOriginatorTier [#isoriginatortier] ```ts function isOriginatorTier(value: unknown): value is OriginatorTier ``` Runtime validation for a tier arriving from outside the type system. ## demandAdmissionStrategy [#demandadmissionstrategy] ```ts const demandAdmissionStrategy: AdaptationStrategy ``` ### DemandAdmissionConfig [#demandadmissionconfig] /> ### Values per level [#values-per-level] | Level | originatorThreshold | acknowledge | acknowledgmentDetail | | ----- | ------------------- | ----------- | -------------------- | | `100` | `all` | false | `full` | | `75` | `known` | true | `full` | | `50` | `exempt` | true | `full` | | `25` | `exempt` | true | `brief` | | `0` | `exempt` | true | `minimal` | At full capacity there is no policy at all: everything reaches the user. At rest the acknowledgment survives, stripped to a fixed template - the originator's social debt still clears, which is the whole point of the loop, but nothing is composed on the user's behalf. Whether an acknowledgment actually goes out at rest depends on the app's classifier. The level permits one at certainty; a heuristic classifier reporting less than `1` will queue in silence instead, which is usually the right answer. ## Disclosure is the app's job [#disclosure-is-the-apps-job] An automated action toward a third party must be identifiable as automated. The library does not enforce this because the medium is the app's: an `Auto-Submitted: auto-replied` header on email (RFC 3834, which also stops two auto-responders looping), an "auto-queued" badge on a comment reply, a system-attributed status chip. Undisclosed automation speaking in a user's name is the failure mode this design exists to avoid. # Engine (/docs/api/core/engine) ## createEnergyEngine [#createenergyengine] ```ts function createEnergyEngine(options?: EnergyEngineOptions): EnergyEngine ``` Creates a stateful engine that owns an [`EnergyState`](/docs/api/core/types#energystate), notifies subscribers on transitions, resolves [adaptation strategies](/docs/api/core/strategies), and (optionally) persists state through an [`EnergyPersistence`](/docs/api/core/types#energypersistence) adapter. When a `persistence` adapter is supplied, the engine **auto-hydrates** on creation (an internal call to `hydrate()`) and, if the adapter implements `observe`, subscribes to externally persisted state updates (cross-tab, worker) for the engine's lifetime. ```ts import { createEnergyEngine } from '@kumbatio/energy-system' import { localStoragePersistence } from '@kumbatio/energy-system/persistence' const engine = createEnergyEngine({ initialLevel: 100, persistence: localStoragePersistence(), onChange: (state, prev) => console.log(prev.level, '->', state.level), }) ``` ### EnergyEngineOptions [#energyengineoptions] ## EnergyEngine [#energyengine] ### start [#start] ```ts start(): void ``` Begins hydration and cross-context observation. Idempotent, and a no-op on a disposed engine or one without persistence. Only needed when the engine was created with `autoStart: false`. Deferring these is what keeps an engine safe to construct during a React render. React discards in-progress renders, and only a committed tree runs effects - an engine that hydrated and subscribed at construction would strand a live `storage` listener with nothing left to release it. [`EnergyProvider`](/docs/api/react#energyprovider) does exactly this internally. ### getState [#getstate] ```ts getState(): EnergyState ``` Returns the current (frozen) energy state snapshot. ### setLevel [#setlevel] ```ts setLevel(level: EnergyLevel, source?: EnergySource): void ``` Sets the energy level. `source` defaults to `'manual'`. The new state's ordering key is guaranteed strictly newer than the previous one: the timestamp is clamped to never go backwards, and when the clock has not advanced the `revision` counter is incremented instead (or the timestamp bumped by 1 ms if `revision` would overflow `Number.MAX_SAFE_INTEGER`). No-op on a disposed engine. ### cycleLevel [#cyclelevel] ```ts cycleLevel(): void ``` Advances to the next level in cycle order `100 -> 75 -> 50 -> 25 -> 0 -> 100`, with source `'manual'`. No-op on a disposed engine. ### subscribe [#subscribe] ```ts subscribe(listener: EnergyChangeListener): () => void ``` Subscribes to state changes; returns an unsubscribe function. Listener exceptions are caught and logged, never propagated. On a disposed engine, returns a no-op unsubscriber without registering the listener. ### resolve [#resolve] ```ts resolve(strategy: AdaptationStrategy): T ``` Resolves an [`AdaptationStrategy`](/docs/api/core/types#adaptationstrategy) against the current level - equivalent to `strategy.resolve(engine.getState().level)`. ### hydrate [#hydrate] ```ts hydrate(): Promise ``` Loads persisted state. Called automatically by [`start()`](#start) when `persistence` is configured - at construction unless `autoStart: false` - but can be called manually. Loaded state is validated (level, source, future-skew via `maxFutureSkewMs`); invalid state is logged and ignored. The loaded state is applied when no local transition happened during the load, or when it wins reconciliation against the current state (see below). No-op without persistence or when disposed. ### flush [#flush] ```ts flush(): Promise ``` Waits until the current state version is durably persisted. Resolves immediately without persistence. Rejects if the engine is disposed, or if an unchanged initial state cannot be reconciled because the persistence hydration read failed (the engine refuses to overwrite an unread stored value with the default state). ### dispose [#dispose] ```ts dispose(): void ``` Releases engine-owned resources: unsubscribes persistence observation, cancels retry timers, clears listeners, and rejects pending `flush()` waiters with `Error('Energy engine disposed before persistence completed')`. A disposed engine is inert - it never mutates state, notifies, or persists again. ## isPreferredEnergyState [#ispreferredenergystate] ```ts function isPreferredEnergyState(candidate: EnergyState, current: EnergyState): boolean ``` Should `candidate` replace `current`? The engine's own reconciliation rule, exported because it is the hardest part of the model to reimplement correctly and because anything sharing energy state across contexts needs exactly this answer. The comparison walks four keys in order, stopping at the first that differs: 1. `timestamp` - greater wins. 2. `revision` - greater wins. Two writes inside one clock tick are not simultaneous. 3. `source` - `manual` > `scheduled` > `inferred`. Nothing the system worked out on its own overwrites what the person said. 4. `origin` - lexicographically greater wins. Key 4 is arbitrary, deliberately. When two producers write the same instant, the same revision, and the same class of source there is no principled winner - and an arbitrary rule every context computes *identically* beats a coin flip each context tosses separately. Convergence is the property that matters. Equal on every key returns `false`: an identical state is not a change and must not fire a notification. ```ts import { isPreferredEnergyState } from '@kumbatio/energy-system' // Merging what another device reported: if (isPreferredEnergyState(remote, engine.getState())) { engine.setLevel(remote.level, remote.source) } ``` The relation is antisymmetric, and an implementation of it must stay that way. If both directions ever reported "preferred", two contexts observing each other would swap states forever. The [conformance vectors](/docs/energy-system/guides/spec-and-conformance) assert both directions of every pair for this reason. ## Behavior notes [#behavior-notes] Saves are queued asynchronously and coalesced: only the newest state version is guaranteed to be written. On save failure the engine logs, invokes `onPersistenceError`, and retries with exponential backoff starting at 250 ms and capped at 30 s. States arriving from hydration or `persistence.observe` are compared to the current state deterministically, in order: higher `timestamp` wins, then higher `revision`, then source priority (`manual` over `scheduled` over `inferred`), then lexicographically greater `origin`, and finally higher `level` as a last-resort tiebreak for malformed duplicate identities. The comparison is exported as [`isPreferredEnergyState`](#ispreferredenergystate) and specified normatively in [SPEC.md §4](/docs/energy-system/guides/spec-and-conformance). All callback errors (onChange, subscribers, onPersistenceError, persistence observation) are caught and logged with a `[energy-system]` prefix via console.error; they never break the engine. # Core Entry (/docs/api/core) ```ts import { createEnergyEngine, uiVisibilityStrategy /* ... */ } from '@kumbatio/energy-system' ``` The core entry is pure TypeScript with no DOM or React dependency. It exports the engine, the domain types, the five-level model, seven built-in adaptation strategies, presence annotation, focus sessions, the notification gate, deferral presets, the inbound-demand policy, derived metrics, and compatibility helpers for external level models. ## Export map [#export-map] | Category | Values | Types | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Engine](/docs/api/core/engine) | `createEnergyEngine` | `EnergyEngine`, `EnergyEngineOptions` | | [Types and constants](/docs/api/core/types) | `ENERGY_LEVEL_VALUES`, `ENERGY_PRESENCE_VALUES`, `ENERGY_SOURCE_VALUES` | `EnergyLevel`, `EnergySource`, `EnergyState`, `EnergyClock`, `EnergyChangeListener`, `EnergyPresence`, `EnergyPresenceMap`, `CognitiveProfile`, `DecisionCapacity`, `FocusDuration`, `TaskComplexity`, `InterruptionTolerance`, `EnergyLevelDefinition`, `AdaptationStrategy`, `EnergyPersistence`, `EnergyMetrics` | | [Levels](/docs/api/core/levels) | `createEnergyState`, `cycleEnergyLevel`, `getEnergyLevel`, `getEnergyLevels`, `isEnergyLevel`, `isEnergySource`, `isHigherEnergy` | - | | [Strategies](/docs/api/core/strategies) | `uiVisibilityStrategy`, `notificationStrategy`, `taskComplexityStrategy`, `interactionForgivenessStrategy`, `deferralStrategy` | `UIVisibilityConfig`, `NotificationConfig`, `TaskComplexityConfig`, `InteractionForgivenessConfig`, `DeferralConfig` | | [Presence](/docs/api/core/presence) | `defineEnergyPresence`, `presenceAtOrAbove`, `presenceAtOrBelow`, `resolveEnergyPresence`, `isEnergyPresence`, `isPresenceVisible`, `createPresenceStrategy` | `EnergyPresenceSpec` | | [Focus sessions](/docs/api/core/sessions-and-gates) | `createFocusSessionController`, `isSessionExpired`, `sessionRemainingMs` | `FocusSession`, `FocusSessionController`, `FocusSessionControllerOptions`, `FocusSessionEvent`, `FocusSessionListener`, `FocusSuppressible`, `StartFocusSessionOptions` | | [Notification gate](/docs/api/core/sessions-and-gates) | `createNotificationGate`, `isNotificationPriority`, `resolveNotificationOutcome` | `EnergyNotification`, `GateScheduler`, `NotificationChannels`, `NotificationDelivery`, `NotificationDeliveryReason`, `NotificationGate`, `NotificationGateOptions`, `NotificationPriority`, `PublishOutcome` | | [Deferral](/docs/api/core/deferral) | `DEFERRAL_PRESET_IDS`, `createDeferralPresets`, `deferralStrategy`, `resolveDeferral` | `DeferralConfig`, `DeferralPreset`, `DeferralPresetOptions` | | [Metrics](/docs/api/core/metrics-and-compat) | `getEnergyMetrics` | `EnergyMetrics` | | [Compatibility](/docs/api/core/metrics-and-compat) | `createExternalLevelCompatibility`, `cycleDiscreteLevel`, `mapToNearestDiscreteLevel`, `mapToNearestEnergyLevel` | `ExternalLevelCompatibility`, `ExternalLevelCompatibilityOptions` | ## Minimal example [#minimal-example] ```ts import { createEnergyEngine, uiVisibilityStrategy } from '@kumbatio/energy-system' const engine = createEnergyEngine({ initialLevel: 75 }) engine.subscribe((state, prev) => { console.log(`energy ${prev.level} -> ${state.level}`) }) engine.setLevel(50) const ui = engine.resolve(uiVisibilityStrategy) // UIVisibilityConfig for level 50 ``` # Levels (/docs/api/core/levels) ## The five levels [#the-five-levels] The level model is fixed. `getEnergyLevels()` returns these definitions, ordered highest to lowest, all deeply frozen: | Value | Key | Label | Description | | ----- | -------- | ------ | ---------------------------------------------------------- | | `100` | `peak` | Peak | High capacity. Planning, complex decisions, creative work. | | `75` | `active` | Active | Good capacity. Focused execution, problem-solving. | | `50` | `steady` | Steady | Moderate capacity. Routine tasks, familiar work. | | `25` | `low` | Low | Limited capacity. Simple tasks, review, light work. | | `0` | `rest` | Rest | Recovery. Consumption only - reading, reflecting. | Cognitive profiles per level: | Level | decisionCapacity | focusDuration | taskComplexity | interruptionTolerance | | ----- | ---------------- | ------------- | -------------- | --------------------- | | `100` | `high` | `extended` | `complex` | `high` | | `75` | `moderate` | `moderate` | `moderate` | `moderate` | | `50` | `low` | `short` | `routine` | `low` | | `25` | `minimal` | `minimal` | `simple` | `minimal` | | `0` | `none` | `none` | `consumption` | `none` | ## getEnergyLevels [#getenergylevels] ```ts function getEnergyLevels(): ReadonlyArray> ``` Get all energy level definitions, ordered highest to lowest (`100, 75, 50, 25, 0`). ## getEnergyLevel [#getenergylevel] ```ts function getEnergyLevel(level: EnergyLevel): Readonly ``` Get the definition for a specific energy level. Throws `Error('Invalid energy level: ...')` for values outside the model. ```ts import { getEnergyLevel } from '@kumbatio/energy-system' getEnergyLevel(75).label // 'Active' ``` ## cycleEnergyLevel [#cycleenergylevel] ```ts function cycleEnergyLevel(current: EnergyLevel): EnergyLevel ``` Cycle to the next energy level: `100 -> 75 -> 50 -> 25 -> 0 -> 100`. Returns `100` when `current` is not a known level. ## isEnergyLevel [#isenergylevel] ```ts function isEnergyLevel(value: unknown): value is EnergyLevel ``` Validate that an unknown value is a valid `EnergyLevel`. ## isEnergySource [#isenergysource] ```ts function isEnergySource(value: unknown): value is EnergySource ``` Validate that an unknown value is a valid `EnergySource`. ## isHigherEnergy [#ishigherenergy] ```ts function isHigherEnergy(a: EnergyLevel, b: EnergyLevel): boolean ``` Returns `true` if level `a` represents higher energy than level `b` (numeric comparison). ## createEnergyOrigin [#createenergyorigin] ```ts function createEnergyOrigin(): string ``` Mint a unique producer identity for deterministic cross-context ordering. Uses `crypto.randomUUID()` when available, falling back to `crypto.getRandomValues()` and then to a time-plus-counter string, so it never throws in older runtimes. The engine calls this internally when you don't pass `originId` to `createEnergyEngine` - you only need it yourself to mint a **stable per-install identity** that survives restarts. Both shipped desktop integrations do exactly that: ```ts import { createEnergyOrigin, createEnergyEngine } from '@kumbatio/energy-system' function stableOriginId(): string { const KEY = 'myapp:energy-origin' let id = localStorage.getItem(KEY) if (!id) { id = `myapp:app:${createEnergyOrigin()}` localStorage.setItem(KEY, id) } return id } const engine = createEnergyEngine({ originId: stableOriginId() }) ``` Available since `0.5.4`; earlier versions used it internally without exporting it. See [Production Patterns](/docs/energy-system/guides/production-patterns) for the write-identity discipline around origins and revisions. ## createEnergyState [#createenergystate] ```ts function createEnergyState( level: EnergyLevel, source?: EnergySource, // default: 'manual' timestamp?: number, // default: Date.now() revision?: number, // default: module-level auto-incrementing counter origin?: string, // default: a stable per-process random origin id ): EnergyState ``` Create a frozen `EnergyState` for the current moment. Intended for standalone use (tests, persistence adapters, external producers); the engine constructs its own states via `setLevel`. Validation - each check throws an `Error` on failure: | Parameter | Requirement | | ----------- | -------------------------- | | `level` | Must pass `isEnergyLevel` | | `source` | Must pass `isEnergySource` | | `timestamp` | Finite number, `>= 0` | | `revision` | Safe integer, `>= 0` | | `origin` | Non-empty string | ```ts import { createEnergyState } from '@kumbatio/energy-system' const state = createEnergyState(50, 'scheduled') // { level: 50, source: 'scheduled', timestamp: ..., revision: ..., origin: '...' } ``` ## The unproduced state [#the-unproduced-state] An engine starts on a state nobody chose: the `initialLevel` default, before any user action, persisted value or observed update has replaced it. That default is stamped with sentinels rather than a real clock reading and a real identity. ```ts const UNPRODUCED_TIMESTAMP = 0 const UNPRODUCED_ORIGIN = '0-initial' ``` Two reasons they exist. Construction reads neither the clock nor the random source, which is what makes building an engine during a React render prerender-safe - Next.js Cache Components fails a build on unstable values baked into static output. And both sentinels sort below any real value, which is also the semantics you want: a persisted or observed state must always beat the default it replaces. ### isUnproducedState [#isunproducedstate] ```ts function isUnproducedState(state: Pick): boolean ``` True for the untouched default - its age and identity are not meaningful. Use it before treating `state.timestamp` as a real moment. ```ts import { isUnproducedState } from '@kumbatio/energy-system' // Without the guard, an untouched engine reports an age measured from the epoch. const ageMs = isUnproducedState(state) ? 0 : Date.now() - state.timestamp ``` [`getEnergyMetrics`](/docs/api/core/metrics-and-compat#getenergymetrics) already applies this guard, reporting `stateAgeMs: 0` for a state nobody has set yet. # Metrics and Compatibility (/docs/api/core/metrics-and-compat) ## getEnergyMetrics [#getenergymetrics] ```ts function getEnergyMetrics(state: EnergyState, now?: number): EnergyMetrics ``` Derive app-agnostic [`EnergyMetrics`](/docs/api/core/types#energymetrics) from the current state. `now` defaults to `Date.now()`; non-finite values fall back to `Date.now()`. State age is clamped to be non-negative. The result is frozen; `recoveryHintMinutes` is only present for levels 50, 25, and 0. ```ts import { createEnergyEngine, getEnergyMetrics } from '@kumbatio/energy-system' const engine = createEnergyEngine({ initialLevel: 50 }) const metrics = getEnergyMetrics(engine.getState()) metrics.expectedProductivityWindowMinutes // 45 metrics.sustainable // true metrics.recoveryHintMinutes // 10 ``` ### Values per level [#values-per-level] | Level | expectedProductivityWindowMinutes | suggestedBreakIntervalMinutes | recommendedTaskComplexity | sustainable | recoveryHintMinutes | | ----- | --------------------------------- | ----------------------------- | ------------------------- | ----------- | ------------------- | | `100` | 120 | 90 | `complex` | false | - | | `75` | 90 | 60 | `moderate` | true | - | | `50` | 45 | 45 | `routine` | true | 10 | | `25` | 25 | 25 | `simple` | true | 20 | | `0` | 0 | 0 | `consumption` | false | 30 | Rest (0) has no break cadence: the user is already resting, so there is nothing to take a break from. `suggestedBreakIntervalMinutes: 0` means "no breaks suggested". *** ## Compatibility helpers [#compatibility-helpers] Bridges for systems that use non-native level values (e.g. a legacy 4-level model) while keeping the package's fixed 5-level model unchanged. ## cycleDiscreteLevel [#cyclediscretelevel] ```ts function cycleDiscreteLevel( current: number, levels: readonly TLevel[], fallback: TLevel, ): TLevel ``` Cycle through any discrete numeric level list. Returns the element after `current` in `levels` (wrapping), or `fallback` when `current` is not in the list. ## mapToNearestDiscreteLevel [#maptonearestdiscretelevel] ```ts function mapToNearestDiscreteLevel( value: number, levels: readonly TLevel[], fallback: TLevel, ): TLevel ``` Map an arbitrary number to the nearest available discrete level. Returns `fallback` for an empty list or a non-finite `value`. ## mapToNearestEnergyLevel [#maptonearestenergylevel] ```ts function mapToNearestEnergyLevel(value: number): EnergyLevel ``` Map any number to the closest native package energy level (`100, 75, 50, 25, 0`); falls back to `100` for non-finite input. ```ts mapToNearestEnergyLevel(66) // 75 mapToNearestEnergyLevel(10) // 0 ``` ## createExternalLevelCompatibility [#createexternallevelcompatibility] ```ts function createExternalLevelCompatibility( options: ExternalLevelCompatibilityOptions, ): ExternalLevelCompatibility ``` Build a compatibility bridge for systems that use non-native level values. Useful during migrations while keeping the native model unchanged. The returned object is frozen. Validation - each throws an `Error`: * `levels` must be non-empty, all finite, and unique * `fallbackLevel` must be present in `levels` * every level in `levels` must have a `toEnergyLevel` mapping to a valid native level * `fallbackEnergyLevel`, when provided, must be a valid native level ### ExternalLevelCompatibilityOptions [#externallevelcompatibilityoptions] /> ### ExternalLevelCompatibility [#externallevelcompatibility] /> ```ts import { createExternalLevelCompatibility } from '@kumbatio/energy-system' const legacy = createExternalLevelCompatibility({ levels: [100, 66, 33, 0], toEnergyLevel: { 100: 100, 66: 75, 33: 25, 0: 0 }, fallbackLevel: 100, }) legacy.toEnergyLevel(66) // 75 legacy.fromEnergyLevel(50) // 66 (closest mapped native level) legacy.cycleExternalLevel(66) // 33 legacy.cycleMappedEnergyLevel(66) // 25 ``` # Presence (/docs/api/core/presence) Presence annotation maps every [`EnergyLevel`](/docs/api/core/types#energylevel) to an [`EnergyPresence`](/docs/api/core/types#energypresence) (`'visible' | 'muted' | 'hidden'`). The React counterpart is [`EnergyGate`](/docs/api/react#energygate) / [`useEnergyPresence`](/docs/api/react#useenergypresence); the CSS-only counterpart is the [`data-energy-min` / `data-energy-max` attributes](/docs/api/css#presence-gating-attributes). ## EnergyPresenceSpec [#energypresencespec] ```ts type EnergyPresenceSpec = Partial> & { default?: EnergyPresence } ``` Per-level presence spec. Unlisted levels fall back to `default` (`'visible'` when omitted). ## defineEnergyPresence [#defineenergypresence] ```ts function defineEnergyPresence(spec?: EnergyPresenceSpec): EnergyPresenceMap ``` Build a complete, frozen presence map from a partial spec. Throws for invalid presence values (in `default` or any level entry). ```ts import { defineEnergyPresence } from '@kumbatio/energy-system' // Hide the AI chat at 50 and below, keep it muted at 75: const aiChatPresence = defineEnergyPresence({ default: 'visible', 75: 'muted', 50: 'hidden', 25: 'hidden', 0: 'hidden', }) ``` ## presenceAtOrAbove [#presenceatorabove] ```ts function presenceAtOrAbove(min: EnergyLevel, below?: EnergyPresence): EnergyPresenceMap ``` Presence map for elements that need at least `min` energy. At `min` and above the element is `visible`; below `min` it is `below` (default `'hidden'`). Throws for an invalid level or presence. ```ts const composerToolbar = presenceAtOrAbove(50) // hidden at 25 and 0 const aiSidebar = presenceAtOrAbove(75, 'muted') // muted below 75 ``` ## presenceAtOrBelow [#presenceatorbelow] ```ts function presenceAtOrBelow(max: EnergyLevel, above?: EnergyPresence): EnergyPresenceMap ``` Presence map for elements that only belong at low energy - recovery hints, "one thing at a time" affordances. At `max` and below the element is `visible`; above `max` it is `above` (default `'hidden'`). Throws for an invalid level or presence. ## resolveEnergyPresence [#resolveenergypresence] ```ts function resolveEnergyPresence(presence: EnergyPresenceMap, level: EnergyLevel): EnergyPresence ``` Resolve the presence of an element for a given energy level. Throws for an invalid level, or when the map has no valid entry for it. ## isPresenceVisible [#ispresencevisible] ```ts function isPresenceVisible(presence: EnergyPresence): boolean ``` Returns `true` unless the presence is `'hidden'` (i.e. `'muted'` counts as visible). ## isEnergyPresence [#isenergypresence] ```ts function isEnergyPresence(value: unknown): value is EnergyPresence ``` Validate that an unknown value is a valid `EnergyPresence`. ## createPresenceStrategy [#createpresencestrategy] ```ts function createPresenceStrategy( name: string, presence: EnergyPresenceMap, ): AdaptationStrategy ``` Lift a presence map into an [`AdaptationStrategy`](/docs/api/core/types#adaptationstrategy) so it can be resolved through the engine like any built-in strategy. The full map is validated once at creation, so the returned `resolve()` can never fail. Throws for an empty/non-string `name` or an incomplete map. ```ts import { createEnergyEngine, createPresenceStrategy, presenceAtOrAbove } from '@kumbatio/energy-system' const engine = createEnergyEngine() const aiChat = createPresenceStrategy('ai-chat', presenceAtOrAbove(75)) engine.resolve(aiChat) // 'visible' | 'muted' | 'hidden' ``` # Sessions and Gates (/docs/api/core/sessions-and-gates) ## Focus sessions [#focus-sessions] Focus sessions are time-boxed "one thing at a time" windows layered on top of the energy model. A session is a temporary commitment, not an energy level: it suppresses interruptions for a bounded duration, surfaces break nudges, and always ends on time. Two invariants come from field evidence: 1. **Sessions auto-expire.** Expiry is an emitted event, not a predicate the app must remember to poll - suppression can never outlive the session. 2. **Suppression is lifted before the end event is emitted**, so an end-of-session notification can never be swallowed by the session's own suppression. ### createFocusSessionController [#createfocussessioncontroller] ```ts function createFocusSessionController( options?: FocusSessionControllerOptions, ): FocusSessionController ``` ```ts import { createEnergyEngine, createFocusSessionController, createNotificationGate, } from '@kumbatio/energy-system' const engine = createEnergyEngine() const gate = createNotificationGate(engine, { onDeliver: (d) => render(d) }) const sessions = createFocusSessionController({ engine, gate }) sessions.subscribe((event, session) => { if (event === 'end') celebrate(session) }) sessions.start({ durationMinutes: 25, breakEveryMinutes: 0 }) ``` #### FocusSessionControllerOptions [#focussessioncontrolleroptions] /> ### FocusSessionController [#focussessioncontroller] ```ts interface FocusSessionController { start(options?: StartFocusSessionOptions): FocusSession stop(): void getSession(): FocusSession | null remainingMs(): number subscribe(listener: FocusSessionListener): () => void dispose(): void } ``` | Method | Behavior | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `start(options?)` | Start a session, replacing any active one (which is stopped first, emitting `stop`). Applies suppression to `gate`, schedules the end timer and break nudges, emits `start`, and returns the frozen session. Throws on a disposed controller, on a non-finite or non-positive duration, or a non-finite or negative break interval. | | `stop()` | End the active session early (emits `stop`). No-op when idle or disposed. | | `getSession()` | The active session snapshot, or `null` when idle. | | `remainingMs()` | Milliseconds left in the active session (0 when idle). | | `subscribe(listener)` | Subscribe to lifecycle events; returns an unsubscribe function. Listener exceptions are caught and logged. Returns a no-op on a disposed controller. | | `dispose()` | Stops any active session (releasing suppression and emitting `stop` before teardown) and releases resources. | ### StartFocusSessionOptions [#startfocussessionoptions] /> ### FocusSession [#focussession] /> ### FocusSessionEvent and FocusSessionListener [#focussessionevent-and-focussessionlistener] ```ts type FocusSessionEvent = 'start' | 'break' | 'end' | 'stop' type FocusSessionListener = (event: FocusSessionEvent, session: FocusSession) => void ``` * `start`: a session began * `break`: a break nudge is due (recurring while the session runs; a nudge that would land at or after `endsAt` is skipped) * `end`: the session reached `endsAt` and auto-expired * `stop`: the session was ended manually before `endsAt` ### FocusSuppressible [#focussuppressible] ```ts interface FocusSuppressible { setSuppressed(suppressed: boolean): void } ``` Anything that can be suppressed for the lifetime of a session. `NotificationGate` satisfies this. ### sessionRemainingMs [#sessionremainingms] ```ts function sessionRemainingMs(session: FocusSession, now?: number): number ``` Milliseconds left in a session (0 when expired). `now` defaults to `Date.now()`. ### isSessionExpired [#issessionexpired] ```ts function isSessionExpired(session: FocusSession, now?: number): boolean ``` True once a session has reached its end time. `now` defaults to `Date.now()`. *** ## Notification gate [#notification-gate] The notification gate is the runtime that **enforces** [`NotificationConfig`](/docs/api/core/strategies#notificationconfig) instead of leaving it as guidance. Apps publish notification intents through the gate; the gate resolves the current energy level's config and decides whether each intent is delivered now, batched, or deferred. The gate never silently drops a notification. Anything not deliverable now is deferred and released when energy rises, suppression lifts, `flush()` is called, or the gate is disposed. ### createNotificationGate [#createnotificationgate] ```ts function createNotificationGate( engine: EnergyEngine, options: NotificationGateOptions, ): NotificationGate ``` Creates a gate bound to an engine. The gate re-resolves its config on every energy change and on every suppression change, then **re-judges everything it is holding** against the new policy - in both directions. Deferred intents the new policy admits are released immediately rather than re-batched, because they already waited once; batched intents the new policy no longer admits move to the deferred queue instead of arriving under a policy that would not have accepted them. The batch deadline is anchored to when the window opened, so a config change moves the deadline rather than restarting the wait. Throws a `TypeError` when `onDeliver` is not a function. #### NotificationGateOptions [#notificationgateoptions] /> ### NotificationGate [#notificationgate] ```ts interface NotificationGate { publish(input: { priority?: NotificationPriority; payload?: unknown }): PublishOutcome setSuppressed(suppressed: boolean): void isSuppressed(): boolean pendingCount(): { batched: number; deferred: number } flush(): void dispose(): void } ``` | Method | Behavior | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `publish(input)` | Publish a notification intent and return what the gate did with it. `priority` defaults to `'normal'`; invalid priorities throw. Throws on a disposed gate. | | `setSuppressed(next)` | Hard-suppress delivery (e.g. during a focus session). While suppressed, every publish defers; lifting suppression releases the deferred queue. No-op when disposed or unchanged. | | `isSuppressed()` | Current suppression flag. | | `pendingCount()` | Counts of undelivered notifications currently held by the gate. | | `flush()` | Deliver the open batch now, subject to the current policy. Flushing overrides the *wait*, not the policy: under suppression or at a level that no longer admits them, the batched intents move to the deferred queue rather than being forced out. No-op when disposed. | | `dispose()` | Release resources. Pending notifications (batched and deferred) are delivered as a final `'released'` delivery first - a disposed gate never swallows intents. | ### resolveNotificationOutcome [#resolvenotificationoutcome] ```ts function resolveNotificationOutcome( config: NotificationConfig, priority: NotificationPriority, suppressed: boolean, ): PublishOutcome ``` Pure gating decision: given the active config, an intent priority, and the suppression flag, decide the outcome. Extracted so apps can unit-test their notification policy without constructing a gate. Decision order: 1. `suppressed` → `'deferred'` 2. `priorityThreshold: 'none'` → `'deferred'` 3. `priorityThreshold: 'critical'` and priority is not `critical` → `'deferred'` 4. `priorityThreshold: 'high'` and priority is `normal` → `'deferred'` 5. Otherwise: `'batched'` when `batchInterval > 0`, else `'delivered'` ### isNotificationPriority [#isnotificationpriority] ```ts function isNotificationPriority(value: unknown): value is NotificationPriority ``` Validate that an unknown value is a valid `NotificationPriority`. ### Gate types [#gate-types] ```ts type NotificationPriority = 'normal' | 'high' | 'critical' type NotificationDeliveryReason = 'immediate' | 'batch' | 'released' type PublishOutcome = 'delivered' | 'batched' | 'deferred' ``` #### EnergyNotification [#energynotification] /> #### NotificationChannels [#notificationchannels] Output channels permitted by the config active at delivery time. /> #### NotificationDelivery [#notificationdelivery] One call to `onDeliver`: one or more notifications plus delivery context. /> #### GateScheduler [#gatescheduler] Timer contract so tests can drive batch windows deterministically. Also used by the focus session controller. ```ts interface GateScheduler { setTimeout(callback: () => void, ms: number): unknown clearTimeout(handle: unknown): void } ``` # Strategies (/docs/api/core/strategies) A strategy is a pure `(level) -> config` mapping implementing [`AdaptationStrategy`](/docs/api/core/types#adaptationstrategy). Resolve them directly (`strategy.resolve(level)`), through the engine (`engine.resolve(strategy)`), or in React via [`useStrategy`](/docs/api/react#usestrategy). Each strategy also implements `describe(level)`, returning a human-readable summary. The package ships seven built-ins. Six are documented on this page: | Export | Strategy `name` | Config type | Module | | -------------------------------- | ------------------------- | ------------------------------ | ---------------------------------------------- | | `uiVisibilityStrategy` | `ui-visibility` | `UIVisibilityConfig` | core | | `notificationStrategy` | `notifications` | `NotificationConfig` | core | | `taskComplexityStrategy` | `task-complexity` | `TaskComplexityConfig` | core | | `interactionForgivenessStrategy` | `interaction-forgiveness` | `InteractionForgivenessConfig` | core | | `deferralStrategy` | `deferral` | `DeferralConfig` | core (see [Deferral](/docs/api/core/deferral)) | | `autonomyStrategy` | `autonomy` | `AutonomyConfig` | core | The seventh, [`demandAdmissionStrategy`](/docs/api/core/demand#demandadmissionstrategy), is documented with the inbound-demand policy it belongs to. `createPresenceStrategy` is a factory rather than a fixed strategy, and lives with [Presence](/docs/api/core/presence). All resolved configs are frozen. All `resolve` implementations throw for invalid levels (they validate through `getEnergyLevel`). ## uiVisibilityStrategy [#uivisibilitystrategy] ```ts const uiVisibilityStrategy: AdaptationStrategy ``` Which chrome elements are shown and how prominent they are. ### UIVisibilityConfig [#uivisibilityconfig] /> ### Values per level [#values-per-level] | Level | sidebar | tabBar | statusBar | toolbar | chromeOpacity | chromeOpacityHover | contentMaxWidth | contentFontScale | readOnlyCursor | | ----- | ------- | ------ | --------- | ------- | ------------- | ------------------ | --------------- | ---------------- | -------------- | | `100` | true | true | true | true | 1 | 1 | `none` | 1 | false | | `75` | true | true | true | true | 0.7 | 1 | `none` | 1 | false | | `50` | true | true | true | true | 0.4 | 1 | `90ch` | 1 | false | | `25` | false | false | false | true | 0.1 | 1 | `80ch` | 1.05 | false | | `0` | false | false | false | false | 0.05 | 0.8 | `75ch` | 1.1 | true | The same values are mirrored by [`applyEnergyLevel`](/docs/api/dom#applyenergylevel) as CSS custom properties and by the [reference stylesheet](/docs/api/css). ## notificationStrategy [#notificationstrategy] ```ts const notificationStrategy: AdaptationStrategy ``` Which notification channels are allowed and how aggressively intents are filtered. Enforced at runtime by the [notification gate](/docs/api/core/sessions-and-gates#createnotificationgate). ### NotificationConfig [#notificationconfig] /> ### Values per level [#values-per-level-1] | Level | allowVisual | allowSound | allowVibration | batchInterval | priorityThreshold | | ----- | ----------- | ---------- | -------------- | --------------- | ----------------- | | `100` | true | true | true | 0 | `all` | | `75` | true | true | false | 0 | `all` | | `50` | true | false | false | 300000 (5 min) | `high` | | `25` | true | false | false | 900000 (15 min) | `critical` | | `0` | false | false | false | 0 | `none` | ## taskComplexityStrategy [#taskcomplexitystrategy] ```ts const taskComplexityStrategy: AdaptationStrategy ``` Which tasks to surface and whether to nudge breaks. ### TaskComplexityConfig [#taskcomplexityconfig] /> ### Values per level [#values-per-level-2] | Level | maxComplexity | suggestBreaks | breakIntervalMinutes | | ----- | ------------- | ------------- | -------------------- | | `100` | `complex` | false | 0 | | `75` | `moderate` | false | 0 | | `50` | `routine` | true | 45 | | `25` | `simple` | true | 25 | | `0` | `consumption` | false | 0 | Rest (`0`) is already a break: prompting someone at 0 to take a break from resting is noise, so break suggestions are disabled entirely. ## interactionForgivenessStrategy [#interactionforgivenessstrategy] ```ts const interactionForgivenessStrategy: AdaptationStrategy ``` How much room the interface gives the user to notice and reverse mistakes. Lower energy means slower error detection, so forgiveness scales inversely with capacity: longer undo windows, confirmation on destructive actions, more frequent autosave. ### InteractionForgivenessConfig [#interactionforgivenessconfig] /> ### Values per level [#values-per-level-3] | Level | undoWindowMs | confirmDestructive | autosaveIntervalMs | | ----- | ------------ | ------------------ | ------------------ | | `100` | 5000 | false | 60000 | | `75` | 8000 | false | 45000 | | `50` | 10000 | true | 30000 | | `25` | 15000 | true | 20000 | | `0` | 20000 | true | 15000 | ## deferralStrategy [#deferralstrategy] ```ts const deferralStrategy: AdaptationStrategy ``` Orders deferral presets so the default "not now" suggestion matches current capacity. Documented with its config and per-level values on the [Deferral](/docs/api/core/deferral#deferralstrategy) page. ## autonomyStrategy [#autonomystrategy] ```ts const autonomyStrategy: AdaptationStrategy ``` How much latitude automation has to act for the user without asking. The mirror of [`interactionForgivenessStrategy`](#interactionforgivenessstrategy): forgiveness protects against the *user's* mistakes at low energy, autonomy against the *agent's*. The system acts on the user's behalf precisely when they are least able to supervise it, so the worst day is the wrong day for it to improvise. What narrows as energy falls is *discretion*, not action. At rest the threshold is `1`, admitting only certainty - rule-based decisions, never a judgment call - and one unattended step. Automation may still take a single, certain, template-only action, which is exactly the shape of an out-of-office reply; it may not chain steps or compose wording. ### AutonomyConfig [#autonomyconfig] /> ### Values per level [#values-per-level-4] | Level | confidenceThreshold | allowGeneratedContent | maxUnattendedSteps | | ----- | ------------------- | --------------------- | ------------------ | | `100` | 0.6 | true | 8 | | `75` | 0.7 | true | 5 | | `50` | 0.8 | true | 3 | | `25` | 0.9 | false | 1 | | `0` | 1 | false | 1 | ```ts const { confidenceThreshold, allowGeneratedContent, maxUnattendedSteps } = engine.resolve(autonomyStrategy) if (classification.confidence < confidenceThreshold) return askTheUser() ``` Useful to any consumer with agentic surfaces, with or without [demand admission](/docs/api/core/demand). ## Custom strategies [#custom-strategies] Any object satisfying `AdaptationStrategy` works with `engine.resolve` and `useStrategy`. For presence-shaped strategies, use [`createPresenceStrategy`](/docs/api/core/presence#createpresencestrategy). # Types (/docs/api/core/types) All types below are exported from `@kumbatio/energy-system`. ## EnergyLevel [#energylevel] ```ts type EnergyLevel = 0 | 25 | 50 | 75 | 100 ``` Discrete cognitive capacity values. The model is fixed at five levels - see [Levels](/docs/api/core/levels) for their definitions and [compatibility helpers](/docs/api/core/metrics-and-compat#createexternallevelcompatibility) for bridging other models. ## EnergySource [#energysource] ```ts type EnergySource = 'manual' | 'scheduled' | 'inferred' ``` How the energy level was set. Source affects [engine reconciliation](/docs/api/core/engine#behavior-notes) priority: `manual` outranks `scheduled`, which outranks `inferred`. ## EnergyPresence [#energypresence] ```ts type EnergyPresence = 'visible' | 'muted' | 'hidden' ``` How a UI element participates at a given energy level: * `visible`: rendered normally * `muted`: rendered but de-emphasized (reduced opacity, secondary styling) * `hidden`: not rendered at all ## EnergyPresenceMap [#energypresencemap] ```ts type EnergyPresenceMap = Readonly> ``` A complete presence declaration: one presence value per energy level. This is the annotation apps attach to components/views to state which energy levels they belong to (e.g. "hide the AI chat at 50 and below"). Build one with [`defineEnergyPresence`](/docs/api/core/presence#defineenergypresence). ## Validation constants [#validation-constants] ```ts const ENERGY_LEVEL_VALUES: ReadonlySet // 0, 25, 50, 75, 100 const ENERGY_SOURCE_VALUES: ReadonlySet // 'manual', 'scheduled', 'inferred' const ENERGY_PRESENCE_VALUES: ReadonlySet // 'visible', 'muted', 'hidden' ``` Frozen, mutation-proof sets for runtime validation (`add`/`delete`/`clear` are disabled). Prefer the guards [`isEnergyLevel`](/docs/api/core/levels#isenergylevel), [`isEnergySource`](/docs/api/core/levels#isenergysource), and [`isEnergyPresence`](/docs/api/core/presence#isenergypresence), which narrow types. ## EnergyState [#energystate] A point-in-time snapshot of cognitive capacity. Created via [`createEnergyState`](/docs/api/core/levels#createenergystate) or by the engine; always frozen. /> ## EnergyClock [#energyclock] ```ts interface EnergyClock { now(): number } ``` Time source contract for deterministic environments (tests, simulations). Everywhere a `clock` option is accepted, a plain `() => number` also works. ## EnergyChangeListener [#energychangelistener] ```ts type EnergyChangeListener = (state: EnergyState, prev: EnergyState) => void ``` Callback for energy state changes. ## CognitiveProfile [#cognitiveprofile] What the brain can handle at a given energy level. /> The four field types are also exported standalone: ```ts type DecisionCapacity = 'high' | 'moderate' | 'low' | 'minimal' | 'none' type FocusDuration = 'extended' | 'moderate' | 'short' | 'minimal' | 'none' type TaskComplexity = 'complex' | 'moderate' | 'routine' | 'simple' | 'consumption' type InterruptionTolerance = 'high' | 'moderate' | 'low' | 'minimal' | 'none' ``` ## EnergyLevelDefinition [#energyleveldefinition] Complete metadata for a single energy level. Retrieved via [`getEnergyLevel`](/docs/api/core/levels#getenergylevel) / [`getEnergyLevels`](/docs/api/core/levels#getenergylevels). /> ## AdaptationStrategy [#adaptationstrategy] ```ts interface AdaptationStrategy { name: string describe(level: EnergyLevel): string resolve(level: EnergyLevel): TConfig } ``` Maps energy levels to application behavior. Pure function contract - given a level, produce a configuration. /> See [Strategies](/docs/api/core/strategies) for the built-in implementations. ## EnergyPersistence [#energypersistence] Storage contract - implement per platform, or use the [built-in adapters](/docs/api/persistence). /> ## EnergyMetrics [#energymetrics] Computed, app-agnostic metrics from an energy state snapshot. Produced by [`getEnergyMetrics`](/docs/api/core/metrics-and-compat#getenergymetrics). /> ## Presence-related types [#presence-related-types] `EnergyPresenceSpec` is documented on the [Presence](/docs/api/core/presence#energypresencespec) page. Session, gate, deferral, and compatibility types are documented alongside their factories on [Sessions and gates](/docs/api/core/sessions-and-gates), [Deferral](/docs/api/core/deferral), and [Metrics and compatibility](/docs/api/core/metrics-and-compat). # Core (/docs/api/reference/core) {/* Generated by scripts/generate-api-reference.mts from @kumbatio/energy-system@2.1.1. Do not edit: run `pnpm docs:api`. */} Every export of `@kumbatio/energy-system`, generated from the declarations of the installed package (**v2.1.1**). ```ts import { DEFERRAL_PRESET_IDS, ENERGY_LEVEL_VALUES, ENERGY_PRESENCE_VALUES } from '@kumbatio/energy-system' ``` 46 value exports and 53 type exports. ## Functions [#functions] ### createDeferralPresets [#createdeferralpresets] ```ts export declare function createDeferralPresets(options?: DeferralPresetOptions): readonly DeferralPreset[]; ``` Build the standard deferral presets. Times are computed in local time - "tomorrow morning" means the user's morning. ### createEnergyEngine [#createenergyengine] ```ts export declare function createEnergyEngine(options?: EnergyEngineOptions): EnergyEngine; ``` ### createEnergyOrigin [#createenergyorigin] ```ts export declare function createEnergyOrigin(): string; ``` Create a unique producer identity for deterministic cross-context ordering. ### createEnergyState [#createenergystate] ```ts export declare function createEnergyState(level: EnergyLevel, source?: EnergySource, timestamp?: number, revision?: number, origin?: string): EnergyState; ``` Create an EnergyState for the current moment ### createExternalLevelCompatibility [#createexternallevelcompatibility] ```ts export declare function createExternalLevelCompatibility(options: ExternalLevelCompatibilityOptions): ExternalLevelCompatibility; ``` Build a compatibility bridge for systems that use non-native level values. This is useful during migrations (e.g. legacy 4-level models) while keeping the package's native fixed 5-level model unchanged. ### createFocusSessionController [#createfocussessioncontroller] ```ts export declare function createFocusSessionController(options?: FocusSessionControllerOptions): FocusSessionController; ``` ### createNotificationGate [#createnotificationgate] ```ts export declare function createNotificationGate(engine: EnergyEngine, options: NotificationGateOptions): NotificationGate; ``` Create a notification gate bound to an engine. The gate re-resolves its config on every energy change and releases deferred notifications the moment the new level's config (or lifted suppression) admits them. ### createPresenceStrategy [#createpresencestrategy] ```ts export declare function createPresenceStrategy(name: string, presence: EnergyPresenceMap): AdaptationStrategy; ``` Lift a presence map into an AdaptationStrategy so it can be resolved through the engine like any built-in strategy: ```ts const aiChat = createPresenceStrategy('ai-chat', presenceAtOrAbove(75)) engine.resolve(aiChat) // 'visible' | 'muted' | 'hidden' ``` ### cycleDiscreteLevel [#cyclediscretelevel] ```ts export declare function cycleDiscreteLevel(current: number, levels: readonly TLevel[], fallback: TLevel): TLevel; ``` Cycle through any discrete numeric level list. ### cycleEnergyLevel [#cycleenergylevel] ```ts export declare function cycleEnergyLevel(current: EnergyLevel): EnergyLevel; ``` Cycle to the next energy level: 100 -> 75 -> 50 -> 25 -> 0 -> 100 ### defineEnergyPresence [#defineenergypresence] ```ts export declare function defineEnergyPresence(spec?: EnergyPresenceSpec): EnergyPresenceMap; ``` Build a complete, frozen presence map from a partial spec. ```ts // Hide the AI chat at 50 and below, keep it muted at 75: const aiChatPresence = defineEnergyPresence({ default: 'visible', 75: 'muted', 50: 'hidden', 25: 'hidden', 0: 'hidden', }) ``` ### getEnergyLevel [#getenergylevel] ```ts export declare function getEnergyLevel(level: EnergyLevel): Readonly; ``` Get definition for a specific energy level ### getEnergyLevels [#getenergylevels] ```ts export declare function getEnergyLevels(): ReadonlyArray>; ``` Get all energy level definitions, ordered highest to lowest ### getEnergyMetrics [#getenergymetrics] ```ts export declare function getEnergyMetrics(state: EnergyState, now?: number): EnergyMetrics; ``` Derive app-agnostic energy metrics from the current state. ### isEnergyLevel [#isenergylevel] ```ts export declare function isEnergyLevel(value: unknown): value is EnergyLevel; ``` Validate that an unknown value is a valid EnergyLevel ### isEnergyPresence [#isenergypresence] ```ts export declare function isEnergyPresence(value: unknown): value is EnergyPresence; ``` Validate that an unknown value is a valid EnergyPresence ### isEnergySource [#isenergysource] ```ts export declare function isEnergySource(value: unknown): value is EnergySource; ``` Validate that an unknown value is a valid EnergySource ### isHigherEnergy [#ishigherenergy] ```ts export declare function isHigherEnergy(a: EnergyLevel, b: EnergyLevel): boolean; ``` Returns true if level `a` represents higher energy than level `b` ### isNotificationPriority [#isnotificationpriority] ```ts export declare function isNotificationPriority(value: unknown): value is NotificationPriority; ``` Validate that an unknown value is a valid NotificationPriority ### isOriginatorTier [#isoriginatortier] ```ts export declare function isOriginatorTier(value: unknown): value is OriginatorTier; ``` Validate that an unknown value is a valid OriginatorTier ### isPreferredEnergyState [#ispreferredenergystate] ```ts export declare function isPreferredEnergyState(candidate: EnergyState, current: EnergyState): boolean; ``` Should `candidate` replace `current`? The comparison walks four keys in order, stopping at the first that differs: 1. **`timestamp`** - later wins. The ordinary case, and the only one most states ever reach. 2. **`revision`** - higher wins. Two writes inside one clock tick are not simultaneous; the producer numbers them so they still order. 3. **`source`** - `manual` > `scheduled` > `inferred`. See `sourcePriority`. 4. **`origin`** - higher string wins. Not meaningful, and deliberately so: when two producers write the same instant, the same revision, and the same kind of source, there is no principled winner, and an arbitrary rule every context computes identically beats a coin flip each context tosses separately. Convergence is the property that matters. A final `level` comparison exists below the four keys as a backstop for producers that reuse one identity for different state, which is a contract violation but should still converge rather than oscillate. Equal on every key means equal: `false`, so an identical state never counts as a change and never fires a notification. ### isPresenceVisible [#ispresencevisible] ```ts export declare function isPresenceVisible(presence: EnergyPresence): boolean; ``` True unless the presence is 'hidden' ### isSessionExpired [#issessionexpired] ```ts export declare function isSessionExpired(session: FocusSession, now?: number): boolean; ``` True once a session has reached its end time ### isUnproducedState [#isunproducedstate] ```ts export declare function isUnproducedState(state: Pick): boolean; ``` True for the untouched default state - its age and identity are not meaningful. ### mapToNearestDiscreteLevel [#maptonearestdiscretelevel] ```ts export declare function mapToNearestDiscreteLevel(value: number, levels: readonly TLevel[], fallback: TLevel): TLevel; ``` Map an arbitrary number to the nearest available discrete level. ### mapToNearestEnergyLevel [#maptonearestenergylevel] ```ts export declare function mapToNearestEnergyLevel(value: number): EnergyLevel; ``` Map any number to the closest native package energy level. ### presenceAtOrAbove [#presenceatorabove] ```ts export declare function presenceAtOrAbove(min: EnergyLevel, below?: EnergyPresence): EnergyPresenceMap; ``` Presence map for elements that need at least `min` energy. Below `min` the element is `below` ('hidden' by default). ```ts const composerToolbar = presenceAtOrAbove(50) // hidden at 25 and 0 const aiSidebar = presenceAtOrAbove(75, 'muted') // muted below 75 ``` ### presenceAtOrBelow [#presenceatorbelow] ```ts export declare function presenceAtOrBelow(max: EnergyLevel, above?: EnergyPresence): EnergyPresenceMap; ``` Presence map for elements that only belong at low energy - recovery hints, "one thing at a time" affordances. Above `max` the element is `above` ('hidden' by default). ### resolveDeferral [#resolvedeferral] ```ts export declare function resolveDeferral(presets: readonly DeferralPreset[], presetId: string, now?: Date): number | null; ``` Resolve a preset id to a resurface timestamp (epoch ms). Returns null for an unknown id - callers decide whether that is an error. ### resolveDemandOutcome [#resolvedemandoutcome] ```ts export declare function resolveDemandOutcome(config: DemandAdmissionConfig, autonomy: AutonomyConfig, demand: DemandInput): DemandOutcome; ``` The pure gating decision, extracted so apps can unit-test their demand policy without wiring any effects - the counterpart of `resolveNotificationOutcome`. Both configs are required because the two questions are genuinely separate: `admission` says what the app's policy wants done, `autonomy` says how much of it may happen without the user watching. ### resolveEnergyPresence [#resolveenergypresence] ```ts export declare function resolveEnergyPresence(presence: EnergyPresenceMap, level: EnergyLevel): EnergyPresence; ``` Resolve the presence of an element for a given energy level ### resolveNotificationOutcome [#resolvenotificationoutcome] ```ts export declare function resolveNotificationOutcome(config: NotificationConfig, priority: NotificationPriority, suppressed: boolean): PublishOutcome; ``` Pure gating decision: given the active config, an intent priority, and the suppression flag, decide the outcome. Extracted so apps can unit-test their notification policy without constructing a gate. ### sessionRemainingMs [#sessionremainingms] ```ts export declare function sessionRemainingMs(session: FocusSession, now?: number): number; ``` Milliseconds left in a session (0 when expired) ## Constants [#constants] ### autonomyStrategy [#autonomystrategy] ```ts export declare const autonomyStrategy: AdaptationStrategy; ``` ### DEFERRAL\_PRESET\_IDS [#deferral_preset_ids] ```ts export declare const DEFERRAL_PRESET_IDS: Readonly<{ readonly inOneHour: 'in-1-hour'; readonly thisEvening: 'this-evening'; readonly tomorrowMorning: 'tomorrow-morning'; readonly nextWorkday: 'next-workday'; readonly nextMonday: 'next-monday'; }>; ``` Stable preset ids, exported so configs/strategies can reference them ### deferralStrategy [#deferralstrategy] ```ts export declare const deferralStrategy: AdaptationStrategy; ``` ### demandAdmissionStrategy [#demandadmissionstrategy] ```ts export declare const demandAdmissionStrategy: AdaptationStrategy; ``` ### ENERGY\_LEVEL\_VALUES [#energy_level_values] ```ts export declare const ENERGY_LEVEL_VALUES: ReadonlySet; ``` Valid energy level values for runtime validation ### ENERGY\_PRESENCE\_VALUES [#energy_presence_values] ```ts export declare const ENERGY_PRESENCE_VALUES: ReadonlySet; ``` Valid energy presence values for runtime validation ### ENERGY\_SOURCE\_VALUES [#energy_source_values] ```ts export declare const ENERGY_SOURCE_VALUES: ReadonlySet; ``` Valid energy source values for runtime validation ### interactionForgivenessStrategy [#interactionforgivenessstrategy] ```ts export declare const interactionForgivenessStrategy: AdaptationStrategy; ``` ### notificationStrategy [#notificationstrategy] ```ts export declare const notificationStrategy: AdaptationStrategy; ``` ### taskComplexityStrategy [#taskcomplexitystrategy] ```ts export declare const taskComplexityStrategy: AdaptationStrategy; ``` ### uiVisibilityStrategy [#uivisibilitystrategy] ```ts export declare const uiVisibilityStrategy: AdaptationStrategy; ``` ### UNPRODUCED\_ORIGIN [#unproduced_origin] ```ts export declare const UNPRODUCED_ORIGIN = "0-initial"; ``` ### UNPRODUCED\_TIMESTAMP [#unproduced_timestamp] ```ts export declare const UNPRODUCED_TIMESTAMP = 0; ``` ## Interfaces [#interfaces] ### AdaptationStrategy [#adaptationstrategy] ```ts export interface AdaptationStrategy { /** Unique name for this strategy */ name: string; /** Human-readable description of what this strategy does at a given level */ describe(level: EnergyLevel): string; /** Compute the configuration for a given energy level */ resolve(level: EnergyLevel): TConfig; } ``` Maps energy levels to application behavior. Pure function - given a level, produce a configuration. ### AutonomyConfig [#autonomyconfig] ```ts export interface AutonomyConfig { /** * Minimum confidence (0–1) an automated decision needs before acting * unattended. `1` admits only certainty, which in practice means rule-based * actions and never a judgment call. */ readonly confidenceThreshold: number; /** Whether automation may compose novel wording, or only fill fixed templates. */ readonly allowGeneratedContent: boolean; /** How many automated steps may chain before control returns to the user. */ readonly maxUnattendedSteps: number; } ``` How much latitude automation has to act for the user without asking. The mirror of `interactionForgivenessStrategy`: forgiveness protects against the *user's* mistakes at low energy, autonomy against the *agent's*. The system acts on the user's behalf precisely when they are least able to supervise it, so the worst day is the wrong day for it to improvise. What narrows as energy falls is *discretion*, not action. At rest the automation may still take a single, certain, template-only step - an out-of-office reply is exactly that shape - but it may not chain steps, compose novel wording, or act on a judgment call. ### CognitiveProfile [#cognitiveprofile] ```ts export interface CognitiveProfile { readonly decisionCapacity: DecisionCapacity; readonly focusDuration: FocusDuration; readonly taskComplexity: TaskComplexity; readonly interruptionTolerance: InterruptionTolerance; } ``` What the brain can handle at a given energy level ### DeferralConfig [#deferralconfig] ```ts export interface DeferralConfig { /** Preset ids in suggestion order for this level (first = most prominent) */ readonly orderedPresetIds: readonly string[]; /** The preset a one-tap "defer" action should use at this level */ readonly defaultPresetId: string; } ``` ### DeferralPreset [#deferralpreset] ```ts export interface DeferralPreset { readonly id: string; readonly label: string; /** Compute the resurface time from a reference moment */ compute(now: Date): Date; } ``` A named deferral option ### DeferralPresetOptions [#deferralpresetoptions] ```ts export interface DeferralPresetOptions { /** Hour (0-23) mornings resolve to. Default 9. */ morningHour?: number; /** Hour (0-23) evenings resolve to. Default 18. */ eveningHour?: number; } ``` ### DemandAcknowledgment [#demandacknowledgment] ```ts export interface DemandAcknowledgment { readonly detail: AcknowledgmentDetail; /** Whether the wording may be composed, or must come from a fixed template. */ readonly allowGeneratedContent: boolean; } ``` What the acknowledgment is permitted to be. The two axes are independent: how much it may say comes from the level's admission config, whether the wording may be composed at all comes from autonomy. ### DemandAdmissionConfig [#demandadmissionconfig] ```ts export interface DemandAdmissionConfig { /** * Lowest originator tier admitted live. Mirrors the notification gate's * `priorityThreshold`: `all` admits everyone, `none` admits no one. */ readonly originatorThreshold: 'all' | 'known' | 'exempt' | 'none'; /** Whether demand held back from the user is acknowledged to its originator. */ readonly acknowledge: boolean; /** Ceiling on acknowledgment detail at this level. */ readonly acknowledgmentDetail: AcknowledgmentDetail; } ``` ### DemandInput [#demandinput] ```ts export interface DemandInput { readonly originatorTier: OriginatorTier; /** * Whether this demand asks something of the user. Informational mail - a * receipt, a newsletter, a build notification - is not this policy's * business and passes through untouched. */ readonly bearsObligation: boolean; /** * Confidence (0–1) that the two classifications above are right. A * deterministic rule reports `1`; an LLM classifier reports what it reports. * Below the level's autonomy threshold, the demand is captured silently * rather than risking a wrong automated action on the user's worst day. * Defaults to `1`, so a caller with no classifier gets rule-based behavior. */ readonly confidence?: number; } ``` The properties of one piece of demand that the policy reads ### DemandOutcome [#demandoutcome] ```ts export interface DemandOutcome { readonly admission: DemandAdmission; /** The acknowledgment to send. Null unless `admission` is `acknowledge`. */ readonly acknowledgment: DemandAcknowledgment | null; readonly reason: DemandOutcomeReason; } ``` The policy decision for one piece of demand ### EnergyClock [#energyclock] ```ts export interface EnergyClock { now(): number; } ``` Time source contract for deterministic environments (tests, simulations) ### EnergyEngine [#energyengine] ```ts export interface EnergyEngine { /** * Begin hydration and cross-context observation. Idempotent, and a no-op on * a disposed engine. Only needed when the engine was created with * `autoStart: false`. */ start(): void; /** Get current energy state */ getState(): EnergyState; /** Set energy level with optional source */ setLevel(level: EnergyLevel, source?: EnergySource): void; /** Cycle to next energy level */ cycleLevel(): void; /** Subscribe to state changes. Returns unsubscribe function. */ subscribe(listener: EnergyChangeListener): () => void; /** Resolve a strategy against current energy state */ resolve(strategy: AdaptationStrategy): T; /** Load persisted state (called automatically, but can be called manually) */ hydrate(): Promise; /** * Wait until the current state version is durably persisted. * Rejects if the engine is disposed or an unchanged initial state cannot be * reconciled because its persistence hydration read failed. */ flush(): Promise; /** Release engine-owned subscriptions/resources */ dispose(): void; } ``` ### EnergyEngineOptions [#energyengineoptions] ```ts export interface EnergyEngineOptions { initialLevel?: EnergyLevel; persistence?: EnergyPersistence; onChange?: EnergyChangeListener; /** Called when a persistence attempt fails before the engine schedules a retry. */ onPersistenceError?: (error: unknown, state: EnergyState) => void; /** Deterministic time source for tests/simulations */ clock?: EnergyClock | (() => number); /** Stable producer identity for deterministic reconciliation. Primarily useful in tests. */ originId?: string; /** * Maximum tolerated future clock skew (ms) for externally supplied state * (hydration and cross-context observation). States stamped further ahead of * the local clock are rejected so one bad clock cannot win reconciliation * until its timestamp passes. Pass Number.POSITIVE_INFINITY to accept any * finite timestamp. Default: 5 minutes. */ maxFutureSkewMs?: number; /** * Whether construction immediately hydrates from persistence and subscribes * to cross-context updates. Default: true. * * Pass false when the engine is constructed somewhere that may never be * committed - a React render, most notably - and call `start()` from a * lifecycle that only runs for trees React kept. Without this, a discarded * render leaves an engine nobody will ever dispose, holding a live * cross-context observer (a `storage` listener, for the localStorage * adapter) for the lifetime of the page. */ autoStart?: boolean; } ``` ### EnergyLevelDefinition [#energyleveldefinition] ```ts export interface EnergyLevelDefinition { readonly value: EnergyLevel; readonly key: string; readonly label: string; readonly description: string; readonly cognitiveProfile: CognitiveProfile; } ``` Complete metadata for a single energy level ### EnergyMetrics [#energymetrics] ```ts export interface EnergyMetrics { /** Milliseconds since this state was set */ readonly stateAgeMs: number; /** Rounded minutes since this state was set */ readonly stateAgeMinutes: number; /** Suggested focused-work window length */ readonly expectedProductivityWindowMinutes: number; /** Suggested break cadence for the current level. 0 means no breaks are suggested (rest is already a break). */ readonly suggestedBreakIntervalMinutes: number; /** Recommended task complexity based on cognitive profile */ readonly recommendedTaskComplexity: TaskComplexity; /** * Heuristic signal for maintainability of this state. * True only for mid-range levels (25/50/75): peak (100) is a burst state * that depletes rather than holds, and rest (0) is recovery, not a working * state to maintain. Both extremes report false. */ readonly sustainable: boolean; /** Optional guidance for recovery horizon */ readonly recoveryHintMinutes?: number; } ``` Computed, app-agnostic metrics from an energy state snapshot. ### EnergyNotification [#energynotification] ```ts export interface EnergyNotification { readonly priority: NotificationPriority; readonly payload: unknown; /** When the intent was published (epoch ms, gate clock) */ readonly createdAt: number; } ``` A single notification intent held or delivered by the gate ### EnergyPersistence [#energypersistence] ```ts export interface EnergyPersistence { load(): Promise; save(state: EnergyState): Promise; /** * Optional observer for externally persisted state updates (cross-tab, worker, etc.) */ observe?(onState: (state: EnergyState) => void): () => void; } ``` Storage contract - implement per platform ### EnergyState [#energystate] ```ts export interface EnergyState { /** Current cognitive capacity */ readonly level: EnergyLevel; /** When this state was set (epoch ms) */ readonly timestamp: number; /** How this state was determined */ readonly source: EnergySource; /** Logical sequence for writes sharing the same timestamp */ readonly revision: number; /** Stable identity of the engine/context that produced this state */ readonly origin: string; } ``` A point-in-time snapshot of cognitive capacity ### ExternalLevelCompatibility [#externallevelcompatibility] ```ts export interface ExternalLevelCompatibility { levels: readonly TExternal[]; fallbackLevel: TExternal; fallbackEnergyLevel: EnergyLevel; toEnergyLevel: (externalLevel: TExternal | number) => EnergyLevel; fromEnergyLevel: (level: EnergyLevel) => TExternal; cycleExternalLevel: (current: TExternal | number) => TExternal; cycleMappedEnergyLevel: (current: TExternal | number) => EnergyLevel; } ``` ### ExternalLevelCompatibilityOptions [#externallevelcompatibilityoptions] ```ts export interface ExternalLevelCompatibilityOptions { /** * External level cycle order (e.g. [100, 66, 33, 0]). */ levels: readonly TExternal[]; /** * Mapping from external level values to native package levels. */ toEnergyLevel: Readonly>; /** * Fallback external level when input is unknown. */ fallbackLevel: TExternal; /** * Fallback native level when mapping is invalid or missing. * Defaults to the mapped value of fallbackLevel. */ fallbackEnergyLevel?: EnergyLevel; } ``` ### FocusSession [#focussession] ```ts export interface FocusSession { /** When the session started (epoch ms) */ readonly startedAt: number; /** When the session ends (epoch ms). Sessions are always bounded. */ readonly endsAt: number; /** Break nudge cadence in ms. 0 = no break nudges. */ readonly breakIntervalMs: number; } ``` An active focus session snapshot ### FocusSessionController [#focussessioncontroller] ```ts export interface FocusSessionController { /** Start a session (replacing any active one, which is stopped first). */ start(options?: StartFocusSessionOptions): FocusSession; /** End the active session early. No-op when idle. */ stop(): void; getSession(): FocusSession | null; /** Milliseconds left in the active session (0 when idle) */ remainingMs(): number; /** Subscribe to session lifecycle events. Returns unsubscribe function. */ subscribe(listener: FocusSessionListener): () => void; /** Stop any active session and release resources */ dispose(): void; } ``` ### FocusSessionControllerOptions [#focussessioncontrolleroptions] ```ts export interface FocusSessionControllerOptions { /** * Engine used for energy-aware defaults: session length from the level's * expected productivity window, break cadence from the task-complexity * strategy. Optional - without it, defaults are 25 minutes / no breaks. */ engine?: EnergyEngine; /** * Suppression target (typically a NotificationGate). Suppressed on start, * released on stop/end/dispose - the controller owns the flag for the * session's lifetime, so it can never be left stuck on. */ gate?: FocusSuppressible; /** Deterministic time source for tests/simulations */ clock?: EnergyEngineOptions['clock']; /** Deterministic timer source for tests/simulations */ scheduler?: GateScheduler; } ``` ### FocusSuppressible [#focussuppressible] ```ts export interface FocusSuppressible { setSuppressed(suppressed: boolean): void; } ``` Anything that can be suppressed for the lifetime of a session ### GateScheduler [#gatescheduler] ```ts export interface GateScheduler { setTimeout(callback: () => void, ms: number): unknown; clearTimeout(handle: unknown): void; } ``` Timer contract so tests can drive batch windows deterministically ### InteractionForgivenessConfig [#interactionforgivenessconfig] ```ts export interface InteractionForgivenessConfig { /** How long an undo affordance stays available after an action */ readonly undoWindowMs: number; /** Whether destructive actions (delete, discard, overwrite) ask first */ readonly confirmDestructive: boolean; /** Suggested autosave cadence for in-progress work */ readonly autosaveIntervalMs: number; } ``` How much room the interface gives the user to notice and reverse mistakes. Lower energy means slower error detection, so forgiveness scales inversely with capacity: longer undo windows, confirmation on destructive actions, more frequent autosave. ### NotificationChannels [#notificationchannels] ```ts export interface NotificationChannels { readonly visual: boolean; readonly sound: boolean; readonly vibration: boolean; } ``` Output channels permitted by the config active at delivery time ### NotificationConfig [#notificationconfig] ```ts export interface NotificationConfig { /** Allow visual notifications (badges, toasts) */ readonly allowVisual: boolean; /** Allow audio notifications */ readonly allowSound: boolean; /** Allow haptic feedback */ readonly allowVibration: boolean; /** Minimum ms between batched notifications (0 = immediate) */ readonly batchInterval: number; /** Minimum priority to show */ readonly priorityThreshold: 'all' | 'high' | 'critical' | 'none'; } ``` ### NotificationDelivery [#notificationdelivery] ```ts export interface NotificationDelivery { readonly notifications: readonly EnergyNotification[]; readonly reason: NotificationDeliveryReason; readonly channels: NotificationChannels; readonly level: EnergyLevel; } ``` One call to `onDeliver`: one or more notifications plus delivery context ### NotificationGate [#notificationgate] ```ts export interface NotificationGate { /** Publish a notification intent. Returns what the gate did with it. */ publish(input: { priority?: NotificationPriority; payload?: unknown; }): PublishOutcome; /** * Hard-suppress delivery (e.g. during a focus session). While suppressed, * every publish defers; lifting suppression releases the deferred queue. */ setSuppressed(suppressed: boolean): void; isSuppressed(): boolean; /** Counts of undelivered notifications currently held by the gate */ pendingCount(): { batched: number; deferred: number; }; /** Deliver the open batch now and release any deferred items that qualify */ flush(): void; /** * Release resources. Pending notifications are delivered as a final * 'released' delivery first - a disposed gate never swallows intents. */ dispose(): void; } ``` ### NotificationGateOptions [#notificationgateoptions] ```ts export interface NotificationGateOptions { /** Delivery sink. Called with everything the gate decides to surface. */ onDeliver(delivery: NotificationDelivery): void; /** Strategy resolving level -> NotificationConfig. Default: `notificationStrategy`. */ strategy?: AdaptationStrategy; /** Deterministic time source for tests/simulations */ clock?: EnergyEngineOptions['clock']; /** Deterministic timer source for tests/simulations */ scheduler?: GateScheduler; } ``` ### StartFocusSessionOptions [#startfocussessionoptions] ```ts export interface StartFocusSessionOptions { /** Session length. Default: engine's expected productivity window, else 25. */ durationMinutes?: number; /** Break nudge cadence. 0 disables. Default: engine's task-complexity guidance. */ breakEveryMinutes?: number; } ``` ### TaskComplexityConfig [#taskcomplexityconfig] ```ts export interface TaskComplexityConfig { /** Maximum task complexity to surface */ readonly maxComplexity: TaskComplexity; /** Whether to proactively suggest breaks */ readonly suggestBreaks: boolean; /** Minutes between break suggestions (when enabled) */ readonly breakIntervalMinutes: number; } ``` ### UIVisibilityConfig [#uivisibilityconfig] ```ts export interface UIVisibilityConfig { readonly sidebar: boolean; readonly tabBar: boolean; readonly statusBar: boolean; readonly toolbar: boolean; readonly chromeOpacity: number; readonly chromeOpacityHover: number; readonly contentMaxWidth: string; readonly contentFontScale: number; readonly readOnlyCursor: boolean; } ``` ## Type aliases [#type-aliases] ### AcknowledgmentDetail [#acknowledgmentdetail] ```ts export type AcknowledgmentDetail = 'full' | 'brief' | 'minimal'; ``` How much an acknowledgment may say ### DecisionCapacity [#decisioncapacity] ```ts export type DecisionCapacity = 'high' | 'moderate' | 'low' | 'minimal' | 'none'; ``` ### DemandAdmission [#demandadmission] ```ts export type DemandAdmission = 'live' | 'acknowledge' | 'silent'; ``` What happens to a piece of inbound demand. * `live`: reaches the user now, untouched by this policy. * `acknowledge`: the originator is acknowledged and the obligation is captured for later. The two are one act - an acknowledgment without a capture is a promise nobody kept, a capture without an acknowledgment leaves the originator in silence. * `silent`: captured for later with no acknowledgment. ### DemandOutcomeReason [#demandoutcomereason] ```ts export type DemandOutcomeReason = 'exempt-originator' | 'tier-admitted' | 'no-obligation' | 'acknowledgment-disabled' | 'below-confidence' | 'acknowledged'; ``` Why the policy reached its decision - for audit trails and explanatory UI ### EnergyChangeListener [#energychangelistener] ```ts export type EnergyChangeListener = (state: EnergyState, prev: EnergyState) => void; ``` Callback for energy state changes ### EnergyLevel [#energylevel] ```ts export type EnergyLevel = 0 | 25 | 50 | 75 | 100; ``` Discrete cognitive capacity values ### EnergyPresence [#energypresence] ```ts export type EnergyPresence = 'visible' | 'muted' | 'hidden'; ``` How a UI element participates at a given energy level. * `visible`: rendered normally * `muted`: rendered but de-emphasized (reduced opacity, secondary styling) * `hidden`: not rendered at all ### EnergyPresenceMap [#energypresencemap] ```ts export type EnergyPresenceMap = Readonly>; ``` A complete presence declaration: one presence value per energy level. This is the annotation apps attach to components/views to state which energy levels they belong to (e.g. "hide the AI chat at 50 and below"). ### EnergyPresenceSpec [#energypresencespec] ```ts export type EnergyPresenceSpec = Partial> & { default?: EnergyPresence; }; ``` Per-level presence spec. Unlisted levels fall back to `default` ('visible' when omitted). ### EnergySource [#energysource] ```ts export type EnergySource = 'manual' | 'scheduled' | 'inferred'; ``` How the energy level was set ### FocusDuration [#focusduration] ```ts export type FocusDuration = 'extended' | 'moderate' | 'short' | 'minimal' | 'none'; ``` ### FocusSessionEvent [#focussessionevent] ```ts export type FocusSessionEvent = 'start' | 'break' | 'end' | 'stop'; ``` Session lifecycle events: * `start`: a session began * `break`: a break nudge is due (recurring while the session runs) * `end`: the session reached `endsAt` and auto-expired * `stop`: the session was ended manually before `endsAt` ### FocusSessionListener [#focussessionlistener] ```ts export type FocusSessionListener = (event: FocusSessionEvent, session: FocusSession) => void; ``` ### InterruptionTolerance [#interruptiontolerance] ```ts export type InterruptionTolerance = 'high' | 'moderate' | 'low' | 'minimal' | 'none'; ``` ### NotificationDeliveryReason [#notificationdeliveryreason] ```ts export type NotificationDeliveryReason = 'immediate' | 'batch' | 'released'; ``` Why a delivery is happening ### NotificationPriority [#notificationpriority] ```ts export type NotificationPriority = 'normal' | 'high' | 'critical'; ``` Priority of a single notification intent ### OriginatorTier [#originatortier] ```ts export type OriginatorTier = 'exempt' | 'known' | 'unknown'; ``` Standing of the originator relative to the user. * `exempt`: the inner circle. Never sees an energy acknowledgment. * `known`: an established correspondent. * `unknown`: no established relationship. Tier assignment is the app's job - a Screener approval, a contacts list, an org chart. The policy only consumes the tier. ### PublishOutcome [#publishoutcome] ```ts export type PublishOutcome = 'delivered' | 'batched' | 'deferred'; ``` What happened to a published notification ### TaskComplexity [#taskcomplexity] ```ts export type TaskComplexity = 'complex' | 'moderate' | 'routine' | 'simple' | 'consumption'; ``` # DOM (/docs/api/reference/dom) {/* Generated by scripts/generate-api-reference.mts from @kumbatio/energy-system@2.1.1. Do not edit: run `pnpm docs:api`. */} Every export of `@kumbatio/energy-system/dom`, generated from the declarations of the installed package (**v2.1.1**). ```ts import { applyEnergyLevel, observeEnergyLevel, readEnergyLevel } from '@kumbatio/energy-system/dom' ``` 3 value exports and 0 type exports. ## Functions [#functions] ### applyEnergyLevel [#applyenergylevel] ```ts export declare function applyEnergyLevel(level: EnergyLevel, root?: HTMLElement): void; ``` Apply energy level to a root element. Sets `data-energy-level` attribute and CSS custom properties derived from the UI visibility strategy. ### observeEnergyLevel [#observeenergylevel] ```ts export declare function observeEnergyLevel(callback: EnergyChangeListener, root?: HTMLElement): () => void; ``` Observe energy level changes on a root element via MutationObserver. Calls back with EnergyState (timestamp will be observation time, source 'inferred'). Returns a cleanup function to disconnect the observer. ### readEnergyLevel [#readenergylevel] ```ts export declare function readEnergyLevel(root?: HTMLElement): EnergyLevel; ``` Read the current energy level from a root element's data attribute. Returns 100 if no valid level is set. # Generated reference (/docs/api/reference) {/* Generated by scripts/generate-api-reference.mts from @kumbatio/energy-system@2.1.1. Do not edit: run `pnpm docs:api`. */} This section is generated from the type declarations of `@kumbatio/energy-system` **v2.1.1** - the exact version this site has installed. Signatures are reproduced verbatim from the shipped `.d.ts`; member tables are compiled from the same declarations at build time. It is complete by construction: **118 exports** across 4 entry points, with nothing transcribed by hand. For explanation, worked examples and the reasoning behind an API, read the [hand-written reference](/docs/api) and the [concept guides](/docs/energy-system). | Entry point | Values | Types | | ------------------------------------------------------------------------ | ------ | ----- | | [`@kumbatio/energy-system`](/docs/api/reference/core) | 46 | 53 | | [`@kumbatio/energy-system/dom`](/docs/api/reference/dom) | 3 | 0 | | [`@kumbatio/energy-system/react`](/docs/api/reference/react) | 9 | 5 | | [`@kumbatio/energy-system/persistence`](/docs/api/reference/persistence) | 2 | 0 | The package root - engine, levels, strategies, sessions, gate, deferral, demand. Provider, hooks, and headless components. Apply and observe energy level on an element. The built-in storage adapters. # Persistence (/docs/api/reference/persistence) {/* Generated by scripts/generate-api-reference.mts from @kumbatio/energy-system@2.1.1. Do not edit: run `pnpm docs:api`. */} Every export of `@kumbatio/energy-system/persistence`, generated from the declarations of the installed package (**v2.1.1**). ```ts import { localStoragePersistence, memoryPersistence } from '@kumbatio/energy-system/persistence' ``` 2 value exports and 0 type exports. ## Functions [#functions] ### localStoragePersistence [#localstoragepersistence] ```ts export declare function localStoragePersistence(key?: string): EnergyPersistence; ``` localStorage-based persistence adapter. Stores the full EnergyState as JSON. ### memoryPersistence [#memorypersistence] ```ts export declare function memoryPersistence(initial?: EnergyState): EnergyPersistence; ``` In-memory persistence adapter. Useful for tests, SSR, or ephemeral sessions. # React (/docs/api/reference/react) {/* Generated by scripts/generate-api-reference.mts from @kumbatio/energy-system@2.1.1. Do not edit: run `pnpm docs:api`. */} Every export of `@kumbatio/energy-system/react`, generated from the declarations of the installed package (**v2.1.1**). ```ts import { EnergyGate, EnergyIndicator, EnergyProvider } from '@kumbatio/energy-system/react' ``` 9 value exports and 5 type exports. ## Functions [#functions] ### EnergyGate [#energygate] ```ts export declare function EnergyGate({ presence, min, max, fallback, whenHidden, children, }: EnergyGateProps): ReactNode; ``` Declarative energy gating for a subtree. ```tsx // Hide the AI chat at 50 and below: // Full presence map, muted state styled by the child: {(presence) => } ``` Headless: renders no wrapper element of its own. ### EnergyIndicator [#energyindicator] ```ts export declare function EnergyIndicator({ children }: EnergyIndicatorProps): ReactNode; ``` Headless energy indicator - bring your own UI ### EnergyProvider [#energyprovider] ```ts export declare function EnergyProvider({ engine: externalEngine, defaultLevel, persistence, onLevelChange, applyToDOM, domTarget, children, }: EnergyProviderProps): import("react").FunctionComponentElement>; ``` ### useEnergyGate [#useenergygate] ```ts export declare function useEnergyGate(minLevel: EnergyLevel): boolean; ``` Returns true if current energy level meets or exceeds the given minimum ### useEnergyLevel [#useenergylevel] ```ts export declare function useEnergyLevel(): [ EnergyLevel, (level: EnergyLevel, source?: EnergySource) => void ]; ``` Read the current energy level and setter ### useEnergyLevelCycler [#useenergylevelcycler] ```ts export declare function useEnergyLevelCycler(): () => void; ``` Returns a function that cycles to the next energy level ### useEnergyPresence [#useenergypresence] ```ts export declare function useEnergyPresence(presence: EnergyPresenceMap): EnergyPresence; ``` Resolve a presence map against the current energy level ### useEnergyState [#useenergystate] ```ts export declare function useEnergyState(): EnergyState; ``` Get the full energy state (level + timestamp + source) ### useStrategy [#usestrategy] ```ts export declare function useStrategy(strategy: AdaptationStrategy): T; ``` Resolve a strategy against current energy level ## Interfaces [#interfaces] ### EnergyIndicatorProps [#energyindicatorprops] ```ts export interface EnergyIndicatorProps { children: (props: EnergyIndicatorRenderProps) => ReactNode; } ``` ### EnergyIndicatorRenderProps [#energyindicatorrenderprops] ```ts export interface EnergyIndicatorRenderProps { level: EnergyLevel; label: string; description: string; cognitiveProfile: EnergyLevelDefinition['cognitiveProfile']; state: EnergyState; definition: EnergyLevelDefinition; levels: readonly EnergyLevelDefinition[]; cycle: () => void; setLevel: (level: EnergyLevel, source?: EnergySource) => void; } ``` ### EnergyProviderProps [#energyproviderprops] ```ts export interface EnergyProviderProps { /** Pre-created engine. When provided, this engine is used directly. */ engine?: EnergyEngine; /** Initial energy level when the provider creates its own engine. */ defaultLevel?: EnergyLevel; /** Persistence adapter when the provider creates its own engine. */ persistence?: EnergyPersistence; /** Called on every level change. */ onLevelChange?: EnergyChangeListener; /** Whether to apply energy level to DOM via data attributes */ applyToDOM?: boolean; /** * Element the level is projected onto. Default: `document.body`. * * Pass `() => document.documentElement` when the stylesheet keys off * `[data-energy-level]` at the root - resolved inside the effect, so the * render phase never touches the DOM. Whatever the target, the provider * snapshots what was there, layers overlapping providers, and restores the * baseline on unmount. */ domTarget?: HTMLElement | (() => HTMLElement | null); children: ReactNode; } ``` ## Type aliases [#type-aliases] ### EnergyGateProps [#energygateprops] ```ts export type EnergyGateProps = EnergyGateBaseProps & ({ /** Full presence declaration for this element */ presence: EnergyPresenceMap; min?: never; max?: never; } | { presence?: never; /** Shorthand: visible at or above this level, hidden below */ min: EnergyLevel; /** Optionally also hidden above this level (band gating) */ max?: EnergyLevel; } | { presence?: never; min?: never; /** Shorthand: visible at or below this level, hidden above */ max: EnergyLevel; }); ``` ### EnergyHiddenBehavior [#energyhiddenbehavior] ```ts export type EnergyHiddenBehavior = 'preserve' | 'unmount'; ``` What happens to the gated subtree when its presence resolves to 'hidden'. * `preserve` (default): kept mounted inside ``, so component state, DOM and scroll position survive. Effects are torn down while hidden and re-run on reveal, and hidden content is not rendered on the server. Energy is expected to move up and down; a half-written message should still be there when capacity returns. * `unmount`: removed from the tree entirely. Use for subtrees whose cost is worth reclaiming at low energy (media, canvases, live connections).