@kumbatio/energy-system 2.0.0 → 2.0.6
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 +71 -14
- package/README.md +49 -33
- package/SPEC.md +31 -31
- package/api-surface.json +1 -1
- package/conformance.json +2 -2
- package/dist/defer.d.ts +2 -2
- package/dist/defer.js +3 -3
- package/dist/demand.d.ts +6 -6
- package/dist/demand.js +5 -5
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +2 -2
- package/dist/gate.d.ts +1 -1
- package/dist/gate.js +3 -3
- package/dist/levels.d.ts +3 -3
- package/dist/levels.js +5 -5
- package/dist/metrics.js +1 -1
- package/dist/persistence.js +1 -1
- package/dist/presence.d.ts +1 -1
- package/dist/presence.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reconcile.d.ts +4 -4
- package/dist/reconcile.js +6 -6
- package/dist/session.d.ts +2 -2
- package/dist/session.js +3 -3
- package/dist/strategies.d.ts +2 -2
- package/dist/strategies.js +1 -1
- package/package.json +33 -29
- package/spec/conformance.schema.json +2 -2
- package/spec/energy-state.schema.json +1 -1
- package/src/energy.css +6 -6
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
|
|
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
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
## [2.0.6] - 2026-09-07
|
|
18
|
+
|
|
19
|
+
### Dependencies
|
|
20
|
+
|
|
21
|
+
- **deps:** update dev tooling and pin pnpm 11.26.0 ([a306b1a](https://github.com/kumbatio/energy-system/commit/a306b1a))
|
|
22
|
+
|
|
23
|
+
## [2.0.5] - 2026-08-21
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `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))
|
|
28
|
+
|
|
29
|
+
## [2.0.4] - 2026-08-21
|
|
30
|
+
|
|
31
|
+
### Miscellaneous Chores
|
|
32
|
+
|
|
33
|
+
- update version to 2.0.3 in api-surface.json and conformance.json ([3a21713](https://github.com/kumbatio/energy-system/commit/3a21713))
|
|
34
|
+
|
|
35
|
+
## [2.0.3] - 2026-08-21
|
|
36
|
+
|
|
37
|
+
### Miscellaneous Chores
|
|
38
|
+
|
|
39
|
+
- regenerate api-surface.json and conformance.json ([427d02d](https://github.com/kumbatio/energy-system/commit/427d02d))
|
|
40
|
+
|
|
41
|
+
## [2.0.2] - 2026-08-21
|
|
42
|
+
|
|
43
|
+
### Bug Fixes
|
|
44
|
+
|
|
45
|
+
- 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))
|
|
46
|
+
|
|
47
|
+
## [2.0.1] - 2026-08-21
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **The Rest level description now opens with "Recovery." instead of
|
|
52
|
+
"Depleted."** Level-description prose is product copy, not semver-covered API
|
|
53
|
+
(see the note at the top of this file). "Depleted" implies damage, which
|
|
54
|
+
conflicts with the Neutral State Language principle the level copy is
|
|
55
|
+
supposed to embody: describe what the person can do at a level, not what they
|
|
56
|
+
cannot. [SPEC.md](./SPEC.md) §3.1 and `conformance.json` updated to match.
|
|
57
|
+
|
|
15
58
|
## [2.0.0]
|
|
16
59
|
|
|
17
60
|
Corrections to `1.0.0`. No type signature changed, which is exactly why this is
|
|
18
61
|
a major: two of these change runtime behavior and one narrows an install range,
|
|
19
|
-
and for this package behavior is API
|
|
62
|
+
and for this package behavior is API - see the note at the top of this file.
|
|
20
63
|
|
|
21
64
|
Upgrading from `1.x`: the breaks are `react`/`@types/react` below 19.2 (which
|
|
22
65
|
never worked with the React entry point), persisted or exchanged states carrying
|
|
@@ -40,7 +83,7 @@ after suppression started or after energy fell below its threshold.
|
|
|
40
83
|
`createEnergyState()` accepted fractional timestamps, which
|
|
41
84
|
[spec/energy-state.schema.json](./spec/energy-state.schema.json) does not
|
|
42
85
|
allow, and persisted state carrying unknown properties was silently trimmed
|
|
43
|
-
to fit rather than rejected
|
|
86
|
+
to fit rather than rejected - so two implementations could exchange a state
|
|
44
87
|
and disagree about what they had exchanged. Persistence loads, cross-context
|
|
45
88
|
observations and `memoryPersistence` now share one strict boundary parser.
|
|
46
89
|
- **A configured `originId` no longer corrupts the unproduced sentinel.**
|
|
@@ -52,7 +95,7 @@ after suppression started or after energy fell below its threshold.
|
|
|
52
95
|
state the engine actually produces.
|
|
53
96
|
- **`api-surface.json` includes `EnergyEngine.resolve()`.** The declaration
|
|
54
97
|
parser did not recognise generic members, so a public method was missing from
|
|
55
|
-
the frozen surface
|
|
98
|
+
the frozen surface - and a method absent from the freeze is a method nobody
|
|
56
99
|
notices removing.
|
|
57
100
|
|
|
58
101
|
### Changed
|
|
@@ -63,13 +106,13 @@ after suppression started or after energy fell below its threshold.
|
|
|
63
106
|
19.0 and 19.1.
|
|
64
107
|
- **Generated artifacts are checked, not regenerated, during validation.**
|
|
65
108
|
`pnpm test` used to run the full build first, so the drift guard compared
|
|
66
|
-
`conformance.json` against a copy it had just written
|
|
109
|
+
`conformance.json` against a copy it had just written - it could not fail,
|
|
67
110
|
whatever was committed. Generation now belongs to `pnpm run build`; both
|
|
68
111
|
generators take `--check`, and both artifacts are verified by the suite.
|
|
69
112
|
|
|
70
113
|
### Added
|
|
71
114
|
|
|
72
|
-
- **[spec/conformance.schema.json](./spec/conformance.schema.json)**
|
|
115
|
+
- **[spec/conformance.schema.json](./spec/conformance.schema.json)** - the
|
|
73
116
|
schema `conformance.json` has always pointed at via `$schema` and which did
|
|
74
117
|
not exist. The generator now validates its own output against it before
|
|
75
118
|
emitting, and it is exported from the package so the relative reference
|
|
@@ -85,20 +128,20 @@ The API is frozen. Everything below documents what that commitment now covers.
|
|
|
85
128
|
|
|
86
129
|
### Added
|
|
87
130
|
|
|
88
|
-
- **[SPEC.md](./SPEC.md)**
|
|
131
|
+
- **[SPEC.md](./SPEC.md)** - the model specified independently of this
|
|
89
132
|
implementation: levels, energy state, reconciliation, the strategy contract,
|
|
90
133
|
autonomy, inbound demand, the runtime invariants, and the accessibility
|
|
91
134
|
requirements. Language-independent and RFC 2119-worded, so an implementation in
|
|
92
135
|
another language is an implementation of the same model rather than a port of
|
|
93
136
|
this one.
|
|
94
|
-
- **[spec/energy-state.schema.json](./spec/energy-state.schema.json)**
|
|
137
|
+
- **[spec/energy-state.schema.json](./spec/energy-state.schema.json)** - the
|
|
95
138
|
interchange format. Sharing one person's energy state across processes or
|
|
96
139
|
languages is a wire-format problem, and this is the wire format.
|
|
97
|
-
- **[conformance.json](./conformance.json)**
|
|
140
|
+
- **[conformance.json](./conformance.json)** - 252 vectors plus every strategy
|
|
98
141
|
table, generated from the built library on each build and shipped in the
|
|
99
142
|
package. An implementation passes by loading and replaying them; a stale file
|
|
100
143
|
fails this package's own build.
|
|
101
|
-
- `isPreferredEnergyState(candidate, current)`
|
|
144
|
+
- `isPreferredEnergyState(candidate, current)` - the reconciliation rule, which
|
|
102
145
|
was internal to the engine. It is the hardest part of the model to reimplement
|
|
103
146
|
correctly, so it is now readable, testable, and covered by vectors on its own.
|
|
104
147
|
- Accessibility handling in the reference stylesheet for
|
|
@@ -107,7 +150,7 @@ The API is frozen. Everything below documents what that commitment now covers.
|
|
|
107
150
|
properties inline, and a preference the person set must outrank a value the
|
|
108
151
|
program computed.
|
|
109
152
|
- Coverage across every one of the 20 level transitions, in both directions, and
|
|
110
|
-
across strategy composition
|
|
153
|
+
across strategy composition - including the model's directional invariants
|
|
111
154
|
(protection never decreases and automation never gains discretion as capacity
|
|
112
155
|
falls), which a future edit to one table would otherwise break silently.
|
|
113
156
|
|
|
@@ -124,13 +167,13 @@ The API is frozen. Everything below documents what that commitment now covers.
|
|
|
124
167
|
|
|
125
168
|
### Added
|
|
126
169
|
|
|
127
|
-
- `autonomyStrategy` / `AutonomyConfig`
|
|
170
|
+
- `autonomyStrategy` / `AutonomyConfig` - what automation may do unattended:
|
|
128
171
|
confidence threshold, whether wording may be composed, and how many steps may
|
|
129
172
|
chain. The mirror of interaction forgiveness, which protects against the
|
|
130
173
|
_user's_ mistakes at low energy where this protects against the _agent's_.
|
|
131
174
|
What narrows as energy falls is discretion, not action: at Rest the threshold
|
|
132
175
|
is 1, admitting only certainty, and a single templated step is still allowed.
|
|
133
|
-
- `demandAdmissionStrategy` and the pure `resolveDemandOutcome`
|
|
176
|
+
- `demandAdmissionStrategy` and the pure `resolveDemandOutcome` - an
|
|
134
177
|
energy-resolved policy for arrivals that ask something of the person: reach
|
|
135
178
|
them now, acknowledge and queue, or queue silently. Originator tiers, the
|
|
136
179
|
exempt-tier invariant, and escalation to silence when a classification is not
|
|
@@ -147,7 +190,7 @@ The API is frozen. Everything below documents what that commitment now covers.
|
|
|
147
190
|
## [0.4.0]
|
|
148
191
|
|
|
149
192
|
Patterns studied in a field ADHD app and reimplemented as first-class, tested
|
|
150
|
-
primitives
|
|
193
|
+
primitives - including guarantees against the two failure modes observed in the
|
|
151
194
|
wild: suppressed reminders destroyed instead of deferred, and focus suppression
|
|
152
195
|
that never auto-expired.
|
|
153
196
|
|
|
@@ -176,3 +219,17 @@ framework-agnostic engine with strategy resolution, the first three built-in
|
|
|
176
219
|
strategies, the DOM adapter, the React provider and hooks, persistence adapters
|
|
177
220
|
with external observation, deterministic clocks, derived metrics, and legacy
|
|
178
221
|
level compatibility mapping.
|
|
222
|
+
|
|
223
|
+
[Unreleased]: https://github.com/kumbatio/energy-system/compare/v2.0.6...HEAD
|
|
224
|
+
[2.0.6]: https://github.com/kumbatio/energy-system/compare/v2.0.5...v2.0.6
|
|
225
|
+
[2.0.5]: https://github.com/kumbatio/energy-system/compare/v2.0.4...v2.0.5
|
|
226
|
+
[2.0.4]: https://github.com/kumbatio/energy-system/compare/v2.0.3...v2.0.4
|
|
227
|
+
[2.0.3]: https://github.com/kumbatio/energy-system/compare/v2.0.2...v2.0.3
|
|
228
|
+
[2.0.2]: https://github.com/kumbatio/energy-system/compare/v2.0.1...v2.0.2
|
|
229
|
+
[2.0.1]: https://github.com/kumbatio/energy-system/compare/v2.0.0...v2.0.1
|
|
230
|
+
[2.0.0]: https://github.com/kumbatio/energy-system/compare/v1.0.0...v2.0.0
|
|
231
|
+
[1.0.0]: https://github.com/kumbatio/energy-system/compare/v0.6.0...v1.0.0
|
|
232
|
+
[0.6.0]: https://github.com/kumbatio/energy-system/compare/v0.5.4...v0.6.0
|
|
233
|
+
[0.5.4]: https://github.com/kumbatio/energy-system/compare/v0.4.0...v0.5.4
|
|
234
|
+
[0.4.0]: https://github.com/kumbatio/energy-system/compare/v0.1.0...v0.4.0
|
|
235
|
+
[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
|
|
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
|
|
165
|
-
'hidden'`) per level
|
|
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
|
|
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
|
|
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
|
|
235
|
-
<div data-energy-max="25">Recovery hint
|
|
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
|
|
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_
|
|
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
|
|
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
|
|
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
|
|
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
|
|
384
|
-
never a judgment call
|
|
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"`
|
|
425
|
-
- `data-energy-max="25"`
|
|
426
|
-
- `data-energy-presence="muted" | "hidden"`
|
|
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)**
|
|
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)**
|
|
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)**
|
|
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)**
|
|
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,11 +588,11 @@ 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
|
|
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
|
|
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
|
|
@@ -601,7 +601,7 @@ The API has been frozen since `1.0.0`. The package follows semver strictly, and
|
|
|
601
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
|
|
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 @@ that means more than the type signatures:
|
|
|
612
612
|
## Development
|
|
613
613
|
|
|
614
614
|
```bash
|
|
615
|
-
pnpm run validate # format, lint, types, tests, packaging
|
|
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
|
|
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)**
|
|
641
|
-
- **[Meltemi](https://meltemi.app)**
|
|
642
|
-
- **[kumbat.io](https://kumbat.io)**
|
|
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)
|
|
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)
|
|
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
|
|
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. [](https://doi.org/10.5281/zenodo.21915159) Energy as State, Not Time - the model this library implements
|
|
664
|
+
2. [](https://doi.org/10.5281/zenodo.21915161) The Myth of the Flat Workday - the cognitive-science evidence
|
|
665
|
+
3. [](https://doi.org/10.5281/zenodo.21915163) Self-Report Over Surveillance - why the state is declared, never inferred
|
|
666
|
+
4. [](https://doi.org/10.5281/zenodo.21915165) Capacity-Aware Software Design - the pattern language this library ships
|
|
667
|
+
5. [](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
|
|
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
|
|
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**
|
|
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 |
|
|
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**
|
|
60
|
-
`focusDuration`, `taskComplexity`, `interruptionTolerance`
|
|
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`
|
|
99
|
-
- `scheduled`
|
|
100
|
-
- `inferred`
|
|
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
|
|
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`**
|
|
150
|
-
2. **`revision`**
|
|
151
|
-
3. **`source`**
|
|
152
|
-
4. **`origin`**
|
|
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
|
|
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
|
|
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
|
|
246
|
-
decisions, never judgement calls
|
|
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_
|
|
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
|
|
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
|
|
298
|
-
network they cannot
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
439
|
-
server
|
|
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
package/conformance.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./spec/conformance.schema.json",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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": "
|
|
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
|
|
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")
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
53
|
-
* receipt, a newsletter, a build notification
|
|
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
|
|
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
|
|
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
|
|
58
|
-
// loop
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
24
|
-
* cross-context observation, a wire message
|
|
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*
|
|
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
|
|
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: '
|
|
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
|
|
183
|
-
* cross-context observation, a wire message
|
|
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
|
|
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);
|
package/dist/persistence.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/presence.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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',
|
package/dist/reconcile.d.ts
CHANGED
|
@@ -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`**
|
|
7
|
+
* 1. **`timestamp`** - later wins. The ordinary case, and the only one most
|
|
8
8
|
* states ever reach.
|
|
9
|
-
* 2. **`revision`**
|
|
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`**
|
|
12
|
-
* 4. **`origin`**
|
|
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
|
|
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*
|
|
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`**
|
|
39
|
+
* 1. **`timestamp`** - later wins. The ordinary case, and the only one most
|
|
40
40
|
* states ever reach.
|
|
41
|
-
* 2. **`revision`**
|
|
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`**
|
|
44
|
-
* 4. **`origin`**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/strategies.d.ts
CHANGED
|
@@ -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
|
|
61
|
-
* out-of-office reply is exactly that shape
|
|
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 {
|
package/dist/strategies.js
CHANGED
|
@@ -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
|
|
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": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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
|
-
"@
|
|
52
|
-
"@
|
|
71
|
+
"@entro314labs/release-kit": "^2.9.3",
|
|
72
|
+
"@happy-dom/global-registrator": "^20.14.0",
|
|
73
|
+
"@types/node": "^26.4.1",
|
|
53
74
|
"@types/react": "^19.2.18",
|
|
54
|
-
"@types/react-dom": "^19.2.
|
|
75
|
+
"@types/react-dom": "^19.2.7",
|
|
55
76
|
"ajv": "8.20.0",
|
|
56
|
-
"oxfmt": "0.
|
|
57
|
-
"oxlint": "1.
|
|
77
|
+
"oxfmt": "0.66.0",
|
|
78
|
+
"oxlint": "1.81.0",
|
|
58
79
|
"oxlint-tsgolint": "^7.0.2001",
|
|
59
80
|
"react": "^19.2.8",
|
|
60
81
|
"react-dom": "^19.2.8",
|
|
61
|
-
"tsx": "^4.23.
|
|
62
|
-
"typescript": "
|
|
82
|
+
"tsx": "^4.23.13",
|
|
83
|
+
"typescript": "next"
|
|
63
84
|
},
|
|
64
85
|
"peerDependencies": {
|
|
65
|
-
"@types/react": ">=19.2.
|
|
66
|
-
"react": ">=19.2.
|
|
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
|
-
"
|
|
80
|
-
|
|
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.26.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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
160
|
-
* <div data-energy-max="25">Recovery hint
|
|
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
|
|
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
|
|
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
|
|
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
|