@kumbatio/energy-system 1.0.1 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,16 +7,59 @@ and from `1.0.0` this project adheres to [Semantic Versioning](https://semver.or
7
7
 
8
8
  For this package, semver covers more than type signatures. **A shipped strategy
9
9
  table's values are API**: changing what `notificationStrategy` returns at level
10
- 50 changes how every consumer behaves, so it is a major-version change and a
10
+ 50 changes how every consumer behaves, so it is a major-version change - and a
11
11
  change to [SPEC.md](./SPEC.md), not only to this library. The same is true of the
12
12
  reconciliation rule, which two implementations must agree on to share state at
13
13
  all. Prose returned by `describe()` is not covered; wording is a product decision.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
- Corrections to `1.0.0`. No type signature changed, but two of these change
18
- runtime behavior and one narrows an install range — see the note at the top of
19
- this file on why that is more than a patch for this package.
17
+ ## [2.0.5] - 2026-08-21
18
+
19
+ ### Fixed
20
+
21
+ - `api-surface.json` and `conformance.json` shipped in the package now carry the version of the release they belong to. Through 2.0.4 both files were published with the previous release's version stamp, so consumers reading the version from either artifact — or checking it against `package.json` — saw a mismatch. ([ec77e2d](https://github.com/kumbatio/energy-system/commit/ec77e2d), [a0f2dcb](https://github.com/kumbatio/energy-system/commit/a0f2dcb))
22
+
23
+ ## [2.0.4] - 2026-08-21
24
+
25
+ ### Miscellaneous Chores
26
+
27
+ - update version to 2.0.3 in api-surface.json and conformance.json ([3a21713](https://github.com/kumbatio/energy-system/commit/3a21713))
28
+
29
+ ## [2.0.3] - 2026-08-21
30
+
31
+ ### Miscellaneous Chores
32
+
33
+ - regenerate api-surface.json and conformance.json ([427d02d](https://github.com/kumbatio/energy-system/commit/427d02d))
34
+
35
+ ## [2.0.2] - 2026-08-21
36
+
37
+ ### Bug Fixes
38
+
39
+ - update @entro314labs/release-kit to version 2.9.0 and standardize string quotes in CITATION.cff ([ea3d9c1](https://github.com/kumbatio/energy-system/commit/ea3d9c1))
40
+
41
+ ## [2.0.1] - 2026-08-21
42
+
43
+ ### Changed
44
+
45
+ - **The Rest level description now opens with "Recovery." instead of
46
+ "Depleted."** Level-description prose is product copy, not semver-covered API
47
+ (see the note at the top of this file). "Depleted" implies damage, which
48
+ conflicts with the Neutral State Language principle the level copy is
49
+ supposed to embody: describe what the person can do at a level, not what they
50
+ cannot. [SPEC.md](./SPEC.md) §3.1 and `conformance.json` updated to match.
51
+
52
+ ## [2.0.0]
53
+
54
+ Corrections to `1.0.0`. No type signature changed, which is exactly why this is
55
+ a major: two of these change runtime behavior and one narrows an install range,
56
+ and for this package behavior is API - see the note at the top of this file.
57
+
58
+ Upgrading from `1.x`: the breaks are `react`/`@types/react` below 19.2 (which
59
+ never worked with the React entry point), persisted or exchanged states carrying
60
+ properties outside the published schema (previously trimmed in silence, now
61
+ rejected), and any code depending on a batched notification being delivered
62
+ after suppression started or after energy fell below its threshold.
20
63
 
21
64
  ### Fixed
22
65
 
@@ -34,7 +77,7 @@ this file on why that is more than a patch for this package.
34
77
  `createEnergyState()` accepted fractional timestamps, which
35
78
  [spec/energy-state.schema.json](./spec/energy-state.schema.json) does not
36
79
  allow, and persisted state carrying unknown properties was silently trimmed
37
- to fit rather than rejected so two implementations could exchange a state
80
+ to fit rather than rejected - so two implementations could exchange a state
38
81
  and disagree about what they had exchanged. Persistence loads, cross-context
39
82
  observations and `memoryPersistence` now share one strict boundary parser.
40
83
  - **A configured `originId` no longer corrupts the unproduced sentinel.**
@@ -46,7 +89,7 @@ this file on why that is more than a patch for this package.
46
89
  state the engine actually produces.
47
90
  - **`api-surface.json` includes `EnergyEngine.resolve()`.** The declaration
48
91
  parser did not recognise generic members, so a public method was missing from
49
- the frozen surface and a method absent from the freeze is a method nobody
92
+ the frozen surface - and a method absent from the freeze is a method nobody
50
93
  notices removing.
51
94
 
52
95
  ### Changed
@@ -57,13 +100,13 @@ this file on why that is more than a patch for this package.
57
100
  19.0 and 19.1.
58
101
  - **Generated artifacts are checked, not regenerated, during validation.**
59
102
  `pnpm test` used to run the full build first, so the drift guard compared
60
- `conformance.json` against a copy it had just written it could not fail,
103
+ `conformance.json` against a copy it had just written - it could not fail,
61
104
  whatever was committed. Generation now belongs to `pnpm run build`; both
62
105
  generators take `--check`, and both artifacts are verified by the suite.
63
106
 
64
107
  ### Added
65
108
 
66
- - **[spec/conformance.schema.json](./spec/conformance.schema.json)** the
109
+ - **[spec/conformance.schema.json](./spec/conformance.schema.json)** - the
67
110
  schema `conformance.json` has always pointed at via `$schema` and which did
68
111
  not exist. The generator now validates its own output against it before
69
112
  emitting, and it is exported from the package so the relative reference
@@ -79,20 +122,20 @@ The API is frozen. Everything below documents what that commitment now covers.
79
122
 
80
123
  ### Added
81
124
 
82
- - **[SPEC.md](./SPEC.md)** the model specified independently of this
125
+ - **[SPEC.md](./SPEC.md)** - the model specified independently of this
83
126
  implementation: levels, energy state, reconciliation, the strategy contract,
84
127
  autonomy, inbound demand, the runtime invariants, and the accessibility
85
128
  requirements. Language-independent and RFC 2119-worded, so an implementation in
86
129
  another language is an implementation of the same model rather than a port of
87
130
  this one.
88
- - **[spec/energy-state.schema.json](./spec/energy-state.schema.json)** the
131
+ - **[spec/energy-state.schema.json](./spec/energy-state.schema.json)** - the
89
132
  interchange format. Sharing one person's energy state across processes or
90
133
  languages is a wire-format problem, and this is the wire format.
91
- - **[conformance.json](./conformance.json)** 252 vectors plus every strategy
134
+ - **[conformance.json](./conformance.json)** - 252 vectors plus every strategy
92
135
  table, generated from the built library on each build and shipped in the
93
136
  package. An implementation passes by loading and replaying them; a stale file
94
137
  fails this package's own build.
95
- - `isPreferredEnergyState(candidate, current)` the reconciliation rule, which
138
+ - `isPreferredEnergyState(candidate, current)` - the reconciliation rule, which
96
139
  was internal to the engine. It is the hardest part of the model to reimplement
97
140
  correctly, so it is now readable, testable, and covered by vectors on its own.
98
141
  - Accessibility handling in the reference stylesheet for
@@ -101,7 +144,7 @@ The API is frozen. Everything below documents what that commitment now covers.
101
144
  properties inline, and a preference the person set must outrank a value the
102
145
  program computed.
103
146
  - Coverage across every one of the 20 level transitions, in both directions, and
104
- across strategy composition including the model's directional invariants
147
+ across strategy composition - including the model's directional invariants
105
148
  (protection never decreases and automation never gains discretion as capacity
106
149
  falls), which a future edit to one table would otherwise break silently.
107
150
 
@@ -118,13 +161,13 @@ The API is frozen. Everything below documents what that commitment now covers.
118
161
 
119
162
  ### Added
120
163
 
121
- - `autonomyStrategy` / `AutonomyConfig` what automation may do unattended:
164
+ - `autonomyStrategy` / `AutonomyConfig` - what automation may do unattended:
122
165
  confidence threshold, whether wording may be composed, and how many steps may
123
166
  chain. The mirror of interaction forgiveness, which protects against the
124
167
  _user's_ mistakes at low energy where this protects against the _agent's_.
125
168
  What narrows as energy falls is discretion, not action: at Rest the threshold
126
169
  is 1, admitting only certainty, and a single templated step is still allowed.
127
- - `demandAdmissionStrategy` and the pure `resolveDemandOutcome` an
170
+ - `demandAdmissionStrategy` and the pure `resolveDemandOutcome` - an
128
171
  energy-resolved policy for arrivals that ask something of the person: reach
129
172
  them now, acknowledge and queue, or queue silently. Originator tiers, the
130
173
  exempt-tier invariant, and escalation to silence when a classification is not
@@ -141,7 +184,7 @@ The API is frozen. Everything below documents what that commitment now covers.
141
184
  ## [0.4.0]
142
185
 
143
186
  Patterns studied in a field ADHD app and reimplemented as first-class, tested
144
- primitives including guarantees against the two failure modes observed in the
187
+ primitives - including guarantees against the two failure modes observed in the
145
188
  wild: suppressed reminders destroyed instead of deferred, and focus suppression
146
189
  that never auto-expired.
147
190
 
@@ -170,3 +213,16 @@ framework-agnostic engine with strategy resolution, the first three built-in
170
213
  strategies, the DOM adapter, the React provider and hooks, persistence adapters
171
214
  with external observation, deterministic clocks, derived metrics, and legacy
172
215
  level compatibility mapping.
216
+
217
+ [Unreleased]: https://github.com/kumbatio/energy-system/compare/v2.0.5...HEAD
218
+ [2.0.5]: https://github.com/kumbatio/energy-system/compare/v2.0.4...v2.0.5
219
+ [2.0.4]: https://github.com/kumbatio/energy-system/compare/v2.0.3...v2.0.4
220
+ [2.0.3]: https://github.com/kumbatio/energy-system/compare/v2.0.2...v2.0.3
221
+ [2.0.2]: https://github.com/kumbatio/energy-system/compare/v2.0.1...v2.0.2
222
+ [2.0.1]: https://github.com/kumbatio/energy-system/compare/v2.0.0...v2.0.1
223
+ [2.0.0]: https://github.com/kumbatio/energy-system/compare/v1.0.0...v2.0.0
224
+ [1.0.0]: https://github.com/kumbatio/energy-system/compare/v0.6.0...v1.0.0
225
+ [0.6.0]: https://github.com/kumbatio/energy-system/compare/v0.5.4...v0.6.0
226
+ [0.5.4]: https://github.com/kumbatio/energy-system/compare/v0.4.0...v0.5.4
227
+ [0.4.0]: https://github.com/kumbatio/energy-system/compare/v0.1.0...v0.4.0
228
+ [0.1.0]: https://github.com/kumbatio/energy-system/releases/tag/v0.1.0
package/README.md CHANGED
@@ -76,7 +76,7 @@ This library gives applications a structured way to adapt to energy state instea
76
76
  - Energy-aware deferral ordering
77
77
  - Autonomy (how much automation may do unattended)
78
78
  - **Inbound demand admission**: an energy-resolved policy for arrivals that ask
79
- something of the user reach them now, acknowledge and queue, or queue
79
+ something of the user - reach them now, acknowledge and queue, or queue
80
80
  silently (`demandAdmissionStrategy`, `resolveDemandOutcome`)
81
81
  - **Presence annotation**: declare which energy levels a component/view belongs
82
82
  to (`defineEnergyPresence`, `presenceAtOrAbove`, `<EnergyGate>`, `data-energy-min`)
@@ -161,8 +161,8 @@ export function App() {
161
161
  ## Presence annotation (which energy states does this element belong to?)
162
162
 
163
163
  Every component/view can declare the energy levels it participates in. The
164
- declaration is a plain typed object one presence (`'visible' | 'muted' |
165
- 'hidden'`) per level so the same annotation drives React, the engine, or
164
+ declaration is a plain typed object - one presence (`'visible' | 'muted' |
165
+ 'hidden'`) per level - so the same annotation drives React, the engine, or
166
166
  plain CSS.
167
167
 
168
168
  ```ts
@@ -218,7 +218,7 @@ capacity dipped. `<EnergyGate>` hides through React 19.2's `<Activity>`:
218
218
  component state, DOM and scroll position survive, effects are torn down while
219
219
  hidden and re-run on reveal, and hidden content is not server-rendered.
220
220
 
221
- Opt out for subtrees whose cost is worth reclaiming media, canvases, live
221
+ Opt out for subtrees whose cost is worth reclaiming - media, canvases, live
222
222
  connections:
223
223
 
224
224
  ```tsx
@@ -227,12 +227,12 @@ connections:
227
227
  </EnergyGate>
228
228
  ```
229
229
 
230
- CSS-only path annotate elements with the range they belong to and the
230
+ CSS-only path - annotate elements with the range they belong to and the
231
231
  stylesheet handles hiding as `data-energy-level` changes:
232
232
 
233
233
  ```html
234
- <div data-energy-min="75">AI chat needs 75+ energy</div>
235
- <div data-energy-max="25">Recovery hint low energy only</div>
234
+ <div data-energy-min="75">AI chat - needs 75+ energy</div>
235
+ <div data-energy-max="25">Recovery hint - low energy only</div>
236
236
  ```
237
237
 
238
238
  ## Focus sessions and the notification gate
@@ -284,7 +284,7 @@ Two invariants are guaranteed by construction:
284
284
 
285
285
  Deferring is an energy statement. Presets are pure `(now) => Date` functions;
286
286
  `deferralStrategy` orders them by level so the one-tap default matches
287
- capacity at low energy the default is "tomorrow morning", not "in 1 hour".
287
+ capacity - at low energy the default is "tomorrow morning", not "in 1 hour".
288
288
 
289
289
  ```ts
290
290
  import {
@@ -307,13 +307,13 @@ attention or action: an email, a document comment, a review request, a task
307
307
  assignment, a collaboration invite.
308
308
 
309
309
  Every triage system in general use is organised around properties of the
310
- _message_ who sent it, how urgent it claims to be, what category it fits.
310
+ _message_ - who sent it, how urgent it claims to be, what category it fits.
311
311
  None is organised around the state of the _recipient_, which is the thing that
312
312
  actually decides whether an arrival is a small task or a crushing weight. This
313
313
  is that variable, applied to the queue.
314
314
 
315
315
  `demandAdmissionStrategy` resolves the policy; `resolveDemandOutcome` applies
316
- it. Both are pure. The library performs no effects here acknowledging an
316
+ it. Both are pure. The library performs no effects here - acknowledging an
317
317
  originator means sending mail, posting a comment, or updating a status chip
318
318
  depending on the app, and those are irreversible in ways an in-process runtime
319
319
  cannot make transactional. The orchestration, with its ordering, retries, and
@@ -343,7 +343,7 @@ switch (outcome.admission) {
343
343
  case 'acknowledge':
344
344
  // One act, never two. An acknowledgment without a capture is a promise
345
345
  // nobody kept; a capture without an acknowledgment leaves the originator in
346
- // silence. Capture first it is the reversible half.
346
+ // silence. Capture first - it is the reversible half.
347
347
  await tasks.capture(message, engine.resolve(deferralStrategy).defaultPresetId)
348
348
  return replies.acknowledge(message, outcome.acknowledgment)
349
349
  case 'silent':
@@ -356,14 +356,14 @@ Two rules are worth stating outright, because each blocks a specific failure:
356
356
  1. **The exempt tier is never handled by machine.** At every level, an exempt
357
357
  originator is admitted live. This is also what defuses the gaming risk: an
358
358
  originator who learns an acknowledgment means "deprioritised" and escalates
359
- elsewhere only succeeds if their escalation is one the user cannot ignore
359
+ elsewhere only succeeds if their escalation is one the user cannot ignore -
360
360
  which is what makes them exempt.
361
361
  2. **Acknowledgments report state, never intent.** "Received and queued,
362
362
  current response horizon early next week" is a fact. "I'll get back to you
363
363
  soon" is a promise the user's Tuesday self has to keep. The horizon comes
364
364
  from `deferralStrategy`, so the queue and the acknowledgment cannot disagree.
365
365
 
366
- Disclosure is the app's job in the app's own medium an `Auto-Submitted:
366
+ Disclosure is the app's job in the app's own medium - an `Auto-Submitted:
367
367
  auto-replied` header, an "auto-queued" badge, a system-attributed status. An
368
368
  automated action toward a third party must be identifiable as automated.
369
369
 
@@ -380,8 +380,8 @@ const { confidenceThreshold, allowGeneratedContent, maxUnattendedSteps } =
380
380
  ```
381
381
 
382
382
  What narrows as energy falls is _discretion_, not action. At rest the
383
- confidence threshold is `1`, which admits only certainty rule-based actions,
384
- never a judgment call and `maxUnattendedSteps` is `1`. Automation may still
383
+ confidence threshold is `1`, which admits only certainty - rule-based actions,
384
+ never a judgment call - and `maxUnattendedSteps` is `1`. Automation may still
385
385
  take a single, certain, template-only step, which is exactly the shape of an
386
386
  out-of-office reply. It may not chain steps or improvise wording. The system
387
387
  acts for the user precisely when they are least able to supervise it, so the
@@ -421,9 +421,9 @@ Then use classes like:
421
421
 
422
422
  And presence attributes:
423
423
 
424
- - `data-energy-min="75"` element hides whenever the current level is below 75
425
- - `data-energy-max="25"` element hides whenever the current level is above 25
426
- - `data-energy-presence="muted" | "hidden"` hooks for JS-resolved presence
424
+ - `data-energy-min="75"` - element hides whenever the current level is below 75
425
+ - `data-energy-max="25"` - element hides whenever the current level is above 25
426
+ - `data-energy-presence="muted" | "hidden"` - hooks for JS-resolved presence
427
427
  (`--energy-muted-opacity` controls the muted treatment)
428
428
 
429
429
  The stylesheet honours `prefers-reduced-motion` for its own transitions; apps
@@ -568,14 +568,14 @@ default state, and rejects rather than overwriting unread storage if that hydrat
568
568
 
569
569
  The model is specified independently of this implementation.
570
570
 
571
- - **[SPEC.md](./SPEC.md)** the normative model: levels, state, reconciliation, the strategy
571
+ - **[SPEC.md](./SPEC.md)** - the normative model: levels, state, reconciliation, the strategy
572
572
  contract, autonomy, inbound demand, the runtime invariants, and the accessibility requirements.
573
573
  Language-independent, RFC 2119 wording.
574
- - **[spec/energy-state.schema.json](./spec/energy-state.schema.json)** the interchange format, so
574
+ - **[spec/energy-state.schema.json](./spec/energy-state.schema.json)** - the interchange format, so
575
575
  two processes (or two languages) can share one person's energy state.
576
- - **[conformance.json](./conformance.json)** every table and every decision above, as vectors.
576
+ - **[conformance.json](./conformance.json)** - every table and every decision above, as vectors.
577
577
  Ships in the package.
578
- - **[spec/conformance.schema.json](./spec/conformance.schema.json)** the shape of that vector
578
+ - **[spec/conformance.schema.json](./spec/conformance.schema.json)** - the shape of that vector
579
579
  file, so a port can tell a file it can trust from one whose structure moved under it. Ships too,
580
580
  and the generator validates its own output against it.
581
581
 
@@ -588,20 +588,20 @@ package's own `test/conformance.test.ts` does exactly that and is a reasonable m
588
588
 
589
589
  The file is generated from the built library, so the vectors cannot drift from the behavior they
590
590
  describe. `pnpm run build` regenerates it; `pnpm test` recomputes it and fails if what is committed
591
- differs, without rewriting anything. The two are deliberately separate a check that regenerates
591
+ differs, without rewriting anything. The two are deliberately separate - a check that regenerates
592
592
  first is comparing a file to itself.
593
593
 
594
594
  Vectors cover the pure surface: tables, and functions of their arguments alone. The stateful
595
- guarantees defer-never-drop, session auto-expiry, persistence ordering are normative in SPEC.md
595
+ guarantees - defer-never-drop, session auto-expiry, persistence ordering - are normative in SPEC.md
596
596
  and checked by this package's suite, because no vector can express _and it must never drop one_.
597
597
 
598
598
  ## Stability
599
599
 
600
- `1.0.0` is an API freeze. From here the package follows semver strictly, and for this package that
601
- means more than the type signatures:
600
+ The API has been frozen since `1.0.0`. The package follows semver strictly, and for this package
601
+ that means more than the type signatures:
602
602
 
603
603
  - **A shipped strategy table's values are API.** Changing what `notificationStrategy` returns at
604
- level 50 changes how every consumer behaves, so it is a major-version change and a change to
604
+ level 50 changes how every consumer behaves, so it is a major-version change - and a change to
605
605
  [SPEC.md](./SPEC.md), not just to this library.
606
606
  - **The reconciliation rule is API.** Two implementations that disagree about it cannot share state.
607
607
  - **The conformance vectors are the contract in machine-readable form.** Within a major version,
@@ -612,7 +612,7 @@ means more than the type signatures:
612
612
  ## Development
613
613
 
614
614
  ```bash
615
- pnpm run validate # format, lint, types, tests, packaging what CI runs
615
+ pnpm run validate # format, lint, types, tests, packaging - what CI runs
616
616
  ```
617
617
 
618
618
  Individually:
@@ -627,7 +627,7 @@ pnpm run pack:dry-run
627
627
 
628
628
  `build` is the only thing that writes `api-surface.json` and `conformance.json`.
629
629
  If a change to the library moves either, `pnpm test` fails and tells you to run
630
- `pnpm run build` and commit the result that is the intended loop, not a
630
+ `pnpm run build` and commit the result - that is the intended loop, not a
631
631
  warning to work around.
632
632
 
633
633
  ## Notes
@@ -637,20 +637,36 @@ adapters (e.g., SQLite-backed desktop stores) should live in consuming apps.
637
637
 
638
638
  ## Who uses this
639
639
 
640
- - **[Anasa](https://anasa.md)** Kumbatio's local-first writing and thinking workspace, in public alpha. Runs its entire adaptive shell on the engine: custom settings-backed persistence, energy-gated AI surfaces, notification filtering, and task-complexity guidance.
641
- - **[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. Uses the notification gate (defer, never drop), focus sessions, deferral ordering, interaction forgiveness, and demand admission integrated without the React adapter. Its demand binding is the reference one: originator tiers come from its VIP list, obligation is classified from RFC 3834 header evidence and its own sender lanes, acknowledgments go out as `Auto-Submitted: auto-replied` auto-replies, and the capture is a snooze to the horizon `deferralStrategy` picked.
642
- - **[kumbat.io](https://kumbat.io)** the site itself runs on this model; change the energy level there and watch the interface adapt.
640
+ - **[Anasa](https://anasa.md)** - Kumbatio's local-first writing and thinking workspace, in public alpha. Runs its entire adaptive shell on the engine: custom settings-backed persistence, energy-gated AI surfaces, notification filtering, and task-complexity guidance.
641
+ - **[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. Uses the notification gate (defer, never drop), focus sessions, deferral ordering, interaction forgiveness, and demand admission - integrated without the React adapter. Its demand binding is the reference one: originator tiers come from its VIP list, obligation is classified from RFC 3834 header evidence and its own sender lanes, acknowledgments go out as `Auto-Submitted: auto-replied` auto-replies, and the capture is a snooze to the horizon `deferralStrategy` picked.
642
+ - **[kumbat.io](https://kumbat.io)** - the site itself runs on this model; change the energy level there and watch the interface adapt.
643
643
 
644
644
  The integration patterns these apps proved out are documented in the [Production Patterns guide](https://docs.kumbat.io/docs/energy-system/guides/production-patterns). If you ship something with `energy-system`, tell us: [hello@kumbat.io](mailto:hello@kumbat.io).
645
645
 
646
646
  ## Kumbatio
647
647
 
648
- `energy-system` is the infrastructure layer of [Kumbatio](https://kumbat.io) an ecosystem of open-source, neuroinclusive software built from lived experience with ADHD and depression. The position behind it, in one line: **energy ≠ time**, and software should adapt to real cognitive capacity instead of assuming a default brain.
648
+ `energy-system` is the infrastructure layer of [Kumbatio](https://kumbat.io) - an ecosystem of open-source, neuroinclusive software built from lived experience with ADHD and depression. The position behind it, in one line: **energy ≠ time**, and software should adapt to real cognitive capacity instead of assuming a default brain.
649
649
 
650
- - The full argument: [kumbat.io/manifesto](https://kumbat.io/manifesto) agree? [Sign it](https://kumbat.io/endorse)
650
+ - The full argument: [kumbat.io/manifesto](https://kumbat.io/manifesto) - agree? [Sign it](https://kumbat.io/endorse)
651
651
  - Where this library is going: [ROADMAP.md](./ROADMAP.md)
652
652
  - How to help: [CONTRIBUTING.md](./CONTRIBUTING.md)
653
- - Live demo: [kumbat.io](https://kumbat.io) adapts its entire interface with this model move the energy control and watch
653
+ - Live demo: [kumbat.io](https://kumbat.io) adapts its entire interface with this model - move the energy control and watch
654
+
655
+ ## Research
656
+
657
+ The thinking behind this library is documented in a five-paper preprint series,
658
+ published on Zenodo under CC BY 4.0. Papers 1 and 4 below describe this library
659
+ directly; the others give the evidence base, the self-report position, and the
660
+ work-design framework. Badges link the concept DOI, which always resolves to
661
+ the latest version.
662
+
663
+ 1. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21915159.svg)](https://doi.org/10.5281/zenodo.21915159) Energy as State, Not Time - the model this library implements
664
+ 2. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21915161.svg)](https://doi.org/10.5281/zenodo.21915161) The Myth of the Flat Workday - the cognitive-science evidence
665
+ 3. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21915163.svg)](https://doi.org/10.5281/zenodo.21915163) Self-Report Over Surveillance - why the state is declared, never inferred
666
+ 4. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21915165.svg)](https://doi.org/10.5281/zenodo.21915165) Capacity-Aware Software Design - the pattern language this library ships
667
+ 5. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21915167.svg)](https://doi.org/10.5281/zenodo.21915167) Energy > Time - the framework, from individuals to teams
668
+
669
+ To cite the software itself, see [CITATION.cff](./CITATION.cff).
654
670
 
655
671
  ## License
656
672
 
package/SPEC.md CHANGED
@@ -1,4 +1,4 @@
1
- # The Energy Model Specification
1
+ # The Energy Model - Specification
2
2
 
3
3
  **Version 1 · normative · language-independent**
4
4
 
@@ -6,7 +6,7 @@ This document specifies the energy model: what an energy state is, how two of
6
6
  them are ordered when they meet, and what each of the five levels means for the
7
7
  behaviors built on top. `@kumbatio/energy-system` is the reference
8
8
  implementation, not the definition. Anything that implements what is written
9
- here in Swift, Kotlin, Rust, Python, Go, or another JavaScript library is an
9
+ here - in Swift, Kotlin, Rust, Python, Go, or another JavaScript library - is an
10
10
  implementation of the same model, and states produced by one may be read by
11
11
  another.
12
12
 
@@ -23,7 +23,7 @@ Key words follow RFC 2119: **MUST**, **MUST NOT**, **SHOULD**, **MAY**.
23
23
  ## 1. Why this exists as a spec at all
24
24
 
25
25
  The claim the model makes is not "here is a nice API". It is that **capacity is
26
- first-class application state** as real as the current user or the current
26
+ first-class application state** - as real as the current user or the current
27
27
  document, and as deserving of a stable representation.
28
28
 
29
29
  That claim only pays off if one person's energy state can be shared by
@@ -48,7 +48,7 @@ The model has exactly **five** levels, and they are discrete.
48
48
  | `75` | `active` | Active | Good capacity. Focused execution, problem-solving. |
49
49
  | `50` | `steady` | Steady | Moderate capacity. Routine tasks, familiar work. |
50
50
  | `25` | `low` | Low | Limited capacity. Simple tasks, review, light work. |
51
- | `0` | `rest` | Rest | Depleted. Consumption only reading, reflecting. |
51
+ | `0` | `rest` | Rest | Recovery. Consumption only - reading, reflecting. |
52
52
 
53
53
  An implementation **MUST** use exactly these five values and **MUST NOT** admit
54
54
  intermediate ones. This is a design commitment, not an arbitrary limit: a
@@ -56,8 +56,8 @@ continuous slider asks for precision nobody has about their own state, and it
56
56
  turns a one-second act into a judgement call at exactly the moment judgement is
57
57
  expensive. Five is also few enough to cycle through with one key.
58
58
 
59
- Each level carries a **cognitive profile** `decisionCapacity`,
60
- `focusDuration`, `taskComplexity`, `interruptionTolerance` enumerated in the
59
+ Each level carries a **cognitive profile** - `decisionCapacity`,
60
+ `focusDuration`, `taskComplexity`, `interruptionTolerance` - enumerated in the
61
61
  vectors under `levels`. Implementations **MUST** reproduce these values;
62
62
  consumers branch on them.
63
63
 
@@ -95,9 +95,9 @@ this with whatever their language offers.
95
95
 
96
96
  `source` is not decoration; §4 gives it authority.
97
97
 
98
- - `manual` the person set it. **The default, and the trust anchor.**
99
- - `scheduled` a rule the person configured applied it (a calendar, a time of day).
100
- - `inferred` the system worked it out from behavior.
98
+ - `manual` - the person set it. **The default, and the trust anchor.**
99
+ - `scheduled` - a rule the person configured applied it (a calendar, a time of day).
100
+ - `inferred` - the system worked it out from behavior.
101
101
 
102
102
  An implementation **MUST** support `manual`. Inference **MAY** be offered but
103
103
  **MUST** be opt-in, and a system that infers **SHOULD** present the result as a
@@ -125,7 +125,7 @@ some frameworks fail the build on outright.
125
125
  An implementation that accepts states from outside itself **SHOULD** reject any
126
126
  whose `timestamp` exceeds local time by more than a bounded skew budget. Without
127
127
  it, one context with a badly-set clock wins every comparison until real time
128
- catches up to its timestamp which may be years.
128
+ catches up to its timestamp - which may be years.
129
129
 
130
130
  The reference default is **5 minutes**. The budget **SHOULD** be configurable,
131
131
  including "accept anything finite" for controlled environments.
@@ -146,17 +146,17 @@ different values depending on timing, so two contexts disagree permanently.
146
146
  Given a `candidate` and a `current`, the candidate replaces the current if and
147
147
  only if the first differing key below favours it:
148
148
 
149
- 1. **`timestamp`** greater wins.
150
- 2. **`revision`** greater wins.
151
- 3. **`source`** `manual` (3) > `scheduled` (2) > `inferred` (1).
152
- 4. **`origin`** greater by lexicographic comparison of the string.
149
+ 1. **`timestamp`** - greater wins.
150
+ 2. **`revision`** - greater wins.
151
+ 3. **`source`** - `manual` (3) > `scheduled` (2) > `inferred` (1).
152
+ 4. **`origin`** - greater by lexicographic comparison of the string.
153
153
 
154
154
  If all four are equal, the states are equal: the candidate **MUST NOT** replace
155
155
  the current, and no change **MUST** be reported.
156
156
 
157
157
  Key 4 is arbitrary and deliberately so. When two producers write the same
158
158
  instant, the same revision, and the same class of source, there is no principled
159
- winner and an arbitrary rule every context computes _identically_ beats a coin
159
+ winner - and an arbitrary rule every context computes _identically_ beats a coin
160
160
  flip each context tosses separately. Convergence is the property that matters.
161
161
 
162
162
  An implementation **MUST** additionally treat a `level` difference as a
@@ -205,7 +205,7 @@ not every host has deferral or automation. Whatever it provides **MUST** match
205
205
  the vectors exactly.
206
206
 
207
207
  Implementations **MAY** ship additional strategies and **SHOULD** allow
208
- consumers to supply their own the type is a contract, not a closed set.
208
+ consumers to supply their own - the type is a contract, not a closed set.
209
209
 
210
210
  ### 5.1 Human-readable descriptions
211
211
 
@@ -242,8 +242,8 @@ come from fixed templates), and `maxUnattendedSteps` (how many automated steps
242
242
  may chain before control returns).
243
243
 
244
244
  The rule the numbers encode: **what narrows as energy falls is discretion, not
245
- action.** At Rest the threshold is `1`, admitting only certainty rule-based
246
- decisions, never judgement calls with one step and no composition. Automation
245
+ action.** At Rest the threshold is `1`, admitting only certainty - rule-based
246
+ decisions, never judgement calls - with one step and no composition. Automation
247
247
  **MAY** still take a single certain templated action there; an out-of-office
248
248
  reply is exactly that shape, and it is _safest_ at Rest precisely because it has
249
249
  stopped improvising.
@@ -261,7 +261,7 @@ attention or action: an email, a comment, a review request, an assignment, an
261
261
  invitation.
262
262
 
263
263
  This is where the model earns its keep. Every triage system in general use is
264
- organised around properties of the _message_ who sent it, what it claims about
264
+ organised around properties of the _message_ - who sent it, what it claims about
265
265
  its own urgency, what category it fits. None is organised around the state of
266
266
  the _recipient_, which is what actually decides whether an arrival is a small
267
267
  task or a crushing weight.
@@ -291,11 +291,11 @@ The decision is pure. Acting on it is not, and four rules bound what acting may
291
291
  look like. An implementation performing these effects **MUST** observe them.
292
292
 
293
293
  1. **Acknowledgment and capture are one act.** An acknowledgment without a
294
- captured obligation is a promise nobody kept strictly worse than silence,
294
+ captured obligation is a promise nobody kept - strictly worse than silence,
295
295
  because it converts ambient guilt into explicit written debt. A capture
296
296
  without an acknowledgment leaves the originator in silence, which is the
297
- problem being solved. Where the two cannot be made atomic and across a
298
- network they cannot the capture **MUST** be performed first, because it is
297
+ problem being solved. Where the two cannot be made atomic - and across a
298
+ network they cannot - the capture **MUST** be performed first, because it is
299
299
  the reversible half, and **MUST** be rolled back if the acknowledgment fails.
300
300
  2. **Acknowledgments state, never promise.** "Received and queued, current
301
301
  response horizon Thursday" is a fact. "I'll get back to you soon" is a
@@ -303,7 +303,7 @@ look like. An implementation performing these effects **MUST** observe them.
303
303
  NOT** emit commitments. The horizon **SHOULD** come from the deferral
304
304
  strategy, so the queue and the acknowledgment cannot disagree.
305
305
  3. **Automated action toward a third party MUST be disclosed as automated.** The
306
- medium decides the mechanism `Auto-Submitted: auto-replied` on email
306
+ medium decides the mechanism - `Auto-Submitted: auto-replied` on email
307
307
  (RFC 3834, which also prevents responder loops), a system-attributed badge in
308
308
  an app. Undisclosed automation speaking in a person's name is the failure
309
309
  this design exists to avoid.
@@ -319,7 +319,7 @@ org chart. The model consumes the tier and does not compute it.
319
319
  The exempt tier is also what defuses the obvious gaming risk. An originator who
320
320
  learns that an acknowledgment means "deprioritised" and escalates through
321
321
  another channel only succeeds if their escalation is one the person cannot
322
- ignore which is what would have made them exempt in the first place.
322
+ ignore - which is what would have made them exempt in the first place.
323
323
 
324
324
  ---
325
325
 
@@ -327,7 +327,7 @@ ignore — which is what would have made them exempt in the first place.
327
327
 
328
328
  Some requirements cannot be expressed as a vector, because they are about
329
329
  sequences rather than functions. They are normative regardless, and they are the
330
- requirements most often got wrong each is here because it was observed failing
330
+ requirements most often got wrong - each is here because it was observed failing
331
331
  in a shipped product.
332
332
 
333
333
  ### 9.1 Notification gating
@@ -346,7 +346,7 @@ entirely.
346
346
 
347
347
  Any suppression window **MUST** expire on its own. Expiry **MUST** be an emitted
348
348
  event, not a condition the host is expected to poll, and suppression **MUST** be
349
- lifted _before_ the end-of-window event is emitted otherwise the window
349
+ lifted _before_ the end-of-window event is emitted - otherwise the window
350
350
  swallows its own completion notice.
351
351
 
352
352
  _Why:_ focus modes that suppress until manually cleared strand the person on
@@ -354,7 +354,7 @@ exactly the day they forget, and the cost lands on someone who already had none.
354
354
 
355
355
  ### 9.3 Deferral
356
356
 
357
- Deferral presets **MUST** compute in the person's local time "tomorrow
357
+ Deferral presets **MUST** compute in the person's local time - "tomorrow
358
358
  morning" means their morning. The ordering **SHOULD** follow the level: at low
359
359
  capacity the one-tap default resurfaces work _later_, not in an hour, because
360
360
  resurfacing into the same depletion helps nobody.
@@ -364,7 +364,7 @@ UTC and an implementation replaying them **MUST** do the same.
364
364
 
365
365
  ### 9.4 Persistence
366
366
 
367
- Persisted state **MUST** round-trip verbatim every field, not just the level.
367
+ Persisted state **MUST** round-trip verbatim - every field, not just the level.
368
368
  An implementation that stores only the level and rebuilds the rest on read
369
369
  produces a new `timestamp` and `origin` on every load, which reads as a fresh
370
370
  write to §4 and causes contexts to fight.
@@ -430,10 +430,10 @@ this specification, not just to the library.
430
430
 
431
431
  - **Wording.** Every user-facing string is a product decision.
432
432
  - **Visual design.** The reference stylesheet is one interpretation.
433
- - **How a level is chosen.** A dial, a keystroke, a menu the model requires
433
+ - **How a level is chosen.** A dial, a keystroke, a menu - the model requires
434
434
  only that the person can set it directly.
435
435
  - **Classification.** How a host decides that an arrival bears an obligation, or
436
436
  what tier an originator is in, is the host's problem entirely. The model
437
437
  consumes the answer and the confidence attached to it.
438
- - **Transport.** How states reach each other shared storage, IPC, a sync
439
- server is out of scope. The schema and §4 are what make any transport work.
438
+ - **Transport.** How states reach each other - shared storage, IPC, a sync
439
+ server - is out of scope. The schema and §4 are what make any transport work.
package/api-surface.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.1",
2
+ "version": "2.0.5",
3
3
  "entryPoints": [
4
4
  {
5
5
  "specifier": "@kumbatio/energy-system",
package/conformance.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./spec/conformance.schema.json",
3
- "version": "1.0.1",
3
+ "version": "2.0.5",
4
4
  "timezone": "UTC",
5
5
  "note": "Generated from the built library by scripts/extract-conformance.ts. See SPEC.md for the normative rules these vectors encode, including the runtime invariants that no vector can express.",
6
6
  "levels": [
@@ -56,7 +56,7 @@
56
56
  "value": 0,
57
57
  "key": "rest",
58
58
  "label": "Rest",
59
- "description": "Depleted. Consumption only — reading, reflecting.",
59
+ "description": "Recovery. Consumption only — reading, reflecting.",
60
60
  "cognitiveProfile": {
61
61
  "decisionCapacity": "none",
62
62
  "focusDuration": "none",
package/dist/defer.d.ts CHANGED
@@ -21,13 +21,13 @@ export declare const DEFERRAL_PRESET_IDS: Readonly<{
21
21
  readonly nextMonday: 'next-monday';
22
22
  }>;
23
23
  /**
24
- * Build the standard deferral presets. Times are computed in local time
24
+ * Build the standard deferral presets. Times are computed in local time -
25
25
  * "tomorrow morning" means the user's morning.
26
26
  */
27
27
  export declare function createDeferralPresets(options?: DeferralPresetOptions): readonly DeferralPreset[];
28
28
  /**
29
29
  * Resolve a preset id to a resurface timestamp (epoch ms).
30
- * Returns null for an unknown id callers decide whether that is an error.
30
+ * Returns null for an unknown id - callers decide whether that is an error.
31
31
  */
32
32
  export declare function resolveDeferral(presets: readonly DeferralPreset[], presetId: string, now?: Date): number | null;
33
33
  export interface DeferralConfig {
package/dist/defer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getEnergyLevel } from './levels.js';
2
2
  /**
3
- * Deferral ("snooze") the "not now" primitive. Deferring an item is an
3
+ * Deferral ("snooze") - the "not now" primitive. Deferring an item is an
4
4
  * energy statement: it declares insufficient capacity for it right now and
5
5
  * names when it should resurface. Presets are pure `(now) -> Date` functions;
6
6
  * the energy-aware strategy orders them so the default suggestion matches
@@ -33,7 +33,7 @@ function isWeekend(day) {
33
33
  return weekday === 0 || weekday === 6;
34
34
  }
35
35
  /**
36
- * Build the standard deferral presets. Times are computed in local time
36
+ * Build the standard deferral presets. Times are computed in local time -
37
37
  * "tomorrow morning" means the user's morning.
38
38
  */
39
39
  export function createDeferralPresets(options = {}) {
@@ -98,7 +98,7 @@ export function createDeferralPresets(options = {}) {
98
98
  }
99
99
  /**
100
100
  * Resolve a preset id to a resurface timestamp (epoch ms).
101
- * Returns null for an unknown id callers decide whether that is an error.
101
+ * Returns null for an unknown id - callers decide whether that is an error.
102
102
  */
103
103
  export function resolveDeferral(presets, presetId, now = new Date()) {
104
104
  const preset = presets.find((candidate) => candidate.id === presetId);
package/dist/demand.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { AdaptationStrategy } from './types.js';
7
7
  * - `known`: an established correspondent.
8
8
  * - `unknown`: no established relationship.
9
9
  *
10
- * Tier assignment is the app's job a Screener approval, a contacts list, an
10
+ * Tier assignment is the app's job - a Screener approval, a contacts list, an
11
11
  * org chart. The policy only consumes the tier.
12
12
  */
13
13
  export type OriginatorTier = 'exempt' | 'known' | 'unknown';
@@ -18,7 +18,7 @@ export declare function isOriginatorTier(value: unknown): value is OriginatorTie
18
18
  *
19
19
  * - `live`: reaches the user now, untouched by this policy.
20
20
  * - `acknowledge`: the originator is acknowledged and the obligation is
21
- * captured for later. The two are one act an acknowledgment without a
21
+ * captured for later. The two are one act - an acknowledgment without a
22
22
  * capture is a promise nobody kept, a capture without an acknowledgment
23
23
  * leaves the originator in silence.
24
24
  * - `silent`: captured for later with no acknowledgment.
@@ -26,7 +26,7 @@ export declare function isOriginatorTier(value: unknown): value is OriginatorTie
26
26
  export type DemandAdmission = 'live' | 'acknowledge' | 'silent';
27
27
  /** How much an acknowledgment may say */
28
28
  export type AcknowledgmentDetail = 'full' | 'brief' | 'minimal';
29
- /** Why the policy reached its decision for audit trails and explanatory UI */
29
+ /** Why the policy reached its decision - for audit trails and explanatory UI */
30
30
  export type DemandOutcomeReason = 'exempt-originator' | 'tier-admitted' | 'no-obligation' | 'acknowledgment-disabled' | 'below-confidence' | 'acknowledged';
31
31
  /**
32
32
  * What the acknowledgment is permitted to be. The two axes are independent:
@@ -49,8 +49,8 @@ export interface DemandOutcome {
49
49
  export interface DemandInput {
50
50
  readonly originatorTier: OriginatorTier;
51
51
  /**
52
- * Whether this demand asks something of the user. Informational mail a
53
- * receipt, a newsletter, a build notification is not this policy's
52
+ * Whether this demand asks something of the user. Informational mail - a
53
+ * receipt, a newsletter, a build notification - is not this policy's
54
54
  * business and passes through untouched.
55
55
  */
56
56
  readonly bearsObligation: boolean;
@@ -77,7 +77,7 @@ export interface DemandAdmissionConfig {
77
77
  export declare const demandAdmissionStrategy: AdaptationStrategy<DemandAdmissionConfig>;
78
78
  /**
79
79
  * The pure gating decision, extracted so apps can unit-test their demand
80
- * policy without wiring any effects the counterpart of
80
+ * policy without wiring any effects - the counterpart of
81
81
  * `resolveNotificationOutcome`.
82
82
  *
83
83
  * Both configs are required because the two questions are genuinely separate:
package/dist/demand.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getEnergyLevel } from './levels.js';
2
2
  /**
3
- * Inbound demand anything arriving from outside that asks for the user's
3
+ * Inbound demand - anything arriving from outside that asks for the user's
4
4
  * attention or action: an email, a document comment, a review request, a task
5
5
  * assignment, a collaboration invite.
6
6
  *
@@ -54,8 +54,8 @@ const ADMISSION_CONFIGS = freezeObject({
54
54
  acknowledgmentDetail: 'brief',
55
55
  }),
56
56
  // Rest: the acknowledgment survives, stripped to a fixed template. The
57
- // originator's social debt still clears that is the whole point of the
58
- // loop but nothing is composed on the user's behalf.
57
+ // originator's social debt still clears - that is the whole point of the
58
+ // loop - but nothing is composed on the user's behalf.
59
59
  0: freezeObject({
60
60
  originatorThreshold: 'exempt',
61
61
  acknowledge: true,
@@ -94,7 +94,7 @@ function isTierAdmitted(threshold, tier) {
94
94
  }
95
95
  /**
96
96
  * The pure gating decision, extracted so apps can unit-test their demand
97
- * policy without wiring any effects the counterpart of
97
+ * policy without wiring any effects - the counterpart of
98
98
  * `resolveNotificationOutcome`.
99
99
  *
100
100
  * Both configs are required because the two questions are genuinely separate:
@@ -112,7 +112,7 @@ export function resolveDemandOutcome(config, autonomy, demand) {
112
112
  // The inner circle is never handled by machine. This is also what defuses the
113
113
  // gaming risk: an originator who learns that an acknowledgment means
114
114
  // "deprioritised" and escalates through another channel only succeeds if
115
- // their escalation is one the user cannot ignore which makes them exempt.
115
+ // their escalation is one the user cannot ignore - which makes them exempt.
116
116
  if (demand.originatorTier === 'exempt') {
117
117
  return freezeObject({
118
118
  admission: 'live',
package/dist/engine.d.ts CHANGED
@@ -22,7 +22,7 @@ export interface EnergyEngineOptions {
22
22
  * to cross-context updates. Default: true.
23
23
  *
24
24
  * Pass false when the engine is constructed somewhere that may never be
25
- * committed a React render, most notably and call `start()` from a
25
+ * committed - a React render, most notably - and call `start()` from a
26
26
  * lifecycle that only runs for trees React kept. Without this, a discarded
27
27
  * render leaves an engine nobody will ever dispose, holding a live
28
28
  * cross-context observer (a `storage` listener, for the localStorage
package/dist/engine.js CHANGED
@@ -23,7 +23,7 @@ function isSameState(a, b) {
23
23
  /**
24
24
  * Validate state that arrived from outside this engine. `candidate` is typed
25
25
  * as `EnergyState`, but it crossed a process, tab or storage boundary to get
26
- * here, so nothing about its runtime shape is guaranteed it is parsed
26
+ * here, so nothing about its runtime shape is guaranteed - it is parsed
27
27
  * against the published schema before anything else looks at it.
28
28
  */
29
29
  function normalizeState(candidate, nowMs, maxFutureSkewMs) {
@@ -63,7 +63,7 @@ export function createEnergyEngine(options = {}) {
63
63
  *
64
64
  * The origin is ALWAYS the sentinel, never `options.originId`. SPEC.md §3.2 requires the
65
65
  * untouched default to stay distinguishable from a real state, and `isUnproducedState()` tests
66
- * exactly this pair stamping a configured producer identity here turns "nobody has chosen
66
+ * exactly this pair - stamping a configured producer identity here turns "nobody has chosen
67
67
  * yet" into a state that looks chosen, which reads downstream as a level set at the epoch.
68
68
  * The configured identity belongs to the first state this engine actually produces.
69
69
  */
package/dist/gate.d.ts CHANGED
@@ -71,7 +71,7 @@ export interface NotificationGate {
71
71
  flush(): void;
72
72
  /**
73
73
  * Release resources. Pending notifications are delivered as a final
74
- * 'released' delivery first a disposed gate never swallows intents.
74
+ * 'released' delivery first - a disposed gate never swallows intents.
75
75
  */
76
76
  dispose(): void;
77
77
  }
package/dist/gate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { notificationStrategy } from './strategies.js';
2
2
  /**
3
- * Notification gate the runtime that ENFORCES `NotificationConfig` instead
3
+ * Notification gate - the runtime that ENFORCES `NotificationConfig` instead
4
4
  * of leaving it as guidance. Apps publish notification intents through the
5
5
  * gate; the gate resolves the current energy level's config and decides
6
6
  * whether each intent is delivered now, batched, or deferred.
@@ -152,7 +152,7 @@ export function createNotificationGate(engine, options) {
152
152
  * Classification happens when an intent is published, but both energy and
153
153
  * suppression move while a batch window is open. Without this pass a
154
154
  * notification batched at Focus is delivered after a focus session starts,
155
- * and one batched at Focus is surfaced at Rest with every channel disabled
155
+ * and one batched at Focus is surfaced at Rest with every channel disabled -
156
156
  * the gate's own policy, applied to the wrong moment.
157
157
  *
158
158
  * Batched intents the current policy no longer admits are demoted to
@@ -201,7 +201,7 @@ export function createNotificationGate(engine, options) {
201
201
  deliver(released, 'released');
202
202
  }
203
203
  function deliverBatch() {
204
- // Flushing a window is not an override of the active policy only of the
204
+ // Flushing a window is not an override of the active policy - only of the
205
205
  // wait. Anything no longer admissible leaves through `deferred` instead.
206
206
  reconcilePending();
207
207
  clearBatchTimer();
package/dist/levels.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { EnergyLevel, EnergyLevelDefinition, EnergySource, EnergyState } fr
3
3
  export declare function createEnergyOrigin(): string;
4
4
  export declare const UNPRODUCED_ORIGIN = "0-initial";
5
5
  export declare const UNPRODUCED_TIMESTAMP = 0;
6
- /** True for the untouched default state its age and identity are not meaningful. */
6
+ /** True for the untouched default state - its age and identity are not meaningful. */
7
7
  export declare function isUnproducedState(state: Pick<EnergyState, 'timestamp' | 'origin'>): boolean;
8
8
  /** Get all energy level definitions, ordered highest to lowest */
9
9
  export declare function getEnergyLevels(): ReadonlyArray<Readonly<EnergyLevelDefinition>>;
@@ -20,8 +20,8 @@ export declare function isHigherEnergy(a: EnergyLevel, b: EnergyLevel): boolean;
20
20
  /** Create an EnergyState for the current moment */
21
21
  export declare function createEnergyState(level: EnergyLevel, source?: EnergySource, timestamp?: number, revision?: number, origin?: string): EnergyState;
22
22
  /**
23
- * Parse a value that came from OUTSIDE this engine persisted JSON, a
24
- * cross-context observation, a wire message into an `EnergyState`.
23
+ * Parse a value that came from OUTSIDE this engine - persisted JSON, a
24
+ * cross-context observation, a wire message - into an `EnergyState`.
25
25
  *
26
26
  * This is the interchange boundary, so it enforces the published JSON Schema
27
27
  * exactly rather than approximately: `additionalProperties: false` means an
package/dist/levels.js CHANGED
@@ -20,7 +20,7 @@ export function createEnergyOrigin() {
20
20
  return `${Date.now().toString(36)}-${originSequence.toString(36)}-${Math.random().toString(36).slice(2)}`;
21
21
  }
22
22
  /*
23
- * Markers for a state that has never been *produced* the untouched default an engine starts with,
23
+ * Markers for a state that has never been *produced* - the untouched default an engine starts with,
24
24
  * as opposed to a level anyone actually chose.
25
25
  *
26
26
  * Both exist so engine construction reads neither the clock nor the random source. React providers
@@ -32,7 +32,7 @@ export function createEnergyOrigin() {
32
32
  */
33
33
  export const UNPRODUCED_ORIGIN = '0-initial';
34
34
  export const UNPRODUCED_TIMESTAMP = 0;
35
- /** True for the untouched default state its age and identity are not meaningful. */
35
+ /** True for the untouched default state - its age and identity are not meaningful. */
36
36
  export function isUnproducedState(state) {
37
37
  return state.timestamp === UNPRODUCED_TIMESTAMP && state.origin === UNPRODUCED_ORIGIN;
38
38
  }
@@ -106,7 +106,7 @@ const LEVELS = Object.freeze([
106
106
  value: 0,
107
107
  key: 'rest',
108
108
  label: 'Rest',
109
- description: 'Depleted. Consumption only \u2014 reading, reflecting.',
109
+ description: 'Recovery. Consumption only \u2014 reading, reflecting.',
110
110
  cognitiveProfile: freezeObject({
111
111
  decisionCapacity: 'none',
112
112
  focusDuration: 'none',
@@ -179,8 +179,8 @@ export function createEnergyState(level, source = 'manual', timestamp = Date.now
179
179
  /** The exact key set `spec/energy-state.schema.json` permits. */
180
180
  const ENERGY_STATE_KEYS = ['level', 'timestamp', 'source', 'revision', 'origin'];
181
181
  /**
182
- * Parse a value that came from OUTSIDE this engine persisted JSON, a
183
- * cross-context observation, a wire message into an `EnergyState`.
182
+ * Parse a value that came from OUTSIDE this engine - persisted JSON, a
183
+ * cross-context observation, a wire message - into an `EnergyState`.
184
184
  *
185
185
  * This is the interchange boundary, so it enforces the published JSON Schema
186
186
  * exactly rather than approximately: `additionalProperties: false` means an
package/dist/metrics.js CHANGED
@@ -31,7 +31,7 @@ export function getEnergyMetrics(state, now = Date.now()) {
31
31
  /*
32
32
  * The untouched default carries a sentinel timestamp rather than the moment its engine happened
33
33
  * to be constructed, so that construction stays prerender-safe. Subtracting from it would report
34
- * an age measured from the epoch decades for a state nobody has set yet. It has no age.
34
+ * an age measured from the epoch - decades - for a state nobody has set yet. It has no age.
35
35
  */
36
36
  const stateAgeMs = isUnproducedState(state) ? 0 : Math.max(0, safeNow - state.timestamp);
37
37
  const definition = getEnergyLevel(state.level);
@@ -18,7 +18,7 @@ function parsePersistedState(source, raw) {
18
18
  }
19
19
  try {
20
20
  // Shape, key set and field invariants are all one boundary rule, enforced
21
- // to the letter of spec/energy-state.schema.json including its
21
+ // to the letter of spec/energy-state.schema.json - including its
22
22
  // `additionalProperties: false`. Storage written by a newer or foreign
23
23
  // producer is rejected, not quietly trimmed to fit.
24
24
  return parseExternalEnergyState(parsed);
@@ -34,7 +34,7 @@ export declare function defineEnergyPresence(spec?: EnergyPresenceSpec): EnergyP
34
34
  */
35
35
  export declare function presenceAtOrAbove(min: EnergyLevel, below?: EnergyPresence): EnergyPresenceMap;
36
36
  /**
37
- * Presence map for elements that only belong at low energy recovery hints,
37
+ * Presence map for elements that only belong at low energy - recovery hints,
38
38
  * "one thing at a time" affordances. Above `max` the element is `above`
39
39
  * ('hidden' by default).
40
40
  */
package/dist/presence.js CHANGED
@@ -59,7 +59,7 @@ export function presenceAtOrAbove(min, below = 'hidden') {
59
59
  return freezeObject(entries);
60
60
  }
61
61
  /**
62
- * Presence map for elements that only belong at low energy recovery hints,
62
+ * Presence map for elements that only belong at low energy - recovery hints,
63
63
  * "one thing at a time" affordances. Above `max` the element is `above`
64
64
  * ('hidden' by default).
65
65
  */
package/dist/react.d.ts CHANGED
@@ -16,7 +16,7 @@ export interface EnergyProviderProps {
16
16
  * Element the level is projected onto. Default: `document.body`.
17
17
  *
18
18
  * Pass `() => document.documentElement` when the stylesheet keys off
19
- * `[data-energy-level]` at the root resolved inside the effect, so the
19
+ * `[data-energy-level]` at the root - resolved inside the effect, so the
20
20
  * render phase never touches the DOM. Whatever the target, the provider
21
21
  * snapshots what was there, layers overlapping providers, and restores the
22
22
  * baseline on unmount.
package/dist/react.js CHANGED
@@ -289,7 +289,7 @@ export function EnergyGate({ presence, min, max, fallback = null, whenHidden = '
289
289
  * render, hidden or not. That stability is the whole mechanism: reconciling the
290
290
  * same boundary is what carries state across the transition. Rendering Activity
291
291
  * only while hidden would swap the element type at that slot and remount the
292
- * subtree on reveal losing exactly the state this is here to keep.
292
+ * subtree on reveal - losing exactly the state this is here to keep.
293
293
  */
294
294
  return createElement(Fragment, null, isHidden ? fallback : null, createElement(Activity, {
295
295
  mode: isHidden ? 'hidden' : 'visible',
@@ -4,12 +4,12 @@ import type { EnergyState } from './types.js';
4
4
  *
5
5
  * The comparison walks four keys in order, stopping at the first that differs:
6
6
  *
7
- * 1. **`timestamp`** later wins. The ordinary case, and the only one most
7
+ * 1. **`timestamp`** - later wins. The ordinary case, and the only one most
8
8
  * states ever reach.
9
- * 2. **`revision`** higher wins. Two writes inside one clock tick are not
9
+ * 2. **`revision`** - higher wins. Two writes inside one clock tick are not
10
10
  * simultaneous; the producer numbers them so they still order.
11
- * 3. **`source`** `manual` > `scheduled` > `inferred`. See `sourcePriority`.
12
- * 4. **`origin`** higher string wins. Not meaningful, and deliberately so:
11
+ * 3. **`source`** - `manual` > `scheduled` > `inferred`. See `sourcePriority`.
12
+ * 4. **`origin`** - higher string wins. Not meaningful, and deliberately so:
13
13
  * when two producers write the same instant, the same revision, and the same
14
14
  * kind of source, there is no principled winner, and an arbitrary rule every
15
15
  * context computes identically beats a coin flip each context tosses
package/dist/reconcile.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * State reconciliation how two `EnergyState` values that describe the same
2
+ * State reconciliation - how two `EnergyState` values that describe the same
3
3
  * user are ordered when they meet.
4
4
  *
5
5
  * They meet constantly: a second browser tab writes the shared store, a desktop
6
6
  * window and its detached child both hold an engine, a sync layer hands back
7
7
  * what another device recorded. Every one of those paths needs the same answer
8
8
  * to "which of these two is the current state?", and the answer has to be
9
- * *deterministic* not "last write wins by arrival order", which converges on
9
+ * *deterministic* - not "last write wins by arrival order", which converges on
10
10
  * different values depending on network timing.
11
11
  *
12
12
  * This is the rule, extracted from the engine so it can be read, tested, and
@@ -36,12 +36,12 @@ function sourcePriority(source) {
36
36
  *
37
37
  * The comparison walks four keys in order, stopping at the first that differs:
38
38
  *
39
- * 1. **`timestamp`** later wins. The ordinary case, and the only one most
39
+ * 1. **`timestamp`** - later wins. The ordinary case, and the only one most
40
40
  * states ever reach.
41
- * 2. **`revision`** higher wins. Two writes inside one clock tick are not
41
+ * 2. **`revision`** - higher wins. Two writes inside one clock tick are not
42
42
  * simultaneous; the producer numbers them so they still order.
43
- * 3. **`source`** `manual` > `scheduled` > `inferred`. See `sourcePriority`.
44
- * 4. **`origin`** higher string wins. Not meaningful, and deliberately so:
43
+ * 3. **`source`** - `manual` > `scheduled` > `inferred`. See `sourcePriority`.
44
+ * 4. **`origin`** - higher string wins. Not meaningful, and deliberately so:
45
45
  * when two producers write the same instant, the same revision, and the same
46
46
  * kind of source, there is no principled winner, and an arbitrary rule every
47
47
  * context computes identically beats a coin flip each context tosses
package/dist/session.d.ts CHANGED
@@ -30,12 +30,12 @@ export interface FocusSessionControllerOptions {
30
30
  /**
31
31
  * Engine used for energy-aware defaults: session length from the level's
32
32
  * expected productivity window, break cadence from the task-complexity
33
- * strategy. Optional without it, defaults are 25 minutes / no breaks.
33
+ * strategy. Optional - without it, defaults are 25 minutes / no breaks.
34
34
  */
35
35
  engine?: EnergyEngine;
36
36
  /**
37
37
  * Suppression target (typically a NotificationGate). Suppressed on start,
38
- * released on stop/end/dispose the controller owns the flag for the
38
+ * released on stop/end/dispose - the controller owns the flag for the
39
39
  * session's lifetime, so it can never be left stuck on.
40
40
  */
41
41
  gate?: FocusSuppressible;
package/dist/session.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getEnergyMetrics } from './metrics.js';
2
2
  import { taskComplexityStrategy } from './strategies.js';
3
3
  /**
4
- * Focus sessions time-boxed "one thing at a time" windows layered on top of
4
+ * Focus sessions - time-boxed "one thing at a time" windows layered on top of
5
5
  * the energy model. A session is a temporary commitment, not an energy level:
6
6
  * it suppresses interruptions for a bounded duration, surfaces break nudges,
7
7
  * and ALWAYS ends on time.
@@ -9,7 +9,7 @@ import { taskComplexityStrategy } from './strategies.js';
9
9
  * Two invariants come from field evidence (GingerMail shipped the opposite
10
10
  * and both were user-hostile):
11
11
  * 1. Sessions auto-expire. Expiry is an emitted event, not a predicate the
12
- * app must remember to poll suppression can never outlive the session.
12
+ * app must remember to poll - suppression can never outlive the session.
13
13
  * 2. Suppression is lifted BEFORE the end event is emitted, so an
14
14
  * end-of-session notification can never be swallowed by the session's
15
15
  * own suppression.
@@ -114,7 +114,7 @@ export function createFocusSessionController(options = {}) {
114
114
  if (!engine)
115
115
  return DEFAULT_SESSION_MINUTES;
116
116
  const window = getEnergyMetrics(engine.getState(), now()).expectedProductivityWindowMinutes;
117
- // At rest (0) the metrics window is 0 an explicit session at rest still
117
+ // At rest (0) the metrics window is 0 - an explicit session at rest still
118
118
  // deserves a real bound rather than an instantly-expiring one.
119
119
  return window > 0 ? window : DEFAULT_SESSION_MINUTES;
120
120
  }
@@ -57,8 +57,8 @@ export declare const interactionForgivenessStrategy: AdaptationStrategy<Interact
57
57
  * supervise it, so the worst day is the wrong day for it to improvise.
58
58
  *
59
59
  * What narrows as energy falls is *discretion*, not action. At rest the
60
- * automation may still take a single, certain, template-only step an
61
- * out-of-office reply is exactly that shape but it may not chain steps,
60
+ * automation may still take a single, certain, template-only step - an
61
+ * out-of-office reply is exactly that shape - but it may not chain steps,
62
62
  * compose novel wording, or act on a judgment call.
63
63
  */
64
64
  export interface AutonomyConfig {
@@ -239,7 +239,7 @@ const AUTONOMY_CONFIGS = freezeObject({
239
239
  allowGeneratedContent: false,
240
240
  maxUnattendedSteps: 1,
241
241
  }),
242
- // Rest: one certain, templated step at most. Not zero an automated action
242
+ // Rest: one certain, templated step at most. Not zero - an automated action
243
243
  // the user has standing consent for (an out-of-office reply is the classic)
244
244
  // is safest at rest precisely because it stops improvising.
245
245
  0: freezeObject({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kumbatio/energy-system",
3
- "version": "1.0.1",
3
+ "version": "2.0.5",
4
4
  "private": false,
5
5
  "description": "Framework-agnostic TypeScript library for energy-aware application behavior.",
6
6
  "license": "MIT",
@@ -47,23 +47,44 @@
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
+ "scripts": {
51
+ "build:ts": "tsc -p tsconfig.build.json",
52
+ "build": "pnpm run build:ts && pnpm run api:surface && pnpm run conformance",
53
+ "api:surface": "tsx scripts/extract-api-surface.ts",
54
+ "api:surface:check": "tsx scripts/extract-api-surface.ts --check",
55
+ "check-types": "tsc --noEmit -p tsconfig.json",
56
+ "test": "pnpm run build:ts && node --test --import tsx",
57
+ "validate": "pnpm run format:check && pnpm run lint && pnpm run check-types && pnpm run test && pnpm run pack:dry-run",
58
+ "prepack": "pnpm run format:check && pnpm run lint && pnpm run check-types && pnpm run test",
59
+ "pack:dry-run": "pnpm pack --dry-run",
60
+ "format": "oxfmt --write .",
61
+ "format:check": "oxfmt --check .",
62
+ "lint": "oxlint --import-plugin --type-aware --type-check --vitest-plugin .",
63
+ "lint:fix": "oxlint --import-plugin --type-aware --type-check --vitest-plugin --fix . && oxfmt --write .",
64
+ "lint:fix-unsafe": "oxlint --import-plugin --type-aware --type-check --vitest-plugin --fix --fix-suggestions --fix-dangerously . && oxfmt --write .",
65
+ "check:fix-suggestions": "oxlint --import-plugin --type-aware --type-check --vitest-plugin --fix --fix-suggestions . && oxfmt --write .",
66
+ "conformance": "TZ=UTC tsx scripts/extract-conformance.ts",
67
+ "conformance:check": "TZ=UTC tsx scripts/extract-conformance.ts --check",
68
+ "release": "release-kit"
69
+ },
50
70
  "devDependencies": {
51
- "@happy-dom/global-registrator": "^20.11.2",
71
+ "@entro314labs/release-kit": "^2.9.0",
72
+ "@happy-dom/global-registrator": "^20.11.6",
52
73
  "@types/node": "^26.2.0",
53
74
  "@types/react": "^19.2.18",
54
75
  "@types/react-dom": "^19.2.4",
55
76
  "ajv": "8.20.0",
56
- "oxfmt": "0.62.0",
57
- "oxlint": "1.77.0",
77
+ "oxfmt": "0.64.0",
78
+ "oxlint": "1.79.0",
58
79
  "oxlint-tsgolint": "^7.0.2001",
59
80
  "react": "^19.2.8",
60
81
  "react-dom": "^19.2.8",
61
82
  "tsx": "^4.23.12",
62
- "typescript": "^7.0.2"
83
+ "typescript": "next"
63
84
  },
64
85
  "peerDependencies": {
65
- "@types/react": ">=19.2.0",
66
- "react": ">=19.2.0"
86
+ "@types/react": ">=19.2.18",
87
+ "react": ">=19.2.8"
67
88
  },
68
89
  "peerDependenciesMeta": {
69
90
  "@types/react": {
@@ -76,22 +97,5 @@
76
97
  "engines": {
77
98
  "node": ">=24.19.0"
78
99
  },
79
- "scripts": {
80
- "build:ts": "tsc -p tsconfig.build.json",
81
- "build": "pnpm run build:ts && pnpm run api:surface && pnpm run conformance",
82
- "api:surface": "tsx scripts/extract-api-surface.ts",
83
- "api:surface:check": "tsx scripts/extract-api-surface.ts --check",
84
- "check-types": "tsc --noEmit -p tsconfig.json",
85
- "test": "pnpm run build:ts && node --test --import tsx",
86
- "validate": "pnpm run format:check && pnpm run lint && pnpm run check-types && pnpm run test && pnpm run pack:dry-run",
87
- "pack:dry-run": "pnpm pack --dry-run",
88
- "format": "oxfmt --write .",
89
- "format:check": "oxfmt --check .",
90
- "lint": "oxlint --import-plugin --type-aware --type-check --vitest-plugin .",
91
- "lint:fix": "oxlint --import-plugin --type-aware --type-check --vitest-plugin --fix . && oxfmt --write .",
92
- "lint:fix-unsafe": "oxlint --import-plugin --type-aware --type-check --vitest-plugin --fix --fix-suggestions --fix-dangerously . && oxfmt --write .",
93
- "check:fix-suggestions": "oxlint --import-plugin --type-aware --type-check --vitest-plugin --fix --fix-suggestions . && oxfmt --write .",
94
- "conformance": "TZ=UTC tsx scripts/extract-conformance.ts",
95
- "conformance:check": "TZ=UTC tsx scripts/extract-conformance.ts --check"
96
- }
97
- }
100
+ "packageManager": "pnpm@11.22.0"
101
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://kumbat.io/spec/conformance.schema.json",
4
4
  "title": "EnergyConformance",
5
- "description": "The shape of conformance.json: the executable half of SPEC.md. A port in any language passes by loading that file and asserting its own output against each vector. This schema is what lets that port know the file it loaded is the file it expects vectors that silently changed shape are worse than vectors that failed to load. scripts/extract-conformance.ts validates its own output against this schema before writing, so the two cannot drift apart unnoticed.",
5
+ "description": "The shape of conformance.json: the executable half of SPEC.md. A port in any language passes by loading that file and asserting its own output against each vector. This schema is what lets that port know the file it loaded is the file it expects - vectors that silently changed shape are worse than vectors that failed to load. scripts/extract-conformance.ts validates its own output against this schema before writing, so the two cannot drift apart unnoticed.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -378,7 +378,7 @@
378
378
  },
379
379
 
380
380
  "externalLevelMapping": {
381
- "description": "Percentages an external model might hand over, including both midpoints 12.5 and 37.5 sit exactly between two levels, which is where rounding implementations disagree.",
381
+ "description": "Percentages an external model might hand over, including both midpoints - 12.5 and 37.5 sit exactly between two levels, which is where rounding implementations disagree.",
382
382
  "type": "array",
383
383
  "minItems": 1,
384
384
  "items": {
@@ -29,7 +29,7 @@
29
29
  "maximum": 9007199254740991
30
30
  },
31
31
  "origin": {
32
- "description": "Stable identity of the producer an engine instance, a device, a process. Must be unique per producer and stable across that producer's writes. The final tiebreaker compares it lexicographically, so a producer that reuses one identity for different state breaks convergence.",
32
+ "description": "Stable identity of the producer - an engine instance, a device, a process. Must be unique per producer and stable across that producer's writes. The final tiebreaker compares it lexicographically, so a producer that reuses one identity for different state breaks convergence.",
33
33
  "type": "string",
34
34
  "minLength": 1
35
35
  }
package/src/energy.css CHANGED
@@ -146,7 +146,7 @@
146
146
 
147
147
  /* A cursor is a HINT, and one only a pointer user receives. It changes nothing
148
148
  * about what is focusable or clickable, so an app that means "read only" at Rest
149
- * must also disable the controls otherwise a keyboard user gets no signal at
149
+ * must also disable the controls - otherwise a keyboard user gets no signal at
150
150
  * all and a pointer user gets a signal that turns out to be false. */
151
151
  [data-energy-level='0'] .energy-content * {
152
152
  cursor: default !important;
@@ -156,8 +156,8 @@
156
156
  *
157
157
  * Annotate any element with the energy range it belongs to:
158
158
  *
159
- * <div data-energy-min="75">AI chat needs 75+ energy</div>
160
- * <div data-energy-max="25">Recovery hint low energy only</div>
159
+ * <div data-energy-min="75">AI chat - needs 75+ energy</div>
160
+ * <div data-energy-max="25">Recovery hint - low energy only</div>
161
161
  *
162
162
  * The element hides automatically whenever the root's data-energy-level
163
163
  * falls outside its declared range. This mirrors the JS presence API
@@ -218,7 +218,7 @@
218
218
  * Be honest about what the resting values are: chrome sits at 10% opacity at
219
219
  * level 25 and 5% at level 0, and neither meets WCAG 1.4.11 (Non-text Contrast,
220
220
  * 3:1 for UI components) in its resting state. That is a deliberate design
221
- * default for a receding interface, not a conformance claim and it is why
221
+ * default for a receding interface, not a conformance claim - and it is why
222
222
  * every value here is a custom property an app can raise in one line.
223
223
  *
224
224
  * Two populations must not be left to do that themselves:
@@ -229,12 +229,12 @@
229
229
  *
230
230
  * `forced-colors: active` (Windows High Contrast and friends) overrides colors
231
231
  * but NOT opacity, so faded chrome stays faded while everything around it snaps
232
- * to a system palette the fade is no longer a soft de-emphasis, it is the only
232
+ * to a system palette - the fade is no longer a soft de-emphasis, it is the only
233
233
  * thing still hiding content. Opacity comes off entirely.
234
234
  *
235
235
  * Both blocks declare `!important`, which is load-bearing rather than a shortcut.
236
236
  * `applyEnergyLevel()` writes these same custom properties as INLINE styles, and
237
- * an inline declaration outranks an ordinary rule so without this, the JS path
237
+ * an inline declaration outranks an ordinary rule - so without this, the JS path
238
238
  * would silently defeat the user's stated contrast preference while the CSS-only
239
239
  * path honoured it. An important author declaration beats a normal inline one,
240
240
  * which is exactly the ordering wanted here: a preference the person set outranks