forge-openclaw-plugin 0.2.25 → 0.2.27

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.
Files changed (237) hide show
  1. package/README.md +59 -3
  2. package/dist/assets/{board-VmF4FAfr.js → board-C6jCchjI.js} +3 -3
  3. package/dist/assets/{board-VmF4FAfr.js.map → board-C6jCchjI.js.map} +1 -1
  4. package/dist/assets/index-DVvS8iiU.css +1 -0
  5. package/dist/assets/index-zYB-9Dfo.js +85 -0
  6. package/dist/assets/index-zYB-9Dfo.js.map +1 -0
  7. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
  8. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
  9. package/dist/assets/{motion-DvkU14p-.js → motion-DFHrH2rd.js} +2 -2
  10. package/dist/assets/{motion-DvkU14p-.js.map → motion-DFHrH2rd.js.map} +1 -1
  11. package/dist/assets/{table-DgiPof9E.js → table-ZL7Di_u3.js} +2 -2
  12. package/dist/assets/{table-DgiPof9E.js.map → table-ZL7Di_u3.js.map} +1 -1
  13. package/dist/assets/{ui-nYfoC0Gq.js → ui-CKNPpz7q.js} +2 -2
  14. package/dist/assets/{ui-nYfoC0Gq.js.map → ui-CKNPpz7q.js.map} +1 -1
  15. package/dist/assets/vendor-DoNZuFhn.js +1247 -0
  16. package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
  17. package/dist/index.html +7 -8
  18. package/dist/openclaw/local-runtime.d.ts +3 -1
  19. package/dist/openclaw/local-runtime.js +67 -15
  20. package/dist/openclaw/plugin-entry-shared.js +24 -2
  21. package/dist/openclaw/plugin-sdk-types.d.ts +17 -0
  22. package/dist/openclaw/routes.d.ts +27 -0
  23. package/dist/openclaw/routes.js +16 -12
  24. package/dist/openclaw/tools.js +0 -3
  25. package/dist/server/server/migrations/001_core.sql +411 -0
  26. package/dist/server/server/migrations/002_psyche.sql +392 -0
  27. package/dist/server/server/migrations/003_habits.sql +30 -0
  28. package/dist/server/server/migrations/004_habit_links.sql +8 -0
  29. package/dist/server/server/migrations/005_habit_psyche_links.sql +24 -0
  30. package/dist/server/server/migrations/006_work_adjustments.sql +14 -0
  31. package/dist/server/server/migrations/007_weekly_review_closures.sql +17 -0
  32. package/dist/server/server/migrations/008_calendar_execution.sql +147 -0
  33. package/dist/server/server/migrations/009_true_calendar_events.sql +195 -0
  34. package/dist/server/server/migrations/010_calendar_selection_state.sql +6 -0
  35. package/dist/server/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  36. package/dist/server/server/migrations/012_work_block_ranges.sql +7 -0
  37. package/dist/server/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  38. package/dist/server/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  39. package/dist/server/server/migrations/015_multi_user_and_strategies.sql +244 -0
  40. package/dist/server/server/migrations/016_health_companion.sql +158 -0
  41. package/dist/server/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  42. package/dist/server/server/migrations/017_preferences.sql +131 -0
  43. package/dist/server/server/migrations/018_preference_catalogs.sql +31 -0
  44. package/dist/server/server/migrations/019_wiki_memory.sql +255 -0
  45. package/dist/server/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  46. package/dist/server/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  47. package/dist/server/server/migrations/022_wiki_ingest_background.sql +85 -0
  48. package/dist/server/server/migrations/023_diagnostic_logs.sql +28 -0
  49. package/dist/server/server/migrations/024_questionnaires.sql +96 -0
  50. package/dist/server/server/migrations/025_ai_model_connections.sql +26 -0
  51. package/dist/server/server/migrations/026_custom_theme_settings.sql +2 -0
  52. package/dist/server/server/migrations/027_ai_processors.sql +31 -0
  53. package/dist/server/server/migrations/028_movement_domain.sql +136 -0
  54. package/dist/server/server/migrations/029_watch_micro_capture.sql +23 -0
  55. package/dist/server/server/migrations/030_surface_layouts.sql +5 -0
  56. package/dist/server/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  57. package/dist/server/server/migrations/032_ai_connectors.sql +44 -0
  58. package/dist/server/server/migrations/033_movement_trip_point_sync.sql +36 -0
  59. package/dist/server/server/migrations/034_movement_segment_sync.sql +49 -0
  60. package/dist/server/server/migrations/035_google_local_auth_settings.sql +2 -0
  61. package/dist/server/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  62. package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  63. package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
  64. package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
  65. package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
  66. package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
  67. package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
  68. package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  69. package/dist/server/{app.js → server/src/app.js} +2112 -414
  70. package/dist/server/server/src/connectors/box-registry.js +223 -0
  71. package/dist/server/server/src/data-management-types.js +107 -0
  72. package/dist/server/{db.js → server/src/db.js} +72 -4
  73. package/dist/server/server/src/debug.js +19 -0
  74. package/dist/server/{demo-data.js → server/src/demo-data.js} +2 -2
  75. package/dist/server/{health.js → server/src/health.js} +702 -18
  76. package/dist/server/{managers → server/src/managers}/platform/llm-manager.js +7 -4
  77. package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
  78. package/dist/server/{managers → server/src/managers}/platform/secrets-manager.js +18 -1
  79. package/dist/server/{managers → server/src/managers}/runtime.js +9 -0
  80. package/dist/server/{movement.js → server/src/movement.js} +1971 -112
  81. package/dist/server/{openapi.js → server/src/openapi.js} +491 -3
  82. package/dist/server/{psyche-types.js → server/src/psyche-types.js} +9 -1
  83. package/dist/server/{repositories → server/src/repositories}/activity-events.js +8 -0
  84. package/dist/server/{repositories → server/src/repositories}/ai-connectors.js +758 -47
  85. package/dist/server/{repositories → server/src/repositories}/calendar.js +1 -1
  86. package/dist/server/{repositories → server/src/repositories}/habits.js +37 -1
  87. package/dist/server/{repositories → server/src/repositories}/model-settings.js +13 -3
  88. package/dist/server/{repositories → server/src/repositories}/notes.js +3 -0
  89. package/dist/server/{repositories → server/src/repositories}/settings.js +431 -21
  90. package/dist/server/{repositories → server/src/repositories}/tasks.js +170 -10
  91. package/dist/server/server/src/runtime-data-root.js +82 -0
  92. package/dist/server/server/src/screen-time.js +802 -0
  93. package/dist/server/{services → server/src/services}/calendar-runtime.js +775 -58
  94. package/dist/server/server/src/services/data-management.js +788 -0
  95. package/dist/server/{services → server/src/services}/entity-crud.js +205 -2
  96. package/dist/server/server/src/services/google-calendar-oauth-config.js +176 -0
  97. package/dist/server/server/src/services/knowledge-graph.js +1455 -0
  98. package/dist/server/server/src/services/life-force-model.js +197 -0
  99. package/dist/server/server/src/services/life-force.js +1270 -0
  100. package/dist/server/server/src/services/psyche-observation-calendar.js +413 -0
  101. package/dist/server/{types.js → server/src/types.js} +420 -29
  102. package/dist/server/server/src/web.js +332 -0
  103. package/dist/server/src/components/customization/utility-widgets.js +439 -0
  104. package/dist/server/src/components/ui/info-tooltip.js +25 -0
  105. package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
  106. package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
  107. package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
  108. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +147 -0
  109. package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
  110. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +136 -0
  111. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +47 -0
  112. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +132 -0
  113. package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
  114. package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
  115. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +62 -0
  116. package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
  117. package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
  118. package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
  119. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +6 -0
  120. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +49 -0
  121. package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
  122. package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
  123. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +78 -0
  124. package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
  125. package/dist/server/src/lib/api-error.js +37 -0
  126. package/dist/server/src/lib/api.js +2118 -0
  127. package/dist/server/src/lib/calendar-name-deduper.js +144 -0
  128. package/dist/server/src/lib/data-management-types.js +1 -0
  129. package/dist/server/src/lib/diagnostics.js +67 -0
  130. package/dist/server/src/lib/entity-visuals.js +279 -0
  131. package/dist/server/src/lib/knowledge-graph-types.js +276 -0
  132. package/dist/server/src/lib/knowledge-graph.js +470 -0
  133. package/dist/server/src/lib/psyche-types.js +1 -0
  134. package/dist/server/src/lib/questionnaire-types.js +1 -0
  135. package/dist/server/src/lib/runtime-paths.js +24 -0
  136. package/dist/server/src/lib/schemas.js +238 -0
  137. package/dist/server/src/lib/snapshot-normalizer.js +416 -0
  138. package/dist/server/src/lib/theme-system.js +319 -0
  139. package/dist/server/src/lib/types.js +1 -0
  140. package/dist/server/src/lib/utils.js +22 -0
  141. package/dist/server/src/lib/workbench/boxes.js +16 -0
  142. package/dist/server/src/lib/workbench/contracts.js +229 -0
  143. package/dist/server/src/lib/workbench/nodes.js +215 -0
  144. package/dist/server/src/lib/workbench/registry.js +120 -0
  145. package/dist/server/src/lib/workbench/runtime.js +397 -0
  146. package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
  147. package/openclaw.plugin.json +1 -1
  148. package/package.json +1 -1
  149. package/server/index.js +68 -0
  150. package/server/migrations/035_google_local_auth_settings.sql +2 -0
  151. package/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  152. package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  153. package/server/migrations/038_data_management_settings.sql +11 -0
  154. package/server/migrations/039_life_force_and_action_points.sql +114 -0
  155. package/server/migrations/040_screen_time_domain.sql +89 -0
  156. package/server/migrations/041_companion_source_states.sql +21 -0
  157. package/server/migrations/042_movement_boxes.sql +47 -0
  158. package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  159. package/skills/forge-openclaw/SKILL.md +27 -11
  160. package/skills/forge-openclaw/entity_conversation_playbooks.md +411 -46
  161. package/skills/forge-openclaw/psyche_entity_playbooks.md +195 -20
  162. package/dist/assets/index-CFCKDIMH.js +0 -67
  163. package/dist/assets/index-CFCKDIMH.js.map +0 -1
  164. package/dist/assets/index-ZPY6U1TU.css +0 -1
  165. package/dist/assets/vendor-D9PTEPSB.js +0 -824
  166. package/dist/assets/vendor-D9PTEPSB.js.map +0 -1
  167. package/dist/assets/viz-Cqb6s--o.js +0 -34
  168. package/dist/assets/viz-Cqb6s--o.js.map +0 -1
  169. package/dist/server/connectors/box-registry.js +0 -257
  170. package/dist/server/services/psyche-observation-calendar.js +0 -46
  171. package/dist/server/web.js +0 -98
  172. /package/dist/server/{discovery-advertiser.js → server/src/discovery-advertiser.js} +0 -0
  173. /package/dist/server/{e2e-server.js → server/src/e2e-server.js} +0 -0
  174. /package/dist/server/{errors.js → server/src/errors.js} +0 -0
  175. /package/dist/server/{index.js → server/src/index.js} +0 -0
  176. /package/dist/server/{managers → server/src/managers}/base.js +0 -0
  177. /package/dist/server/{managers → server/src/managers}/contracts.js +0 -0
  178. /package/dist/server/{managers → server/src/managers}/platform/api-gateway-manager.js +0 -0
  179. /package/dist/server/{managers → server/src/managers}/platform/audit-manager.js +0 -0
  180. /package/dist/server/{managers → server/src/managers}/platform/authentication-manager.js +0 -0
  181. /package/dist/server/{managers → server/src/managers}/platform/authorization-manager.js +0 -0
  182. /package/dist/server/{managers → server/src/managers}/platform/background-job-manager.js +0 -0
  183. /package/dist/server/{managers → server/src/managers}/platform/configuration-manager.js +0 -0
  184. /package/dist/server/{managers → server/src/managers}/platform/database-manager.js +0 -0
  185. /package/dist/server/{managers → server/src/managers}/platform/event-bus-manager.js +0 -0
  186. /package/dist/server/{managers → server/src/managers}/platform/external-service-manager.js +0 -0
  187. /package/dist/server/{managers → server/src/managers}/platform/health-manager.js +0 -0
  188. /package/dist/server/{managers → server/src/managers}/platform/migration-manager.js +0 -0
  189. /package/dist/server/{managers → server/src/managers}/platform/openai-responses-provider.js +0 -0
  190. /package/dist/server/{managers → server/src/managers}/platform/search-index-manager.js +0 -0
  191. /package/dist/server/{managers → server/src/managers}/platform/session-manager.js +0 -0
  192. /package/dist/server/{managers → server/src/managers}/platform/storage-manager.js +0 -0
  193. /package/dist/server/{managers → server/src/managers}/platform/token-manager.js +0 -0
  194. /package/dist/server/{managers → server/src/managers}/platform/transaction-manager.js +0 -0
  195. /package/dist/server/{managers → server/src/managers}/platform/trusted-network.js +0 -0
  196. /package/dist/server/{managers → server/src/managers}/type-guards.js +0 -0
  197. /package/dist/server/{preferences-seeds.js → server/src/preferences-seeds.js} +0 -0
  198. /package/dist/server/{preferences-types.js → server/src/preferences-types.js} +0 -0
  199. /package/dist/server/{questionnaire-flow.js → server/src/questionnaire-flow.js} +0 -0
  200. /package/dist/server/{questionnaire-seeds.js → server/src/questionnaire-seeds.js} +0 -0
  201. /package/dist/server/{questionnaire-types.js → server/src/questionnaire-types.js} +0 -0
  202. /package/dist/server/{repositories → server/src/repositories}/ai-processors.js +0 -0
  203. /package/dist/server/{repositories → server/src/repositories}/collaboration.js +0 -0
  204. /package/dist/server/{repositories → server/src/repositories}/deleted-entities.js +0 -0
  205. /package/dist/server/{repositories → server/src/repositories}/diagnostic-logs.js +0 -0
  206. /package/dist/server/{repositories → server/src/repositories}/domains.js +0 -0
  207. /package/dist/server/{repositories → server/src/repositories}/entity-ownership.js +0 -0
  208. /package/dist/server/{repositories → server/src/repositories}/event-log.js +0 -0
  209. /package/dist/server/{repositories → server/src/repositories}/goals.js +0 -0
  210. /package/dist/server/{repositories → server/src/repositories}/preferences.js +0 -0
  211. /package/dist/server/{repositories → server/src/repositories}/projects.js +0 -0
  212. /package/dist/server/{repositories → server/src/repositories}/psyche.js +0 -0
  213. /package/dist/server/{repositories → server/src/repositories}/questionnaires.js +0 -0
  214. /package/dist/server/{repositories → server/src/repositories}/rewards.js +0 -0
  215. /package/dist/server/{repositories → server/src/repositories}/strategies.js +0 -0
  216. /package/dist/server/{repositories → server/src/repositories}/surface-layouts.js +0 -0
  217. /package/dist/server/{repositories → server/src/repositories}/tags.js +0 -0
  218. /package/dist/server/{repositories → server/src/repositories}/task-runs.js +0 -0
  219. /package/dist/server/{repositories → server/src/repositories}/users.js +0 -0
  220. /package/dist/server/{repositories → server/src/repositories}/weekly-reviews.js +0 -0
  221. /package/dist/server/{repositories → server/src/repositories}/wiki-memory.js +0 -0
  222. /package/dist/server/{repositories → server/src/repositories}/work-adjustments.js +0 -0
  223. /package/dist/server/{seed-demo.js → server/src/seed-demo.js} +0 -0
  224. /package/dist/server/{services → server/src/services}/context.js +0 -0
  225. /package/dist/server/{services → server/src/services}/dashboard.js +0 -0
  226. /package/dist/server/{services → server/src/services}/gamification.js +0 -0
  227. /package/dist/server/{services → server/src/services}/insights.js +0 -0
  228. /package/dist/server/{services → server/src/services}/openai-codex-oauth.js +0 -0
  229. /package/dist/server/{services → server/src/services}/projects.js +0 -0
  230. /package/dist/server/{services → server/src/services}/psyche.js +0 -0
  231. /package/dist/server/{services → server/src/services}/relations.js +0 -0
  232. /package/dist/server/{services → server/src/services}/reviews.js +0 -0
  233. /package/dist/server/{services → server/src/services}/run-recovery.js +0 -0
  234. /package/dist/server/{services → server/src/services}/tagging.js +0 -0
  235. /package/dist/server/{services → server/src/services}/task-run-watchdog.js +0 -0
  236. /package/dist/server/{services → server/src/services}/work-time.js +0 -0
  237. /package/dist/server/{watch-mobile.js → server/src/watch-mobile.js} +0 -0
@@ -0,0 +1,1270 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase, runInTransaction } from "../db.js";
3
+ import { getDefaultUser, getUserById } from "../repositories/users.js";
4
+ import { actionProfileSchema, fatigueSignalCreateSchema, lifeForcePayloadSchema, lifeForceProfilePatchSchema, lifeForceTemplateUpdateSchema } from "../types.js";
5
+ import { DEFAULT_TASK_TOTAL_AP, LIFE_FORCE_BASELINE_DAILY_AP, buildDefaultTaskActionProfile, buildTaskActionPointSummary, buildTaskSplitSuggestion, clamp, interpolateCurveRate, normalizeCurveToBudget, resolveBandTotalCostAp, resolveTaskExpectedDurationSeconds } from "./life-force-model.js";
6
+ import { computeWorkTime } from "./work-time.js";
7
+ const DEFAULT_TEMPLATE_POINTS = [
8
+ { minuteOfDay: 0, rateApPerHour: 0 },
9
+ { minuteOfDay: 7 * 60, rateApPerHour: 0 },
10
+ { minuteOfDay: 8 * 60, rateApPerHour: 8 },
11
+ { minuteOfDay: 10 * 60, rateApPerHour: 13 },
12
+ { minuteOfDay: 13 * 60, rateApPerHour: 9 },
13
+ { minuteOfDay: 14 * 60, rateApPerHour: 11 },
14
+ { minuteOfDay: 19 * 60, rateApPerHour: 7 },
15
+ { minuteOfDay: 23 * 60, rateApPerHour: 0 },
16
+ { minuteOfDay: 24 * 60, rateApPerHour: 0 }
17
+ ];
18
+ const LIFE_FORCE_STAT_LABELS = {
19
+ life_force: "Life Force",
20
+ activation: "Activation",
21
+ focus: "Focus",
22
+ vigor: "Vigor",
23
+ composure: "Composure",
24
+ flow: "Flow"
25
+ };
26
+ function nowIso() {
27
+ return new Date().toISOString();
28
+ }
29
+ function toDateKey(date) {
30
+ return date.toISOString().slice(0, 10);
31
+ }
32
+ function startOfUtcDay(date) {
33
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
34
+ }
35
+ function buildDayRange(date) {
36
+ const start = startOfUtcDay(date);
37
+ const end = new Date(start);
38
+ end.setUTCDate(end.getUTCDate() + 1);
39
+ return {
40
+ startMs: start.getTime(),
41
+ endMs: end.getTime(),
42
+ dateKey: toDateKey(start),
43
+ from: start.toISOString(),
44
+ to: end.toISOString()
45
+ };
46
+ }
47
+ function parseCurvePoints(raw) {
48
+ try {
49
+ const parsed = JSON.parse(raw);
50
+ if (!Array.isArray(parsed)) {
51
+ return [];
52
+ }
53
+ return parsed
54
+ .filter((value) => !!value &&
55
+ typeof value === "object" &&
56
+ typeof value.minuteOfDay === "number" &&
57
+ typeof value.rateApPerHour === "number")
58
+ .map((point) => ({
59
+ minuteOfDay: Math.round(point.minuteOfDay),
60
+ rateApPerHour: point.rateApPerHour,
61
+ locked: point.locked
62
+ }))
63
+ .sort((left, right) => left.minuteOfDay - right.minuteOfDay);
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ }
69
+ function defaultTemplatePoints() {
70
+ return normalizeCurveToBudget(DEFAULT_TEMPLATE_POINTS, LIFE_FORCE_BASELINE_DAILY_AP);
71
+ }
72
+ function seededActionProfiles() {
73
+ const now = nowIso();
74
+ const taskDefault = buildDefaultTaskActionProfile({});
75
+ return [
76
+ taskDefault,
77
+ actionProfileSchema.parse({
78
+ id: "profile_note_quick",
79
+ profileKey: "note_quick",
80
+ title: "Quick note",
81
+ entityType: "note",
82
+ mode: "impulse",
83
+ startupAp: 1,
84
+ totalCostAp: 1,
85
+ expectedDurationSeconds: null,
86
+ sustainRateApPerHour: 0,
87
+ demandWeights: {
88
+ activation: 0.15,
89
+ focus: 0.5,
90
+ vigor: 0,
91
+ composure: 0,
92
+ flow: 0.35
93
+ },
94
+ doubleCountPolicy: "primary_only",
95
+ sourceMethod: "seeded",
96
+ costBand: "tiny",
97
+ recoveryEffect: 0,
98
+ metadata: {},
99
+ createdAt: now,
100
+ updatedAt: now
101
+ }),
102
+ actionProfileSchema.parse({
103
+ id: "profile_habit_default",
104
+ profileKey: "habit_default",
105
+ title: "Habit check-in",
106
+ entityType: "habit",
107
+ mode: "impulse",
108
+ startupAp: 3,
109
+ totalCostAp: 3,
110
+ expectedDurationSeconds: null,
111
+ sustainRateApPerHour: 0,
112
+ demandWeights: {
113
+ activation: 0.4,
114
+ focus: 0.1,
115
+ vigor: 0.15,
116
+ composure: 0.05,
117
+ flow: 0.3
118
+ },
119
+ doubleCountPolicy: "primary_only",
120
+ sourceMethod: "seeded",
121
+ costBand: "light",
122
+ recoveryEffect: 0,
123
+ metadata: {},
124
+ createdAt: now,
125
+ updatedAt: now
126
+ }),
127
+ actionProfileSchema.parse({
128
+ id: "profile_workout_default",
129
+ profileKey: "workout_default",
130
+ title: "Workout",
131
+ entityType: "workout_session",
132
+ mode: "rate",
133
+ startupAp: 1,
134
+ totalCostAp: 24,
135
+ expectedDurationSeconds: 3_600,
136
+ sustainRateApPerHour: 24,
137
+ demandWeights: {
138
+ activation: 0.1,
139
+ focus: 0.05,
140
+ vigor: 0.75,
141
+ composure: 0,
142
+ flow: 0.1
143
+ },
144
+ doubleCountPolicy: "primary_only",
145
+ sourceMethod: "seeded",
146
+ costBand: "standard",
147
+ recoveryEffect: 0,
148
+ metadata: {},
149
+ createdAt: now,
150
+ updatedAt: now
151
+ }),
152
+ actionProfileSchema.parse({
153
+ id: "profile_calendar_default",
154
+ profileKey: "calendar_event_default",
155
+ title: "Calendar event",
156
+ entityType: "calendar_event",
157
+ mode: "container",
158
+ startupAp: 0,
159
+ totalCostAp: 12,
160
+ expectedDurationSeconds: 3_600,
161
+ sustainRateApPerHour: 12,
162
+ demandWeights: {
163
+ activation: 0.05,
164
+ focus: 0.35,
165
+ vigor: 0.05,
166
+ composure: 0.35,
167
+ flow: 0.2
168
+ },
169
+ doubleCountPolicy: "container_only",
170
+ sourceMethod: "seeded",
171
+ costBand: "light",
172
+ recoveryEffect: 0,
173
+ metadata: {},
174
+ createdAt: now,
175
+ updatedAt: now
176
+ }),
177
+ actionProfileSchema.parse({
178
+ id: "profile_recovery_break",
179
+ profileKey: "recovery_break",
180
+ title: "Recovery break",
181
+ entityType: "system",
182
+ mode: "recovery",
183
+ startupAp: 0,
184
+ totalCostAp: 0,
185
+ expectedDurationSeconds: null,
186
+ sustainRateApPerHour: 0,
187
+ demandWeights: {
188
+ activation: 0,
189
+ focus: 0,
190
+ vigor: 0,
191
+ composure: 0,
192
+ flow: 0
193
+ },
194
+ doubleCountPolicy: "primary_only",
195
+ sourceMethod: "seeded",
196
+ costBand: "tiny",
197
+ recoveryEffect: 6,
198
+ metadata: {},
199
+ createdAt: now,
200
+ updatedAt: now
201
+ })
202
+ ];
203
+ }
204
+ function mapTemplateProfileRow(row) {
205
+ return actionProfileSchema.parse({
206
+ id: row.id,
207
+ profileKey: row.profile_key,
208
+ entityType: row.entity_type,
209
+ title: row.title,
210
+ createdAt: row.created_at,
211
+ updatedAt: row.updated_at,
212
+ ...JSON.parse(row.profile_json)
213
+ });
214
+ }
215
+ function mapEntityProfileRow(row, fallback) {
216
+ return actionProfileSchema.parse({
217
+ id: row.id,
218
+ profileKey: fallback.profileKey,
219
+ title: fallback.title,
220
+ entityType: fallback.entityType,
221
+ createdAt: row.created_at,
222
+ updatedAt: row.updated_at,
223
+ ...JSON.parse(row.profile_json)
224
+ });
225
+ }
226
+ function ensureActionProfileTemplates() {
227
+ const database = getDatabase();
228
+ const insert = database.prepare(`INSERT OR IGNORE INTO action_profile_templates (
229
+ id,
230
+ profile_key,
231
+ entity_type,
232
+ title,
233
+ profile_json,
234
+ created_at,
235
+ updated_at
236
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`);
237
+ for (const profile of seededActionProfiles()) {
238
+ insert.run(profile.id, profile.profileKey, profile.entityType, profile.title, JSON.stringify({
239
+ mode: profile.mode,
240
+ startupAp: profile.startupAp,
241
+ totalCostAp: profile.totalCostAp,
242
+ expectedDurationSeconds: profile.expectedDurationSeconds,
243
+ sustainRateApPerHour: profile.sustainRateApPerHour,
244
+ demandWeights: profile.demandWeights,
245
+ doubleCountPolicy: profile.doubleCountPolicy,
246
+ sourceMethod: profile.sourceMethod,
247
+ costBand: profile.costBand,
248
+ recoveryEffect: profile.recoveryEffect,
249
+ metadata: profile.metadata
250
+ }), profile.createdAt, profile.updatedAt);
251
+ }
252
+ }
253
+ export function upsertTaskActionProfile(input) {
254
+ const now = nowIso();
255
+ const profile = buildDefaultTaskActionProfile({
256
+ id: `profile_task_${input.taskId}`,
257
+ profileKey: `task_${input.taskId}`,
258
+ title: input.title || "Task",
259
+ expectedDurationSeconds: input.plannedDurationSeconds,
260
+ totalCostAp: input.totalCostAp ?? resolveBandTotalCostAp(input.actionCostBand ?? "standard"),
261
+ costBand: input.actionCostBand ?? "standard",
262
+ sourceMethod: "manual"
263
+ });
264
+ getDatabase()
265
+ .prepare(`INSERT INTO entity_action_profiles (
266
+ id,
267
+ entity_type,
268
+ entity_id,
269
+ profile_json,
270
+ created_at,
271
+ updated_at
272
+ ) VALUES (?, 'task', ?, ?, ?, ?)
273
+ ON CONFLICT(entity_type, entity_id) DO UPDATE SET
274
+ profile_json = excluded.profile_json,
275
+ updated_at = excluded.updated_at`)
276
+ .run(profile.id, input.taskId, JSON.stringify({
277
+ profileKey: profile.profileKey,
278
+ title: profile.title,
279
+ entityType: profile.entityType,
280
+ mode: profile.mode,
281
+ startupAp: profile.startupAp,
282
+ totalCostAp: profile.totalCostAp,
283
+ expectedDurationSeconds: profile.expectedDurationSeconds,
284
+ sustainRateApPerHour: profile.sustainRateApPerHour,
285
+ demandWeights: profile.demandWeights,
286
+ doubleCountPolicy: profile.doubleCountPolicy,
287
+ sourceMethod: profile.sourceMethod,
288
+ costBand: profile.costBand,
289
+ recoveryEffect: profile.recoveryEffect,
290
+ metadata: profile.metadata
291
+ }), now, now);
292
+ }
293
+ function ensureLifeForceProfile(userId) {
294
+ const database = getDatabase();
295
+ const existing = database
296
+ .prepare(`SELECT *
297
+ FROM life_force_profiles
298
+ WHERE user_id = ?`)
299
+ .get(userId);
300
+ if (existing) {
301
+ return existing;
302
+ }
303
+ const now = nowIso();
304
+ database
305
+ .prepare(`INSERT INTO life_force_profiles (
306
+ user_id,
307
+ base_daily_ap,
308
+ readiness_multiplier,
309
+ life_force_level,
310
+ activation_level,
311
+ focus_level,
312
+ vigor_level,
313
+ composure_level,
314
+ flow_level,
315
+ created_at,
316
+ updated_at
317
+ ) VALUES (?, 200, 1.0, 1, 1, 1, 1, 1, 1, ?, ?)`)
318
+ .run(userId, now, now);
319
+ return database
320
+ .prepare(`SELECT *
321
+ FROM life_force_profiles
322
+ WHERE user_id = ?`)
323
+ .get(userId);
324
+ }
325
+ function ensureWeekdayTemplates(userId) {
326
+ const database = getDatabase();
327
+ const insert = database.prepare(`INSERT OR IGNORE INTO life_force_weekday_templates (
328
+ id,
329
+ user_id,
330
+ weekday,
331
+ baseline_daily_ap,
332
+ points_json,
333
+ created_at,
334
+ updated_at
335
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`);
336
+ const now = nowIso();
337
+ const defaultPoints = JSON.stringify(defaultTemplatePoints());
338
+ for (let weekday = 0; weekday < 7; weekday += 1) {
339
+ insert.run(`lf_template_${userId}_${weekday}`, userId, weekday, LIFE_FORCE_BASELINE_DAILY_AP, defaultPoints, now, now);
340
+ }
341
+ }
342
+ function readWeekdayTemplate(userId, weekday) {
343
+ ensureWeekdayTemplates(userId);
344
+ return getDatabase()
345
+ .prepare(`SELECT *
346
+ FROM life_force_weekday_templates
347
+ WHERE user_id = ? AND weekday = ?`)
348
+ .get(userId, weekday);
349
+ }
350
+ function readTaskRunRows(range, userId) {
351
+ return getDatabase()
352
+ .prepare(`SELECT
353
+ task_runs.id,
354
+ task_runs.task_id,
355
+ task_runs.actor,
356
+ task_runs.status,
357
+ task_runs.is_current,
358
+ task_runs.claimed_at,
359
+ task_runs.heartbeat_at,
360
+ task_runs.lease_expires_at,
361
+ task_runs.completed_at,
362
+ task_runs.released_at,
363
+ task_runs.timed_out_at,
364
+ task_runs.updated_at,
365
+ tasks.title AS task_title,
366
+ task_runs.planned_duration_seconds,
367
+ tasks.planned_duration_seconds AS task_expected_duration_seconds
368
+ FROM task_runs
369
+ INNER JOIN tasks ON tasks.id = task_runs.task_id
370
+ INNER JOIN entity_owners
371
+ ON entity_owners.entity_type = 'task'
372
+ AND entity_owners.entity_id = tasks.id
373
+ AND entity_owners.role = 'owner'
374
+ WHERE entity_owners.user_id = ?
375
+ AND task_runs.claimed_at < ?
376
+ AND COALESCE(task_runs.completed_at, task_runs.released_at, task_runs.timed_out_at, task_runs.updated_at, task_runs.lease_expires_at, task_runs.heartbeat_at) >= ?`)
377
+ .all(userId, range.to, range.from);
378
+ }
379
+ function terminalRunMs(row, now) {
380
+ if (row.status === "active") {
381
+ return Math.max(Date.parse(row.claimed_at), Math.min(now.getTime(), Date.parse(row.lease_expires_at)));
382
+ }
383
+ const terminal = row.completed_at ??
384
+ row.released_at ??
385
+ row.timed_out_at ??
386
+ row.updated_at ??
387
+ row.lease_expires_at ??
388
+ row.heartbeat_at;
389
+ return Math.max(Date.parse(row.claimed_at), Date.parse(terminal));
390
+ }
391
+ function overlapSeconds(range, row, now) {
392
+ const start = Math.max(range.startMs, Date.parse(row.claimed_at));
393
+ const end = Math.min(range.endMs, terminalRunMs(row, now));
394
+ return Math.max(0, Math.floor((end - start) / 1000));
395
+ }
396
+ function readStatXpByKey(userId) {
397
+ const rows = getDatabase()
398
+ .prepare(`SELECT stat_key, COALESCE(SUM(delta_xp), 0) AS xp
399
+ FROM stat_xp_events
400
+ WHERE user_id = ?
401
+ GROUP BY stat_key`)
402
+ .all(userId);
403
+ return new Map(rows.map((row) => [row.stat_key, row.xp]));
404
+ }
405
+ function buildStats(profile, userId) {
406
+ const xpByKey = readStatXpByKey(userId);
407
+ const levelByKey = {
408
+ life_force: profile.life_force_level,
409
+ activation: profile.activation_level,
410
+ focus: profile.focus_level,
411
+ vigor: profile.vigor_level,
412
+ composure: profile.composure_level,
413
+ flow: profile.flow_level
414
+ };
415
+ return Object.keys(levelByKey).map((key) => {
416
+ const level = levelByKey[key];
417
+ return {
418
+ key,
419
+ label: LIFE_FORCE_STAT_LABELS[key],
420
+ level,
421
+ xp: xpByKey.get(key) ?? 0,
422
+ xpToNextLevel: level * 100,
423
+ costModifier: key === "life_force"
424
+ ? 1 + level * 0.03
425
+ : Math.max(0.55, Number((1 - level * 0.02).toFixed(3)))
426
+ };
427
+ });
428
+ }
429
+ function computeLifeForceMultiplier(profile) {
430
+ return 1 + (profile.life_force_level - 1) * 0.03;
431
+ }
432
+ function computeSleepRecoveryMultiplier() {
433
+ return 1;
434
+ }
435
+ function computeFatigueDebtCarry(userId, date) {
436
+ const previous = new Date(date);
437
+ previous.setUTCDate(previous.getUTCDate() - 1);
438
+ const previousKey = toDateKey(previous);
439
+ const snapshot = getDatabase()
440
+ .prepare(`SELECT daily_budget_ap
441
+ FROM life_force_day_snapshots
442
+ WHERE user_id = ? AND date_key = ?`)
443
+ .get(userId, previousKey);
444
+ if (!snapshot) {
445
+ return 0;
446
+ }
447
+ const spent = getDatabase()
448
+ .prepare(`SELECT COALESCE(SUM(total_ap), 0) AS total_ap
449
+ FROM ap_ledger_events
450
+ WHERE user_id = ? AND date_key = ?`)
451
+ .get(userId, previousKey);
452
+ return Math.max(0, Number((spent.total_ap - snapshot.daily_budget_ap).toFixed(2)));
453
+ }
454
+ function getOrCreateDaySnapshot(userId, date) {
455
+ const range = buildDayRange(date);
456
+ const existing = getDatabase()
457
+ .prepare(`SELECT *
458
+ FROM life_force_day_snapshots
459
+ WHERE user_id = ? AND date_key = ?`)
460
+ .get(userId, range.dateKey);
461
+ if (existing) {
462
+ return existing;
463
+ }
464
+ const profile = ensureLifeForceProfile(userId);
465
+ const template = readWeekdayTemplate(userId, date.getUTCDay());
466
+ const sleepRecoveryMultiplier = computeSleepRecoveryMultiplier();
467
+ const fatigueDebtCarry = computeFatigueDebtCarry(userId, date);
468
+ const readinessMultiplier = profile.readiness_multiplier;
469
+ const dailyBudgetAp = Math.max(40, Math.round(profile.base_daily_ap *
470
+ computeLifeForceMultiplier(profile) *
471
+ sleepRecoveryMultiplier *
472
+ readinessMultiplier) - fatigueDebtCarry);
473
+ const points = normalizeCurveToBudget(parseCurvePoints(template.points_json), dailyBudgetAp);
474
+ const now = nowIso();
475
+ const id = `lf_day_${userId}_${range.dateKey}`;
476
+ getDatabase()
477
+ .prepare(`INSERT INTO life_force_day_snapshots (
478
+ id,
479
+ user_id,
480
+ date_key,
481
+ daily_budget_ap,
482
+ sleep_recovery_multiplier,
483
+ readiness_multiplier,
484
+ fatigue_debt_carry,
485
+ points_json,
486
+ created_at,
487
+ updated_at
488
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
489
+ .run(id, userId, range.dateKey, dailyBudgetAp, sleepRecoveryMultiplier, readinessMultiplier, fatigueDebtCarry, JSON.stringify(points), now, now);
490
+ return getDatabase()
491
+ .prepare(`SELECT *
492
+ FROM life_force_day_snapshots
493
+ WHERE id = ?`)
494
+ .get(id);
495
+ }
496
+ function resolveTaskActionProfile(task) {
497
+ const row = getDatabase()
498
+ .prepare(`SELECT id, entity_type, entity_id, profile_json, created_at, updated_at
499
+ FROM entity_action_profiles
500
+ WHERE entity_type = 'task' AND entity_id = ?`)
501
+ .get(task.id);
502
+ if (!row) {
503
+ return buildDefaultTaskActionProfile({
504
+ id: `profile_task_${task.id}`,
505
+ expectedDurationSeconds: task.plannedDurationSeconds
506
+ });
507
+ }
508
+ const profile = mapEntityProfileRow(row, {
509
+ profileKey: `task_${task.id}`,
510
+ title: "Task",
511
+ entityType: "task"
512
+ });
513
+ return {
514
+ ...profile,
515
+ expectedDurationSeconds: resolveTaskExpectedDurationSeconds(profile.expectedDurationSeconds ?? task.plannedDurationSeconds)
516
+ };
517
+ }
518
+ function readTodayAdjustmentRows(userId, range) {
519
+ return getDatabase()
520
+ .prepare(`SELECT
521
+ work_adjustments.id,
522
+ work_adjustments.entity_type,
523
+ work_adjustments.entity_id,
524
+ work_adjustments.applied_delta_minutes,
525
+ work_adjustments.note,
526
+ work_adjustments.created_at,
527
+ tasks.planned_duration_seconds
528
+ FROM work_adjustments
529
+ LEFT JOIN tasks
530
+ ON work_adjustments.entity_type = 'task'
531
+ AND tasks.id = work_adjustments.entity_id
532
+ INNER JOIN entity_owners
533
+ ON entity_owners.entity_type = work_adjustments.entity_type
534
+ AND entity_owners.entity_id = work_adjustments.entity_id
535
+ AND entity_owners.role = 'owner'
536
+ WHERE entity_owners.user_id = ?
537
+ AND work_adjustments.created_at >= ?
538
+ AND work_adjustments.created_at < ?`)
539
+ .all(userId, range.from, range.to);
540
+ }
541
+ function readTodayAdjustmentApByTaskId(userId, range) {
542
+ const rows = readTodayAdjustmentRows(userId, range);
543
+ const totals = new Map();
544
+ for (const row of rows) {
545
+ if (row.entity_type !== "task") {
546
+ continue;
547
+ }
548
+ const expectedDurationSeconds = resolveTaskExpectedDurationSeconds(row.planned_duration_seconds);
549
+ const deltaAp = (DEFAULT_TASK_TOTAL_AP / expectedDurationSeconds) *
550
+ row.applied_delta_minutes *
551
+ 60;
552
+ totals.set(row.entity_id, (totals.get(row.entity_id) ?? 0) + deltaAp);
553
+ }
554
+ return totals;
555
+ }
556
+ function readTodayAdjustmentSecondsByTaskId(userId, range) {
557
+ const rows = readTodayAdjustmentRows(userId, range);
558
+ const totals = new Map();
559
+ for (const row of rows) {
560
+ if (row.entity_type !== "task") {
561
+ continue;
562
+ }
563
+ totals.set(row.entity_id, (totals.get(row.entity_id) ?? 0) + row.applied_delta_minutes * 60);
564
+ }
565
+ return totals;
566
+ }
567
+ function readActiveTaskRunProjectionRows(taskId) {
568
+ return getDatabase()
569
+ .prepare(`SELECT
570
+ id,
571
+ task_id,
572
+ timer_mode,
573
+ planned_duration_seconds,
574
+ claimed_at,
575
+ lease_expires_at,
576
+ status
577
+ FROM task_runs
578
+ WHERE task_id = ?
579
+ AND status = 'active'`)
580
+ .all(taskId);
581
+ }
582
+ function computeProjectedRemainingSeconds(row, now) {
583
+ if (row.timer_mode !== "planned" || row.planned_duration_seconds === null) {
584
+ return 0;
585
+ }
586
+ const endMs = Math.min(now.getTime(), Date.parse(row.lease_expires_at));
587
+ const elapsedWallSeconds = Math.max(0, Math.floor((endMs - Date.parse(row.claimed_at)) / 1000));
588
+ return Math.max(0, row.planned_duration_seconds - elapsedWallSeconds);
589
+ }
590
+ function buildTaskLifeForceRuntime(task, userId, now = new Date()) {
591
+ const range = buildDayRange(now);
592
+ const profile = resolveTaskActionProfile(task);
593
+ const todayRunSeconds = readTaskRunRows(range, userId)
594
+ .filter((row) => row.task_id === task.id)
595
+ .reduce((sum, row) => sum + overlapSeconds(range, row, now), 0);
596
+ const todayAdjustmentSeconds = readTodayAdjustmentSecondsByTaskId(userId, range).get(task.id) ?? 0;
597
+ const todayCreditedSeconds = todayRunSeconds + todayAdjustmentSeconds;
598
+ const spentTodayAp = (todayCreditedSeconds / 3600) * profile.sustainRateApPerHour;
599
+ const spentTotalAp = (task.time.totalCreditedSeconds / 3600) * profile.sustainRateApPerHour;
600
+ const projectedTotalSeconds = task.time.totalCreditedSeconds +
601
+ readActiveTaskRunProjectionRows(task.id).reduce((sum, row) => sum + computeProjectedRemainingSeconds(row, now), 0);
602
+ return {
603
+ taskId: task.id,
604
+ profile,
605
+ todayRunSeconds,
606
+ todayAdjustmentSeconds,
607
+ todayCreditedSeconds,
608
+ spentTodayAp,
609
+ spentTotalAp,
610
+ projectedTotalSeconds
611
+ };
612
+ }
613
+ function readTaskRunWindowsByTaskId(userId, range, now) {
614
+ const windows = new Map();
615
+ for (const row of readTaskRunRows(range, userId)) {
616
+ const startMs = Math.max(range.startMs, Date.parse(row.claimed_at));
617
+ const endMs = Math.min(range.endMs, terminalRunMs(row, now));
618
+ if (endMs <= startMs) {
619
+ continue;
620
+ }
621
+ const list = windows.get(row.task_id) ?? [];
622
+ list.push({ startMs, endMs });
623
+ windows.set(row.task_id, list);
624
+ }
625
+ return windows;
626
+ }
627
+ function buildWorkAdjustmentContributions(userId, range) {
628
+ return readTodayAdjustmentRows(userId, range)
629
+ .filter((row) => row.entity_type === "task")
630
+ .map((row) => {
631
+ const expectedDurationSeconds = resolveTaskExpectedDurationSeconds(row.planned_duration_seconds);
632
+ const totalAp = (DEFAULT_TASK_TOTAL_AP / expectedDurationSeconds) *
633
+ row.applied_delta_minutes *
634
+ 60;
635
+ return {
636
+ entityType: "task",
637
+ entityId: row.entity_id,
638
+ eventKind: "work_adjustment",
639
+ sourceKind: "work_adjustment",
640
+ totalAp,
641
+ rateApPerHour: null,
642
+ title: row.note?.trim() || "Manual work adjustment",
643
+ why: "Manual time adjustments count toward today's Action Point spend.",
644
+ startsAt: row.created_at,
645
+ endsAt: row.created_at,
646
+ role: "background",
647
+ metadata: {
648
+ adjustmentId: row.id,
649
+ appliedDeltaMinutes: row.applied_delta_minutes
650
+ }
651
+ };
652
+ });
653
+ }
654
+ function buildTaskRunContributions(userId, range, now) {
655
+ const contributions = [];
656
+ const totalsByTaskId = new Map();
657
+ const activeDrains = [];
658
+ for (const row of readTaskRunRows(range, userId)) {
659
+ const seconds = overlapSeconds(range, row, now);
660
+ if (seconds <= 0) {
661
+ continue;
662
+ }
663
+ const profile = resolveTaskActionProfile({
664
+ id: row.task_id,
665
+ plannedDurationSeconds: row.task_expected_duration_seconds ?? row.planned_duration_seconds
666
+ });
667
+ const totalAp = (seconds / 3600) * profile.sustainRateApPerHour;
668
+ const startsAt = new Date(Math.max(range.startMs, Date.parse(row.claimed_at))).toISOString();
669
+ const endsAt = new Date(Math.min(range.endMs, terminalRunMs(row, now))).toISOString();
670
+ const contribution = {
671
+ entityType: "task",
672
+ entityId: row.task_id,
673
+ eventKind: "task_run",
674
+ sourceKind: "task_run",
675
+ totalAp,
676
+ rateApPerHour: profile.sustainRateApPerHour,
677
+ title: row.task_title,
678
+ why: "Active timed work consumes Action Points proportionally to actual time worked today.",
679
+ startsAt,
680
+ endsAt,
681
+ role: row.is_current === 1 ? "primary" : "secondary",
682
+ metadata: { taskRunId: row.id }
683
+ };
684
+ contributions.push(contribution);
685
+ const existing = totalsByTaskId.get(row.task_id) ?? { todayAp: 0, totalAp: 0 };
686
+ existing.todayAp += totalAp;
687
+ existing.totalAp += totalAp;
688
+ totalsByTaskId.set(row.task_id, existing);
689
+ if (row.status === "active" && Date.parse(row.lease_expires_at) > now.getTime()) {
690
+ activeDrains.push({
691
+ ...contribution,
692
+ totalAp: 0
693
+ });
694
+ }
695
+ }
696
+ return { contributions, totalsByTaskId, activeDrains };
697
+ }
698
+ function buildNoteContributions(userId, range, now) {
699
+ try {
700
+ const taskRunWindowsByTaskId = readTaskRunWindowsByTaskId(userId, range, now);
701
+ const rows = getDatabase()
702
+ .prepare(`SELECT
703
+ notes.id,
704
+ notes.title,
705
+ notes.created_at,
706
+ GROUP_CONCAT(
707
+ CASE
708
+ WHEN note_links.entity_type = 'task' THEN note_links.entity_id
709
+ ELSE NULL
710
+ END
711
+ ) AS linked_task_ids
712
+ FROM notes
713
+ LEFT JOIN note_links ON note_links.note_id = notes.id
714
+ INNER JOIN entity_owners
715
+ ON entity_owners.entity_type = 'note'
716
+ AND entity_owners.entity_id = notes.id
717
+ AND entity_owners.role = 'owner'
718
+ WHERE entity_owners.user_id = ?
719
+ AND notes.created_at >= ?
720
+ AND notes.created_at < ?
721
+ GROUP BY notes.id, notes.title, notes.created_at`)
722
+ .all(userId, range.from, range.to);
723
+ return rows
724
+ .filter((row) => {
725
+ const createdAtMs = Date.parse(row.created_at);
726
+ const linkedTaskIds = (row.linked_task_ids ?? "")
727
+ .split(",")
728
+ .map((entry) => entry.trim())
729
+ .filter(Boolean);
730
+ return !linkedTaskIds.some((taskId) => (taskRunWindowsByTaskId.get(taskId) ?? []).some((window) => createdAtMs >= window.startMs && createdAtMs <= window.endMs));
731
+ })
732
+ .map((row) => ({
733
+ entityType: "note",
734
+ entityId: row.id,
735
+ eventKind: "note_created",
736
+ sourceKind: "note",
737
+ totalAp: 1,
738
+ rateApPerHour: null,
739
+ title: row.title || "Note",
740
+ why: "Standalone capture takes a small impulse of activation and focus.",
741
+ startsAt: row.created_at,
742
+ endsAt: row.created_at,
743
+ role: "background"
744
+ }));
745
+ }
746
+ catch {
747
+ return [];
748
+ }
749
+ }
750
+ function buildHabitContributions(userId, range) {
751
+ try {
752
+ const rows = getDatabase()
753
+ .prepare(`SELECT
754
+ habits.id,
755
+ habits.title,
756
+ habit_check_ins.created_at,
757
+ health_workout_sessions.id AS generated_workout_id
758
+ FROM habit_check_ins
759
+ INNER JOIN habits ON habits.id = habit_check_ins.habit_id
760
+ LEFT JOIN health_workout_sessions
761
+ ON health_workout_sessions.generated_from_check_in_id = habit_check_ins.id
762
+ INNER JOIN entity_owners
763
+ ON entity_owners.entity_type = 'habit'
764
+ AND entity_owners.entity_id = habits.id
765
+ AND entity_owners.role = 'owner'
766
+ WHERE entity_owners.user_id = ?
767
+ AND habit_check_ins.created_at >= ?
768
+ AND habit_check_ins.created_at < ?`)
769
+ .all(userId, range.from, range.to);
770
+ return rows
771
+ .filter((row) => row.generated_workout_id === null)
772
+ .map((row) => ({
773
+ entityType: "habit",
774
+ entityId: row.id,
775
+ eventKind: "habit_check_in",
776
+ sourceKind: "habit",
777
+ totalAp: 3,
778
+ rateApPerHour: null,
779
+ title: row.title,
780
+ why: "Habit execution still costs activation even when the action is short.",
781
+ startsAt: row.created_at,
782
+ endsAt: row.created_at,
783
+ role: "background"
784
+ }));
785
+ }
786
+ catch {
787
+ return [];
788
+ }
789
+ }
790
+ function buildWorkoutContributions(userId, range, now) {
791
+ let rows = [];
792
+ try {
793
+ rows = getDatabase()
794
+ .prepare(`SELECT id, workout_type, started_at, ended_at, duration_seconds, subjective_effort
795
+ FROM health_workout_sessions
796
+ WHERE user_id = ?
797
+ AND started_at < ?
798
+ AND ended_at >= ?`)
799
+ .all(userId, range.to, range.from);
800
+ }
801
+ catch {
802
+ rows = [];
803
+ }
804
+ const contributions = [];
805
+ const activeDrains = [];
806
+ for (const row of rows) {
807
+ const startMs = Math.max(range.startMs, Date.parse(row.started_at));
808
+ const endMs = Math.min(range.endMs, Date.parse(row.ended_at));
809
+ const seconds = Math.max(0, Math.floor((endMs - startMs) / 1000));
810
+ if (seconds <= 0) {
811
+ continue;
812
+ }
813
+ const effortMultiplier = row.subjective_effort
814
+ ? clamp(row.subjective_effort / 6, 0.8, 1.6)
815
+ : 1;
816
+ const rateApPerHour = 24 * effortMultiplier;
817
+ const contribution = {
818
+ entityType: "workout_session",
819
+ entityId: row.id,
820
+ eventKind: "workout_session",
821
+ sourceKind: "workout",
822
+ totalAp: (seconds / 3600) * rateApPerHour,
823
+ rateApPerHour,
824
+ title: row.workout_type,
825
+ why: "Workout sessions consume real physical capacity and should affect current load.",
826
+ startsAt: new Date(startMs).toISOString(),
827
+ endsAt: new Date(endMs).toISOString(),
828
+ role: "secondary"
829
+ };
830
+ contributions.push(contribution);
831
+ if (Date.parse(row.ended_at) > now.getTime()) {
832
+ activeDrains.push({ ...contribution, totalAp: 0 });
833
+ }
834
+ }
835
+ return { contributions, activeDrains };
836
+ }
837
+ function buildCalendarDrains(userId, now) {
838
+ const nowIsoValue = now.toISOString();
839
+ try {
840
+ const rows = getDatabase()
841
+ .prepare(`SELECT calendar_events.id, calendar_events.title, calendar_events.start_at, calendar_events.end_at
842
+ FROM calendar_events
843
+ INNER JOIN calendar_event_links
844
+ ON calendar_event_links.forge_event_id = calendar_events.id
845
+ INNER JOIN entity_owners
846
+ ON entity_owners.entity_type = calendar_event_links.entity_type
847
+ AND entity_owners.entity_id = calendar_event_links.entity_id
848
+ AND entity_owners.role = 'owner'
849
+ WHERE entity_owners.user_id = ?
850
+ AND calendar_events.deleted_at IS NULL
851
+ AND calendar_events.start_at <= ?
852
+ AND calendar_events.end_at > ?
853
+ GROUP BY calendar_events.id, calendar_events.title, calendar_events.start_at, calendar_events.end_at`)
854
+ .all(userId, nowIsoValue, nowIsoValue);
855
+ return rows.map((row) => ({
856
+ entityType: "calendar_event",
857
+ entityId: row.id,
858
+ eventKind: "calendar_context",
859
+ sourceKind: "calendar",
860
+ totalAp: 0,
861
+ rateApPerHour: 12,
862
+ title: row.title,
863
+ why: "Calendar context occupies mental and social capacity even before task work is logged.",
864
+ startsAt: row.start_at,
865
+ endsAt: row.end_at,
866
+ role: "background"
867
+ }));
868
+ }
869
+ catch {
870
+ return [];
871
+ }
872
+ }
873
+ function syncApLedger(userId, range, contributions) {
874
+ runInTransaction(() => {
875
+ const database = getDatabase();
876
+ database
877
+ .prepare(`DELETE FROM ap_ledger_events
878
+ WHERE user_id = ? AND date_key = ?`)
879
+ .run(userId, range.dateKey);
880
+ const insert = database.prepare(`INSERT INTO ap_ledger_events (
881
+ id,
882
+ user_id,
883
+ date_key,
884
+ entity_type,
885
+ entity_id,
886
+ event_kind,
887
+ source_kind,
888
+ starts_at,
889
+ ends_at,
890
+ total_ap,
891
+ rate_ap_per_hour,
892
+ metadata_json,
893
+ created_at
894
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
895
+ const createdAt = nowIso();
896
+ for (const contribution of contributions) {
897
+ insert.run(`ap_${randomUUID().replaceAll("-", "").slice(0, 12)}`, userId, range.dateKey, contribution.entityType, contribution.entityId, contribution.eventKind, contribution.sourceKind, contribution.startsAt, contribution.endsAt, Number(contribution.totalAp.toFixed(4)), contribution.rateApPerHour, JSON.stringify({
898
+ title: contribution.title,
899
+ why: contribution.why,
900
+ role: contribution.role,
901
+ ...(contribution.metadata ?? {})
902
+ }), createdAt);
903
+ }
904
+ });
905
+ }
906
+ function syncStatXpEvents(userId, dateKey, contributions) {
907
+ const totals = new Map();
908
+ for (const contribution of contributions) {
909
+ if (contribution.totalAp <= 0) {
910
+ continue;
911
+ }
912
+ const weights = contribution.profile?.demandWeights ?? {
913
+ activation: 0.2,
914
+ focus: 0.25,
915
+ vigor: 0.2,
916
+ composure: 0.15,
917
+ flow: 0.2
918
+ };
919
+ for (const [key, weight] of Object.entries(weights)) {
920
+ totals.set(key, Number(((totals.get(key) ?? 0) + contribution.totalAp * weight).toFixed(4)));
921
+ }
922
+ totals.set("life_force", Number(((totals.get("life_force") ?? 0) + contribution.totalAp * 0.35).toFixed(4)));
923
+ }
924
+ runInTransaction(() => {
925
+ const database = getDatabase();
926
+ database
927
+ .prepare(`DELETE FROM stat_xp_events
928
+ WHERE user_id = ?
929
+ AND json_extract(metadata_json, '$.dateKey') = ?`)
930
+ .run(userId, dateKey);
931
+ const insert = database.prepare(`INSERT INTO stat_xp_events (
932
+ id,
933
+ user_id,
934
+ stat_key,
935
+ delta_xp,
936
+ entity_type,
937
+ entity_id,
938
+ metadata_json,
939
+ created_at
940
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
941
+ const createdAt = nowIso();
942
+ for (const [statKey, deltaXp] of totals.entries()) {
943
+ insert.run(`statxp_${userId}_${dateKey}_${statKey}`, userId, statKey, deltaXp, "system", dateKey, JSON.stringify({
944
+ source: "life_force_daily_rollup",
945
+ dateKey
946
+ }), createdAt);
947
+ }
948
+ });
949
+ }
950
+ function readTodayLedger(userId, dateKey) {
951
+ return getDatabase()
952
+ .prepare(`SELECT *
953
+ FROM ap_ledger_events
954
+ WHERE user_id = ? AND date_key = ?
955
+ ORDER BY created_at ASC`)
956
+ .all(userId, dateKey);
957
+ }
958
+ function readTodayFatigueSignals(userId, dateKey) {
959
+ return getDatabase()
960
+ .prepare(`SELECT signal_type, delta
961
+ FROM fatigue_signals
962
+ WHERE user_id = ? AND date_key = ?`)
963
+ .all(userId, dateKey);
964
+ }
965
+ function buildWarnings(input) {
966
+ const warnings = [];
967
+ if (input.isOverloadedNow) {
968
+ warnings.push({
969
+ id: "lf_overload",
970
+ tone: "danger",
971
+ title: "You are overloaded right now",
972
+ detail: "Current concurrent work is draining more than the available instant capacity."
973
+ });
974
+ }
975
+ if (input.spentTodayAp > input.dailyBudgetAp) {
976
+ warnings.push({
977
+ id: "lf_overspent",
978
+ tone: "warning",
979
+ title: "Daily AP is in debt",
980
+ detail: "Today has already exceeded the calibrated Action Point budget."
981
+ });
982
+ }
983
+ if (input.topTaskIdsNeedingSplit.length > 0) {
984
+ warnings.push({
985
+ id: "lf_split",
986
+ tone: "info",
987
+ title: "A task wants to be split",
988
+ detail: "One or more tasks have grown beyond a healthy expected duration."
989
+ });
990
+ }
991
+ if (warnings.length === 0) {
992
+ warnings.push({
993
+ id: "lf_stable",
994
+ tone: "success",
995
+ title: "Life Force is stable",
996
+ detail: "Today is still inside a healthy capacity band."
997
+ });
998
+ }
999
+ return warnings;
1000
+ }
1001
+ export function resolveLifeForceUser(userIds) {
1002
+ if (userIds && userIds.length > 0) {
1003
+ return getUserById(userIds[0]) ?? getDefaultUser();
1004
+ }
1005
+ return getDefaultUser();
1006
+ }
1007
+ export function buildLifeForcePayload(now = new Date(), userIds) {
1008
+ ensureActionProfileTemplates();
1009
+ const user = resolveLifeForceUser(userIds);
1010
+ const profile = ensureLifeForceProfile(user.id);
1011
+ const snapshot = getOrCreateDaySnapshot(user.id, now);
1012
+ const range = buildDayRange(now);
1013
+ const taskRuns = buildTaskRunContributions(user.id, range, now);
1014
+ const notes = buildNoteContributions(user.id, range, now);
1015
+ const habits = buildHabitContributions(user.id, range);
1016
+ const workouts = buildWorkoutContributions(user.id, range, now);
1017
+ const adjustments = buildWorkAdjustmentContributions(user.id, range);
1018
+ const contributions = [
1019
+ ...taskRuns.contributions,
1020
+ ...adjustments,
1021
+ ...notes,
1022
+ ...habits,
1023
+ ...workouts.contributions
1024
+ ];
1025
+ const seededProfilesByKey = new Map(seededActionProfiles().map((entry) => [entry.profileKey, entry]));
1026
+ const taskDurationRows = getDatabase()
1027
+ .prepare(`SELECT id, planned_duration_seconds
1028
+ FROM tasks`)
1029
+ .all();
1030
+ const taskDurationById = new Map(taskDurationRows.map((row) => [row.id, row.planned_duration_seconds]));
1031
+ const profileLookup = new Map();
1032
+ for (const contribution of contributions) {
1033
+ if (contribution.entityType === "task") {
1034
+ profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, resolveTaskActionProfile({
1035
+ id: contribution.entityId,
1036
+ plannedDurationSeconds: taskDurationById.get(contribution.entityId) ?? null
1037
+ }));
1038
+ continue;
1039
+ }
1040
+ if (contribution.entityType === "note") {
1041
+ profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("note_quick") ?? null);
1042
+ continue;
1043
+ }
1044
+ if (contribution.entityType === "habit") {
1045
+ profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("habit_default") ?? null);
1046
+ continue;
1047
+ }
1048
+ if (contribution.entityType === "workout_session") {
1049
+ profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("workout_default") ?? null);
1050
+ }
1051
+ }
1052
+ const adjustmentApByTaskId = readTodayAdjustmentApByTaskId(user.id, range);
1053
+ for (const [taskId, adjustmentAp] of adjustmentApByTaskId.entries()) {
1054
+ const existing = taskRuns.totalsByTaskId.get(taskId) ?? { todayAp: 0, totalAp: 0 };
1055
+ existing.todayAp += adjustmentAp;
1056
+ existing.totalAp += adjustmentAp;
1057
+ taskRuns.totalsByTaskId.set(taskId, existing);
1058
+ }
1059
+ syncApLedger(user.id, range, contributions);
1060
+ syncStatXpEvents(user.id, range.dateKey, contributions.map((contribution) => ({
1061
+ ...contribution,
1062
+ profile: profileLookup.get(`${contribution.entityType}:${contribution.entityId}`) ?? null
1063
+ })));
1064
+ const ledger = readTodayLedger(user.id, range.dateKey);
1065
+ const spentTodayAp = ledger.reduce((sum, row) => sum + row.total_ap, 0);
1066
+ const currentCurve = parseCurvePoints(snapshot.points_json);
1067
+ const minuteOfDay = now.getUTCHours() * 60 + now.getUTCMinutes();
1068
+ const instantCapacityApPerHour = interpolateCurveRate(currentCurve, minuteOfDay);
1069
+ const activeDrains = [
1070
+ ...taskRuns.activeDrains,
1071
+ ...workouts.activeDrains,
1072
+ ...(taskRuns.activeDrains.length === 0 && workouts.activeDrains.length === 0
1073
+ ? buildCalendarDrains(user.id, now)
1074
+ : [])
1075
+ ]
1076
+ .sort((left, right) => (right.rateApPerHour ?? 0) - (left.rateApPerHour ?? 0))
1077
+ .map((entry, index) => ({
1078
+ id: `${entry.sourceKind}:${entry.entityId}`,
1079
+ sourceType: entry.entityType,
1080
+ sourceId: entry.entityId,
1081
+ title: entry.title,
1082
+ role: index === 0 ? "primary" : entry.role,
1083
+ apPerHour: Number((entry.rateApPerHour ?? 0).toFixed(2)),
1084
+ instantAp: Number((entry.totalAp ?? 0).toFixed(2)),
1085
+ why: entry.why,
1086
+ startedAt: entry.startsAt,
1087
+ endsAt: entry.endsAt
1088
+ }));
1089
+ const sortedRates = activeDrains
1090
+ .map((entry) => entry.apPerHour)
1091
+ .sort((left, right) => right - left);
1092
+ const currentDrainApPerHour = (sortedRates[0] ?? 0) +
1093
+ (sortedRates[1] ?? 0) * 0.6 +
1094
+ (sortedRates[2] ?? 0) * 0.35 +
1095
+ sortedRates.slice(3).reduce((sum, value) => sum + value * 0.2, 0);
1096
+ const fatigueFromSignals = readTodayFatigueSignals(user.id, range.dateKey).reduce((sum, signal) => sum + signal.delta, 0);
1097
+ const fatigueBufferApPerHour = Math.max(0, Number((fatigueFromSignals + Math.max(0, activeDrains.length - 1) * 1.5).toFixed(2)));
1098
+ const rawInstantFreeApPerHour = Number((instantCapacityApPerHour - currentDrainApPerHour - fatigueBufferApPerHour).toFixed(2));
1099
+ const instantFreeApPerHour = Math.max(0, rawInstantFreeApPerHour);
1100
+ const overloadApPerHour = Math.max(0, Number((-rawInstantFreeApPerHour).toFixed(2)));
1101
+ const remainingAp = Number((snapshot.daily_budget_ap - spentTodayAp).toFixed(2));
1102
+ const forecastAp = Number((spentTodayAp + currentDrainApPerHour * 2).toFixed(2));
1103
+ const targetBandMinAp = Number((snapshot.daily_budget_ap * 0.85).toFixed(2));
1104
+ const targetBandMaxAp = Number(snapshot.daily_budget_ap.toFixed(2));
1105
+ const workTime = computeWorkTime(now);
1106
+ const topTaskIdsNeedingSplit = getDatabase()
1107
+ .prepare(`SELECT tasks.id, tasks.planned_duration_seconds
1108
+ FROM tasks
1109
+ INNER JOIN entity_owners
1110
+ ON entity_owners.entity_type = 'task'
1111
+ AND entity_owners.entity_id = tasks.id
1112
+ AND entity_owners.role = 'owner'
1113
+ WHERE entity_owners.user_id = ?`)
1114
+ .all(user.id)
1115
+ .map((row) => row)
1116
+ .filter((row) => {
1117
+ const time = workTime.taskSummaries.get(row.id);
1118
+ return buildTaskSplitSuggestion({
1119
+ plannedDurationSeconds: row.planned_duration_seconds,
1120
+ totalTrackedSeconds: time?.totalCreditedSeconds ?? 0,
1121
+ projectedTotalSeconds: (time?.totalCreditedSeconds ?? 0) +
1122
+ readActiveTaskRunProjectionRows(row.id).reduce((sum, activeRow) => sum + computeProjectedRemainingSeconds(activeRow, now), 0)
1123
+ }).shouldSplit;
1124
+ })
1125
+ .map((row) => row.id)
1126
+ .slice(0, 3);
1127
+ return lifeForcePayloadSchema.parse({
1128
+ userId: user.id,
1129
+ dateKey: range.dateKey,
1130
+ baselineDailyAp: profile.base_daily_ap,
1131
+ dailyBudgetAp: Number(snapshot.daily_budget_ap.toFixed(2)),
1132
+ spentTodayAp: Number(spentTodayAp.toFixed(2)),
1133
+ remainingAp,
1134
+ forecastAp,
1135
+ targetBandMinAp,
1136
+ targetBandMaxAp,
1137
+ instantCapacityApPerHour: Number(instantCapacityApPerHour.toFixed(2)),
1138
+ instantFreeApPerHour,
1139
+ overloadApPerHour,
1140
+ currentDrainApPerHour: Number(currentDrainApPerHour.toFixed(2)),
1141
+ fatigueBufferApPerHour,
1142
+ sleepRecoveryMultiplier: snapshot.sleep_recovery_multiplier,
1143
+ readinessMultiplier: snapshot.readiness_multiplier,
1144
+ fatigueDebtCarry: snapshot.fatigue_debt_carry,
1145
+ stats: buildStats(profile, user.id),
1146
+ currentCurve: currentCurve.map((point) => ({
1147
+ ...point,
1148
+ locked: point.minuteOfDay <= minuteOfDay
1149
+ })),
1150
+ activeDrains,
1151
+ warnings: buildWarnings({
1152
+ spentTodayAp,
1153
+ dailyBudgetAp: snapshot.daily_budget_ap,
1154
+ isOverloadedNow: rawInstantFreeApPerHour < 0,
1155
+ topTaskIdsNeedingSplit
1156
+ }),
1157
+ recommendations: [
1158
+ instantFreeApPerHour <= 0
1159
+ ? "Reduce overlap or take a recovery action before starting something new."
1160
+ : instantCapacityApPerHour > currentDrainApPerHour + 4
1161
+ ? "This is a good moment for deep work."
1162
+ : "Favor lower-friction admin or recovery until headroom increases."
1163
+ ],
1164
+ topTaskIdsNeedingSplit,
1165
+ updatedAt: now.toISOString()
1166
+ });
1167
+ }
1168
+ export function buildTaskLifeForceFields(task, userId) {
1169
+ const effectiveUserId = userId ?? task.userId ?? getDefaultUser().id;
1170
+ const runtime = buildTaskLifeForceRuntime(task, effectiveUserId);
1171
+ return {
1172
+ actionPointSummary: buildTaskActionPointSummary({
1173
+ plannedDurationSeconds: task.plannedDurationSeconds,
1174
+ totalCostAp: runtime.profile.totalCostAp,
1175
+ spentTodayAp: runtime.spentTodayAp,
1176
+ spentTotalAp: runtime.spentTotalAp
1177
+ }),
1178
+ splitSuggestion: buildTaskSplitSuggestion({
1179
+ plannedDurationSeconds: task.plannedDurationSeconds,
1180
+ totalTrackedSeconds: task.time.totalCreditedSeconds,
1181
+ projectedTotalSeconds: runtime.projectedTotalSeconds
1182
+ })
1183
+ };
1184
+ }
1185
+ export function getTaskCompletionRequirement(task, userId) {
1186
+ const effectiveUserId = userId ?? task.userId ?? getDefaultUser().id;
1187
+ const runtime = buildTaskLifeForceRuntime(task, effectiveUserId);
1188
+ return {
1189
+ todayCreditedSeconds: runtime.todayCreditedSeconds,
1190
+ requiresWorkLog: runtime.todayCreditedSeconds <= 0
1191
+ };
1192
+ }
1193
+ export function updateLifeForceProfile(userId, patch) {
1194
+ const parsed = lifeForceProfilePatchSchema.parse(patch);
1195
+ const current = ensureLifeForceProfile(userId);
1196
+ const next = {
1197
+ base_daily_ap: parsed.baseDailyAp ?? current.base_daily_ap,
1198
+ readiness_multiplier: parsed.readinessMultiplier ?? current.readiness_multiplier,
1199
+ life_force_level: parsed.stats?.life_force ?? current.life_force_level,
1200
+ activation_level: parsed.stats?.activation ?? current.activation_level,
1201
+ focus_level: parsed.stats?.focus ?? current.focus_level,
1202
+ vigor_level: parsed.stats?.vigor ?? current.vigor_level,
1203
+ composure_level: parsed.stats?.composure ?? current.composure_level,
1204
+ flow_level: parsed.stats?.flow ?? current.flow_level
1205
+ };
1206
+ getDatabase()
1207
+ .prepare(`UPDATE life_force_profiles
1208
+ SET base_daily_ap = ?,
1209
+ readiness_multiplier = ?,
1210
+ life_force_level = ?,
1211
+ activation_level = ?,
1212
+ focus_level = ?,
1213
+ vigor_level = ?,
1214
+ composure_level = ?,
1215
+ flow_level = ?,
1216
+ updated_at = ?
1217
+ WHERE user_id = ?`)
1218
+ .run(next.base_daily_ap, next.readiness_multiplier, next.life_force_level, next.activation_level, next.focus_level, next.vigor_level, next.composure_level, next.flow_level, nowIso(), userId);
1219
+ const todayKey = toDateKey(new Date());
1220
+ getDatabase()
1221
+ .prepare(`DELETE FROM life_force_day_snapshots
1222
+ WHERE user_id = ? AND date_key = ?`)
1223
+ .run(userId, todayKey);
1224
+ return buildLifeForcePayload(new Date(), [userId]);
1225
+ }
1226
+ export function updateLifeForceTemplate(userId, weekday, input) {
1227
+ const parsed = lifeForceTemplateUpdateSchema.parse(input);
1228
+ ensureWeekdayTemplates(userId);
1229
+ const normalized = normalizeCurveToBudget([...parsed.points].sort((left, right) => left.minuteOfDay - right.minuteOfDay), LIFE_FORCE_BASELINE_DAILY_AP);
1230
+ getDatabase()
1231
+ .prepare(`UPDATE life_force_weekday_templates
1232
+ SET points_json = ?, updated_at = ?
1233
+ WHERE user_id = ? AND weekday = ?`)
1234
+ .run(JSON.stringify(normalized), nowIso(), userId, weekday);
1235
+ return normalized;
1236
+ }
1237
+ export function createFatigueSignal(userId, input) {
1238
+ const parsed = fatigueSignalCreateSchema.parse(input);
1239
+ const observedAt = parsed.observedAt ?? nowIso();
1240
+ const dateKey = observedAt.slice(0, 10);
1241
+ const delta = parsed.signalType === "tired" ? 4 : -4;
1242
+ getDatabase()
1243
+ .prepare(`INSERT INTO fatigue_signals (
1244
+ id,
1245
+ user_id,
1246
+ date_key,
1247
+ signal_type,
1248
+ observed_at,
1249
+ note,
1250
+ delta,
1251
+ created_at
1252
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1253
+ .run(`fatigue_${randomUUID().replaceAll("-", "").slice(0, 12)}`, userId, dateKey, parsed.signalType, observedAt, parsed.note ?? "", delta, nowIso());
1254
+ return buildLifeForcePayload(new Date(observedAt), [userId]);
1255
+ }
1256
+ export function listLifeForceTemplates(userId) {
1257
+ ensureWeekdayTemplates(userId);
1258
+ return getDatabase()
1259
+ .prepare(`SELECT *
1260
+ FROM life_force_weekday_templates
1261
+ WHERE user_id = ?
1262
+ ORDER BY weekday ASC`)
1263
+ .all(userId)
1264
+ .map((row) => row)
1265
+ .map((row) => ({
1266
+ weekday: row.weekday,
1267
+ baselineDailyAp: row.baseline_daily_ap,
1268
+ points: parseCurvePoints(row.points_json)
1269
+ }));
1270
+ }