Spec and conformance
The energy model is specified independently of this library - so an implementation in another language is the same model, not a port
@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
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
The vectors ship in the package and are importable directly:
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:
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
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
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
timestampandoriginon 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
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
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?
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.
