@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { logger } from "@elizaos/core";
|
|
2
|
+
import {
|
|
3
|
+
HEALTH_ANCHORS,
|
|
4
|
+
HEALTH_BUS_FAMILIES,
|
|
5
|
+
HEALTH_CONNECTOR_KINDS,
|
|
6
|
+
registerHealthAnchors,
|
|
7
|
+
registerHealthBusFamilies,
|
|
8
|
+
registerHealthConnectors
|
|
9
|
+
} from "./connectors/index.js";
|
|
10
|
+
import { registerCircadianInsightContract } from "./contracts/circadian.js";
|
|
11
|
+
import { createDefaultCircadianInsightContract } from "./contracts/circadian-default.js";
|
|
12
|
+
import {
|
|
13
|
+
HEALTH_DEFAULT_PACKS,
|
|
14
|
+
registerHealthDefaultPacks
|
|
15
|
+
} from "./default-packs/index.js";
|
|
16
|
+
export * from "./actions/index.js";
|
|
17
|
+
export * from "./anchors/index.js";
|
|
18
|
+
export * from "./connectors/index.js";
|
|
19
|
+
export * from "./contracts/circadian.js";
|
|
20
|
+
export * from "./contracts/circadian-default.js";
|
|
21
|
+
export * from "./contracts/health.js";
|
|
22
|
+
export * from "./default-packs/index.js";
|
|
23
|
+
export * from "./health-bridge/index.js";
|
|
24
|
+
export * from "./screen-time/index.js";
|
|
25
|
+
export * from "./sleep/index.js";
|
|
26
|
+
export * from "./util/index.js";
|
|
27
|
+
const HEALTH_PLUGIN_NAME = "plugin-health";
|
|
28
|
+
const healthPlugin = {
|
|
29
|
+
name: HEALTH_PLUGIN_NAME,
|
|
30
|
+
description: "Health, sleep, circadian and screen-time domain plugin \u2014 extracted from app-lifeops in Wave-1 (W1-B).",
|
|
31
|
+
services: [],
|
|
32
|
+
actions: [],
|
|
33
|
+
providers: [],
|
|
34
|
+
tests: [],
|
|
35
|
+
init: async (_config, runtime) => {
|
|
36
|
+
logger.info(
|
|
37
|
+
{
|
|
38
|
+
src: "plugin:health",
|
|
39
|
+
connectors: HEALTH_CONNECTOR_KINDS,
|
|
40
|
+
anchors: HEALTH_ANCHORS,
|
|
41
|
+
busFamilies: HEALTH_BUS_FAMILIES,
|
|
42
|
+
defaultPacks: HEALTH_DEFAULT_PACKS.map((p) => p.key)
|
|
43
|
+
},
|
|
44
|
+
"Initializing plugin-health (Wave-1 W1-B)"
|
|
45
|
+
);
|
|
46
|
+
registerHealthConnectors(runtime);
|
|
47
|
+
registerHealthAnchors(runtime);
|
|
48
|
+
registerHealthBusFamilies(runtime);
|
|
49
|
+
registerHealthDefaultPacks(runtime);
|
|
50
|
+
registerCircadianInsightContract(
|
|
51
|
+
runtime,
|
|
52
|
+
createDefaultCircadianInsightContract()
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var index_default = healthPlugin;
|
|
57
|
+
export {
|
|
58
|
+
HEALTH_PLUGIN_NAME,
|
|
59
|
+
index_default as default,
|
|
60
|
+
healthPlugin
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @elizaos/plugin-health — Wave-1 (W1-B) extraction.\n *\n * Owns the sleep / circadian / health-metric / screen-time domain previously\n * intermingled with `app-lifeops`. LifeOps consumes plugin-health through:\n *\n * - `ConnectorRegistry` contributions (apple_health, google_fit, strava,\n * fitbit, withings, oura)\n * - `ActivitySignalBus` publications (`health.sleep.detected`,\n * `health.wake.observed`, `health.wake.confirmed`,\n * `health.bedtime.imminent`, `health.regularity.changed`,\n * `health.workout.completed`, …)\n * - `AnchorRegistry` contributions (`wake.observed`, `wake.confirmed`,\n * `bedtime.target`, `nap.start`)\n * - Default-pack `ScheduledTask` records (bedtime / wake-up / sleep-recap)\n *\n * See `eliza/plugins/app-lifeops/docs/audit/IMPLEMENTATION_PLAN.md` §3.2 and\n * `wave1-interfaces.md` §5 for the canonical scope.\n */\n\nimport type { IAgentRuntime, Plugin } from \"@elizaos/core\";\nimport { logger } from \"@elizaos/core\";\nimport {\n HEALTH_ANCHORS,\n HEALTH_BUS_FAMILIES,\n HEALTH_CONNECTOR_KINDS,\n registerHealthAnchors,\n registerHealthBusFamilies,\n registerHealthConnectors,\n} from \"./connectors/index.js\";\nimport { registerCircadianInsightContract } from \"./contracts/circadian.js\";\nimport { createDefaultCircadianInsightContract } from \"./contracts/circadian-default.js\";\nimport {\n HEALTH_DEFAULT_PACKS,\n registerHealthDefaultPacks,\n} from \"./default-packs/index.js\";\n\n// Public surface — consumers (app-lifeops + future plugins) import the\n// helpers they need by name from `@elizaos/plugin-health`.\n\nexport * from \"./actions/index.js\";\nexport * from \"./anchors/index.js\";\nexport * from \"./connectors/index.js\";\nexport * from \"./contracts/circadian.js\";\nexport * from \"./contracts/circadian-default.js\";\nexport * from \"./contracts/health.js\";\nexport * from \"./default-packs/index.js\";\nexport * from \"./health-bridge/index.js\";\nexport * from \"./screen-time/index.js\";\nexport * from \"./sleep/index.js\";\nexport * from \"./util/index.js\";\n\nexport const HEALTH_PLUGIN_NAME = \"plugin-health\";\n\n/**\n * elizaOS plugin entry. Registers connector / anchor / bus-family / default-pack\n * contributions when the W1-A and W1-F runtime registries are available; logs\n * a one-line skip reason when they are not (Wave-1 soft dependency posture\n * per `IMPLEMENTATION_PLAN.md` §3.2).\n */\nexport const healthPlugin: Plugin = {\n name: HEALTH_PLUGIN_NAME,\n description:\n \"Health, sleep, circadian and screen-time domain plugin — extracted from app-lifeops in Wave-1 (W1-B).\",\n services: [],\n actions: [],\n providers: [],\n tests: [],\n init: async (\n _config: Record<string, string>,\n runtime: IAgentRuntime,\n ): Promise<void> => {\n logger.info(\n {\n src: \"plugin:health\",\n connectors: HEALTH_CONNECTOR_KINDS,\n anchors: HEALTH_ANCHORS,\n busFamilies: HEALTH_BUS_FAMILIES,\n defaultPacks: HEALTH_DEFAULT_PACKS.map((p) => p.key),\n },\n \"Initializing plugin-health (Wave-1 W1-B)\",\n );\n registerHealthConnectors(runtime);\n registerHealthAnchors(runtime);\n registerHealthBusFamilies(runtime);\n registerHealthDefaultPacks(runtime);\n // W3-C drift D-4: register the CircadianInsightContract so consumers\n // (SCHEDULE, SCHEDULED_TASK, future planner reads) read through a\n // typed runtime seam instead of reaching into plugin-health internals.\n registerCircadianInsightContract(\n runtime,\n createDefaultCircadianInsightContract(),\n );\n },\n};\n\nexport default healthPlugin;\n// `./<name>.js` (without /index) is a TypeScript-only directory-shorthand\n// that Bun's runtime ESM resolver does not honor. The `./sleep`,\n// `./health-bridge`, `./screen-time`, and `./actions` are all directories\n// that already have proper `./<name>/index.js` re-exports above (lines\n// 39, 44-46) so these duplicate flat-file forms only break runtime\n// imports without adding anything.\n"],"mappings":"AAqBA,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wCAAwC;AACjD,SAAS,6CAA6C;AACtD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAKP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEP,MAAM,qBAAqB;AAQ3B,MAAM,eAAuB;AAAA,EAClC,MAAM;AAAA,EACN,aACE;AAAA,EACF,UAAU,CAAC;AAAA,EACX,SAAS,CAAC;AAAA,EACV,WAAW,CAAC;AAAA,EACZ,OAAO,CAAC;AAAA,EACR,MAAM,OACJ,SACA,YACkB;AAClB,WAAO;AAAA,MACL;AAAA,QACE,KAAK;AAAA,QACL,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,aAAa;AAAA,QACb,cAAc,qBAAqB,IAAI,CAAC,MAAM,EAAE,GAAG;AAAA,MACrD;AAAA,MACA;AAAA,IACF;AACA,6BAAyB,OAAO;AAChC,0BAAsB,OAAO;AAC7B,8BAA0B,OAAO;AACjC,+BAA2B,OAAO;AAIlC;AAAA,MACE;AAAA,MACA,sCAAsC;AAAA,IACxC;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen-time domain entry point.
|
|
3
|
+
*
|
|
4
|
+
* Wave-1 (W1-B) note: the bulk of the screen-time aggregation lives in
|
|
5
|
+
* `app-lifeops/src/lifeops/service-mixin-screentime.ts`. That mixin
|
|
6
|
+
* pre-dates the plugin-health extraction and is deeply coupled to the
|
|
7
|
+
* `LifeOpsServiceBase` repository / activity-profile reporting layer
|
|
8
|
+
* (`getActivityReportBetween`, `isSystemInactivityApp`, etc.). Moving it
|
|
9
|
+
* physically to plugin-health would require either dragging the entire
|
|
10
|
+
* activity-profile module or introducing a circular package dependency
|
|
11
|
+
* (plugin-health → app-lifeops). Wave-2 (W2-D: Signal-bus + anchors +
|
|
12
|
+
* identity-observation cleanup) is the right home for that decoupling.
|
|
13
|
+
*
|
|
14
|
+
* For now, plugin-health publishes the `LifeOpsScreenTimeSummaryPayload`
|
|
15
|
+
* contract (re-exported from `../contracts/health.ts`); app-lifeops
|
|
16
|
+
* continues to host the aggregator and emits payloads on the W2-D bus once
|
|
17
|
+
* it lands.
|
|
18
|
+
*
|
|
19
|
+
* No runtime exports here — the type re-export carries the canonical
|
|
20
|
+
* contract surface.
|
|
21
|
+
*/
|
|
22
|
+
export type { LifeOpsScreenTimePerAppUsage, LifeOpsScreenTimeSummaryPayload, } from "../contracts/health.js";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/screen-time/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,YAAY,EACV,4BAA4B,EAC5B,+BAA+B,GAChC,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LifeOpsActivitySignal, LifeOpsAwakeProbability, LifeOpsScheduleRegularity, LifeOpsSleepCycle } from "../contracts/health.js";
|
|
2
|
+
import type { LifeOpsActivityWindow } from "./sleep-cycle.js";
|
|
3
|
+
export declare function computeAwakeProbability(args: {
|
|
4
|
+
nowMs: number;
|
|
5
|
+
timezone: string;
|
|
6
|
+
signals: readonly LifeOpsActivitySignal[];
|
|
7
|
+
windows: readonly LifeOpsActivityWindow[];
|
|
8
|
+
sleepCycle: Pick<LifeOpsSleepCycle, "isProbablySleeping" | "sleepConfidence" | "currentSleepStartedAt" | "lastSleepEndedAt" | "sleepStatus" | "evidence">;
|
|
9
|
+
regularity: LifeOpsScheduleRegularity;
|
|
10
|
+
}): LifeOpsAwakeProbability;
|
|
11
|
+
//# sourceMappingURL=awake-probability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"awake-probability.d.ts","sourceRoot":"","sources":["../../src/sleep/awake-probability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,uBAAuB,EACvB,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAoB9D,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1C,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1C,UAAU,EAAE,IAAI,CACd,iBAAiB,EACf,oBAAoB,GACpB,iBAAiB,GACjB,uBAAuB,GACvB,kBAAkB,GAClB,aAAa,GACb,UAAU,CACb,CAAC;IACF,UAAU,EAAE,yBAAyB,CAAC;CACvC,GAAG,uBAAuB,CA6L1B"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { getZonedDateParts } from "../util/time.js";
|
|
2
|
+
import { parseIsoMs } from "../util/time-util.js";
|
|
3
|
+
import { resolveActivitySignalReliability } from "./source-reliability.js";
|
|
4
|
+
function clamp(value, min, max) {
|
|
5
|
+
return Math.min(max, Math.max(min, value));
|
|
6
|
+
}
|
|
7
|
+
function round(value) {
|
|
8
|
+
return Math.round(clamp(value, 0, 1) * 100) / 100;
|
|
9
|
+
}
|
|
10
|
+
function logistic(value) {
|
|
11
|
+
return 1 / (1 + Math.exp(-value));
|
|
12
|
+
}
|
|
13
|
+
function localHour(nowMs, timezone) {
|
|
14
|
+
const parts = getZonedDateParts(new Date(nowMs), timezone);
|
|
15
|
+
return parts.hour + parts.minute / 60;
|
|
16
|
+
}
|
|
17
|
+
function computeAwakeProbability(args) {
|
|
18
|
+
const contributors = [];
|
|
19
|
+
let llr = 0;
|
|
20
|
+
const latestSignal = [...args.signals].map((signal) => ({
|
|
21
|
+
signal,
|
|
22
|
+
observedAtMs: parseIsoMs(signal.observedAt)
|
|
23
|
+
})).filter(
|
|
24
|
+
(candidate) => candidate.observedAtMs !== null
|
|
25
|
+
).sort((left, right) => right.observedAtMs - left.observedAtMs)[0];
|
|
26
|
+
const hasConcurrentOwnerInteraction = args.signals.some((signal) => {
|
|
27
|
+
const observedAt = parseIsoMs(signal.observedAt);
|
|
28
|
+
if (observedAt === null) return false;
|
|
29
|
+
if (args.nowMs - observedAt > 5 * 6e4) return false;
|
|
30
|
+
if (signal.source === "desktop_interaction") return true;
|
|
31
|
+
if (signal.source === "mobile_device" && signal.state === "active") {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (signal.source === "app_lifecycle" && signal.platform === "manual_override") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (typeof signal.idleTimeSeconds === "number" && signal.idleTimeSeconds <= 60) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
if (latestSignal) {
|
|
43
|
+
const ageMs = args.nowMs - latestSignal.observedAtMs;
|
|
44
|
+
const state = latestSignal.signal.state;
|
|
45
|
+
const reliability = resolveActivitySignalReliability(
|
|
46
|
+
latestSignal.signal.source,
|
|
47
|
+
latestSignal.signal.platform
|
|
48
|
+
);
|
|
49
|
+
let scale = clamp(reliability, 0, 1);
|
|
50
|
+
const isSharedDeviceRisk = latestSignal.signal.source === "app_lifecycle" && latestSignal.signal.platform !== "manual_override" && state === "active";
|
|
51
|
+
if (isSharedDeviceRisk && !hasConcurrentOwnerInteraction) {
|
|
52
|
+
scale *= 0.25;
|
|
53
|
+
}
|
|
54
|
+
let baseWeight = 0;
|
|
55
|
+
if (state === "active" && ageMs <= 5 * 6e4) {
|
|
56
|
+
baseWeight = 2.4;
|
|
57
|
+
} else if (state === "active" && ageMs <= 15 * 6e4) {
|
|
58
|
+
baseWeight = 1.4;
|
|
59
|
+
} else if ((state === "idle" || state === "locked" || state === "sleeping") && ageMs <= 90 * 6e4) {
|
|
60
|
+
baseWeight = state === "sleeping" ? -2.2 : state === "locked" ? -1.2 : -0.8;
|
|
61
|
+
}
|
|
62
|
+
if (baseWeight !== 0) {
|
|
63
|
+
const scaledWeight = baseWeight * scale;
|
|
64
|
+
contributors.push({
|
|
65
|
+
source: latestSignal.signal.source,
|
|
66
|
+
logLikelihoodRatio: scaledWeight
|
|
67
|
+
});
|
|
68
|
+
llr += scaledWeight;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const currentSleepStartMs = parseIsoMs(args.sleepCycle.currentSleepStartedAt);
|
|
72
|
+
if (args.sleepCycle.sleepStatus === "sleeping_now" && currentSleepStartMs !== null && args.nowMs >= currentSleepStartMs) {
|
|
73
|
+
const strongestEvidence = [...args.sleepCycle.evidence].sort(
|
|
74
|
+
(left, right) => right.confidence - left.confidence
|
|
75
|
+
)[0]?.source ?? "activity_gap";
|
|
76
|
+
const sleepWeight = -2.8 * clamp(args.sleepCycle.sleepConfidence, 0.3, 1);
|
|
77
|
+
contributors.push({
|
|
78
|
+
source: strongestEvidence,
|
|
79
|
+
logLikelihoodRatio: sleepWeight
|
|
80
|
+
});
|
|
81
|
+
llr += sleepWeight;
|
|
82
|
+
}
|
|
83
|
+
const wakeAtMs = parseIsoMs(args.sleepCycle.lastSleepEndedAt);
|
|
84
|
+
if (wakeAtMs !== null) {
|
|
85
|
+
const minutesSinceWake = (args.nowMs - wakeAtMs) / 6e4;
|
|
86
|
+
if (minutesSinceWake >= 0 && minutesSinceWake <= 120) {
|
|
87
|
+
contributors.push({
|
|
88
|
+
source: "health",
|
|
89
|
+
logLikelihoodRatio: 1.6
|
|
90
|
+
});
|
|
91
|
+
llr += 1.6;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const latestWindowEndMs = args.windows.length > 0 ? args.windows[args.windows.length - 1]?.endMs ?? null : null;
|
|
95
|
+
if (latestWindowEndMs !== null) {
|
|
96
|
+
const gapMinutes = Math.max(
|
|
97
|
+
0,
|
|
98
|
+
Math.round((args.nowMs - latestWindowEndMs) / 6e4)
|
|
99
|
+
);
|
|
100
|
+
if (gapMinutes >= 180) {
|
|
101
|
+
const sleepGapWeight = -clamp(gapMinutes / 240, 0.8, 1.8);
|
|
102
|
+
contributors.push({
|
|
103
|
+
source: "activity_gap",
|
|
104
|
+
logLikelihoodRatio: sleepGapWeight
|
|
105
|
+
});
|
|
106
|
+
llr += sleepGapWeight;
|
|
107
|
+
} else if (gapMinutes <= 15) {
|
|
108
|
+
contributors.push({
|
|
109
|
+
source: "activity_gap",
|
|
110
|
+
logLikelihoodRatio: 0.8
|
|
111
|
+
});
|
|
112
|
+
llr += 0.8;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (args.regularity.regularityClass === "regular" || args.regularity.regularityClass === "very_regular") {
|
|
116
|
+
const hour = localHour(args.nowMs, args.timezone);
|
|
117
|
+
const sleepWindowWeight = hour >= 22 || hour < 6 ? -0.9 : hour >= 6 && hour < 10 ? 0.5 : 0.1;
|
|
118
|
+
const scaledWeight = sleepWindowWeight * clamp(args.regularity.sri / 100, 0.4, 1);
|
|
119
|
+
contributors.push({
|
|
120
|
+
source: "prior",
|
|
121
|
+
logLikelihoodRatio: scaledWeight
|
|
122
|
+
});
|
|
123
|
+
llr += scaledWeight;
|
|
124
|
+
}
|
|
125
|
+
const signalCoverage = clamp(args.signals.length / 12, 0, 1);
|
|
126
|
+
const windowCoverage = args.windows.length > 0 ? 1 : 0;
|
|
127
|
+
let evidenceCoverage = clamp(
|
|
128
|
+
signalCoverage * 0.7 + windowCoverage * 0.2 + Math.min(contributors.length, 4) * 0.1,
|
|
129
|
+
0.15,
|
|
130
|
+
1
|
|
131
|
+
);
|
|
132
|
+
if (args.sleepCycle.sleepStatus === "sleeping_now") {
|
|
133
|
+
evidenceCoverage = Math.max(evidenceCoverage, 0.9);
|
|
134
|
+
} else if (wakeAtMs !== null && args.nowMs - wakeAtMs <= 2 * 60 * 60 * 1e3) {
|
|
135
|
+
evidenceCoverage = Math.max(evidenceCoverage, 0.75);
|
|
136
|
+
}
|
|
137
|
+
const pKnown = round(evidenceCoverage);
|
|
138
|
+
const awakeKnown = logistic(llr);
|
|
139
|
+
const pAwake = round(awakeKnown * pKnown);
|
|
140
|
+
const pAsleep = round((1 - awakeKnown) * pKnown);
|
|
141
|
+
const pUnknown = round(clamp(1 - pKnown, 0, 1));
|
|
142
|
+
const total = pAwake + pAsleep + pUnknown;
|
|
143
|
+
if (total <= 0) {
|
|
144
|
+
return {
|
|
145
|
+
pAwake: 0,
|
|
146
|
+
pAsleep: 0,
|
|
147
|
+
pUnknown: 1,
|
|
148
|
+
contributingSources: [],
|
|
149
|
+
computedAt: new Date(args.nowMs).toISOString()
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
pAwake: round(pAwake / total),
|
|
154
|
+
pAsleep: round(pAsleep / total),
|
|
155
|
+
pUnknown: round(pUnknown / total),
|
|
156
|
+
contributingSources: contributors,
|
|
157
|
+
computedAt: new Date(args.nowMs).toISOString()
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
computeAwakeProbability
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=awake-probability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sleep/awake-probability.ts"],"sourcesContent":["import type {\n LifeOpsActivitySignal,\n LifeOpsAwakeProbability,\n LifeOpsScheduleRegularity,\n LifeOpsSleepCycle,\n} from \"../contracts/health.js\";\nimport { getZonedDateParts } from \"../util/time.js\";\nimport { parseIsoMs } from \"../util/time-util.js\";\nimport type { LifeOpsActivityWindow } from \"./sleep-cycle.js\";\nimport { resolveActivitySignalReliability } from \"./source-reliability.js\";\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction round(value: number): number {\n return Math.round(clamp(value, 0, 1) * 100) / 100;\n}\n\nfunction logistic(value: number): number {\n return 1 / (1 + Math.exp(-value));\n}\n\nfunction localHour(nowMs: number, timezone: string): number {\n const parts = getZonedDateParts(new Date(nowMs), timezone);\n return parts.hour + parts.minute / 60;\n}\n\nexport function computeAwakeProbability(args: {\n nowMs: number;\n timezone: string;\n signals: readonly LifeOpsActivitySignal[];\n windows: readonly LifeOpsActivityWindow[];\n sleepCycle: Pick<\n LifeOpsSleepCycle,\n | \"isProbablySleeping\"\n | \"sleepConfidence\"\n | \"currentSleepStartedAt\"\n | \"lastSleepEndedAt\"\n | \"sleepStatus\"\n | \"evidence\"\n >;\n regularity: LifeOpsScheduleRegularity;\n}): LifeOpsAwakeProbability {\n const contributors: LifeOpsAwakeProbability[\"contributingSources\"] = [];\n let llr = 0;\n\n const latestSignal = [...args.signals]\n .map((signal) => ({\n signal,\n observedAtMs: parseIsoMs(signal.observedAt),\n }))\n .filter(\n (\n candidate,\n ): candidate is { signal: LifeOpsActivitySignal; observedAtMs: number } =>\n candidate.observedAtMs !== null,\n )\n .sort((left, right) => right.observedAtMs - left.observedAtMs)[0];\n\n const hasConcurrentOwnerInteraction = args.signals.some((signal) => {\n const observedAt = parseIsoMs(signal.observedAt);\n if (observedAt === null) return false;\n if (args.nowMs - observedAt > 5 * 60_000) return false;\n if (signal.source === \"desktop_interaction\") return true;\n if (signal.source === \"mobile_device\" && signal.state === \"active\") {\n return true;\n }\n if (\n signal.source === \"app_lifecycle\" &&\n signal.platform === \"manual_override\"\n ) {\n return true;\n }\n if (\n typeof signal.idleTimeSeconds === \"number\" &&\n signal.idleTimeSeconds <= 60\n ) {\n return true;\n }\n return false;\n });\n\n if (latestSignal) {\n const ageMs = args.nowMs - latestSignal.observedAtMs;\n const state = latestSignal.signal.state;\n const reliability = resolveActivitySignalReliability(\n latestSignal.signal.source,\n latestSignal.signal.platform,\n );\n let scale = clamp(reliability, 0, 1);\n const isSharedDeviceRisk =\n latestSignal.signal.source === \"app_lifecycle\" &&\n latestSignal.signal.platform !== \"manual_override\" &&\n state === \"active\";\n if (isSharedDeviceRisk && !hasConcurrentOwnerInteraction) {\n scale *= 0.25;\n }\n let baseWeight = 0;\n if (state === \"active\" && ageMs <= 5 * 60_000) {\n baseWeight = 2.4;\n } else if (state === \"active\" && ageMs <= 15 * 60_000) {\n baseWeight = 1.4;\n } else if (\n (state === \"idle\" || state === \"locked\" || state === \"sleeping\") &&\n ageMs <= 90 * 60_000\n ) {\n baseWeight =\n state === \"sleeping\" ? -2.2 : state === \"locked\" ? -1.2 : -0.8;\n }\n if (baseWeight !== 0) {\n const scaledWeight = baseWeight * scale;\n contributors.push({\n source: latestSignal.signal.source,\n logLikelihoodRatio: scaledWeight,\n });\n llr += scaledWeight;\n }\n }\n\n const currentSleepStartMs = parseIsoMs(args.sleepCycle.currentSleepStartedAt);\n if (\n args.sleepCycle.sleepStatus === \"sleeping_now\" &&\n currentSleepStartMs !== null &&\n args.nowMs >= currentSleepStartMs\n ) {\n const strongestEvidence =\n [...args.sleepCycle.evidence].sort(\n (left, right) => right.confidence - left.confidence,\n )[0]?.source ?? \"activity_gap\";\n const sleepWeight = -2.8 * clamp(args.sleepCycle.sleepConfidence, 0.3, 1);\n contributors.push({\n source: strongestEvidence,\n logLikelihoodRatio: sleepWeight,\n });\n llr += sleepWeight;\n }\n\n const wakeAtMs = parseIsoMs(args.sleepCycle.lastSleepEndedAt);\n if (wakeAtMs !== null) {\n const minutesSinceWake = (args.nowMs - wakeAtMs) / 60_000;\n if (minutesSinceWake >= 0 && minutesSinceWake <= 120) {\n contributors.push({\n source: \"health\",\n logLikelihoodRatio: 1.6,\n });\n llr += 1.6;\n }\n }\n\n const latestWindowEndMs =\n args.windows.length > 0\n ? (args.windows[args.windows.length - 1]?.endMs ?? null)\n : null;\n if (latestWindowEndMs !== null) {\n const gapMinutes = Math.max(\n 0,\n Math.round((args.nowMs - latestWindowEndMs) / 60_000),\n );\n if (gapMinutes >= 180) {\n const sleepGapWeight = -clamp(gapMinutes / 240, 0.8, 1.8);\n contributors.push({\n source: \"activity_gap\",\n logLikelihoodRatio: sleepGapWeight,\n });\n llr += sleepGapWeight;\n } else if (gapMinutes <= 15) {\n contributors.push({\n source: \"activity_gap\",\n logLikelihoodRatio: 0.8,\n });\n llr += 0.8;\n }\n }\n\n if (\n args.regularity.regularityClass === \"regular\" ||\n args.regularity.regularityClass === \"very_regular\"\n ) {\n const hour = localHour(args.nowMs, args.timezone);\n const sleepWindowWeight =\n hour >= 22 || hour < 6 ? -0.9 : hour >= 6 && hour < 10 ? 0.5 : 0.1;\n const scaledWeight =\n sleepWindowWeight * clamp(args.regularity.sri / 100, 0.4, 1);\n contributors.push({\n source: \"prior\",\n logLikelihoodRatio: scaledWeight,\n });\n llr += scaledWeight;\n }\n\n const signalCoverage = clamp(args.signals.length / 12, 0, 1);\n const windowCoverage = args.windows.length > 0 ? 1 : 0;\n let evidenceCoverage = clamp(\n signalCoverage * 0.7 +\n windowCoverage * 0.2 +\n Math.min(contributors.length, 4) * 0.1,\n 0.15,\n 1,\n );\n if (args.sleepCycle.sleepStatus === \"sleeping_now\") {\n evidenceCoverage = Math.max(evidenceCoverage, 0.9);\n } else if (\n wakeAtMs !== null &&\n args.nowMs - wakeAtMs <= 2 * 60 * 60 * 1_000\n ) {\n evidenceCoverage = Math.max(evidenceCoverage, 0.75);\n }\n const pKnown = round(evidenceCoverage);\n const awakeKnown = logistic(llr);\n const pAwake = round(awakeKnown * pKnown);\n const pAsleep = round((1 - awakeKnown) * pKnown);\n const pUnknown = round(clamp(1 - pKnown, 0, 1));\n const total = pAwake + pAsleep + pUnknown;\n\n if (total <= 0) {\n return {\n pAwake: 0,\n pAsleep: 0,\n pUnknown: 1,\n contributingSources: [],\n computedAt: new Date(args.nowMs).toISOString(),\n };\n }\n\n return {\n pAwake: round(pAwake / total),\n pAsleep: round(pAsleep / total),\n pUnknown: round(pUnknown / total),\n contributingSources: contributors,\n computedAt: new Date(args.nowMs).toISOString(),\n };\n}\n"],"mappings":"AAMA,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AAE3B,SAAS,wCAAwC;AAEjD,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC9D,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC3C;AAEA,SAAS,MAAM,OAAuB;AACpC,SAAO,KAAK,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,GAAG,IAAI;AAChD;AAEA,SAAS,SAAS,OAAuB;AACvC,SAAO,KAAK,IAAI,KAAK,IAAI,CAAC,KAAK;AACjC;AAEA,SAAS,UAAU,OAAe,UAA0B;AAC1D,QAAM,QAAQ,kBAAkB,IAAI,KAAK,KAAK,GAAG,QAAQ;AACzD,SAAO,MAAM,OAAO,MAAM,SAAS;AACrC;AAEO,SAAS,wBAAwB,MAeZ;AAC1B,QAAM,eAA+D,CAAC;AACtE,MAAI,MAAM;AAEV,QAAM,eAAe,CAAC,GAAG,KAAK,OAAO,EAClC,IAAI,CAAC,YAAY;AAAA,IAChB;AAAA,IACA,cAAc,WAAW,OAAO,UAAU;AAAA,EAC5C,EAAE,EACD;AAAA,IACC,CACE,cAEA,UAAU,iBAAiB;AAAA,EAC/B,EACC,KAAK,CAAC,MAAM,UAAU,MAAM,eAAe,KAAK,YAAY,EAAE,CAAC;AAElE,QAAM,gCAAgC,KAAK,QAAQ,KAAK,CAAC,WAAW;AAClE,UAAM,aAAa,WAAW,OAAO,UAAU;AAC/C,QAAI,eAAe,KAAM,QAAO;AAChC,QAAI,KAAK,QAAQ,aAAa,IAAI,IAAQ,QAAO;AACjD,QAAI,OAAO,WAAW,sBAAuB,QAAO;AACpD,QAAI,OAAO,WAAW,mBAAmB,OAAO,UAAU,UAAU;AAClE,aAAO;AAAA,IACT;AACA,QACE,OAAO,WAAW,mBAClB,OAAO,aAAa,mBACpB;AACA,aAAO;AAAA,IACT;AACA,QACE,OAAO,OAAO,oBAAoB,YAClC,OAAO,mBAAmB,IAC1B;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AAED,MAAI,cAAc;AAChB,UAAM,QAAQ,KAAK,QAAQ,aAAa;AACxC,UAAM,QAAQ,aAAa,OAAO;AAClC,UAAM,cAAc;AAAA,MAClB,aAAa,OAAO;AAAA,MACpB,aAAa,OAAO;AAAA,IACtB;AACA,QAAI,QAAQ,MAAM,aAAa,GAAG,CAAC;AACnC,UAAM,qBACJ,aAAa,OAAO,WAAW,mBAC/B,aAAa,OAAO,aAAa,qBACjC,UAAU;AACZ,QAAI,sBAAsB,CAAC,+BAA+B;AACxD,eAAS;AAAA,IACX;AACA,QAAI,aAAa;AACjB,QAAI,UAAU,YAAY,SAAS,IAAI,KAAQ;AAC7C,mBAAa;AAAA,IACf,WAAW,UAAU,YAAY,SAAS,KAAK,KAAQ;AACrD,mBAAa;AAAA,IACf,YACG,UAAU,UAAU,UAAU,YAAY,UAAU,eACrD,SAAS,KAAK,KACd;AACA,mBACE,UAAU,aAAa,OAAO,UAAU,WAAW,OAAO;AAAA,IAC9D;AACA,QAAI,eAAe,GAAG;AACpB,YAAM,eAAe,aAAa;AAClC,mBAAa,KAAK;AAAA,QAChB,QAAQ,aAAa,OAAO;AAAA,QAC5B,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,sBAAsB,WAAW,KAAK,WAAW,qBAAqB;AAC5E,MACE,KAAK,WAAW,gBAAgB,kBAChC,wBAAwB,QACxB,KAAK,SAAS,qBACd;AACA,UAAM,oBACJ,CAAC,GAAG,KAAK,WAAW,QAAQ,EAAE;AAAA,MAC5B,CAAC,MAAM,UAAU,MAAM,aAAa,KAAK;AAAA,IAC3C,EAAE,CAAC,GAAG,UAAU;AAClB,UAAM,cAAc,OAAO,MAAM,KAAK,WAAW,iBAAiB,KAAK,CAAC;AACxE,iBAAa,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,WAAW,KAAK,WAAW,gBAAgB;AAC5D,MAAI,aAAa,MAAM;AACrB,UAAM,oBAAoB,KAAK,QAAQ,YAAY;AACnD,QAAI,oBAAoB,KAAK,oBAAoB,KAAK;AACpD,mBAAa,KAAK;AAAA,QAChB,QAAQ;AAAA,QACR,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,oBACJ,KAAK,QAAQ,SAAS,IACjB,KAAK,QAAQ,KAAK,QAAQ,SAAS,CAAC,GAAG,SAAS,OACjD;AACN,MAAI,sBAAsB,MAAM;AAC9B,UAAM,aAAa,KAAK;AAAA,MACtB;AAAA,MACA,KAAK,OAAO,KAAK,QAAQ,qBAAqB,GAAM;AAAA,IACtD;AACA,QAAI,cAAc,KAAK;AACrB,YAAM,iBAAiB,CAAC,MAAM,aAAa,KAAK,KAAK,GAAG;AACxD,mBAAa,KAAK;AAAA,QAChB,QAAQ;AAAA,QACR,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO;AAAA,IACT,WAAW,cAAc,IAAI;AAC3B,mBAAa,KAAK;AAAA,QAChB,QAAQ;AAAA,QACR,oBAAoB;AAAA,MACtB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MACE,KAAK,WAAW,oBAAoB,aACpC,KAAK,WAAW,oBAAoB,gBACpC;AACA,UAAM,OAAO,UAAU,KAAK,OAAO,KAAK,QAAQ;AAChD,UAAM,oBACJ,QAAQ,MAAM,OAAO,IAAI,OAAO,QAAQ,KAAK,OAAO,KAAK,MAAM;AACjE,UAAM,eACJ,oBAAoB,MAAM,KAAK,WAAW,MAAM,KAAK,KAAK,CAAC;AAC7D,iBAAa,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR,oBAAoB;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,MAAM,KAAK,QAAQ,SAAS,IAAI,GAAG,CAAC;AAC3D,QAAM,iBAAiB,KAAK,QAAQ,SAAS,IAAI,IAAI;AACrD,MAAI,mBAAmB;AAAA,IACrB,iBAAiB,MACf,iBAAiB,MACjB,KAAK,IAAI,aAAa,QAAQ,CAAC,IAAI;AAAA,IACrC;AAAA,IACA;AAAA,EACF;AACA,MAAI,KAAK,WAAW,gBAAgB,gBAAgB;AAClD,uBAAmB,KAAK,IAAI,kBAAkB,GAAG;AAAA,EACnD,WACE,aAAa,QACb,KAAK,QAAQ,YAAY,IAAI,KAAK,KAAK,KACvC;AACA,uBAAmB,KAAK,IAAI,kBAAkB,IAAI;AAAA,EACpD;AACA,QAAM,SAAS,MAAM,gBAAgB;AACrC,QAAM,aAAa,SAAS,GAAG;AAC/B,QAAM,SAAS,MAAM,aAAa,MAAM;AACxC,QAAM,UAAU,OAAO,IAAI,cAAc,MAAM;AAC/C,QAAM,WAAW,MAAM,MAAM,IAAI,QAAQ,GAAG,CAAC,CAAC;AAC9C,QAAM,QAAQ,SAAS,UAAU;AAEjC,MAAI,SAAS,GAAG;AACd,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,UAAU;AAAA,MACV,qBAAqB,CAAC;AAAA,MACtB,YAAY,IAAI,KAAK,KAAK,KAAK,EAAE,YAAY;AAAA,IAC/C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ,MAAM,SAAS,KAAK;AAAA,IAC5B,SAAS,MAAM,UAAU,KAAK;AAAA,IAC9B,UAAU,MAAM,WAAW,KAAK;AAAA,IAChC,qBAAqB;AAAA,IACrB,YAAY,IAAI,KAAK,KAAK,KAAK,EAAE,YAAY;AAAA,EAC/C;AACF;","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named-rules evidence scorer for the circadian state machine.
|
|
3
|
+
*
|
|
4
|
+
* Rules are declared as a flat table so each rule is trivial to audit and
|
|
5
|
+
* easy to extend. Every rule is a pure predicate over the scorer inputs
|
|
6
|
+
* that may return one `CircadianRuleFiring` (or `null` when the rule
|
|
7
|
+
* doesn't apply this tick). The runner accumulates firings into a per-state
|
|
8
|
+
* totals map that the state machine layer then ranks.
|
|
9
|
+
*
|
|
10
|
+
* Weights, thresholds, and stability windows are the canonical ones from
|
|
11
|
+
* `docs/sleep-wake-spec.md` §3. Anything tunable lives at the top of this
|
|
12
|
+
* file so spec and code never drift.
|
|
13
|
+
*/
|
|
14
|
+
import type { LifeOpsActivitySignal, LifeOpsCircadianRuleFiring, LifeOpsCircadianState, LifeOpsPersonalBaseline, LifeOpsRegularityClass } from "../contracts/health.js";
|
|
15
|
+
import type { LifeOpsActivityWindow } from "./sleep-cycle.js";
|
|
16
|
+
export declare const MIN_STABILITY_WINDOW_MS: number;
|
|
17
|
+
export declare const WAKE_CONFIRM_WINDOW_MS: number;
|
|
18
|
+
export declare const SLEEP_ONSET_WINDOW_MS: number;
|
|
19
|
+
export type CircadianRuleFiring = LifeOpsCircadianRuleFiring;
|
|
20
|
+
export interface CircadianScorerResult {
|
|
21
|
+
firings: CircadianRuleFiring[];
|
|
22
|
+
totals: Record<LifeOpsCircadianState, number>;
|
|
23
|
+
}
|
|
24
|
+
interface ScorerInputs {
|
|
25
|
+
nowMs: number;
|
|
26
|
+
timezone: string;
|
|
27
|
+
signals: readonly LifeOpsActivitySignal[];
|
|
28
|
+
windows: readonly LifeOpsActivityWindow[];
|
|
29
|
+
baseline: LifeOpsPersonalBaseline | null;
|
|
30
|
+
regularityClass: LifeOpsRegularityClass;
|
|
31
|
+
hasCurrentSleepEpisode: boolean;
|
|
32
|
+
currentSleepStartedAtMs: number | null;
|
|
33
|
+
lastSleepEndedAtMs: number | null;
|
|
34
|
+
currentEpisodeLikelyNap: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Evaluate every rule in the table and aggregate firings by state.
|
|
38
|
+
*
|
|
39
|
+
* Pure; no I/O. The state machine layer is responsible for picking the
|
|
40
|
+
* top bucket, applying stability-window hysteresis, and translating the
|
|
41
|
+
* result into a calibrated confidence.
|
|
42
|
+
*/
|
|
43
|
+
export declare function scoreCircadianRules(inputs: ScorerInputs): CircadianScorerResult;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=circadian-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circadian-rules.d.ts","sourceRoot":"","sources":["../../src/sleep/circadian-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,uBAAuB,QAAa,CAAC;AAClD,eAAO,MAAM,sBAAsB,QAAc,CAAC;AAClD,eAAO,MAAM,qBAAqB,QAAc,CAAC;AAIjD,MAAM,MAAM,mBAAmB,GAAG,0BAA0B,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;CAC/C;AAED,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1C,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1C,QAAQ,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACzC,eAAe,EAAE,sBAAsB,CAAC;IACxC,sBAAsB,EAAE,OAAO,CAAC;IAChC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,uBAAuB,EAAE,OAAO,CAAC;CAClC;AA4SD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,GACnB,qBAAqB,CAUvB"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { getZonedDateParts } from "../util/time.js";
|
|
2
|
+
import { parseIsoMs } from "../util/time-util.js";
|
|
3
|
+
const MIN_STABILITY_WINDOW_MS = 5 * 6e4;
|
|
4
|
+
const WAKE_CONFIRM_WINDOW_MS = 10 * 6e4;
|
|
5
|
+
const SLEEP_ONSET_WINDOW_MS = 20 * 6e4;
|
|
6
|
+
const AWAKE_EVIDENCE_MAX_AGE_MS = 20 * 6e4;
|
|
7
|
+
const NAP_MAX_DURATION_MS = 4 * 60 * 6e4;
|
|
8
|
+
function localHour(nowMs, timezone) {
|
|
9
|
+
const parts = getZonedDateParts(new Date(nowMs), timezone);
|
|
10
|
+
return parts.hour + parts.minute / 60;
|
|
11
|
+
}
|
|
12
|
+
function isOvernight(nowMs, timezone) {
|
|
13
|
+
const hour = localHour(nowMs, timezone);
|
|
14
|
+
return hour >= 22 || hour < 6;
|
|
15
|
+
}
|
|
16
|
+
function signalAge(signal, nowMs) {
|
|
17
|
+
const observedAt = parseIsoMs(signal.observedAt);
|
|
18
|
+
return observedAt === null ? null : nowMs - observedAt;
|
|
19
|
+
}
|
|
20
|
+
function findSignal(inputs, predicate) {
|
|
21
|
+
for (const signal of inputs.signals) {
|
|
22
|
+
if (!predicate(signal)) continue;
|
|
23
|
+
const ageMs = signalAge(signal, inputs.nowMs);
|
|
24
|
+
if (ageMs === null) continue;
|
|
25
|
+
return { signal, ageMs };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const RULES = [
|
|
30
|
+
// manual.override — user attestation, 4h TTL.
|
|
31
|
+
function manualOverride(inputs) {
|
|
32
|
+
const hit = findSignal(
|
|
33
|
+
inputs,
|
|
34
|
+
(s) => s.platform === "manual_override" && s.metadata.userAttested === true
|
|
35
|
+
);
|
|
36
|
+
if (!hit || hit.ageMs > 4 * 60 * 6e4) return null;
|
|
37
|
+
const kind = String(hit.signal.metadata.manualOverrideKind ?? "");
|
|
38
|
+
return {
|
|
39
|
+
name: "manual.override",
|
|
40
|
+
contributes: kind === "going_to_bed" ? "sleeping" : "awake",
|
|
41
|
+
weight: 1,
|
|
42
|
+
observedAt: hit.signal.observedAt,
|
|
43
|
+
reason: `user attested ${kind}`
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
// healthkit.isSleepingNow — any fresh sleep sample.
|
|
47
|
+
function healthkitSleep(inputs) {
|
|
48
|
+
const hit = findSignal(
|
|
49
|
+
inputs,
|
|
50
|
+
(s) => s.source === "mobile_health" && s.health?.sleep.isSleeping === true
|
|
51
|
+
);
|
|
52
|
+
if (!hit || hit.ageMs > 2 * 60 * 6e4) return null;
|
|
53
|
+
return {
|
|
54
|
+
name: "healthkit.isSleepingNow",
|
|
55
|
+
contributes: "sleeping",
|
|
56
|
+
weight: 0.95,
|
|
57
|
+
observedAt: hit.signal.observedAt,
|
|
58
|
+
reason: "HealthKit reports isSleeping=true"
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
// hid.idleGt20m — HID idle past the awake-evidence timeout.
|
|
62
|
+
function hidIdle(inputs) {
|
|
63
|
+
const hit = findSignal(
|
|
64
|
+
inputs,
|
|
65
|
+
(s) => s.source === "desktop_interaction" && typeof s.idleTimeSeconds === "number" && s.idleTimeSeconds >= 20 * 60
|
|
66
|
+
);
|
|
67
|
+
if (!hit || hit.ageMs > AWAKE_EVIDENCE_MAX_AGE_MS) return null;
|
|
68
|
+
return {
|
|
69
|
+
name: "hid.idleGt20m",
|
|
70
|
+
contributes: isOvernight(inputs.nowMs, inputs.timezone) ? "sleeping" : "winding_down",
|
|
71
|
+
weight: 0.8,
|
|
72
|
+
observedAt: hit.signal.observedAt,
|
|
73
|
+
reason: `HID idle >=20 min (${hit.signal.idleTimeSeconds}s)`
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
// desktop.lockedGt30m — session lock sustained past 30 min.
|
|
77
|
+
function desktopLocked(inputs) {
|
|
78
|
+
const hit = findSignal(
|
|
79
|
+
inputs,
|
|
80
|
+
(s) => s.source === "desktop_power" && s.state === "locked"
|
|
81
|
+
);
|
|
82
|
+
if (!hit || hit.ageMs < 30 * 6e4) return null;
|
|
83
|
+
return {
|
|
84
|
+
name: "desktop.lockedGt30m",
|
|
85
|
+
contributes: isOvernight(inputs.nowMs, inputs.timezone) ? "sleeping" : "winding_down",
|
|
86
|
+
weight: 0.85,
|
|
87
|
+
observedAt: hit.signal.observedAt,
|
|
88
|
+
reason: "session locked >=30 min"
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
// desktop.wakeNotification — recent system wake event.
|
|
92
|
+
function desktopWake(inputs) {
|
|
93
|
+
const hit = findSignal(
|
|
94
|
+
inputs,
|
|
95
|
+
(s) => s.source === "desktop_power" && (s.state === "active" || s.metadata.event === "didWake" || s.metadata.event === "screensDidWake")
|
|
96
|
+
);
|
|
97
|
+
if (!hit || hit.ageMs > WAKE_CONFIRM_WINDOW_MS) return null;
|
|
98
|
+
return {
|
|
99
|
+
name: "desktop.wakeNotification",
|
|
100
|
+
contributes: "waking",
|
|
101
|
+
weight: 0.92,
|
|
102
|
+
observedAt: hit.signal.observedAt,
|
|
103
|
+
reason: "recent NSWorkspace wake notification"
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
// message.outboundRecent — outbound owner message in the last 10 minutes.
|
|
107
|
+
function messageOutbound(inputs) {
|
|
108
|
+
const hit = findSignal(
|
|
109
|
+
inputs,
|
|
110
|
+
(s) => s.source === "imessage_outbound" || s.source === "connector_activity" && (s.metadata.eventType === "MESSAGE_RECEIVED" || s.metadata.direction === "outbound_by_owner")
|
|
111
|
+
);
|
|
112
|
+
if (!hit || hit.ageMs > 10 * 6e4) return null;
|
|
113
|
+
return {
|
|
114
|
+
name: "message.outboundRecent",
|
|
115
|
+
contributes: "awake",
|
|
116
|
+
weight: 0.88,
|
|
117
|
+
observedAt: hit.signal.observedAt,
|
|
118
|
+
reason: "outbound message within 10 min"
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
// continuity.iphoneDisconnected — paired iPhone absent overnight.
|
|
122
|
+
function continuityIPhone(inputs) {
|
|
123
|
+
if (!isOvernight(inputs.nowMs, inputs.timezone)) return null;
|
|
124
|
+
const hit = findSignal(
|
|
125
|
+
inputs,
|
|
126
|
+
(s) => s.source === "mobile_device" && typeof s.platform === "string" && s.platform.startsWith("macos_continuity") && s.state !== "active"
|
|
127
|
+
);
|
|
128
|
+
if (!hit || hit.ageMs > 60 * 6e4) return null;
|
|
129
|
+
return {
|
|
130
|
+
name: "continuity.iphoneDisconnected",
|
|
131
|
+
contributes: "sleeping",
|
|
132
|
+
weight: 0.5,
|
|
133
|
+
observedAt: hit.signal.observedAt,
|
|
134
|
+
reason: "paired iPhone disconnected overnight"
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
// gap.noSignalsGt2hOvernight — no activity windows for 2h+ at night.
|
|
138
|
+
function activityGap(inputs) {
|
|
139
|
+
const latestWindow = inputs.windows[inputs.windows.length - 1];
|
|
140
|
+
if (!latestWindow) return null;
|
|
141
|
+
const gapMs = inputs.nowMs - latestWindow.endMs;
|
|
142
|
+
if (gapMs < 2 * 60 * 6e4) return null;
|
|
143
|
+
const hour = localHour(inputs.nowMs, inputs.timezone);
|
|
144
|
+
if (!(hour >= 22 || hour < 10)) return null;
|
|
145
|
+
return {
|
|
146
|
+
name: "gap.noSignalsGt2hOvernight",
|
|
147
|
+
contributes: "sleeping",
|
|
148
|
+
weight: Math.min(0.9, 0.3 + gapMs / (8 * 60 * 6e4)),
|
|
149
|
+
observedAt: new Date(latestWindow.endMs).toISOString(),
|
|
150
|
+
reason: `no activity for ${Math.round(gapMs / 6e4)} min overnight`
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
// baseline.currentHourLikely[Asleep|Awake] — personal bedtime prior.
|
|
154
|
+
function baselinePrior(inputs) {
|
|
155
|
+
if (!inputs.baseline) return null;
|
|
156
|
+
if (inputs.regularityClass !== "regular" && inputs.regularityClass !== "very_regular") {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const hour = localHour(inputs.nowMs, inputs.timezone);
|
|
160
|
+
const { medianBedtimeLocalHour: bedtime, medianWakeLocalHour: wake } = inputs.baseline;
|
|
161
|
+
const normalized = hour < 12 ? hour + 24 : hour;
|
|
162
|
+
if (normalized >= bedtime || normalized < wake + 12) {
|
|
163
|
+
return {
|
|
164
|
+
name: "baseline.currentHourLikelyAsleep",
|
|
165
|
+
contributes: "sleeping",
|
|
166
|
+
weight: 0.35,
|
|
167
|
+
observedAt: new Date(inputs.nowMs).toISOString(),
|
|
168
|
+
reason: `within baseline bedtime window (${bedtime.toFixed(1)}h-${wake.toFixed(1)}h)`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (normalized >= wake && normalized < wake + 4) {
|
|
172
|
+
return {
|
|
173
|
+
name: "baseline.currentHourLikelyAwake",
|
|
174
|
+
contributes: "awake",
|
|
175
|
+
weight: 0.3,
|
|
176
|
+
observedAt: new Date(inputs.nowMs).toISOString(),
|
|
177
|
+
reason: "within baseline morning window"
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
// active.signalRecent — generic active presence within 5 min.
|
|
183
|
+
function activeSignalRecent(inputs) {
|
|
184
|
+
const latest = [...inputs.signals].map((signal) => {
|
|
185
|
+
const ageMs = signalAge(signal, inputs.nowMs);
|
|
186
|
+
return ageMs === null ? null : { signal, ageMs };
|
|
187
|
+
}).filter(
|
|
188
|
+
(candidate) => candidate !== null
|
|
189
|
+
).sort((left, right) => left.ageMs - right.ageMs)[0];
|
|
190
|
+
if (!latest) return null;
|
|
191
|
+
if (latest.signal.state !== "active" || latest.ageMs > 5 * 6e4) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
name: "active.signalRecent",
|
|
196
|
+
contributes: "awake",
|
|
197
|
+
weight: 0.7,
|
|
198
|
+
observedAt: latest.signal.observedAt,
|
|
199
|
+
reason: "active signal within 5 min"
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
// episode.(sleep|nap)InProgress — current sleep episode.
|
|
203
|
+
function currentEpisode(inputs) {
|
|
204
|
+
if (!inputs.hasCurrentSleepEpisode || inputs.currentSleepStartedAtMs === null) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const duration = inputs.nowMs - inputs.currentSleepStartedAtMs;
|
|
208
|
+
const isNap = inputs.currentEpisodeLikelyNap && duration < NAP_MAX_DURATION_MS;
|
|
209
|
+
return {
|
|
210
|
+
name: isNap ? "episode.napInProgress" : "episode.sleepInProgress",
|
|
211
|
+
contributes: isNap ? "napping" : "sleeping",
|
|
212
|
+
weight: 0.85,
|
|
213
|
+
observedAt: new Date(inputs.currentSleepStartedAtMs).toISOString(),
|
|
214
|
+
reason: isNap ? "nap episode in progress" : "sleep episode in progress"
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
// episode.justWoke — wake anchor inside the confirm window.
|
|
218
|
+
function justWoke(inputs) {
|
|
219
|
+
if (inputs.lastSleepEndedAtMs === null) return null;
|
|
220
|
+
const age = inputs.nowMs - inputs.lastSleepEndedAtMs;
|
|
221
|
+
if (age < 0 || age > WAKE_CONFIRM_WINDOW_MS) return null;
|
|
222
|
+
return {
|
|
223
|
+
name: "episode.justWoke",
|
|
224
|
+
contributes: "waking",
|
|
225
|
+
weight: 0.7,
|
|
226
|
+
observedAt: new Date(inputs.lastSleepEndedAtMs).toISOString(),
|
|
227
|
+
reason: "wake anchor within stability window"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
];
|
|
231
|
+
function emptyTotals() {
|
|
232
|
+
return {
|
|
233
|
+
awake: 0,
|
|
234
|
+
winding_down: 0,
|
|
235
|
+
sleeping: 0,
|
|
236
|
+
waking: 0,
|
|
237
|
+
napping: 0,
|
|
238
|
+
unclear: 0
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function scoreCircadianRules(inputs) {
|
|
242
|
+
const firings = [];
|
|
243
|
+
const totals = emptyTotals();
|
|
244
|
+
for (const rule of RULES) {
|
|
245
|
+
const firing = rule(inputs);
|
|
246
|
+
if (!firing) continue;
|
|
247
|
+
firings.push(firing);
|
|
248
|
+
totals[firing.contributes] += firing.weight;
|
|
249
|
+
}
|
|
250
|
+
return { firings, totals };
|
|
251
|
+
}
|
|
252
|
+
export {
|
|
253
|
+
MIN_STABILITY_WINDOW_MS,
|
|
254
|
+
SLEEP_ONSET_WINDOW_MS,
|
|
255
|
+
WAKE_CONFIRM_WINDOW_MS,
|
|
256
|
+
scoreCircadianRules
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=circadian-rules.js.map
|