@elizaos/plugin-health 2.0.0-beta.1
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/README.md +107 -0
- package/dist/actions/index.d.ts +20 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +5 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/anchors/index.d.ts +19 -0
- package/dist/anchors/index.d.ts.map +1 -0
- package/dist/anchors/index.js +9 -0
- package/dist/anchors/index.js.map +1 -0
- package/dist/connectors/contract-stubs.d.ts +112 -0
- package/dist/connectors/contract-stubs.d.ts.map +1 -0
- package/dist/connectors/contract-stubs.js +1 -0
- package/dist/connectors/contract-stubs.js.map +1 -0
- package/dist/connectors/index.d.ts +28 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +202 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/contracts/circadian-default.d.ts +15 -0
- package/dist/contracts/circadian-default.d.ts.map +1 -0
- package/dist/contracts/circadian-default.js +30 -0
- package/dist/contracts/circadian-default.js.map +1 -0
- package/dist/contracts/circadian.d.ts +92 -0
- package/dist/contracts/circadian.d.ts.map +1 -0
- package/dist/contracts/circadian.js +14 -0
- package/dist/contracts/circadian.js.map +1 -0
- package/dist/contracts/health.d.ts +9 -0
- package/dist/contracts/health.d.ts.map +1 -0
- package/dist/contracts/health.js +21 -0
- package/dist/contracts/health.js.map +1 -0
- package/dist/contracts/lifeops-connector-degradation.d.ts +9 -0
- package/dist/contracts/lifeops-connector-degradation.d.ts.map +1 -0
- package/dist/contracts/lifeops-connector-degradation.js +17 -0
- package/dist/contracts/lifeops-connector-degradation.js.map +1 -0
- package/dist/contracts/lifeops.d.ts +3123 -0
- package/dist/contracts/lifeops.d.ts.map +1 -0
- package/dist/contracts/lifeops.js +635 -0
- package/dist/contracts/lifeops.js.map +1 -0
- package/dist/contracts/permissions.d.ts +39 -0
- package/dist/contracts/permissions.d.ts.map +1 -0
- package/dist/contracts/permissions.js +1 -0
- package/dist/contracts/permissions.js.map +1 -0
- package/dist/default-packs/bedtime.d.ts +14 -0
- package/dist/default-packs/bedtime.d.ts.map +1 -0
- package/dist/default-packs/bedtime.js +48 -0
- package/dist/default-packs/bedtime.js.map +1 -0
- package/dist/default-packs/contract-stubs.d.ts +161 -0
- package/dist/default-packs/contract-stubs.d.ts.map +1 -0
- package/dist/default-packs/contract-stubs.js +1 -0
- package/dist/default-packs/contract-stubs.js.map +1 -0
- package/dist/default-packs/index.d.ts +18 -0
- package/dist/default-packs/index.d.ts.map +1 -0
- package/dist/default-packs/index.js +39 -0
- package/dist/default-packs/index.js.map +1 -0
- package/dist/default-packs/sleep-recap.d.ts +14 -0
- package/dist/default-packs/sleep-recap.d.ts.map +1 -0
- package/dist/default-packs/sleep-recap.js +51 -0
- package/dist/default-packs/sleep-recap.js.map +1 -0
- package/dist/default-packs/wake-up.d.ts +14 -0
- package/dist/default-packs/wake-up.d.ts.map +1 -0
- package/dist/default-packs/wake-up.js +61 -0
- package/dist/default-packs/wake-up.js.map +1 -0
- package/dist/health-bridge/health-bridge.d.ts +57 -0
- package/dist/health-bridge/health-bridge.d.ts.map +1 -0
- package/dist/health-bridge/health-bridge.js +558 -0
- package/dist/health-bridge/health-bridge.js.map +1 -0
- package/dist/health-bridge/health-connectors.d.ts +23 -0
- package/dist/health-bridge/health-connectors.d.ts.map +1 -0
- package/dist/health-bridge/health-connectors.js +1018 -0
- package/dist/health-bridge/health-connectors.js.map +1 -0
- package/dist/health-bridge/health-oauth.d.ts +62 -0
- package/dist/health-bridge/health-oauth.d.ts.map +1 -0
- package/dist/health-bridge/health-oauth.js +432 -0
- package/dist/health-bridge/health-oauth.js.map +1 -0
- package/dist/health-bridge/health-provider-registry.d.ts +89 -0
- package/dist/health-bridge/health-provider-registry.d.ts.map +1 -0
- package/dist/health-bridge/health-provider-registry.js +141 -0
- package/dist/health-bridge/health-provider-registry.js.map +1 -0
- package/dist/health-bridge/health-records.d.ts +14 -0
- package/dist/health-bridge/health-records.d.ts.map +1 -0
- package/dist/health-bridge/health-records.js +45 -0
- package/dist/health-bridge/health-records.js.map +1 -0
- package/dist/health-bridge/index.d.ts +22 -0
- package/dist/health-bridge/index.d.ts.map +1 -0
- package/dist/health-bridge/index.js +7 -0
- package/dist/health-bridge/index.js.map +1 -0
- package/dist/health-bridge/service-normalize-health.d.ts +3 -0
- package/dist/health-bridge/service-normalize-health.d.ts.map +1 -0
- package/dist/health-bridge/service-normalize-health.js +96 -0
- package/dist/health-bridge/service-normalize-health.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/screen-time/index.d.ts +23 -0
- package/dist/screen-time/index.d.ts.map +1 -0
- package/dist/screen-time/index.js +1 -0
- package/dist/screen-time/index.js.map +1 -0
- package/dist/sleep/awake-probability.d.ts +11 -0
- package/dist/sleep/awake-probability.d.ts.map +1 -0
- package/dist/sleep/awake-probability.js +163 -0
- package/dist/sleep/awake-probability.js.map +1 -0
- package/dist/sleep/circadian-rules.d.ts +45 -0
- package/dist/sleep/circadian-rules.d.ts.map +1 -0
- package/dist/sleep/circadian-rules.js +258 -0
- package/dist/sleep/circadian-rules.js.map +1 -0
- package/dist/sleep/index.d.ts +21 -0
- package/dist/sleep/index.d.ts.map +1 -0
- package/dist/sleep/index.js +11 -0
- package/dist/sleep/index.js.map +1 -0
- package/dist/sleep/sleep-cycle-dispatch.d.ts +75 -0
- package/dist/sleep/sleep-cycle-dispatch.d.ts.map +1 -0
- package/dist/sleep/sleep-cycle-dispatch.js +102 -0
- package/dist/sleep/sleep-cycle-dispatch.js.map +1 -0
- package/dist/sleep/sleep-cycle.d.ts +38 -0
- package/dist/sleep/sleep-cycle.d.ts.map +1 -0
- package/dist/sleep/sleep-cycle.js +418 -0
- package/dist/sleep/sleep-cycle.js.map +1 -0
- package/dist/sleep/sleep-episode-store.d.ts +25 -0
- package/dist/sleep/sleep-episode-store.d.ts.map +1 -0
- package/dist/sleep/sleep-episode-store.js +69 -0
- package/dist/sleep/sleep-episode-store.js.map +1 -0
- package/dist/sleep/sleep-episode-types.d.ts +38 -0
- package/dist/sleep/sleep-episode-types.d.ts.map +1 -0
- package/dist/sleep/sleep-episode-types.js +14 -0
- package/dist/sleep/sleep-episode-types.js.map +1 -0
- package/dist/sleep/sleep-recap.d.ts +19 -0
- package/dist/sleep/sleep-recap.d.ts.map +1 -0
- package/dist/sleep/sleep-recap.js +1 -0
- package/dist/sleep/sleep-recap.js.map +1 -0
- package/dist/sleep/sleep-regularity.d.ts +19 -0
- package/dist/sleep/sleep-regularity.d.ts.map +1 -0
- package/dist/sleep/sleep-regularity.js +242 -0
- package/dist/sleep/sleep-regularity.js.map +1 -0
- package/dist/sleep/sleep-wake-events.d.ts +58 -0
- package/dist/sleep/sleep-wake-events.d.ts.map +1 -0
- package/dist/sleep/sleep-wake-events.js +135 -0
- package/dist/sleep/sleep-wake-events.js.map +1 -0
- package/dist/sleep/source-reliability.d.ts +38 -0
- package/dist/sleep/source-reliability.d.ts.map +1 -0
- package/dist/sleep/source-reliability.js +62 -0
- package/dist/sleep/source-reliability.js.map +1 -0
- package/dist/util/index.d.ts +10 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +3 -0
- package/dist/util/index.js.map +1 -0
- package/dist/util/normalize.d.ts +22 -0
- package/dist/util/normalize.d.ts.map +1 -0
- package/dist/util/normalize.js +62 -0
- package/dist/util/normalize.js.map +1 -0
- package/dist/util/time-util.d.ts +10 -0
- package/dist/util/time-util.d.ts.map +1 -0
- package/dist/util/time-util.js +14 -0
- package/dist/util/time-util.js.map +1 -0
- package/dist/util/time.d.ts +17 -0
- package/dist/util/time.d.ts.map +1 -0
- package/dist/util/time.js +152 -0
- package/dist/util/time.js.map +1 -0
- package/dist/util/token-encryption.d.ts +42 -0
- package/dist/util/token-encryption.d.ts.map +1 -0
- package/dist/util/token-encryption.js +96 -0
- package/dist/util/token-encryption.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { LIFEOPS_HEALTH_SIGNAL_SOURCES } from "../contracts/health.js";
|
|
2
|
+
import {
|
|
3
|
+
buildUtcDateFromLocalParts,
|
|
4
|
+
getLocalDateKey,
|
|
5
|
+
getZonedDateParts
|
|
6
|
+
} from "../util/time.js";
|
|
7
|
+
import { roundConfidence } from "../util/time-util.js";
|
|
8
|
+
const COMPLETED_SLEEP_GAP_MIN_MS = 3 * 60 * 60 * 1e3;
|
|
9
|
+
const CURRENT_SLEEP_GAP_MIN_MS = 2 * 60 * 60 * 1e3;
|
|
10
|
+
const CURRENT_SLEEP_GAP_STRONG_MIN_MS = 5 * 60 * 60 * 1e3;
|
|
11
|
+
const HEALTH_CURRENT_SLEEP_MAX_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
12
|
+
const HEALTH_CURRENT_SLEEP_MAX_DURATION_MS = 16 * 60 * 60 * 1e3;
|
|
13
|
+
const MIN_SLEEP_CONFIDENCE = 0.45;
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function clamp(value, min, max) {
|
|
18
|
+
return Math.min(max, Math.max(min, value));
|
|
19
|
+
}
|
|
20
|
+
function intervalDurationMs(startMs, endMs, nowMs) {
|
|
21
|
+
const safeEndMs = endMs ?? nowMs;
|
|
22
|
+
return Math.max(0, safeEndMs - startMs);
|
|
23
|
+
}
|
|
24
|
+
function toIso(ms) {
|
|
25
|
+
if (ms === null || !Number.isFinite(ms)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return new Date(ms).toISOString();
|
|
29
|
+
}
|
|
30
|
+
function localDateKey(ms, timezone) {
|
|
31
|
+
return getLocalDateKey(getZonedDateParts(new Date(ms), timezone));
|
|
32
|
+
}
|
|
33
|
+
function localHour(ms, timezone) {
|
|
34
|
+
return getZonedDateParts(new Date(ms), timezone).hour;
|
|
35
|
+
}
|
|
36
|
+
function normalizeSleepHour(hour) {
|
|
37
|
+
return hour < 12 ? hour + 24 : hour;
|
|
38
|
+
}
|
|
39
|
+
function median(values) {
|
|
40
|
+
if (values.length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const sorted = [...values].sort((left2, right2) => left2 - right2);
|
|
44
|
+
const middle = Math.floor(sorted.length / 2);
|
|
45
|
+
if (sorted.length % 2 === 1) {
|
|
46
|
+
return sorted[middle] ?? null;
|
|
47
|
+
}
|
|
48
|
+
const left = sorted[middle - 1];
|
|
49
|
+
const right = sorted[middle];
|
|
50
|
+
if (left === void 0 || right === void 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return Math.round((left + right) / 2 * 100) / 100;
|
|
54
|
+
}
|
|
55
|
+
function resolveHealthSignal(signal) {
|
|
56
|
+
if (signal.health) {
|
|
57
|
+
return signal.health;
|
|
58
|
+
}
|
|
59
|
+
const metadataHealth = isHealthSignal(signal.metadata.health) ? signal.metadata.health : null;
|
|
60
|
+
return metadataHealth ?? null;
|
|
61
|
+
}
|
|
62
|
+
function isNullableString(value) {
|
|
63
|
+
return value === null || typeof value === "string";
|
|
64
|
+
}
|
|
65
|
+
function isNullableNumber(value) {
|
|
66
|
+
return value === null || typeof value === "number";
|
|
67
|
+
}
|
|
68
|
+
function isHealthSignal(value) {
|
|
69
|
+
if (!isRecord(value)) return false;
|
|
70
|
+
if (typeof value.source !== "string" || !LIFEOPS_HEALTH_SIGNAL_SOURCES.includes(value.source)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const permissions = value.permissions;
|
|
74
|
+
const sleep = value.sleep;
|
|
75
|
+
const biometrics = value.biometrics;
|
|
76
|
+
return isRecord(permissions) && typeof permissions.sleep === "boolean" && typeof permissions.biometrics === "boolean" && isRecord(sleep) && typeof sleep.available === "boolean" && typeof sleep.isSleeping === "boolean" && isNullableString(sleep.asleepAt) && isNullableString(sleep.awakeAt) && isNullableNumber(sleep.durationMinutes) && isNullableString(sleep.stage) && isRecord(biometrics) && isNullableString(biometrics.sampleAt) && isNullableNumber(biometrics.heartRateBpm) && isNullableNumber(biometrics.restingHeartRateBpm) && isNullableNumber(biometrics.heartRateVariabilityMs) && isNullableNumber(biometrics.respiratoryRate) && isNullableNumber(biometrics.bloodOxygenPercent) && Array.isArray(value.warnings) && value.warnings.every((warning) => typeof warning === "string");
|
|
77
|
+
}
|
|
78
|
+
function normalizeSleepEndMs(args) {
|
|
79
|
+
if (Number.isFinite(args.awakeAtMs) && args.awakeAtMs > args.asleepAtMs) {
|
|
80
|
+
return args.awakeAtMs;
|
|
81
|
+
}
|
|
82
|
+
if (typeof args.durationMinutes === "number" && Number.isFinite(args.durationMinutes)) {
|
|
83
|
+
const durationMs = args.durationMinutes * 6e4;
|
|
84
|
+
if (durationMs > 0) {
|
|
85
|
+
return args.asleepAtMs + durationMs;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function isFreshCurrentHealthSleep(args) {
|
|
91
|
+
if (!Number.isFinite(args.observedAtMs)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (args.nowMs - args.observedAtMs > HEALTH_CURRENT_SLEEP_MAX_AGE_MS) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (args.asleepAtMs !== null) {
|
|
98
|
+
if (args.observedAtMs < args.asleepAtMs - 5 * 6e4) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (args.nowMs - args.asleepAtMs > HEALTH_CURRENT_SLEEP_MAX_DURATION_MS) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function hasActiveSignalAfter(signals, thresholdMs) {
|
|
108
|
+
return signals.some((signal) => {
|
|
109
|
+
if (signal.state !== "active") {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const observedAt = Date.parse(signal.observedAt);
|
|
113
|
+
return Number.isFinite(observedAt) && observedAt > thresholdMs;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function parseHealthSleepEpisodes(args) {
|
|
117
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const signal of args.signals) {
|
|
119
|
+
const health = resolveHealthSignal(signal);
|
|
120
|
+
const sleep = health && isRecord(health.sleep) ? health.sleep : null;
|
|
121
|
+
if (!sleep) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const asleepAt = typeof sleep.asleepAt === "string" ? Date.parse(sleep.asleepAt) : Number.NaN;
|
|
125
|
+
const awakeAt = typeof sleep.awakeAt === "string" ? Date.parse(sleep.awakeAt) : Number.NaN;
|
|
126
|
+
const durationMinutes = typeof sleep.durationMinutes === "number" && Number.isFinite(sleep.durationMinutes) ? sleep.durationMinutes : null;
|
|
127
|
+
const observedAt = Date.parse(signal.observedAt);
|
|
128
|
+
if (sleep.isSleeping === true && Number.isFinite(asleepAt) && isFreshCurrentHealthSleep({
|
|
129
|
+
asleepAtMs: asleepAt,
|
|
130
|
+
observedAtMs: observedAt,
|
|
131
|
+
nowMs: args.nowMs
|
|
132
|
+
}) && !hasActiveSignalAfter(args.signals, observedAt + 5 * 6e4)) {
|
|
133
|
+
deduped.set(`health-current:${asleepAt}`, {
|
|
134
|
+
startMs: asleepAt,
|
|
135
|
+
endMs: null,
|
|
136
|
+
current: true,
|
|
137
|
+
confidence: 0.96,
|
|
138
|
+
source: "health",
|
|
139
|
+
observedMs: observedAt
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (Number.isFinite(asleepAt)) {
|
|
144
|
+
const normalizedEndMs = normalizeSleepEndMs({
|
|
145
|
+
asleepAtMs: asleepAt,
|
|
146
|
+
awakeAtMs: awakeAt,
|
|
147
|
+
durationMinutes
|
|
148
|
+
});
|
|
149
|
+
if (normalizedEndMs !== null) {
|
|
150
|
+
deduped.set(`health:${asleepAt}:${normalizedEndMs}`, {
|
|
151
|
+
startMs: asleepAt,
|
|
152
|
+
endMs: normalizedEndMs,
|
|
153
|
+
current: false,
|
|
154
|
+
confidence: 0.93,
|
|
155
|
+
source: "health"
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (sleep.isSleeping === true && isFreshCurrentHealthSleep({
|
|
161
|
+
asleepAtMs: null,
|
|
162
|
+
observedAtMs: observedAt,
|
|
163
|
+
nowMs: args.nowMs
|
|
164
|
+
}) && !hasActiveSignalAfter(args.signals, observedAt + 5 * 6e4)) {
|
|
165
|
+
deduped.set(`health-observed:${observedAt}`, {
|
|
166
|
+
startMs: observedAt,
|
|
167
|
+
endMs: null,
|
|
168
|
+
current: true,
|
|
169
|
+
confidence: 0.88,
|
|
170
|
+
source: "health",
|
|
171
|
+
observedMs: observedAt
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [...deduped.values()].sort(
|
|
176
|
+
(left, right) => left.startMs - right.startMs
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function hasSignalNear(signals, targetMs, windowMs, predicate) {
|
|
180
|
+
for (const signal of signals) {
|
|
181
|
+
const observedAt = Date.parse(signal.observedAt);
|
|
182
|
+
if (!Number.isFinite(observedAt)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (Math.abs(observedAt - targetMs) <= windowMs && predicate(signal)) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
function buildGapSleepEpisodes(args) {
|
|
192
|
+
const episodes = [];
|
|
193
|
+
if (args.windows.length === 0) {
|
|
194
|
+
return episodes;
|
|
195
|
+
}
|
|
196
|
+
for (let index = 0; index < args.windows.length; index += 1) {
|
|
197
|
+
const current = args.windows[index];
|
|
198
|
+
if (!current) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const next = args.windows[index + 1] ?? null;
|
|
202
|
+
const gapStartMs = current.endMs;
|
|
203
|
+
const gapEndMs = next ? next.startMs : args.nowMs;
|
|
204
|
+
const gapMs = Math.max(0, gapEndMs - gapStartMs);
|
|
205
|
+
const currentGap = next === null;
|
|
206
|
+
const minDurationMs = currentGap ? CURRENT_SLEEP_GAP_MIN_MS : COMPLETED_SLEEP_GAP_MIN_MS;
|
|
207
|
+
if (gapMs < minDurationMs) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const startHour = localHour(gapStartMs, args.timezone);
|
|
211
|
+
const endHour = localHour(gapEndMs, args.timezone);
|
|
212
|
+
const durationFactor = clamp(gapMs / (8 * 60 * 60 * 1e3), 0, 1);
|
|
213
|
+
let score = 0.3 + durationFactor * 0.35;
|
|
214
|
+
if (startHour >= 20 || startHour < 4) {
|
|
215
|
+
score += 0.15;
|
|
216
|
+
}
|
|
217
|
+
if (endHour >= 4 && endHour < 13) {
|
|
218
|
+
score += 0.15;
|
|
219
|
+
}
|
|
220
|
+
const hasChargingCue = hasSignalNear(
|
|
221
|
+
args.signals,
|
|
222
|
+
gapStartMs,
|
|
223
|
+
90 * 60 * 1e3,
|
|
224
|
+
(signal) => signal.onBattery === false
|
|
225
|
+
);
|
|
226
|
+
const hasRestCue = hasSignalNear(
|
|
227
|
+
args.signals,
|
|
228
|
+
gapStartMs,
|
|
229
|
+
45 * 60 * 1e3,
|
|
230
|
+
(signal) => signal.state === "locked" || signal.state === "background" || signal.state === "idle" || signal.state === "sleeping"
|
|
231
|
+
);
|
|
232
|
+
if (currentGap) {
|
|
233
|
+
const nowHour = localHour(args.nowMs, args.timezone);
|
|
234
|
+
const looksLikeOvernight = startHour >= 20 || startHour < 5 || nowHour < 10;
|
|
235
|
+
const hasStrongCue = hasChargingCue || hasRestCue;
|
|
236
|
+
if (!looksLikeOvernight || gapMs < CURRENT_SLEEP_GAP_STRONG_MIN_MS && !hasStrongCue) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (hasChargingCue) {
|
|
241
|
+
score += 0.1;
|
|
242
|
+
}
|
|
243
|
+
if (hasRestCue) {
|
|
244
|
+
score += 0.1;
|
|
245
|
+
}
|
|
246
|
+
if (gapMs < 4 * 60 * 60 * 1e3) {
|
|
247
|
+
score -= 0.1;
|
|
248
|
+
}
|
|
249
|
+
score = roundConfidence(score);
|
|
250
|
+
if (score < MIN_SLEEP_CONFIDENCE) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
episodes.push({
|
|
254
|
+
startMs: gapStartMs,
|
|
255
|
+
endMs: currentGap ? null : gapEndMs,
|
|
256
|
+
current: currentGap,
|
|
257
|
+
confidence: score,
|
|
258
|
+
source: "activity_gap"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return episodes;
|
|
262
|
+
}
|
|
263
|
+
function selectLatestCompletedSleep(episodes, nowMs, timezone) {
|
|
264
|
+
const completed = [...episodes].filter(
|
|
265
|
+
(episode) => episode.endMs !== null && episode.endMs <= nowMs
|
|
266
|
+
);
|
|
267
|
+
const dayAnchoring = completed.filter((episode) => {
|
|
268
|
+
const sleepType = classifySleepType(episode, nowMs, timezone);
|
|
269
|
+
if (sleepType === "overnight") {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return sleepType !== "nap" && intervalDurationMs(episode.startMs, episode.endMs, nowMs) >= 4 * 60 * 60 * 1e3;
|
|
273
|
+
});
|
|
274
|
+
const candidates = dayAnchoring.length > 0 ? dayAnchoring : completed;
|
|
275
|
+
return candidates.sort((left, right) => {
|
|
276
|
+
const leftEnd = left.endMs ?? 0;
|
|
277
|
+
const rightEnd = right.endMs ?? 0;
|
|
278
|
+
if (rightEnd !== leftEnd) {
|
|
279
|
+
return rightEnd - leftEnd;
|
|
280
|
+
}
|
|
281
|
+
return right.confidence - left.confidence;
|
|
282
|
+
})[0] ?? null;
|
|
283
|
+
}
|
|
284
|
+
function selectCurrentSleep(episodes) {
|
|
285
|
+
return [...episodes].filter((episode) => episode.current).sort((left, right) => right.confidence - left.confidence)[0] ?? null;
|
|
286
|
+
}
|
|
287
|
+
function classifyLifeOpsSleepCycleType(args) {
|
|
288
|
+
const endMs = args.endMs ?? args.nowMs;
|
|
289
|
+
const durationMs = intervalDurationMs(args.startMs, args.endMs, args.nowMs);
|
|
290
|
+
const durationHours = durationMs / (60 * 60 * 1e3);
|
|
291
|
+
const startHour = localHour(args.startMs, args.timezone);
|
|
292
|
+
const endHour = localHour(endMs, args.timezone);
|
|
293
|
+
if (durationHours >= 4 && (startHour >= 18 || startHour < 6 || endHour <= 11)) {
|
|
294
|
+
return "overnight";
|
|
295
|
+
}
|
|
296
|
+
if (durationHours > 0 && durationHours < 4) {
|
|
297
|
+
return "nap";
|
|
298
|
+
}
|
|
299
|
+
return "unknown";
|
|
300
|
+
}
|
|
301
|
+
function classifySleepType(episode, nowMs, timezone) {
|
|
302
|
+
return classifyLifeOpsSleepCycleType({
|
|
303
|
+
startMs: episode.startMs,
|
|
304
|
+
endMs: episode.endMs,
|
|
305
|
+
nowMs,
|
|
306
|
+
timezone
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function resolveLifeOpsSleepCycle(args) {
|
|
310
|
+
const healthEpisodes = parseHealthSleepEpisodes({
|
|
311
|
+
signals: args.signals,
|
|
312
|
+
nowMs: args.nowMs
|
|
313
|
+
});
|
|
314
|
+
const gapEpisodes = buildGapSleepEpisodes({
|
|
315
|
+
windows: args.windows,
|
|
316
|
+
signals: args.signals,
|
|
317
|
+
nowMs: args.nowMs,
|
|
318
|
+
timezone: args.timezone
|
|
319
|
+
});
|
|
320
|
+
const episodes = [...healthEpisodes, ...gapEpisodes];
|
|
321
|
+
const currentSleep = selectCurrentSleep(episodes);
|
|
322
|
+
const lastCompletedSleep = selectLatestCompletedSleep(
|
|
323
|
+
episodes,
|
|
324
|
+
args.nowMs,
|
|
325
|
+
args.timezone
|
|
326
|
+
);
|
|
327
|
+
const candidateSleepStarts = episodes.filter(
|
|
328
|
+
(episode) => classifySleepType(episode, args.nowMs, args.timezone) !== "nap" && intervalDurationMs(episode.startMs, episode.endMs, args.nowMs) >= COMPLETED_SLEEP_GAP_MIN_MS
|
|
329
|
+
).map(
|
|
330
|
+
(episode) => normalizeSleepHour(localHour(episode.startMs, args.timezone))
|
|
331
|
+
);
|
|
332
|
+
const candidateWakeHours = episodes.filter(
|
|
333
|
+
(episode) => episode.endMs !== null && classifySleepType(episode, args.nowMs, args.timezone) !== "nap"
|
|
334
|
+
).map((episode) => localHour(episode.endMs, args.timezone));
|
|
335
|
+
const typicalSleepHour = median(candidateSleepStarts);
|
|
336
|
+
const sleepCycle = {
|
|
337
|
+
cycleType: currentSleep ? classifySleepType(currentSleep, args.nowMs, args.timezone) : lastCompletedSleep ? classifySleepType(lastCompletedSleep, args.nowMs, args.timezone) : "unknown",
|
|
338
|
+
sleepStatus: currentSleep?.confidence !== void 0 && currentSleep.confidence >= 0.55 ? "sleeping_now" : lastCompletedSleep?.endMs && args.nowMs - lastCompletedSleep.endMs <= 30 * 60 * 60 * 1e3 ? "slept" : lastCompletedSleep?.endMs && args.nowMs - lastCompletedSleep.endMs >= 20 * 60 * 60 * 1e3 ? "likely_missed" : "unknown",
|
|
339
|
+
isProbablySleeping: currentSleep?.confidence !== void 0 && currentSleep.confidence >= 0.55,
|
|
340
|
+
sleepConfidence: roundConfidence(
|
|
341
|
+
currentSleep?.confidence ?? lastCompletedSleep?.confidence ?? 0
|
|
342
|
+
),
|
|
343
|
+
currentSleepStartedAt: toIso(currentSleep?.startMs ?? null),
|
|
344
|
+
lastSleepStartedAt: toIso(
|
|
345
|
+
(currentSleep ?? lastCompletedSleep)?.startMs ?? null
|
|
346
|
+
),
|
|
347
|
+
lastSleepEndedAt: toIso(lastCompletedSleep?.endMs ?? null),
|
|
348
|
+
lastSleepDurationMinutes: (() => {
|
|
349
|
+
const target = currentSleep ?? lastCompletedSleep;
|
|
350
|
+
if (!target) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
return Math.round(
|
|
354
|
+
intervalDurationMs(target.startMs, target.endMs, args.nowMs) / 6e4
|
|
355
|
+
);
|
|
356
|
+
})(),
|
|
357
|
+
evidence: episodes.map(
|
|
358
|
+
(episode) => ({
|
|
359
|
+
startAt: new Date(episode.startMs).toISOString(),
|
|
360
|
+
endAt: toIso(episode.endMs),
|
|
361
|
+
source: episode.source,
|
|
362
|
+
confidence: episode.confidence
|
|
363
|
+
})
|
|
364
|
+
).sort(
|
|
365
|
+
(left, right) => Date.parse(left.startAt) - Date.parse(right.startAt)
|
|
366
|
+
)
|
|
367
|
+
};
|
|
368
|
+
return {
|
|
369
|
+
sleepCycle,
|
|
370
|
+
sleepEpisodes: episodes,
|
|
371
|
+
typicalWakeHour: median(candidateWakeHours),
|
|
372
|
+
typicalSleepHour
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function resolveLifeOpsDayBoundary(args) {
|
|
376
|
+
const nowDate = new Date(args.nowMs);
|
|
377
|
+
const localDateParts = getZonedDateParts(nowDate, args.timezone);
|
|
378
|
+
const startOfDay = buildUtcDateFromLocalParts(args.timezone, {
|
|
379
|
+
year: localDateParts.year,
|
|
380
|
+
month: localDateParts.month,
|
|
381
|
+
day: localDateParts.day,
|
|
382
|
+
hour: 0,
|
|
383
|
+
minute: 0,
|
|
384
|
+
second: 0
|
|
385
|
+
});
|
|
386
|
+
const nextDateParts = getZonedDateParts(
|
|
387
|
+
new Date(startOfDay.getTime() + 24 * 60 * 60 * 1e3),
|
|
388
|
+
args.timezone
|
|
389
|
+
);
|
|
390
|
+
const endOfDay = buildUtcDateFromLocalParts(args.timezone, {
|
|
391
|
+
year: nextDateParts.year,
|
|
392
|
+
month: nextDateParts.month,
|
|
393
|
+
day: nextDateParts.day,
|
|
394
|
+
hour: 0,
|
|
395
|
+
minute: 0,
|
|
396
|
+
second: 0
|
|
397
|
+
});
|
|
398
|
+
const beforeSleepAt = args.sleepCycle.currentSleepStartedAt ?? args.sleepCycle.lastSleepStartedAt ?? null;
|
|
399
|
+
const overnightAnchor = args.sleepCycle.cycleType === "overnight" && beforeSleepAt;
|
|
400
|
+
const anchor = overnightAnchor ? "before_sleep" : "start_of_day";
|
|
401
|
+
const effectiveDaySourceMs = args.sleepCycle.cycleType === "overnight" && args.sleepCycle.lastSleepEndedAt ? Date.parse(args.sleepCycle.lastSleepEndedAt) : args.sleepCycle.cycleType === "overnight" && args.sleepCycle.currentSleepStartedAt ? Date.parse(args.sleepCycle.currentSleepStartedAt) : args.nowMs;
|
|
402
|
+
return {
|
|
403
|
+
effectiveDayKey: localDateKey(effectiveDaySourceMs, args.timezone),
|
|
404
|
+
localDate: localDateKey(args.nowMs, args.timezone),
|
|
405
|
+
timezone: args.timezone,
|
|
406
|
+
anchor,
|
|
407
|
+
startOfDayAt: startOfDay.toISOString(),
|
|
408
|
+
endOfDayAt: endOfDay.toISOString(),
|
|
409
|
+
beforeSleepAt,
|
|
410
|
+
confidence: roundConfidence(args.sleepCycle.sleepConfidence)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
export {
|
|
414
|
+
classifyLifeOpsSleepCycleType,
|
|
415
|
+
resolveLifeOpsDayBoundary,
|
|
416
|
+
resolveLifeOpsSleepCycle
|
|
417
|
+
};
|
|
418
|
+
//# sourceMappingURL=sleep-cycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sleep/sleep-cycle.ts"],"sourcesContent":["import type {\n LifeOpsActivitySignal,\n LifeOpsDayBoundary,\n LifeOpsHealthSignal,\n LifeOpsSleepCycle,\n LifeOpsSleepCycleEvidence,\n LifeOpsSleepCycleType,\n} from \"../contracts/health.js\";\nimport { LIFEOPS_HEALTH_SIGNAL_SOURCES } from \"../contracts/health.js\";\nimport {\n buildUtcDateFromLocalParts,\n getLocalDateKey,\n getZonedDateParts,\n} from \"../util/time.js\";\nimport { roundConfidence } from \"../util/time-util.js\";\n\nconst COMPLETED_SLEEP_GAP_MIN_MS = 3 * 60 * 60 * 1_000;\nconst CURRENT_SLEEP_GAP_MIN_MS = 2 * 60 * 60 * 1_000;\nconst CURRENT_SLEEP_GAP_STRONG_MIN_MS = 5 * 60 * 60 * 1_000;\nconst HEALTH_CURRENT_SLEEP_MAX_AGE_MS = 2 * 60 * 60 * 1_000;\nconst HEALTH_CURRENT_SLEEP_MAX_DURATION_MS = 16 * 60 * 60 * 1_000;\nconst MIN_SLEEP_CONFIDENCE = 0.45;\n\nexport type LifeOpsActivityWindow = {\n startMs: number;\n endMs: number;\n source: \"app\" | \"website\" | \"signal\";\n};\n\nexport type LifeOpsSleepEpisode = {\n startMs: number;\n endMs: number | null;\n current: boolean;\n confidence: number;\n source: \"health\" | \"activity_gap\";\n observedMs?: number;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction intervalDurationMs(\n startMs: number,\n endMs: number | null,\n nowMs: number,\n): number {\n const safeEndMs = endMs ?? nowMs;\n return Math.max(0, safeEndMs - startMs);\n}\n\nfunction toIso(ms: number | null): string | null {\n if (ms === null || !Number.isFinite(ms)) {\n return null;\n }\n return new Date(ms).toISOString();\n}\n\nfunction localDateKey(ms: number, timezone: string): string {\n return getLocalDateKey(getZonedDateParts(new Date(ms), timezone));\n}\n\nfunction localHour(ms: number, timezone: string): number {\n return getZonedDateParts(new Date(ms), timezone).hour;\n}\n\nfunction normalizeSleepHour(hour: number): number {\n return hour < 12 ? hour + 24 : hour;\n}\n\nfunction median(values: number[]): number | null {\n if (values.length === 0) {\n return null;\n }\n const sorted = [...values].sort((left, right) => left - right);\n const middle = Math.floor(sorted.length / 2);\n if (sorted.length % 2 === 1) {\n return sorted[middle] ?? null;\n }\n const left = sorted[middle - 1];\n const right = sorted[middle];\n if (left === undefined || right === undefined) {\n return null;\n }\n return Math.round(((left + right) / 2) * 100) / 100;\n}\n\nfunction resolveHealthSignal(\n signal: LifeOpsActivitySignal,\n): LifeOpsHealthSignal | null {\n if (signal.health) {\n return signal.health;\n }\n const metadataHealth = isHealthSignal(signal.metadata.health)\n ? signal.metadata.health\n : null;\n return metadataHealth ?? null;\n}\n\nfunction isNullableString(value: unknown): value is string | null {\n return value === null || typeof value === \"string\";\n}\n\nfunction isNullableNumber(value: unknown): value is number | null {\n return value === null || typeof value === \"number\";\n}\n\nfunction isHealthSignal(value: unknown): value is LifeOpsHealthSignal {\n if (!isRecord(value)) return false;\n if (\n typeof value.source !== \"string\" ||\n !(LIFEOPS_HEALTH_SIGNAL_SOURCES as readonly string[]).includes(value.source)\n ) {\n return false;\n }\n const permissions = value.permissions;\n const sleep = value.sleep;\n const biometrics = value.biometrics;\n return (\n isRecord(permissions) &&\n typeof permissions.sleep === \"boolean\" &&\n typeof permissions.biometrics === \"boolean\" &&\n isRecord(sleep) &&\n typeof sleep.available === \"boolean\" &&\n typeof sleep.isSleeping === \"boolean\" &&\n isNullableString(sleep.asleepAt) &&\n isNullableString(sleep.awakeAt) &&\n isNullableNumber(sleep.durationMinutes) &&\n isNullableString(sleep.stage) &&\n isRecord(biometrics) &&\n isNullableString(biometrics.sampleAt) &&\n isNullableNumber(biometrics.heartRateBpm) &&\n isNullableNumber(biometrics.restingHeartRateBpm) &&\n isNullableNumber(biometrics.heartRateVariabilityMs) &&\n isNullableNumber(biometrics.respiratoryRate) &&\n isNullableNumber(biometrics.bloodOxygenPercent) &&\n Array.isArray(value.warnings) &&\n value.warnings.every((warning) => typeof warning === \"string\")\n );\n}\n\nfunction normalizeSleepEndMs(args: {\n asleepAtMs: number;\n awakeAtMs: number;\n durationMinutes: number | null;\n}): number | null {\n if (Number.isFinite(args.awakeAtMs) && args.awakeAtMs > args.asleepAtMs) {\n return args.awakeAtMs;\n }\n if (\n typeof args.durationMinutes === \"number\" &&\n Number.isFinite(args.durationMinutes)\n ) {\n const durationMs = args.durationMinutes * 60_000;\n if (durationMs > 0) {\n return args.asleepAtMs + durationMs;\n }\n }\n return null;\n}\n\nfunction isFreshCurrentHealthSleep(args: {\n asleepAtMs: number | null;\n observedAtMs: number;\n nowMs: number;\n}): boolean {\n if (!Number.isFinite(args.observedAtMs)) {\n return false;\n }\n if (args.nowMs - args.observedAtMs > HEALTH_CURRENT_SLEEP_MAX_AGE_MS) {\n return false;\n }\n if (args.asleepAtMs !== null) {\n if (args.observedAtMs < args.asleepAtMs - 5 * 60_000) {\n return false;\n }\n if (args.nowMs - args.asleepAtMs > HEALTH_CURRENT_SLEEP_MAX_DURATION_MS) {\n return false;\n }\n }\n return true;\n}\n\nfunction hasActiveSignalAfter(\n signals: LifeOpsActivitySignal[],\n thresholdMs: number,\n): boolean {\n return signals.some((signal) => {\n if (signal.state !== \"active\") {\n return false;\n }\n const observedAt = Date.parse(signal.observedAt);\n return Number.isFinite(observedAt) && observedAt > thresholdMs;\n });\n}\n\nfunction parseHealthSleepEpisodes(args: {\n signals: LifeOpsActivitySignal[];\n nowMs: number;\n}): LifeOpsSleepEpisode[] {\n const deduped = new Map<string, LifeOpsSleepEpisode>();\n for (const signal of args.signals) {\n const health = resolveHealthSignal(signal);\n const sleep = health && isRecord(health.sleep) ? health.sleep : null;\n if (!sleep) {\n continue;\n }\n const asleepAt =\n typeof sleep.asleepAt === \"string\"\n ? Date.parse(sleep.asleepAt)\n : Number.NaN;\n const awakeAt =\n typeof sleep.awakeAt === \"string\"\n ? Date.parse(sleep.awakeAt)\n : Number.NaN;\n const durationMinutes =\n typeof sleep.durationMinutes === \"number\" &&\n Number.isFinite(sleep.durationMinutes)\n ? sleep.durationMinutes\n : null;\n const observedAt = Date.parse(signal.observedAt);\n\n if (\n sleep.isSleeping === true &&\n Number.isFinite(asleepAt) &&\n isFreshCurrentHealthSleep({\n asleepAtMs: asleepAt,\n observedAtMs: observedAt,\n nowMs: args.nowMs,\n }) &&\n !hasActiveSignalAfter(args.signals, observedAt + 5 * 60_000)\n ) {\n deduped.set(`health-current:${asleepAt}`, {\n startMs: asleepAt,\n endMs: null,\n current: true,\n confidence: 0.96,\n source: \"health\",\n observedMs: observedAt,\n });\n continue;\n }\n\n if (Number.isFinite(asleepAt)) {\n const normalizedEndMs = normalizeSleepEndMs({\n asleepAtMs: asleepAt,\n awakeAtMs: awakeAt,\n durationMinutes,\n });\n if (normalizedEndMs !== null) {\n deduped.set(`health:${asleepAt}:${normalizedEndMs}`, {\n startMs: asleepAt,\n endMs: normalizedEndMs,\n current: false,\n confidence: 0.93,\n source: \"health\",\n });\n continue;\n }\n }\n\n if (\n sleep.isSleeping === true &&\n isFreshCurrentHealthSleep({\n asleepAtMs: null,\n observedAtMs: observedAt,\n nowMs: args.nowMs,\n }) &&\n !hasActiveSignalAfter(args.signals, observedAt + 5 * 60_000)\n ) {\n deduped.set(`health-observed:${observedAt}`, {\n startMs: observedAt,\n endMs: null,\n current: true,\n confidence: 0.88,\n source: \"health\",\n observedMs: observedAt,\n });\n }\n }\n return [...deduped.values()].sort(\n (left, right) => left.startMs - right.startMs,\n );\n}\n\nfunction hasSignalNear(\n signals: LifeOpsActivitySignal[],\n targetMs: number,\n windowMs: number,\n predicate: (signal: LifeOpsActivitySignal) => boolean,\n): boolean {\n for (const signal of signals) {\n const observedAt = Date.parse(signal.observedAt);\n if (!Number.isFinite(observedAt)) {\n continue;\n }\n if (Math.abs(observedAt - targetMs) <= windowMs && predicate(signal)) {\n return true;\n }\n }\n return false;\n}\n\nfunction buildGapSleepEpisodes(args: {\n windows: LifeOpsActivityWindow[];\n signals: LifeOpsActivitySignal[];\n nowMs: number;\n timezone: string;\n}): LifeOpsSleepEpisode[] {\n const episodes: LifeOpsSleepEpisode[] = [];\n if (args.windows.length === 0) {\n return episodes;\n }\n\n for (let index = 0; index < args.windows.length; index += 1) {\n const current = args.windows[index];\n if (!current) {\n continue;\n }\n const next = args.windows[index + 1] ?? null;\n const gapStartMs = current.endMs;\n const gapEndMs = next ? next.startMs : args.nowMs;\n const gapMs = Math.max(0, gapEndMs - gapStartMs);\n const currentGap = next === null;\n const minDurationMs = currentGap\n ? CURRENT_SLEEP_GAP_MIN_MS\n : COMPLETED_SLEEP_GAP_MIN_MS;\n if (gapMs < minDurationMs) {\n continue;\n }\n\n const startHour = localHour(gapStartMs, args.timezone);\n const endHour = localHour(gapEndMs, args.timezone);\n const durationFactor = clamp(gapMs / (8 * 60 * 60 * 1_000), 0, 1);\n let score = 0.3 + durationFactor * 0.35;\n\n if (startHour >= 20 || startHour < 4) {\n score += 0.15;\n }\n if (endHour >= 4 && endHour < 13) {\n score += 0.15;\n }\n const hasChargingCue = hasSignalNear(\n args.signals,\n gapStartMs,\n 90 * 60 * 1_000,\n (signal) => signal.onBattery === false,\n );\n const hasRestCue = hasSignalNear(\n args.signals,\n gapStartMs,\n 45 * 60 * 1_000,\n (signal) =>\n signal.state === \"locked\" ||\n signal.state === \"background\" ||\n signal.state === \"idle\" ||\n signal.state === \"sleeping\",\n );\n\n if (currentGap) {\n const nowHour = localHour(args.nowMs, args.timezone);\n const looksLikeOvernight =\n startHour >= 20 || startHour < 5 || nowHour < 10;\n const hasStrongCue = hasChargingCue || hasRestCue;\n if (\n !looksLikeOvernight ||\n (gapMs < CURRENT_SLEEP_GAP_STRONG_MIN_MS && !hasStrongCue)\n ) {\n continue;\n }\n }\n\n if (hasChargingCue) {\n score += 0.1;\n }\n if (hasRestCue) {\n score += 0.1;\n }\n if (gapMs < 4 * 60 * 60 * 1_000) {\n score -= 0.1;\n }\n score = roundConfidence(score);\n if (score < MIN_SLEEP_CONFIDENCE) {\n continue;\n }\n episodes.push({\n startMs: gapStartMs,\n endMs: currentGap ? null : gapEndMs,\n current: currentGap,\n confidence: score,\n source: \"activity_gap\",\n });\n }\n\n return episodes;\n}\n\nfunction selectLatestCompletedSleep(\n episodes: LifeOpsSleepEpisode[],\n nowMs: number,\n timezone: string,\n): LifeOpsSleepEpisode | null {\n const completed = [...episodes].filter(\n (episode) => episode.endMs !== null && episode.endMs <= nowMs,\n );\n const dayAnchoring = completed.filter((episode) => {\n const sleepType = classifySleepType(episode, nowMs, timezone);\n if (sleepType === \"overnight\") {\n return true;\n }\n return (\n sleepType !== \"nap\" &&\n intervalDurationMs(episode.startMs, episode.endMs, nowMs) >=\n 4 * 60 * 60 * 1_000\n );\n });\n const candidates = dayAnchoring.length > 0 ? dayAnchoring : completed;\n return (\n candidates.sort((left, right) => {\n const leftEnd = left.endMs ?? 0;\n const rightEnd = right.endMs ?? 0;\n if (rightEnd !== leftEnd) {\n return rightEnd - leftEnd;\n }\n return right.confidence - left.confidence;\n })[0] ?? null\n );\n}\n\nfunction selectCurrentSleep(\n episodes: LifeOpsSleepEpisode[],\n): LifeOpsSleepEpisode | null {\n return (\n [...episodes]\n .filter((episode) => episode.current)\n .sort((left, right) => right.confidence - left.confidence)[0] ?? null\n );\n}\n\nexport function classifyLifeOpsSleepCycleType(args: {\n startMs: number;\n endMs: number | null;\n nowMs: number;\n timezone: string;\n}): LifeOpsSleepCycleType {\n const endMs = args.endMs ?? args.nowMs;\n const durationMs = intervalDurationMs(args.startMs, args.endMs, args.nowMs);\n const durationHours = durationMs / (60 * 60 * 1_000);\n const startHour = localHour(args.startMs, args.timezone);\n const endHour = localHour(endMs, args.timezone);\n if (\n durationHours >= 4 &&\n (startHour >= 18 || startHour < 6 || endHour <= 11)\n ) {\n return \"overnight\";\n }\n if (durationHours > 0 && durationHours < 4) {\n return \"nap\";\n }\n return \"unknown\";\n}\n\nfunction classifySleepType(\n episode: LifeOpsSleepEpisode,\n nowMs: number,\n timezone: string,\n): LifeOpsSleepCycleType {\n return classifyLifeOpsSleepCycleType({\n startMs: episode.startMs,\n endMs: episode.endMs,\n nowMs,\n timezone,\n });\n}\n\nexport interface LifeOpsSleepCycleResolution {\n sleepCycle: LifeOpsSleepCycle;\n sleepEpisodes: LifeOpsSleepEpisode[];\n typicalWakeHour: number | null;\n typicalSleepHour: number | null;\n}\n\nexport function resolveLifeOpsSleepCycle(args: {\n nowMs: number;\n timezone: string;\n windows: LifeOpsActivityWindow[];\n signals: LifeOpsActivitySignal[];\n}): LifeOpsSleepCycleResolution {\n const healthEpisodes = parseHealthSleepEpisodes({\n signals: args.signals,\n nowMs: args.nowMs,\n });\n const gapEpisodes = buildGapSleepEpisodes({\n windows: args.windows,\n signals: args.signals,\n nowMs: args.nowMs,\n timezone: args.timezone,\n });\n const episodes = [...healthEpisodes, ...gapEpisodes];\n const currentSleep = selectCurrentSleep(episodes);\n const lastCompletedSleep = selectLatestCompletedSleep(\n episodes,\n args.nowMs,\n args.timezone,\n );\n const candidateSleepStarts = episodes\n .filter(\n (episode) =>\n classifySleepType(episode, args.nowMs, args.timezone) !== \"nap\" &&\n intervalDurationMs(episode.startMs, episode.endMs, args.nowMs) >=\n COMPLETED_SLEEP_GAP_MIN_MS,\n )\n .map((episode) =>\n normalizeSleepHour(localHour(episode.startMs, args.timezone)),\n );\n const candidateWakeHours = episodes\n .filter(\n (episode): episode is LifeOpsSleepEpisode & { endMs: number } =>\n episode.endMs !== null &&\n classifySleepType(episode, args.nowMs, args.timezone) !== \"nap\",\n )\n .map((episode) => localHour(episode.endMs, args.timezone));\n const typicalSleepHour = median(candidateSleepStarts);\n const sleepCycle: LifeOpsSleepCycle = {\n cycleType: currentSleep\n ? classifySleepType(currentSleep, args.nowMs, args.timezone)\n : lastCompletedSleep\n ? classifySleepType(lastCompletedSleep, args.nowMs, args.timezone)\n : \"unknown\",\n sleepStatus:\n currentSleep?.confidence !== undefined && currentSleep.confidence >= 0.55\n ? \"sleeping_now\"\n : lastCompletedSleep?.endMs &&\n args.nowMs - lastCompletedSleep.endMs <= 30 * 60 * 60 * 1_000\n ? \"slept\"\n : lastCompletedSleep?.endMs &&\n args.nowMs - lastCompletedSleep.endMs >= 20 * 60 * 60 * 1_000\n ? \"likely_missed\"\n : \"unknown\",\n isProbablySleeping:\n currentSleep?.confidence !== undefined && currentSleep.confidence >= 0.55,\n sleepConfidence: roundConfidence(\n currentSleep?.confidence ?? lastCompletedSleep?.confidence ?? 0,\n ),\n currentSleepStartedAt: toIso(currentSleep?.startMs ?? null),\n lastSleepStartedAt: toIso(\n (currentSleep ?? lastCompletedSleep)?.startMs ?? null,\n ),\n lastSleepEndedAt: toIso(lastCompletedSleep?.endMs ?? null),\n lastSleepDurationMinutes: (() => {\n const target = currentSleep ?? lastCompletedSleep;\n if (!target) {\n return null;\n }\n return Math.round(\n intervalDurationMs(target.startMs, target.endMs, args.nowMs) / 60_000,\n );\n })(),\n evidence: episodes\n .map(\n (episode): LifeOpsSleepCycleEvidence => ({\n startAt: new Date(episode.startMs).toISOString(),\n endAt: toIso(episode.endMs),\n source: episode.source,\n confidence: episode.confidence,\n }),\n )\n .sort(\n (left, right) => Date.parse(left.startAt) - Date.parse(right.startAt),\n ),\n };\n\n return {\n sleepCycle,\n sleepEpisodes: episodes,\n typicalWakeHour: median(candidateWakeHours),\n typicalSleepHour,\n };\n}\n\nexport function resolveLifeOpsDayBoundary(args: {\n nowMs: number;\n timezone: string;\n sleepCycle: Pick<\n LifeOpsSleepCycle,\n | \"cycleType\"\n | \"sleepConfidence\"\n | \"currentSleepStartedAt\"\n | \"lastSleepStartedAt\"\n | \"lastSleepEndedAt\"\n >;\n}): LifeOpsDayBoundary {\n const nowDate = new Date(args.nowMs);\n const localDateParts = getZonedDateParts(nowDate, args.timezone);\n const startOfDay = buildUtcDateFromLocalParts(args.timezone, {\n year: localDateParts.year,\n month: localDateParts.month,\n day: localDateParts.day,\n hour: 0,\n minute: 0,\n second: 0,\n });\n const nextDateParts = getZonedDateParts(\n new Date(startOfDay.getTime() + 24 * 60 * 60 * 1_000),\n args.timezone,\n );\n const endOfDay = buildUtcDateFromLocalParts(args.timezone, {\n year: nextDateParts.year,\n month: nextDateParts.month,\n day: nextDateParts.day,\n hour: 0,\n minute: 0,\n second: 0,\n });\n const beforeSleepAt =\n args.sleepCycle.currentSleepStartedAt ??\n args.sleepCycle.lastSleepStartedAt ??\n null;\n const overnightAnchor =\n args.sleepCycle.cycleType === \"overnight\" && beforeSleepAt;\n const anchor: LifeOpsDayBoundary[\"anchor\"] = overnightAnchor\n ? \"before_sleep\"\n : \"start_of_day\";\n const effectiveDaySourceMs =\n args.sleepCycle.cycleType === \"overnight\" &&\n args.sleepCycle.lastSleepEndedAt\n ? Date.parse(args.sleepCycle.lastSleepEndedAt)\n : args.sleepCycle.cycleType === \"overnight\" &&\n args.sleepCycle.currentSleepStartedAt\n ? Date.parse(args.sleepCycle.currentSleepStartedAt)\n : args.nowMs;\n return {\n effectiveDayKey: localDateKey(effectiveDaySourceMs, args.timezone),\n localDate: localDateKey(args.nowMs, args.timezone),\n timezone: args.timezone,\n anchor,\n startOfDayAt: startOfDay.toISOString(),\n endOfDayAt: endOfDay.toISOString(),\n beforeSleepAt,\n confidence: roundConfidence(args.sleepCycle.sleepConfidence),\n };\n}\n"],"mappings":"AAQA,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAEhC,MAAM,6BAA6B,IAAI,KAAK,KAAK;AACjD,MAAM,2BAA2B,IAAI,KAAK,KAAK;AAC/C,MAAM,kCAAkC,IAAI,KAAK,KAAK;AACtD,MAAM,kCAAkC,IAAI,KAAK,KAAK;AACtD,MAAM,uCAAuC,KAAK,KAAK,KAAK;AAC5D,MAAM,uBAAuB;AAiB7B,SAAS,SAAS,OAAkD;AAClE,SAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC9D,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC3C;AAEA,SAAS,mBACP,SACA,OACA,OACQ;AACR,QAAM,YAAY,SAAS;AAC3B,SAAO,KAAK,IAAI,GAAG,YAAY,OAAO;AACxC;AAEA,SAAS,MAAM,IAAkC;AAC/C,MAAI,OAAO,QAAQ,CAAC,OAAO,SAAS,EAAE,GAAG;AACvC,WAAO;AAAA,EACT;AACA,SAAO,IAAI,KAAK,EAAE,EAAE,YAAY;AAClC;AAEA,SAAS,aAAa,IAAY,UAA0B;AAC1D,SAAO,gBAAgB,kBAAkB,IAAI,KAAK,EAAE,GAAG,QAAQ,CAAC;AAClE;AAEA,SAAS,UAAU,IAAY,UAA0B;AACvD,SAAO,kBAAkB,IAAI,KAAK,EAAE,GAAG,QAAQ,EAAE;AACnD;AAEA,SAAS,mBAAmB,MAAsB;AAChD,SAAO,OAAO,KAAK,OAAO,KAAK;AACjC;AAEA,SAAS,OAAO,QAAiC;AAC/C,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,EACT;AACA,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAACA,OAAMC,WAAUD,QAAOC,MAAK;AAC7D,QAAM,SAAS,KAAK,MAAM,OAAO,SAAS,CAAC;AAC3C,MAAI,OAAO,SAAS,MAAM,GAAG;AAC3B,WAAO,OAAO,MAAM,KAAK;AAAA,EAC3B;AACA,QAAM,OAAO,OAAO,SAAS,CAAC;AAC9B,QAAM,QAAQ,OAAO,MAAM;AAC3B,MAAI,SAAS,UAAa,UAAU,QAAW;AAC7C,WAAO;AAAA,EACT;AACA,SAAO,KAAK,OAAQ,OAAO,SAAS,IAAK,GAAG,IAAI;AAClD;AAEA,SAAS,oBACP,QAC4B;AAC5B,MAAI,OAAO,QAAQ;AACjB,WAAO,OAAO;AAAA,EAChB;AACA,QAAM,iBAAiB,eAAe,OAAO,SAAS,MAAM,IACxD,OAAO,SAAS,SAChB;AACJ,SAAO,kBAAkB;AAC3B;AAEA,SAAS,iBAAiB,OAAwC;AAChE,SAAO,UAAU,QAAQ,OAAO,UAAU;AAC5C;AAEA,SAAS,iBAAiB,OAAwC;AAChE,SAAO,UAAU,QAAQ,OAAO,UAAU;AAC5C;AAEA,SAAS,eAAe,OAA8C;AACpE,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,MACE,OAAO,MAAM,WAAW,YACxB,CAAE,8BAAoD,SAAS,MAAM,MAAM,GAC3E;AACA,WAAO;AAAA,EACT;AACA,QAAM,cAAc,MAAM;AAC1B,QAAM,QAAQ,MAAM;AACpB,QAAM,aAAa,MAAM;AACzB,SACE,SAAS,WAAW,KACpB,OAAO,YAAY,UAAU,aAC7B,OAAO,YAAY,eAAe,aAClC,SAAS,KAAK,KACd,OAAO,MAAM,cAAc,aAC3B,OAAO,MAAM,eAAe,aAC5B,iBAAiB,MAAM,QAAQ,KAC/B,iBAAiB,MAAM,OAAO,KAC9B,iBAAiB,MAAM,eAAe,KACtC,iBAAiB,MAAM,KAAK,KAC5B,SAAS,UAAU,KACnB,iBAAiB,WAAW,QAAQ,KACpC,iBAAiB,WAAW,YAAY,KACxC,iBAAiB,WAAW,mBAAmB,KAC/C,iBAAiB,WAAW,sBAAsB,KAClD,iBAAiB,WAAW,eAAe,KAC3C,iBAAiB,WAAW,kBAAkB,KAC9C,MAAM,QAAQ,MAAM,QAAQ,KAC5B,MAAM,SAAS,MAAM,CAAC,YAAY,OAAO,YAAY,QAAQ;AAEjE;AAEA,SAAS,oBAAoB,MAIX;AAChB,MAAI,OAAO,SAAS,KAAK,SAAS,KAAK,KAAK,YAAY,KAAK,YAAY;AACvE,WAAO,KAAK;AAAA,EACd;AACA,MACE,OAAO,KAAK,oBAAoB,YAChC,OAAO,SAAS,KAAK,eAAe,GACpC;AACA,UAAM,aAAa,KAAK,kBAAkB;AAC1C,QAAI,aAAa,GAAG;AAClB,aAAO,KAAK,aAAa;AAAA,IAC3B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,0BAA0B,MAIvB;AACV,MAAI,CAAC,OAAO,SAAS,KAAK,YAAY,GAAG;AACvC,WAAO;AAAA,EACT;AACA,MAAI,KAAK,QAAQ,KAAK,eAAe,iCAAiC;AACpE,WAAO;AAAA,EACT;AACA,MAAI,KAAK,eAAe,MAAM;AAC5B,QAAI,KAAK,eAAe,KAAK,aAAa,IAAI,KAAQ;AACpD,aAAO;AAAA,IACT;AACA,QAAI,KAAK,QAAQ,KAAK,aAAa,sCAAsC;AACvE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,qBACP,SACA,aACS;AACT,SAAO,QAAQ,KAAK,CAAC,WAAW;AAC9B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO;AAAA,IACT;AACA,UAAM,aAAa,KAAK,MAAM,OAAO,UAAU;AAC/C,WAAO,OAAO,SAAS,UAAU,KAAK,aAAa;AAAA,EACrD,CAAC;AACH;AAEA,SAAS,yBAAyB,MAGR;AACxB,QAAM,UAAU,oBAAI,IAAiC;AACrD,aAAW,UAAU,KAAK,SAAS;AACjC,UAAM,SAAS,oBAAoB,MAAM;AACzC,UAAM,QAAQ,UAAU,SAAS,OAAO,KAAK,IAAI,OAAO,QAAQ;AAChE,QAAI,CAAC,OAAO;AACV;AAAA,IACF;AACA,UAAM,WACJ,OAAO,MAAM,aAAa,WACtB,KAAK,MAAM,MAAM,QAAQ,IACzB,OAAO;AACb,UAAM,UACJ,OAAO,MAAM,YAAY,WACrB,KAAK,MAAM,MAAM,OAAO,IACxB,OAAO;AACb,UAAM,kBACJ,OAAO,MAAM,oBAAoB,YACjC,OAAO,SAAS,MAAM,eAAe,IACjC,MAAM,kBACN;AACN,UAAM,aAAa,KAAK,MAAM,OAAO,UAAU;AAE/C,QACE,MAAM,eAAe,QACrB,OAAO,SAAS,QAAQ,KACxB,0BAA0B;AAAA,MACxB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,OAAO,KAAK;AAAA,IACd,CAAC,KACD,CAAC,qBAAqB,KAAK,SAAS,aAAa,IAAI,GAAM,GAC3D;AACA,cAAQ,IAAI,kBAAkB,QAAQ,IAAI;AAAA,QACxC,SAAS;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,YAAY;AAAA,MACd,CAAC;AACD;AAAA,IACF;AAEA,QAAI,OAAO,SAAS,QAAQ,GAAG;AAC7B,YAAM,kBAAkB,oBAAoB;AAAA,QAC1C,YAAY;AAAA,QACZ,WAAW;AAAA,QACX;AAAA,MACF,CAAC;AACD,UAAI,oBAAoB,MAAM;AAC5B,gBAAQ,IAAI,UAAU,QAAQ,IAAI,eAAe,IAAI;AAAA,UACnD,SAAS;AAAA,UACT,OAAO;AAAA,UACP,SAAS;AAAA,UACT,YAAY;AAAA,UACZ,QAAQ;AAAA,QACV,CAAC;AACD;AAAA,MACF;AAAA,IACF;AAEA,QACE,MAAM,eAAe,QACrB,0BAA0B;AAAA,MACxB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,OAAO,KAAK;AAAA,IACd,CAAC,KACD,CAAC,qBAAqB,KAAK,SAAS,aAAa,IAAI,GAAM,GAC3D;AACA,cAAQ,IAAI,mBAAmB,UAAU,IAAI;AAAA,QAC3C,SAAS;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO,CAAC,GAAG,QAAQ,OAAO,CAAC,EAAE;AAAA,IAC3B,CAAC,MAAM,UAAU,KAAK,UAAU,MAAM;AAAA,EACxC;AACF;AAEA,SAAS,cACP,SACA,UACA,UACA,WACS;AACT,aAAW,UAAU,SAAS;AAC5B,UAAM,aAAa,KAAK,MAAM,OAAO,UAAU;AAC/C,QAAI,CAAC,OAAO,SAAS,UAAU,GAAG;AAChC;AAAA,IACF;AACA,QAAI,KAAK,IAAI,aAAa,QAAQ,KAAK,YAAY,UAAU,MAAM,GAAG;AACpE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,MAKL;AACxB,QAAM,WAAkC,CAAC;AACzC,MAAI,KAAK,QAAQ,WAAW,GAAG;AAC7B,WAAO;AAAA,EACT;AAEA,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,QAAQ,SAAS,GAAG;AAC3D,UAAM,UAAU,KAAK,QAAQ,KAAK;AAClC,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AACA,UAAM,OAAO,KAAK,QAAQ,QAAQ,CAAC,KAAK;AACxC,UAAM,aAAa,QAAQ;AAC3B,UAAM,WAAW,OAAO,KAAK,UAAU,KAAK;AAC5C,UAAM,QAAQ,KAAK,IAAI,GAAG,WAAW,UAAU;AAC/C,UAAM,aAAa,SAAS;AAC5B,UAAM,gBAAgB,aAClB,2BACA;AACJ,QAAI,QAAQ,eAAe;AACzB;AAAA,IACF;AAEA,UAAM,YAAY,UAAU,YAAY,KAAK,QAAQ;AACrD,UAAM,UAAU,UAAU,UAAU,KAAK,QAAQ;AACjD,UAAM,iBAAiB,MAAM,SAAS,IAAI,KAAK,KAAK,MAAQ,GAAG,CAAC;AAChE,QAAI,QAAQ,MAAM,iBAAiB;AAEnC,QAAI,aAAa,MAAM,YAAY,GAAG;AACpC,eAAS;AAAA,IACX;AACA,QAAI,WAAW,KAAK,UAAU,IAAI;AAChC,eAAS;AAAA,IACX;AACA,UAAM,iBAAiB;AAAA,MACrB,KAAK;AAAA,MACL;AAAA,MACA,KAAK,KAAK;AAAA,MACV,CAAC,WAAW,OAAO,cAAc;AAAA,IACnC;AACA,UAAM,aAAa;AAAA,MACjB,KAAK;AAAA,MACL;AAAA,MACA,KAAK,KAAK;AAAA,MACV,CAAC,WACC,OAAO,UAAU,YACjB,OAAO,UAAU,gBACjB,OAAO,UAAU,UACjB,OAAO,UAAU;AAAA,IACrB;AAEA,QAAI,YAAY;AACd,YAAM,UAAU,UAAU,KAAK,OAAO,KAAK,QAAQ;AACnD,YAAM,qBACJ,aAAa,MAAM,YAAY,KAAK,UAAU;AAChD,YAAM,eAAe,kBAAkB;AACvC,UACE,CAAC,sBACA,QAAQ,mCAAmC,CAAC,cAC7C;AACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,gBAAgB;AAClB,eAAS;AAAA,IACX;AACA,QAAI,YAAY;AACd,eAAS;AAAA,IACX;AACA,QAAI,QAAQ,IAAI,KAAK,KAAK,KAAO;AAC/B,eAAS;AAAA,IACX;AACA,YAAQ,gBAAgB,KAAK;AAC7B,QAAI,QAAQ,sBAAsB;AAChC;AAAA,IACF;AACA,aAAS,KAAK;AAAA,MACZ,SAAS;AAAA,MACT,OAAO,aAAa,OAAO;AAAA,MAC3B,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEA,SAAS,2BACP,UACA,OACA,UAC4B;AAC5B,QAAM,YAAY,CAAC,GAAG,QAAQ,EAAE;AAAA,IAC9B,CAAC,YAAY,QAAQ,UAAU,QAAQ,QAAQ,SAAS;AAAA,EAC1D;AACA,QAAM,eAAe,UAAU,OAAO,CAAC,YAAY;AACjD,UAAM,YAAY,kBAAkB,SAAS,OAAO,QAAQ;AAC5D,QAAI,cAAc,aAAa;AAC7B,aAAO;AAAA,IACT;AACA,WACE,cAAc,SACd,mBAAmB,QAAQ,SAAS,QAAQ,OAAO,KAAK,KACtD,IAAI,KAAK,KAAK;AAAA,EAEpB,CAAC;AACD,QAAM,aAAa,aAAa,SAAS,IAAI,eAAe;AAC5D,SACE,WAAW,KAAK,CAAC,MAAM,UAAU;AAC/B,UAAM,UAAU,KAAK,SAAS;AAC9B,UAAM,WAAW,MAAM,SAAS;AAChC,QAAI,aAAa,SAAS;AACxB,aAAO,WAAW;AAAA,IACpB;AACA,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,CAAC,EAAE,CAAC,KAAK;AAEb;AAEA,SAAS,mBACP,UAC4B;AAC5B,SACE,CAAC,GAAG,QAAQ,EACT,OAAO,CAAC,YAAY,QAAQ,OAAO,EACnC,KAAK,CAAC,MAAM,UAAU,MAAM,aAAa,KAAK,UAAU,EAAE,CAAC,KAAK;AAEvE;AAEO,SAAS,8BAA8B,MAKpB;AACxB,QAAM,QAAQ,KAAK,SAAS,KAAK;AACjC,QAAM,aAAa,mBAAmB,KAAK,SAAS,KAAK,OAAO,KAAK,KAAK;AAC1E,QAAM,gBAAgB,cAAc,KAAK,KAAK;AAC9C,QAAM,YAAY,UAAU,KAAK,SAAS,KAAK,QAAQ;AACvD,QAAM,UAAU,UAAU,OAAO,KAAK,QAAQ;AAC9C,MACE,iBAAiB,MAChB,aAAa,MAAM,YAAY,KAAK,WAAW,KAChD;AACA,WAAO;AAAA,EACT;AACA,MAAI,gBAAgB,KAAK,gBAAgB,GAAG;AAC1C,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,kBACP,SACA,OACA,UACuB;AACvB,SAAO,8BAA8B;AAAA,IACnC,SAAS,QAAQ;AAAA,IACjB,OAAO,QAAQ;AAAA,IACf;AAAA,IACA;AAAA,EACF,CAAC;AACH;AASO,SAAS,yBAAyB,MAKT;AAC9B,QAAM,iBAAiB,yBAAyB;AAAA,IAC9C,SAAS,KAAK;AAAA,IACd,OAAO,KAAK;AAAA,EACd,CAAC;AACD,QAAM,cAAc,sBAAsB;AAAA,IACxC,SAAS,KAAK;AAAA,IACd,SAAS,KAAK;AAAA,IACd,OAAO,KAAK;AAAA,IACZ,UAAU,KAAK;AAAA,EACjB,CAAC;AACD,QAAM,WAAW,CAAC,GAAG,gBAAgB,GAAG,WAAW;AACnD,QAAM,eAAe,mBAAmB,QAAQ;AAChD,QAAM,qBAAqB;AAAA,IACzB;AAAA,IACA,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACA,QAAM,uBAAuB,SAC1B;AAAA,IACC,CAAC,YACC,kBAAkB,SAAS,KAAK,OAAO,KAAK,QAAQ,MAAM,SAC1D,mBAAmB,QAAQ,SAAS,QAAQ,OAAO,KAAK,KAAK,KAC3D;AAAA,EACN,EACC;AAAA,IAAI,CAAC,YACJ,mBAAmB,UAAU,QAAQ,SAAS,KAAK,QAAQ,CAAC;AAAA,EAC9D;AACF,QAAM,qBAAqB,SACxB;AAAA,IACC,CAAC,YACC,QAAQ,UAAU,QAClB,kBAAkB,SAAS,KAAK,OAAO,KAAK,QAAQ,MAAM;AAAA,EAC9D,EACC,IAAI,CAAC,YAAY,UAAU,QAAQ,OAAO,KAAK,QAAQ,CAAC;AAC3D,QAAM,mBAAmB,OAAO,oBAAoB;AACpD,QAAM,aAAgC;AAAA,IACpC,WAAW,eACP,kBAAkB,cAAc,KAAK,OAAO,KAAK,QAAQ,IACzD,qBACE,kBAAkB,oBAAoB,KAAK,OAAO,KAAK,QAAQ,IAC/D;AAAA,IACN,aACE,cAAc,eAAe,UAAa,aAAa,cAAc,OACjE,iBACA,oBAAoB,SAClB,KAAK,QAAQ,mBAAmB,SAAS,KAAK,KAAK,KAAK,MACxD,UACA,oBAAoB,SAClB,KAAK,QAAQ,mBAAmB,SAAS,KAAK,KAAK,KAAK,MACxD,kBACA;AAAA,IACV,oBACE,cAAc,eAAe,UAAa,aAAa,cAAc;AAAA,IACvE,iBAAiB;AAAA,MACf,cAAc,cAAc,oBAAoB,cAAc;AAAA,IAChE;AAAA,IACA,uBAAuB,MAAM,cAAc,WAAW,IAAI;AAAA,IAC1D,oBAAoB;AAAA,OACjB,gBAAgB,qBAAqB,WAAW;AAAA,IACnD;AAAA,IACA,kBAAkB,MAAM,oBAAoB,SAAS,IAAI;AAAA,IACzD,2BAA2B,MAAM;AAC/B,YAAM,SAAS,gBAAgB;AAC/B,UAAI,CAAC,QAAQ;AACX,eAAO;AAAA,MACT;AACA,aAAO,KAAK;AAAA,QACV,mBAAmB,OAAO,SAAS,OAAO,OAAO,KAAK,KAAK,IAAI;AAAA,MACjE;AAAA,IACF,GAAG;AAAA,IACH,UAAU,SACP;AAAA,MACC,CAAC,aAAwC;AAAA,QACvC,SAAS,IAAI,KAAK,QAAQ,OAAO,EAAE,YAAY;AAAA,QAC/C,OAAO,MAAM,QAAQ,KAAK;AAAA,QAC1B,QAAQ,QAAQ;AAAA,QAChB,YAAY,QAAQ;AAAA,MACtB;AAAA,IACF,EACC;AAAA,MACC,CAAC,MAAM,UAAU,KAAK,MAAM,KAAK,OAAO,IAAI,KAAK,MAAM,MAAM,OAAO;AAAA,IACtE;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,IACA,eAAe;AAAA,IACf,iBAAiB,OAAO,kBAAkB;AAAA,IAC1C;AAAA,EACF;AACF;AAEO,SAAS,0BAA0B,MAWnB;AACrB,QAAM,UAAU,IAAI,KAAK,KAAK,KAAK;AACnC,QAAM,iBAAiB,kBAAkB,SAAS,KAAK,QAAQ;AAC/D,QAAM,aAAa,2BAA2B,KAAK,UAAU;AAAA,IAC3D,MAAM,eAAe;AAAA,IACrB,OAAO,eAAe;AAAA,IACtB,KAAK,eAAe;AAAA,IACpB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV,CAAC;AACD,QAAM,gBAAgB;AAAA,IACpB,IAAI,KAAK,WAAW,QAAQ,IAAI,KAAK,KAAK,KAAK,GAAK;AAAA,IACpD,KAAK;AAAA,EACP;AACA,QAAM,WAAW,2BAA2B,KAAK,UAAU;AAAA,IACzD,MAAM,cAAc;AAAA,IACpB,OAAO,cAAc;AAAA,IACrB,KAAK,cAAc;AAAA,IACnB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV,CAAC;AACD,QAAM,gBACJ,KAAK,WAAW,yBAChB,KAAK,WAAW,sBAChB;AACF,QAAM,kBACJ,KAAK,WAAW,cAAc,eAAe;AAC/C,QAAM,SAAuC,kBACzC,iBACA;AACJ,QAAM,uBACJ,KAAK,WAAW,cAAc,eAC9B,KAAK,WAAW,mBACZ,KAAK,MAAM,KAAK,WAAW,gBAAgB,IAC3C,KAAK,WAAW,cAAc,eAC5B,KAAK,WAAW,wBAChB,KAAK,MAAM,KAAK,WAAW,qBAAqB,IAChD,KAAK;AACb,SAAO;AAAA,IACL,iBAAiB,aAAa,sBAAsB,KAAK,QAAQ;AAAA,IACjE,WAAW,aAAa,KAAK,OAAO,KAAK,QAAQ;AAAA,IACjD,UAAU,KAAK;AAAA,IACf;AAAA,IACA,cAAc,WAAW,YAAY;AAAA,IACrC,YAAY,SAAS,YAAY;AAAA,IACjC;AAAA,IACA,YAAY,gBAAgB,KAAK,WAAW,eAAe;AAAA,EAC7D;AACF;","names":["left","right"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LifeOpsSleepCycleEvidenceSource, LifeOpsSleepCycleType } from "../contracts/health.js";
|
|
2
|
+
import { type LifeOpsSleepEpisode } from "./sleep-cycle.js";
|
|
3
|
+
import { type SleepEpisodeRepository } from "./sleep-episode-types.js";
|
|
4
|
+
export interface PersistSleepEpisodesArgs {
|
|
5
|
+
repository: SleepEpisodeRepository;
|
|
6
|
+
agentId: string;
|
|
7
|
+
episodes: readonly LifeOpsSleepEpisode[];
|
|
8
|
+
nowMs: number;
|
|
9
|
+
timezone: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HistoricalSleepEpisode {
|
|
12
|
+
startAt: string;
|
|
13
|
+
endAt: string | null;
|
|
14
|
+
source: LifeOpsSleepCycleEvidenceSource | "manual";
|
|
15
|
+
confidence: number;
|
|
16
|
+
cycleType: LifeOpsSleepCycleType;
|
|
17
|
+
}
|
|
18
|
+
export declare function persistSleepEpisodes(args: PersistSleepEpisodesArgs): Promise<void>;
|
|
19
|
+
export declare function listHistoricalSleepEpisodes(args: {
|
|
20
|
+
repository: SleepEpisodeRepository;
|
|
21
|
+
agentId: string;
|
|
22
|
+
nowMs: number;
|
|
23
|
+
windowDays?: number;
|
|
24
|
+
}): Promise<HistoricalSleepEpisode[]>;
|
|
25
|
+
//# sourceMappingURL=sleep-episode-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep-episode-store.d.ts","sourceRoot":"","sources":["../../src/sleep/sleep-episode-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,+BAA+B,EAC/B,qBAAqB,EACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,0BAA0B,CAAC;AAIlC,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,sBAAsB,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,+BAA+B,GAAG,QAAQ,CAAC;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,qBAAqB,CAAC;CAClC;AAaD,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,2BAA2B,CAAC,IAAI,EAAE;IACtD,UAAU,EAAE,sBAAsB,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAmBpC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyLifeOpsSleepCycleType
|
|
3
|
+
} from "./sleep-cycle.js";
|
|
4
|
+
import {
|
|
5
|
+
createLifeOpsSleepEpisode
|
|
6
|
+
} from "./sleep-episode-types.js";
|
|
7
|
+
const EPISODE_SEAL_DELAY_MS = 2 * 60 * 60 * 1e3;
|
|
8
|
+
function round(value) {
|
|
9
|
+
return Math.round(value * 100) / 100;
|
|
10
|
+
}
|
|
11
|
+
function toIso(value) {
|
|
12
|
+
if (value === null || !Number.isFinite(value)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return new Date(value).toISOString();
|
|
16
|
+
}
|
|
17
|
+
async function persistSleepEpisodes(args) {
|
|
18
|
+
for (const episode of args.episodes) {
|
|
19
|
+
const endAt = toIso(episode.endMs);
|
|
20
|
+
const record = createLifeOpsSleepEpisode({
|
|
21
|
+
agentId: args.agentId,
|
|
22
|
+
startAt: new Date(episode.startMs).toISOString(),
|
|
23
|
+
endAt,
|
|
24
|
+
source: episode.source,
|
|
25
|
+
confidence: round(episode.confidence),
|
|
26
|
+
cycleType: classifyLifeOpsSleepCycleType({
|
|
27
|
+
startMs: episode.startMs,
|
|
28
|
+
endMs: episode.endMs,
|
|
29
|
+
nowMs: args.nowMs,
|
|
30
|
+
timezone: args.timezone
|
|
31
|
+
}),
|
|
32
|
+
sealed: episode.endMs !== null && args.nowMs - episode.endMs >= EPISODE_SEAL_DELAY_MS,
|
|
33
|
+
evidence: [
|
|
34
|
+
{
|
|
35
|
+
startAt: new Date(episode.startMs).toISOString(),
|
|
36
|
+
endAt,
|
|
37
|
+
source: episode.source,
|
|
38
|
+
confidence: round(episode.confidence)
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
await args.repository.upsertSleepEpisode(record);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function listHistoricalSleepEpisodes(args) {
|
|
46
|
+
const windowDays = args.windowDays ?? 60;
|
|
47
|
+
const startAt = new Date(
|
|
48
|
+
args.nowMs - windowDays * 24 * 60 * 60 * 1e3
|
|
49
|
+
).toISOString();
|
|
50
|
+
const endAt = new Date(args.nowMs).toISOString();
|
|
51
|
+
const rows = await args.repository.listSleepEpisodesBetween(
|
|
52
|
+
args.agentId,
|
|
53
|
+
startAt,
|
|
54
|
+
endAt,
|
|
55
|
+
{ includeOpen: true }
|
|
56
|
+
);
|
|
57
|
+
return rows.map((row) => ({
|
|
58
|
+
startAt: row.startAt,
|
|
59
|
+
endAt: row.endAt,
|
|
60
|
+
source: row.source,
|
|
61
|
+
confidence: row.confidence,
|
|
62
|
+
cycleType: row.cycleType
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
listHistoricalSleepEpisodes,
|
|
67
|
+
persistSleepEpisodes
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=sleep-episode-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sleep/sleep-episode-store.ts"],"sourcesContent":["import type {\n LifeOpsSleepCycleEvidenceSource,\n LifeOpsSleepCycleType,\n} from \"../contracts/health.js\";\nimport {\n classifyLifeOpsSleepCycleType,\n type LifeOpsSleepEpisode,\n} from \"./sleep-cycle.js\";\nimport {\n createLifeOpsSleepEpisode,\n type LifeOpsSleepEpisodeRecord,\n type SleepEpisodeRepository,\n} from \"./sleep-episode-types.js\";\n\nconst EPISODE_SEAL_DELAY_MS = 2 * 60 * 60 * 1_000;\n\nexport interface PersistSleepEpisodesArgs {\n repository: SleepEpisodeRepository;\n agentId: string;\n episodes: readonly LifeOpsSleepEpisode[];\n nowMs: number;\n timezone: string;\n}\n\nexport interface HistoricalSleepEpisode {\n startAt: string;\n endAt: string | null;\n source: LifeOpsSleepCycleEvidenceSource | \"manual\";\n confidence: number;\n cycleType: LifeOpsSleepCycleType;\n}\n\nfunction round(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\nfunction toIso(value: number | null): string | null {\n if (value === null || !Number.isFinite(value)) {\n return null;\n }\n return new Date(value).toISOString();\n}\n\nexport async function persistSleepEpisodes(\n args: PersistSleepEpisodesArgs,\n): Promise<void> {\n for (const episode of args.episodes) {\n const endAt = toIso(episode.endMs);\n const record = createLifeOpsSleepEpisode({\n agentId: args.agentId,\n startAt: new Date(episode.startMs).toISOString(),\n endAt,\n source: episode.source,\n confidence: round(episode.confidence),\n cycleType: classifyLifeOpsSleepCycleType({\n startMs: episode.startMs,\n endMs: episode.endMs,\n nowMs: args.nowMs,\n timezone: args.timezone,\n }),\n sealed:\n episode.endMs !== null &&\n args.nowMs - episode.endMs >= EPISODE_SEAL_DELAY_MS,\n evidence: [\n {\n startAt: new Date(episode.startMs).toISOString(),\n endAt,\n source: episode.source,\n confidence: round(episode.confidence),\n },\n ],\n });\n await args.repository.upsertSleepEpisode(record);\n }\n}\n\nexport async function listHistoricalSleepEpisodes(args: {\n repository: SleepEpisodeRepository;\n agentId: string;\n nowMs: number;\n windowDays?: number;\n}): Promise<HistoricalSleepEpisode[]> {\n const windowDays = args.windowDays ?? 60;\n const startAt = new Date(\n args.nowMs - windowDays * 24 * 60 * 60 * 1_000,\n ).toISOString();\n const endAt = new Date(args.nowMs).toISOString();\n const rows = await args.repository.listSleepEpisodesBetween(\n args.agentId,\n startAt,\n endAt,\n { includeOpen: true },\n );\n return rows.map((row: LifeOpsSleepEpisodeRecord) => ({\n startAt: row.startAt,\n endAt: row.endAt,\n source: row.source,\n confidence: row.confidence,\n cycleType: row.cycleType,\n }));\n}\n"],"mappings":"AAIA;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,OAGK;AAEP,MAAM,wBAAwB,IAAI,KAAK,KAAK;AAkB5C,SAAS,MAAM,OAAuB;AACpC,SAAO,KAAK,MAAM,QAAQ,GAAG,IAAI;AACnC;AAEA,SAAS,MAAM,OAAqC;AAClD,MAAI,UAAU,QAAQ,CAAC,OAAO,SAAS,KAAK,GAAG;AAC7C,WAAO;AAAA,EACT;AACA,SAAO,IAAI,KAAK,KAAK,EAAE,YAAY;AACrC;AAEA,eAAsB,qBACpB,MACe;AACf,aAAW,WAAW,KAAK,UAAU;AACnC,UAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,UAAM,SAAS,0BAA0B;AAAA,MACvC,SAAS,KAAK;AAAA,MACd,SAAS,IAAI,KAAK,QAAQ,OAAO,EAAE,YAAY;AAAA,MAC/C;AAAA,MACA,QAAQ,QAAQ;AAAA,MAChB,YAAY,MAAM,QAAQ,UAAU;AAAA,MACpC,WAAW,8BAA8B;AAAA,QACvC,SAAS,QAAQ;AAAA,QACjB,OAAO,QAAQ;AAAA,QACf,OAAO,KAAK;AAAA,QACZ,UAAU,KAAK;AAAA,MACjB,CAAC;AAAA,MACD,QACE,QAAQ,UAAU,QAClB,KAAK,QAAQ,QAAQ,SAAS;AAAA,MAChC,UAAU;AAAA,QACR;AAAA,UACE,SAAS,IAAI,KAAK,QAAQ,OAAO,EAAE,YAAY;AAAA,UAC/C;AAAA,UACA,QAAQ,QAAQ;AAAA,UAChB,YAAY,MAAM,QAAQ,UAAU;AAAA,QACtC;AAAA,MACF;AAAA,IACF,CAAC;AACD,UAAM,KAAK,WAAW,mBAAmB,MAAM;AAAA,EACjD;AACF;AAEA,eAAsB,4BAA4B,MAKZ;AACpC,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,UAAU,IAAI;AAAA,IAClB,KAAK,QAAQ,aAAa,KAAK,KAAK,KAAK;AAAA,EAC3C,EAAE,YAAY;AACd,QAAM,QAAQ,IAAI,KAAK,KAAK,KAAK,EAAE,YAAY;AAC/C,QAAM,OAAO,MAAM,KAAK,WAAW;AAAA,IACjC,KAAK;AAAA,IACL;AAAA,IACA;AAAA,IACA,EAAE,aAAa,KAAK;AAAA,EACtB;AACA,SAAO,KAAK,IAAI,CAAC,SAAoC;AAAA,IACnD,SAAS,IAAI;AAAA,IACb,OAAO,IAAI;AAAA,IACX,QAAQ,IAAI;AAAA,IACZ,YAAY,IAAI;AAAA,IAChB,WAAW,IAAI;AAAA,EACjB,EAAE;AACJ;","names":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep-episode persistence types and helpers used by `sleep-episode-store.ts`.
|
|
3
|
+
*
|
|
4
|
+
* `LifeOpsSleepEpisodeRecord` and `createLifeOpsSleepEpisode` originally lived
|
|
5
|
+
* in `app-lifeops/src/lifeops/repository.ts`. plugin-health needs the type
|
|
6
|
+
* (for typed access) and the factory (for record construction) without
|
|
7
|
+
* pulling in the entire LifeOps repository / SQL layer. The record shape is
|
|
8
|
+
* unchanged; the factory is reproduced byte-identically.
|
|
9
|
+
*
|
|
10
|
+
* The `SleepEpisodeRepository` interface narrows `LifeOpsRepository` to the
|
|
11
|
+
* two methods `sleep-episode-store.ts` calls. app-lifeops' `LifeOpsRepository`
|
|
12
|
+
* is structurally compatible with this interface, so no adapter is needed at
|
|
13
|
+
* the call site.
|
|
14
|
+
*/
|
|
15
|
+
import type { LifeOpsSleepCycleEvidence, LifeOpsSleepCycleType } from "../contracts/health.js";
|
|
16
|
+
export type LifeOpsPersistedSleepEpisodeSource = LifeOpsSleepCycleEvidence["source"] | "manual";
|
|
17
|
+
export interface LifeOpsSleepEpisodeRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
startAt: string;
|
|
21
|
+
endAt: string | null;
|
|
22
|
+
source: LifeOpsPersistedSleepEpisodeSource;
|
|
23
|
+
confidence: number;
|
|
24
|
+
cycleType: LifeOpsSleepCycleType;
|
|
25
|
+
sealed: boolean;
|
|
26
|
+
evidence: LifeOpsSleepCycleEvidence[];
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SleepEpisodeRepository {
|
|
31
|
+
upsertSleepEpisode(episode: LifeOpsSleepEpisodeRecord): Promise<void>;
|
|
32
|
+
listSleepEpisodesBetween(agentId: string, startAt: string, endAt: string, opts?: {
|
|
33
|
+
includeOpen?: boolean;
|
|
34
|
+
limit?: number;
|
|
35
|
+
}): Promise<LifeOpsSleepEpisodeRecord[]>;
|
|
36
|
+
}
|
|
37
|
+
export declare function createLifeOpsSleepEpisode(params: Omit<LifeOpsSleepEpisodeRecord, "id" | "createdAt" | "updatedAt">): LifeOpsSleepEpisodeRecord;
|
|
38
|
+
//# sourceMappingURL=sleep-episode-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep-episode-types.d.ts","sourceRoot":"","sources":["../../src/sleep/sleep-episode-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EACV,yBAAyB,EACzB,qBAAqB,EACtB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,kCAAkC,GAC1C,yBAAyB,CAAC,QAAQ,CAAC,GACnC,QAAQ,CAAC;AAEb,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,kCAAkC,CAAC;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,qBAAqB,CAAC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,yBAAyB,EAAE,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,wBAAwB,CACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC/C,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAAC;CACzC;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,IAAI,CAAC,yBAAyB,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GACxE,yBAAyB,CAQ3B"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
function createLifeOpsSleepEpisode(params) {
|
|
3
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4
|
+
return {
|
|
5
|
+
...params,
|
|
6
|
+
id: crypto.randomUUID(),
|
|
7
|
+
createdAt: timestamp,
|
|
8
|
+
updatedAt: timestamp
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
createLifeOpsSleepEpisode
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=sleep-episode-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sleep/sleep-episode-types.ts"],"sourcesContent":["/**\n * Sleep-episode persistence types and helpers used by `sleep-episode-store.ts`.\n *\n * `LifeOpsSleepEpisodeRecord` and `createLifeOpsSleepEpisode` originally lived\n * in `app-lifeops/src/lifeops/repository.ts`. plugin-health needs the type\n * (for typed access) and the factory (for record construction) without\n * pulling in the entire LifeOps repository / SQL layer. The record shape is\n * unchanged; the factory is reproduced byte-identically.\n *\n * The `SleepEpisodeRepository` interface narrows `LifeOpsRepository` to the\n * two methods `sleep-episode-store.ts` calls. app-lifeops' `LifeOpsRepository`\n * is structurally compatible with this interface, so no adapter is needed at\n * the call site.\n */\n\nimport crypto from \"node:crypto\";\nimport type {\n LifeOpsSleepCycleEvidence,\n LifeOpsSleepCycleType,\n} from \"../contracts/health.js\";\n\nexport type LifeOpsPersistedSleepEpisodeSource =\n | LifeOpsSleepCycleEvidence[\"source\"]\n | \"manual\";\n\nexport interface LifeOpsSleepEpisodeRecord {\n id: string;\n agentId: string;\n startAt: string;\n endAt: string | null;\n source: LifeOpsPersistedSleepEpisodeSource;\n confidence: number;\n cycleType: LifeOpsSleepCycleType;\n sealed: boolean;\n evidence: LifeOpsSleepCycleEvidence[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface SleepEpisodeRepository {\n upsertSleepEpisode(episode: LifeOpsSleepEpisodeRecord): Promise<void>;\n listSleepEpisodesBetween(\n agentId: string,\n startAt: string,\n endAt: string,\n opts?: { includeOpen?: boolean; limit?: number },\n ): Promise<LifeOpsSleepEpisodeRecord[]>;\n}\n\nexport function createLifeOpsSleepEpisode(\n params: Omit<LifeOpsSleepEpisodeRecord, \"id\" | \"createdAt\" | \"updatedAt\">,\n): LifeOpsSleepEpisodeRecord {\n const timestamp = new Date().toISOString();\n return {\n ...params,\n id: crypto.randomUUID(),\n createdAt: timestamp,\n updatedAt: timestamp,\n };\n}\n"],"mappings":"AAeA,OAAO,YAAY;AAkCZ,SAAS,0BACd,QAC2B;AAC3B,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,OAAO,WAAW;AAAA,IACtB,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;","names":[]}
|