@murphai/murph 1.3.1 → 1.3.2

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 (779) hide show
  1. package/CHANGELOG.md +808 -0
  2. package/config.schema.json +91 -40
  3. package/dist/bin.js +6 -4
  4. package/dist/bin.js.map +1 -1
  5. package/dist/cli-entry.d.ts +8 -1
  6. package/dist/cli-entry.d.ts.map +1 -1
  7. package/dist/cli-entry.js +79 -3
  8. package/dist/cli-entry.js.map +1 -1
  9. package/dist/commands/audit-command-helpers.d.ts.map +1 -1
  10. package/dist/commands/audit-command-helpers.js +7 -5
  11. package/dist/commands/audit-command-helpers.js.map +1 -1
  12. package/dist/commands/batch.d.ts +5 -5
  13. package/dist/commands/batch.d.ts.map +1 -1
  14. package/dist/commands/batch.js +2 -21
  15. package/dist/commands/batch.js.map +1 -1
  16. package/dist/commands/commons.d.ts.map +1 -1
  17. package/dist/commands/commons.js +2 -4
  18. package/dist/commands/commons.js.map +1 -1
  19. package/dist/commands/exercise.d.ts.map +1 -1
  20. package/dist/commands/exercise.js +16 -3
  21. package/dist/commands/exercise.js.map +1 -1
  22. package/dist/commands/experiment-progress-card-image.d.ts.map +1 -1
  23. package/dist/commands/experiment-progress-card-image.js +63 -12
  24. package/dist/commands/experiment-progress-card-image.js.map +1 -1
  25. package/dist/commands/experiment.d.ts.map +1 -1
  26. package/dist/commands/experiment.js +45 -5
  27. package/dist/commands/experiment.js.map +1 -1
  28. package/dist/commands/export-intake-read-helpers.d.ts.map +1 -1
  29. package/dist/commands/export-intake-read-helpers.js +2 -3
  30. package/dist/commands/export-intake-read-helpers.js.map +1 -1
  31. package/dist/commands/food.d.ts.map +1 -1
  32. package/dist/commands/food.js +1 -7
  33. package/dist/commands/food.js.map +1 -1
  34. package/dist/commands/habitat.d.ts.map +1 -1
  35. package/dist/commands/habitat.js +120 -9
  36. package/dist/commands/habitat.js.map +1 -1
  37. package/dist/commands/health-blood-test-save.d.ts.map +1 -1
  38. package/dist/commands/health-blood-test-save.js +42 -8
  39. package/dist/commands/health-blood-test-save.js.map +1 -1
  40. package/dist/commands/meal.d.ts.map +1 -1
  41. package/dist/commands/meal.js +4 -1
  42. package/dist/commands/meal.js.map +1 -1
  43. package/dist/commands/measurement.d.ts.map +1 -1
  44. package/dist/commands/measurement.js +3 -0
  45. package/dist/commands/measurement.js.map +1 -1
  46. package/dist/commands/murph-age.d.ts +9 -1
  47. package/dist/commands/murph-age.d.ts.map +1 -1
  48. package/dist/commands/murph-age.js +132 -7
  49. package/dist/commands/murph-age.js.map +1 -1
  50. package/dist/commands/public-validation-issue.d.ts +7 -0
  51. package/dist/commands/public-validation-issue.d.ts.map +1 -0
  52. package/dist/commands/public-validation-issue.js +7 -0
  53. package/dist/commands/public-validation-issue.js.map +1 -0
  54. package/dist/commands/scheduled-log.d.ts +12 -0
  55. package/dist/commands/scheduled-log.d.ts.map +1 -1
  56. package/dist/commands/scheduled-log.js +456 -106
  57. package/dist/commands/scheduled-log.js.map +1 -1
  58. package/dist/commands/wearables.d.ts +286 -0
  59. package/dist/commands/wearables.d.ts.map +1 -1
  60. package/dist/commands/wearables.js +33 -1
  61. package/dist/commands/wearables.js.map +1 -1
  62. package/dist/commands/workout-live.d.ts.map +1 -1
  63. package/dist/commands/workout-live.js +123 -42
  64. package/dist/commands/workout-live.js.map +1 -1
  65. package/dist/commands/workout-typed-options.d.ts +38 -0
  66. package/dist/commands/workout-typed-options.d.ts.map +1 -0
  67. package/dist/commands/workout-typed-options.js +90 -0
  68. package/dist/commands/workout-typed-options.js.map +1 -0
  69. package/dist/commands/workout.d.ts.map +1 -1
  70. package/dist/commands/workout.js +80 -73
  71. package/dist/commands/workout.js.map +1 -1
  72. package/dist/hosted-data-api-labels.d.ts.map +1 -1
  73. package/dist/hosted-data-api-labels.js +161 -30
  74. package/dist/hosted-data-api-labels.js.map +1 -1
  75. package/dist/incur-error-bridge.d.ts.map +1 -1
  76. package/dist/incur-error-bridge.js +7 -15
  77. package/dist/incur-error-bridge.js.map +1 -1
  78. package/dist/incur.generated.d.ts +22 -13
  79. package/dist/incur.generated.d.ts.map +1 -1
  80. package/dist/mapbox-address.d.ts.map +1 -1
  81. package/dist/mapbox-address.js +3 -4
  82. package/dist/mapbox-address.js.map +1 -1
  83. package/dist/mapbox-route-client.d.ts +2 -2
  84. package/dist/mapbox-route-client.d.ts.map +1 -1
  85. package/dist/mapbox-route-client.js +49 -12
  86. package/dist/mapbox-route-client.js.map +1 -1
  87. package/dist/mapbox-route-directions.d.ts.map +1 -1
  88. package/dist/mapbox-route-directions.js +0 -1
  89. package/dist/mapbox-route-directions.js.map +1 -1
  90. package/dist/mapbox-route-elevation.js +0 -1
  91. package/dist/mapbox-route-elevation.js.map +1 -1
  92. package/dist/mapbox-route-points.js +0 -2
  93. package/dist/mapbox-route-points.js.map +1 -1
  94. package/dist/mapbox-route.d.ts.map +1 -1
  95. package/dist/mapbox-route.js +1 -1
  96. package/dist/mapbox-route.js.map +1 -1
  97. package/dist/vault-cli-command-manifest.d.ts +304 -6
  98. package/dist/vault-cli-command-manifest.d.ts.map +1 -1
  99. package/dist/vault-cli-skill-hash.generated.d.ts +1 -1
  100. package/dist/vault-cli-skill-hash.generated.js +1 -1
  101. package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.d.ts.map +1 -1
  102. package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js +6 -2
  103. package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js.map +1 -1
  104. package/node_modules/@murphai/assistant-engine/README.md +4 -3
  105. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts +15 -1
  106. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts.map +1 -1
  107. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js +121 -24
  108. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js.map +1 -1
  109. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js +2 -2
  110. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js.map +1 -1
  111. package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.d.ts.map +1 -1
  112. package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js +1 -0
  113. package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js.map +1 -1
  114. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.d.ts.map +1 -1
  115. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js +55 -7
  116. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js.map +1 -1
  117. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.d.ts.map +1 -1
  118. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js +9 -1
  119. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js.map +1 -1
  120. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts +1 -0
  121. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts.map +1 -1
  122. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.js.map +1 -1
  123. package/node_modules/@murphai/assistant-engine/dist/assistant/cli-surface-contract.generated.json +1 -1
  124. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts +1 -1
  125. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts.map +1 -1
  126. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js +7 -7
  127. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js.map +1 -1
  128. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts +1 -1
  129. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts.map +1 -1
  130. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js +123 -22
  131. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js.map +1 -1
  132. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.d.ts.map +1 -1
  133. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js +48 -19
  134. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js.map +1 -1
  135. package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js +3 -7
  136. package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js.map +1 -1
  137. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts +5 -0
  138. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts.map +1 -1
  139. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js +24 -10
  140. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js.map +1 -1
  141. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts +2 -0
  142. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts.map +1 -1
  143. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js +12 -0
  144. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js.map +1 -1
  145. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts +5 -0
  146. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts.map +1 -1
  147. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js +9 -10
  148. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js.map +1 -1
  149. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts +41 -9
  150. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts.map +1 -1
  151. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js +9 -0
  152. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js.map +1 -1
  153. package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.d.ts.map +1 -1
  154. package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js +2 -0
  155. package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js.map +1 -1
  156. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts +3 -0
  157. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts.map +1 -1
  158. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js +8 -0
  159. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js.map +1 -1
  160. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts +3 -0
  161. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts.map +1 -1
  162. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js +26 -1
  163. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js.map +1 -1
  164. package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.d.ts.map +1 -1
  165. package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js +687 -370
  166. package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js.map +1 -1
  167. package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.d.ts.map +1 -1
  168. package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js +54 -18
  169. package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js.map +1 -1
  170. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts +2 -3
  171. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts.map +1 -1
  172. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js +10 -9
  173. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js.map +1 -1
  174. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts +1 -1
  175. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts.map +1 -1
  176. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js +146 -18
  177. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js.map +1 -1
  178. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts +25 -0
  179. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts.map +1 -0
  180. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js +72 -0
  181. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js.map +1 -0
  182. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts +4 -0
  183. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts.map +1 -1
  184. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js +14 -0
  185. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js.map +1 -1
  186. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts +1 -1
  187. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts.map +1 -1
  188. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js +8 -5
  189. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js.map +1 -1
  190. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts +4 -0
  191. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts.map +1 -1
  192. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js +8 -0
  193. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js.map +1 -1
  194. package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js +1 -0
  195. package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js.map +1 -1
  196. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.d.ts.map +1 -1
  197. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js +25 -2
  198. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js.map +1 -1
  199. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts +11 -3
  200. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts.map +1 -1
  201. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js +67 -176
  202. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js.map +1 -1
  203. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts +17 -0
  204. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts.map +1 -1
  205. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.js.map +1 -1
  206. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts +1 -1
  207. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts.map +1 -1
  208. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js +10 -0
  209. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js.map +1 -1
  210. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts +19 -4
  211. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts.map +1 -1
  212. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js +76 -40
  213. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js.map +1 -1
  214. package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts +3 -1
  215. package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts.map +1 -1
  216. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts +4 -2
  217. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts.map +1 -1
  218. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js +8 -7
  219. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js.map +1 -1
  220. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts +1 -1
  221. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts.map +1 -1
  222. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js +1 -1
  223. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js.map +1 -1
  224. package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts +1 -0
  225. package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts.map +1 -1
  226. package/node_modules/@murphai/assistant-engine/dist/assistant/store.d.ts.map +1 -1
  227. package/node_modules/@murphai/assistant-engine/dist/assistant/store.js +6 -0
  228. package/node_modules/@murphai/assistant-engine/dist/assistant/store.js.map +1 -1
  229. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts +3 -0
  230. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts.map +1 -1
  231. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js +57 -29
  232. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js.map +1 -1
  233. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts +1 -0
  234. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts.map +1 -1
  235. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js +17 -3
  236. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js.map +1 -1
  237. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts +4 -7
  238. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts.map +1 -1
  239. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js +21 -6
  240. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js.map +1 -1
  241. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts +1 -0
  242. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts.map +1 -1
  243. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js +6 -0
  244. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js.map +1 -1
  245. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts +1 -1
  246. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts.map +1 -1
  247. package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts +1 -1
  248. package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts.map +1 -1
  249. package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.js.map +1 -1
  250. package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js +8 -0
  251. package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js.map +1 -1
  252. package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.d.ts.map +1 -1
  253. package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js +1 -0
  254. package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js.map +1 -1
  255. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts +49 -0
  256. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts.map +1 -0
  257. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js +387 -0
  258. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js.map +1 -0
  259. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts +1 -1
  260. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts.map +1 -1
  261. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js +12 -13
  262. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js.map +1 -1
  263. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts +1 -0
  264. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts.map +1 -1
  265. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js +16 -5
  266. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js.map +1 -1
  267. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts +1 -1
  268. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts.map +1 -1
  269. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js +2 -1
  270. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js.map +1 -1
  271. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts +4 -0
  272. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts.map +1 -1
  273. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js +26 -13
  274. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js.map +1 -1
  275. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts +1024 -1091
  276. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts.map +1 -1
  277. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js +354 -330
  278. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js.map +1 -1
  279. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts +50 -0
  280. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts.map +1 -0
  281. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js +79 -0
  282. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js.map +1 -0
  283. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts +4 -0
  284. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts.map +1 -0
  285. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js +153 -0
  286. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js.map +1 -0
  287. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts +4 -1
  288. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts.map +1 -1
  289. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js +29 -35
  290. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js.map +1 -1
  291. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts +4 -1
  292. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts.map +1 -1
  293. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js +43 -9
  294. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js.map +1 -1
  295. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts +6 -0
  296. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts.map +1 -1
  297. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.js.map +1 -1
  298. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts +60 -0
  299. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts.map +1 -0
  300. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js +91 -0
  301. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js.map +1 -0
  302. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts +41 -1
  303. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts.map +1 -1
  304. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js +85 -9
  305. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js.map +1 -1
  306. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts +25 -2
  307. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts.map +1 -1
  308. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js +280 -89
  309. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js.map +1 -1
  310. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/failures.js +1 -1
  311. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.d.ts.map +1 -1
  312. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js +1 -3
  313. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js.map +1 -1
  314. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts +19 -0
  315. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts.map +1 -0
  316. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js +203 -0
  317. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js.map +1 -0
  318. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts +9 -1
  319. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts.map +1 -1
  320. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js +220 -130
  321. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js.map +1 -1
  322. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts +1 -1
  323. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts.map +1 -1
  324. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js +1 -1
  325. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js.map +1 -1
  326. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts +1 -1
  327. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts.map +1 -1
  328. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js +1 -1
  329. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js.map +1 -1
  330. package/node_modules/@murphai/assistant-engine/dist/index.d.ts +2 -1
  331. package/node_modules/@murphai/assistant-engine/dist/index.d.ts.map +1 -1
  332. package/node_modules/@murphai/assistant-engine/dist/index.js +1 -0
  333. package/node_modules/@murphai/assistant-engine/dist/index.js.map +1 -1
  334. package/node_modules/@murphai/assistant-engine/skills/automatic-meal-capture/SKILL.md +111 -68
  335. package/node_modules/@murphai/assistant-engine/skills/behavior-followthrough/SKILL.md +0 -2
  336. package/node_modules/@murphai/assistant-engine/skills/daily-activity/SKILL.md +11 -4
  337. package/node_modules/@murphai/assistant-engine/skills/food-journal/SKILL.md +83 -22
  338. package/node_modules/@murphai/assistant-engine/skills/hosted-low-usage/SKILL.md +3 -3
  339. package/node_modules/@murphai/assistant-engine/skills/murph-family/SKILL.md +1 -1
  340. package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/SKILL.md +17 -11
  341. package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/references/daily-nutrition-card-goals.md +62 -34
  342. package/node_modules/@murphai/assistant-engine/skills/physical-notes/SKILL.md +47 -0
  343. package/node_modules/@murphai/assistant-engine/skills/shared/exercise-catalog-runtime.md +6 -0
  344. package/node_modules/@murphai/assistant-engine/skills/signup-link/SKILL.md +1 -1
  345. package/node_modules/@murphai/assistant-engine/skills/strength-training/SKILL.md +12 -5
  346. package/node_modules/@murphai/assistant-engine/skills/tracked-table/SKILL.md +156 -66
  347. package/node_modules/@murphai/core/dist/automation.d.ts.map +1 -1
  348. package/node_modules/@murphai/core/dist/automation.js +17 -9
  349. package/node_modules/@murphai/core/dist/automation.js.map +1 -1
  350. package/node_modules/@murphai/core/dist/bank/habitat.d.ts +2 -0
  351. package/node_modules/@murphai/core/dist/bank/habitat.d.ts.map +1 -1
  352. package/node_modules/@murphai/core/dist/bank/habitat.js +27 -0
  353. package/node_modules/@murphai/core/dist/bank/habitat.js.map +1 -1
  354. package/node_modules/@murphai/core/dist/bank/regimens.js +1 -1
  355. package/node_modules/@murphai/core/dist/bank/regimens.js.map +1 -1
  356. package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts +21 -0
  357. package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts.map +1 -1
  358. package/node_modules/@murphai/core/dist/domains/events/ledger.js +102 -8
  359. package/node_modules/@murphai/core/dist/domains/events/ledger.js.map +1 -1
  360. package/node_modules/@murphai/core/dist/domains/events.d.ts +2 -2
  361. package/node_modules/@murphai/core/dist/domains/events.d.ts.map +1 -1
  362. package/node_modules/@murphai/core/dist/domains/events.js +1 -1
  363. package/node_modules/@murphai/core/dist/domains/events.js.map +1 -1
  364. package/node_modules/@murphai/core/dist/history/api.d.ts.map +1 -1
  365. package/node_modules/@murphai/core/dist/history/api.js +18 -3
  366. package/node_modules/@murphai/core/dist/history/api.js.map +1 -1
  367. package/node_modules/@murphai/core/dist/index.d.ts +4 -2
  368. package/node_modules/@murphai/core/dist/index.d.ts.map +1 -1
  369. package/node_modules/@murphai/core/dist/index.js +3 -1
  370. package/node_modules/@murphai/core/dist/index.js.map +1 -1
  371. package/node_modules/@murphai/core/dist/mutations.d.ts +25 -2
  372. package/node_modules/@murphai/core/dist/mutations.d.ts.map +1 -1
  373. package/node_modules/@murphai/core/dist/mutations.js +1588 -78
  374. package/node_modules/@murphai/core/dist/mutations.js.map +1 -1
  375. package/node_modules/@murphai/core/dist/operations/index.d.ts +1 -1
  376. package/node_modules/@murphai/core/dist/operations/index.d.ts.map +1 -1
  377. package/node_modules/@murphai/core/dist/operations/index.js +1 -1
  378. package/node_modules/@murphai/core/dist/operations/index.js.map +1 -1
  379. package/node_modules/@murphai/core/dist/operations/write-batch.d.ts +2 -0
  380. package/node_modules/@murphai/core/dist/operations/write-batch.d.ts.map +1 -1
  381. package/node_modules/@murphai/core/dist/operations/write-batch.js +7 -0
  382. package/node_modules/@murphai/core/dist/operations/write-batch.js.map +1 -1
  383. package/node_modules/@murphai/core/dist/protocols.d.ts.map +1 -1
  384. package/node_modules/@murphai/core/dist/protocols.js +52 -9
  385. package/node_modules/@murphai/core/dist/protocols.js.map +1 -1
  386. package/node_modules/@murphai/core/dist/public-mutations.d.ts +1 -1
  387. package/node_modules/@murphai/core/dist/public-mutations.d.ts.map +1 -1
  388. package/node_modules/@murphai/core/dist/public-mutations.js +2 -2
  389. package/node_modules/@murphai/core/dist/public-mutations.js.map +1 -1
  390. package/node_modules/@murphai/core/dist/scheduled-logs.d.ts +3 -0
  391. package/node_modules/@murphai/core/dist/scheduled-logs.d.ts.map +1 -1
  392. package/node_modules/@murphai/core/dist/scheduled-logs.js +65 -29
  393. package/node_modules/@murphai/core/dist/scheduled-logs.js.map +1 -1
  394. package/node_modules/@murphai/device-syncd/README.md +140 -46
  395. package/node_modules/@murphai/device-syncd/dist/client.d.ts +14 -0
  396. package/node_modules/@murphai/device-syncd/dist/client.d.ts.map +1 -1
  397. package/node_modules/@murphai/device-syncd/dist/client.js.map +1 -1
  398. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts +4 -1
  399. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts.map +1 -1
  400. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js +24 -2
  401. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js.map +1 -1
  402. package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js +2 -2
  403. package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js.map +1 -1
  404. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts +53 -0
  405. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts.map +1 -1
  406. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js +13 -0
  407. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js.map +1 -1
  408. package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts +1 -1
  409. package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts.map +1 -1
  410. package/node_modules/@murphai/device-syncd/dist/connect-config.js +1 -1
  411. package/node_modules/@murphai/device-syncd/dist/connect-config.js.map +1 -1
  412. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts +57 -0
  413. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts.map +1 -0
  414. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js +166 -0
  415. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js.map +1 -0
  416. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts +43 -0
  417. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts.map +1 -1
  418. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js +129 -5
  419. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js.map +1 -1
  420. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts +3 -0
  421. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts.map +1 -1
  422. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js +36 -16
  423. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js.map +1 -1
  424. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts +17 -2
  425. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts.map +1 -1
  426. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js +99 -40
  427. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js.map +1 -1
  428. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts +17 -0
  429. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts.map +1 -0
  430. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js +45 -0
  431. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js.map +1 -0
  432. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts +8 -1
  433. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts.map +1 -1
  434. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js +12 -1
  435. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js.map +1 -1
  436. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts +7 -0
  437. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts.map +1 -1
  438. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js +285 -46
  439. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js.map +1 -1
  440. package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts +9 -0
  441. package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts.map +1 -1
  442. package/node_modules/@murphai/device-syncd/dist/providers/junction.js +2045 -367
  443. package/node_modules/@murphai/device-syncd/dist/providers/junction.js.map +1 -1
  444. package/node_modules/@murphai/device-syncd/dist/public-account.d.ts +2 -0
  445. package/node_modules/@murphai/device-syncd/dist/public-account.d.ts.map +1 -1
  446. package/node_modules/@murphai/device-syncd/dist/public-account.js +96 -0
  447. package/node_modules/@murphai/device-syncd/dist/public-account.js.map +1 -1
  448. package/node_modules/@murphai/device-syncd/dist/public-ingress.d.ts.map +1 -1
  449. package/node_modules/@murphai/device-syncd/dist/public-ingress.js +6 -4
  450. package/node_modules/@murphai/device-syncd/dist/public-ingress.js.map +1 -1
  451. package/node_modules/@murphai/device-syncd/dist/service.d.ts +6 -2
  452. package/node_modules/@murphai/device-syncd/dist/service.d.ts.map +1 -1
  453. package/node_modules/@murphai/device-syncd/dist/service.js +385 -44
  454. package/node_modules/@murphai/device-syncd/dist/service.js.map +1 -1
  455. package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts +15 -0
  456. package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts.map +1 -1
  457. package/node_modules/@murphai/device-syncd/dist/source-staleness.js +25 -1
  458. package/node_modules/@murphai/device-syncd/dist/source-staleness.js.map +1 -1
  459. package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts +10 -1
  460. package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts.map +1 -1
  461. package/node_modules/@murphai/device-syncd/dist/store/jobs.js +95 -1
  462. package/node_modules/@murphai/device-syncd/dist/store/jobs.js.map +1 -1
  463. package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts +1 -1
  464. package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts.map +1 -1
  465. package/node_modules/@murphai/device-syncd/dist/store/schema.js +25 -3
  466. package/node_modules/@murphai/device-syncd/dist/store/schema.js.map +1 -1
  467. package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts +24 -2
  468. package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts.map +1 -1
  469. package/node_modules/@murphai/device-syncd/dist/store/sources.js +223 -14
  470. package/node_modules/@murphai/device-syncd/dist/store/sources.js.map +1 -1
  471. package/node_modules/@murphai/device-syncd/dist/store.d.ts +13 -3
  472. package/node_modules/@murphai/device-syncd/dist/store.d.ts.map +1 -1
  473. package/node_modules/@murphai/device-syncd/dist/store.js +104 -22
  474. package/node_modules/@murphai/device-syncd/dist/store.js.map +1 -1
  475. package/node_modules/@murphai/device-syncd/dist/types.d.ts +96 -9
  476. package/node_modules/@murphai/device-syncd/dist/types.d.ts.map +1 -1
  477. package/node_modules/@murphai/device-syncd/dist/types.js +4 -0
  478. package/node_modules/@murphai/device-syncd/dist/types.js.map +1 -1
  479. package/node_modules/@murphai/device-syncd/package.json +8 -0
  480. package/node_modules/@murphai/health-metrics/dist/definitions/activity.d.ts.map +1 -1
  481. package/node_modules/@murphai/health-metrics/dist/definitions/activity.js +66 -0
  482. package/node_modules/@murphai/health-metrics/dist/definitions/activity.js.map +1 -1
  483. package/node_modules/@murphai/health-metrics/dist/definitions/body.js +2 -2
  484. package/node_modules/@murphai/health-metrics/dist/definitions/body.js.map +1 -1
  485. package/node_modules/@murphai/health-metrics/dist/normalize.d.ts.map +1 -1
  486. package/node_modules/@murphai/health-metrics/dist/normalize.js +3 -0
  487. package/node_modules/@murphai/health-metrics/dist/normalize.js.map +1 -1
  488. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts +3 -1
  489. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts.map +1 -1
  490. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js +5 -1
  491. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js.map +1 -1
  492. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts +79 -0
  493. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts.map +1 -1
  494. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js +19 -0
  495. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js.map +1 -1
  496. package/node_modules/@murphai/importers/dist/core-port.d.ts +15 -1
  497. package/node_modules/@murphai/importers/dist/core-port.d.ts.map +1 -1
  498. package/node_modules/@murphai/importers/dist/core-port.js.map +1 -1
  499. package/node_modules/@murphai/importers/dist/create-importers.d.ts +9 -1
  500. package/node_modules/@murphai/importers/dist/create-importers.d.ts.map +1 -1
  501. package/node_modules/@murphai/importers/dist/create-importers.js +12 -2
  502. package/node_modules/@murphai/importers/dist/create-importers.js.map +1 -1
  503. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts +17 -3
  504. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts.map +1 -1
  505. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js +102 -8
  506. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js.map +1 -1
  507. package/node_modules/@murphai/importers/dist/device-providers/index.d.ts +5 -4
  508. package/node_modules/@murphai/importers/dist/device-providers/index.d.ts.map +1 -1
  509. package/node_modules/@murphai/importers/dist/device-providers/index.js +4 -3
  510. package/node_modules/@murphai/importers/dist/device-providers/index.js.map +1 -1
  511. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts +11 -1
  512. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts.map +1 -1
  513. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js +420 -27
  514. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js.map +1 -1
  515. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts +1 -0
  516. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts.map +1 -1
  517. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js +7 -2
  518. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js.map +1 -1
  519. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts +4 -1
  520. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts.map +1 -1
  521. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js +42 -1
  522. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js.map +1 -1
  523. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts +79 -0
  524. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts.map +1 -0
  525. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js +356 -0
  526. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js.map +1 -0
  527. package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts +76 -3
  528. package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts.map +1 -1
  529. package/node_modules/@murphai/importers/dist/device-providers/junction.js +1676 -347
  530. package/node_modules/@murphai/importers/dist/device-providers/junction.js.map +1 -1
  531. package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.d.ts.map +1 -1
  532. package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js +1 -0
  533. package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js.map +1 -1
  534. package/node_modules/@murphai/importers/dist/device-providers/types.d.ts +9 -1
  535. package/node_modules/@murphai/importers/dist/device-providers/types.d.ts.map +1 -1
  536. package/node_modules/@murphai/importers/dist/index.d.ts +2 -2
  537. package/node_modules/@murphai/importers/dist/index.d.ts.map +1 -1
  538. package/node_modules/@murphai/importers/dist/index.js +1 -1
  539. package/node_modules/@murphai/importers/dist/index.js.map +1 -1
  540. package/node_modules/@murphai/inboxd/README.md +1 -1
  541. package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts +6 -0
  542. package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts.map +1 -1
  543. package/node_modules/@murphai/inboxd/dist/indexing/retention.js +64 -18
  544. package/node_modules/@murphai/inboxd/dist/indexing/retention.js.map +1 -1
  545. package/node_modules/@murphai/inboxd/dist/retention.d.ts +1 -1
  546. package/node_modules/@murphai/inboxd/dist/retention.d.ts.map +1 -1
  547. package/node_modules/@murphai/inboxd/dist/retention.js +1 -1
  548. package/node_modules/@murphai/inboxd/dist/retention.js.map +1 -1
  549. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts +1 -0
  550. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts.map +1 -1
  551. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js +5 -0
  552. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js.map +1 -1
  553. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts +175 -152
  554. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts.map +1 -1
  555. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js +62 -41
  556. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js.map +1 -1
  557. package/node_modules/@murphai/operator-config/dist/device-sync-client.d.ts.map +1 -1
  558. package/node_modules/@murphai/operator-config/dist/device-sync-client.js +1 -5
  559. package/node_modules/@murphai/operator-config/dist/device-sync-client.js.map +1 -1
  560. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts +66 -0
  561. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts.map +1 -1
  562. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js +58 -0
  563. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js.map +1 -1
  564. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts +24 -0
  565. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts.map +1 -0
  566. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js +219 -0
  567. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js.map +1 -0
  568. package/node_modules/@murphai/operator-config/dist/vault-cli-errors.d.ts.map +1 -1
  569. package/node_modules/@murphai/operator-config/dist/vault-cli-errors.js.map +1 -1
  570. package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts +9 -0
  571. package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts.map +1 -1
  572. package/node_modules/@murphai/operator-config/dist/workout-action-binding.js +48 -5
  573. package/node_modules/@murphai/operator-config/dist/workout-action-binding.js.map +1 -1
  574. package/node_modules/@murphai/operator-config/package.json +5 -0
  575. package/node_modules/@murphai/query/README.md +20 -5
  576. package/node_modules/@murphai/query/dist/browser-replica/build.d.ts +1 -1
  577. package/node_modules/@murphai/query/dist/browser-replica/build.d.ts.map +1 -1
  578. package/node_modules/@murphai/query/dist/browser-replica/build.js +25 -16
  579. package/node_modules/@murphai/query/dist/browser-replica/build.js.map +1 -1
  580. package/node_modules/@murphai/query/dist/browser-replica/json.d.ts +6 -0
  581. package/node_modules/@murphai/query/dist/browser-replica/json.d.ts.map +1 -0
  582. package/node_modules/@murphai/query/dist/browser-replica/json.js +103 -0
  583. package/node_modules/@murphai/query/dist/browser-replica/json.js.map +1 -0
  584. package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts +1 -0
  585. package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts.map +1 -1
  586. package/node_modules/@murphai/query/dist/browser-replica/source.d.ts +13 -0
  587. package/node_modules/@murphai/query/dist/browser-replica/source.d.ts.map +1 -0
  588. package/node_modules/@murphai/query/dist/browser-replica/source.js +45 -0
  589. package/node_modules/@murphai/query/dist/browser-replica/source.js.map +1 -0
  590. package/node_modules/@murphai/query/dist/browser-replica-server.d.ts +2 -0
  591. package/node_modules/@murphai/query/dist/browser-replica-server.d.ts.map +1 -1
  592. package/node_modules/@murphai/query/dist/browser-replica-server.js +2 -0
  593. package/node_modules/@murphai/query/dist/browser-replica-server.js.map +1 -1
  594. package/node_modules/@murphai/query/dist/health/blood-tests.d.ts.map +1 -1
  595. package/node_modules/@murphai/query/dist/health/blood-tests.js +3 -2
  596. package/node_modules/@murphai/query/dist/health/blood-tests.js.map +1 -1
  597. package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts +11 -1
  598. package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts.map +1 -1
  599. package/node_modules/@murphai/query/dist/health/canonical-collector.js +39 -16
  600. package/node_modules/@murphai/query/dist/health/canonical-collector.js.map +1 -1
  601. package/node_modules/@murphai/query/dist/health/entity-slices.d.ts +9 -3
  602. package/node_modules/@murphai/query/dist/health/entity-slices.d.ts.map +1 -1
  603. package/node_modules/@murphai/query/dist/health/entity-slices.js +6 -6
  604. package/node_modules/@murphai/query/dist/health/entity-slices.js.map +1 -1
  605. package/node_modules/@murphai/query/dist/health/immunizations.d.ts.map +1 -1
  606. package/node_modules/@murphai/query/dist/health/immunizations.js +14 -11
  607. package/node_modules/@murphai/query/dist/health/immunizations.js.map +1 -1
  608. package/node_modules/@murphai/query/dist/health/loaders.d.ts +21 -7
  609. package/node_modules/@murphai/query/dist/health/loaders.d.ts.map +1 -1
  610. package/node_modules/@murphai/query/dist/health/loaders.js +53 -15
  611. package/node_modules/@murphai/query/dist/health/loaders.js.map +1 -1
  612. package/node_modules/@murphai/query/dist/health/registries.d.ts +1 -0
  613. package/node_modules/@murphai/query/dist/health/registries.d.ts.map +1 -1
  614. package/node_modules/@murphai/query/dist/health/registries.js.map +1 -1
  615. package/node_modules/@murphai/query/dist/index.d.ts +4 -3
  616. package/node_modules/@murphai/query/dist/index.d.ts.map +1 -1
  617. package/node_modules/@murphai/query/dist/index.js +4 -3
  618. package/node_modules/@murphai/query/dist/index.js.map +1 -1
  619. package/node_modules/@murphai/query/dist/meal-nutrition.d.ts +1 -1
  620. package/node_modules/@murphai/query/dist/meal-nutrition.d.ts.map +1 -1
  621. package/node_modules/@murphai/query/dist/meal-nutrition.js +10 -3
  622. package/node_modules/@murphai/query/dist/meal-nutrition.js.map +1 -1
  623. package/node_modules/@murphai/query/dist/metrics/projection.d.ts.map +1 -1
  624. package/node_modules/@murphai/query/dist/metrics/projection.js +19 -6
  625. package/node_modules/@murphai/query/dist/metrics/projection.js.map +1 -1
  626. package/node_modules/@murphai/query/dist/murph-age.d.ts +5 -1
  627. package/node_modules/@murphai/query/dist/murph-age.d.ts.map +1 -1
  628. package/node_modules/@murphai/query/dist/murph-age.js +22 -8
  629. package/node_modules/@murphai/query/dist/murph-age.js.map +1 -1
  630. package/node_modules/@murphai/query/dist/projection/entity-store.js +4 -4
  631. package/node_modules/@murphai/query/dist/projection/entity-store.js.map +1 -1
  632. package/node_modules/@murphai/query/dist/projection/schema.d.ts +1 -1
  633. package/node_modules/@murphai/query/dist/projection/schema.d.ts.map +1 -1
  634. package/node_modules/@murphai/query/dist/projection/schema.js +3 -1
  635. package/node_modules/@murphai/query/dist/projection/schema.js.map +1 -1
  636. package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js +4 -0
  637. package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js.map +1 -1
  638. package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js +4 -0
  639. package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js.map +1 -1
  640. package/node_modules/@murphai/query/dist/query-visibility.d.ts +5 -0
  641. package/node_modules/@murphai/query/dist/query-visibility.d.ts.map +1 -1
  642. package/node_modules/@murphai/query/dist/query-visibility.js +11 -5
  643. package/node_modules/@murphai/query/dist/query-visibility.js.map +1 -1
  644. package/node_modules/@murphai/query/dist/read-model.d.ts +8 -0
  645. package/node_modules/@murphai/query/dist/read-model.d.ts.map +1 -1
  646. package/node_modules/@murphai/query/dist/read-model.js +11 -1
  647. package/node_modules/@murphai/query/dist/read-model.js.map +1 -1
  648. package/node_modules/@murphai/query/dist/scheduled-logs.d.ts.map +1 -1
  649. package/node_modules/@murphai/query/dist/scheduled-logs.js +55 -79
  650. package/node_modules/@murphai/query/dist/scheduled-logs.js.map +1 -1
  651. package/node_modules/@murphai/query/dist/vault-source.d.ts +22 -4
  652. package/node_modules/@murphai/query/dist/vault-source.d.ts.map +1 -1
  653. package/node_modules/@murphai/query/dist/vault-source.js +188 -59
  654. package/node_modules/@murphai/query/dist/vault-source.js.map +1 -1
  655. package/node_modules/@murphai/query/dist/wearables/candidates.d.ts.map +1 -1
  656. package/node_modules/@murphai/query/dist/wearables/candidates.js +26 -15
  657. package/node_modules/@murphai/query/dist/wearables/candidates.js.map +1 -1
  658. package/node_modules/@murphai/query/dist/wearables/public-output.d.ts.map +1 -1
  659. package/node_modules/@murphai/query/dist/wearables/public-output.js +6 -0
  660. package/node_modules/@murphai/query/dist/wearables/public-output.js.map +1 -1
  661. package/node_modules/@murphai/query/dist/wearables/source-health.d.ts.map +1 -1
  662. package/node_modules/@murphai/query/dist/wearables/source-health.js +4 -0
  663. package/node_modules/@murphai/query/dist/wearables/source-health.js.map +1 -1
  664. package/node_modules/@murphai/query/dist/wearables/types.d.ts +41 -4
  665. package/node_modules/@murphai/query/dist/wearables/types.d.ts.map +1 -1
  666. package/node_modules/@murphai/query/dist/wearables/types.js +2 -0
  667. package/node_modules/@murphai/query/dist/wearables/types.js.map +1 -1
  668. package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts +5 -0
  669. package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts.map +1 -0
  670. package/node_modules/@murphai/query/dist/wearables/workout-features.js +179 -0
  671. package/node_modules/@murphai/query/dist/wearables/workout-features.js.map +1 -0
  672. package/node_modules/@murphai/query/dist/wearables.d.ts.map +1 -1
  673. package/node_modules/@murphai/query/dist/wearables.js +23 -6
  674. package/node_modules/@murphai/query/dist/wearables.js.map +1 -1
  675. package/node_modules/@murphai/query/package.json +5 -0
  676. package/node_modules/@murphai/runtime-state/README.md +1 -1
  677. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts +4 -0
  678. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts.map +1 -1
  679. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js +16 -3
  680. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js.map +1 -1
  681. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts +1 -0
  682. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts.map +1 -1
  683. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js +31 -0
  684. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js.map +1 -1
  685. package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts +5 -0
  686. package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts.map +1 -1
  687. package/node_modules/@murphai/runtime-state/dist/hosted-email.js +10 -0
  688. package/node_modules/@murphai/runtime-state/dist/hosted-email.js.map +1 -1
  689. package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.d.ts.map +1 -1
  690. package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js +6 -15
  691. package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js.map +1 -1
  692. package/node_modules/@murphai/vault-usecases/README.md +9 -0
  693. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts +6 -0
  694. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts.map +1 -1
  695. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js +49 -1
  696. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js.map +1 -1
  697. package/node_modules/@murphai/vault-usecases/dist/usecases/capture.d.ts.map +1 -1
  698. package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js +22 -12
  699. package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js.map +1 -1
  700. package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.d.ts.map +1 -1
  701. package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js +1 -3
  702. package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js.map +1 -1
  703. package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.d.ts.map +1 -1
  704. package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js +24 -18
  705. package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js.map +1 -1
  706. package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.d.ts.map +1 -1
  707. package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js +1 -3
  708. package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js.map +1 -1
  709. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts +8 -0
  710. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts.map +1 -1
  711. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js +56 -5
  712. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js.map +1 -1
  713. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts +20 -0
  714. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts.map +1 -0
  715. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js +72 -0
  716. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js.map +1 -0
  717. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts +1 -1
  718. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts.map +1 -1
  719. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js +42 -9
  720. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js.map +1 -1
  721. package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.d.ts.map +1 -1
  722. package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js +158 -24
  723. package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js.map +1 -1
  724. package/node_modules/@murphai/vault-usecases/dist/usecases/food.d.ts.map +1 -1
  725. package/node_modules/@murphai/vault-usecases/dist/usecases/food.js +98 -5
  726. package/node_modules/@murphai/vault-usecases/dist/usecases/food.js.map +1 -1
  727. package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.d.ts.map +1 -1
  728. package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js +20 -4
  729. package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js.map +1 -1
  730. package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.d.ts.map +1 -1
  731. package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js +49 -24
  732. package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js.map +1 -1
  733. package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.d.ts.map +1 -1
  734. package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js +13 -11
  735. package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js.map +1 -1
  736. package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.d.ts.map +1 -1
  737. package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js +18 -11
  738. package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js.map +1 -1
  739. package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.d.ts.map +1 -1
  740. package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js +43 -1
  741. package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js.map +1 -1
  742. package/node_modules/@murphai/vault-usecases/dist/usecases/shared.d.ts.map +1 -1
  743. package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js +1 -3
  744. package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js.map +1 -1
  745. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts +2 -0
  746. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts.map +1 -1
  747. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js +24 -1
  748. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js.map +1 -1
  749. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts +2 -0
  750. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts.map +1 -1
  751. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts +36 -4
  752. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts.map +1 -1
  753. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js +48 -12
  754. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js.map +1 -1
  755. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts +14 -14
  756. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts.map +1 -1
  757. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js +84 -82
  758. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js.map +1 -1
  759. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts +16 -4
  760. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts.map +1 -1
  761. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js +316 -79
  762. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js.map +1 -1
  763. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.d.ts.map +1 -1
  764. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js +19 -17
  765. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js.map +1 -1
  766. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts +20 -7
  767. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts.map +1 -1
  768. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js +56 -31
  769. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js.map +1 -1
  770. package/node_modules/incur/dist/Cli.js +7 -32
  771. package/node_modules/incur/dist/Errors.d.ts +17 -0
  772. package/node_modules/incur/dist/Errors.js +23 -0
  773. package/node_modules/incur/dist/internal/command.d.ts +2 -0
  774. package/node_modules/incur/dist/internal/command.js +2 -6
  775. package/node_modules/incur/src/Cli.ts +11 -32
  776. package/node_modules/incur/src/Errors.ts +36 -0
  777. package/node_modules/incur/src/internal/command.ts +4 -6
  778. package/package.json +4 -4
  779. package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/references/daily-nutrition-card-safety.md +0 -211
@@ -1,16 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
- import { extractIsoDatePrefix, ID_PREFIXES, JUNCTION_WEARABLE_TAG_EXTERNAL_REF_FACET, JUNCTION_WEARABLE_TAG_NOTE_TYPE, MEAL_MICRONUTRIENT_KEYS, parseCompanionHrvRmssdAdmissionId, parseCompanionHrvRmssdObservation, serializeCompanionHrvRmssdObservation, toLocalDayKey, } from "@murphai/contracts";
2
+ import { addDaysToIsoDate, extractIsoDatePrefix, formatTimeZoneDateTimeParts, ID_PREFIXES, isStrictIsoDate, JUNCTION_WEARABLE_TAG_EXTERNAL_REF_FACET, JUNCTION_WEARABLE_TAG_NOTE_TYPE, MEAL_MICRONUTRIENT_KEYS, normalizeIanaTimeZone, parseCompanionHrvRmssdAdmissionId, parseCompanionHrvRmssdObservation, resolveFloatingIsoTimestampInTimeZone, serializeCompanionHrvRmssdObservation, toLocalDayKey, } from "@murphai/contracts";
3
3
  import * as z from "@murphai/contracts/zod-runtime";
4
4
  import { stripUndefined } from "../shared.js";
5
5
  import { asArray, asPlainObject, createEvidencePart, finiteNumber, kilojoulesToKilocalories, makeNormalizedDeviceBatch, makeProviderExternalRef, minutesBetween, pushEvidencePart, slugify, stringId, trimToLength, } from "./shared-normalization.js";
6
- import { JUNCTION_ALLOWED_SUMMARY_RESOURCES, JUNCTION_ALLOWED_TIMESERIES_RESOURCES, JUNCTION_SLEEP_AVERAGE_HEART_RATE_PATHS, JUNCTION_SLEEP_AWAKE_MINUTE_PATHS, JUNCTION_SLEEP_AWAKE_SECOND_PATHS, JUNCTION_SLEEP_CONSISTENCY_PATHS, JUNCTION_SLEEP_DEEP_MINUTE_PATHS, JUNCTION_SLEEP_DEEP_SECOND_PATHS, JUNCTION_SLEEP_DURATION_MILLISECOND_PATHS, JUNCTION_SLEEP_DURATION_MINUTE_PATHS, JUNCTION_SLEEP_DURATION_SECOND_PATHS, JUNCTION_SLEEP_EFFICIENCY_RATIO_PATHS, JUNCTION_SLEEP_END_TIMESTAMP_PATHS, JUNCTION_SLEEP_HRV_PATHS, JUNCTION_SLEEP_LATENCY_MINUTE_PATHS, JUNCTION_SLEEP_LATENCY_SECOND_PATHS, JUNCTION_SLEEP_LIGHT_MINUTE_PATHS, JUNCTION_SLEEP_LIGHT_SECOND_PATHS, JUNCTION_SLEEP_LOWEST_HEART_RATE_PATHS, JUNCTION_SLEEP_PERFORMANCE_PATHS, JUNCTION_SLEEP_REM_MINUTE_PATHS, JUNCTION_SLEEP_REM_SECOND_PATHS, JUNCTION_SLEEP_RESPIRATORY_RATE_PATHS, JUNCTION_SLEEP_RESTING_HEART_RATE_PATHS, JUNCTION_SLEEP_SCORE_PATHS, JUNCTION_SLEEP_SPO2_PATHS, JUNCTION_SLEEP_STAGE_ARRAY_PATHS, JUNCTION_SLEEP_STAGE_DURATION_MILLISECOND_PATHS, JUNCTION_SLEEP_STAGE_DURATION_MINUTE_PATHS, JUNCTION_SLEEP_STAGE_DURATION_SECOND_PATHS, JUNCTION_SLEEP_STAGE_VALUE_PATHS, JUNCTION_SLEEP_START_TIMESTAMP_PATHS, JUNCTION_SLEEP_TEMPERATURE_DEVIATION_PATHS, JUNCTION_SLEEP_TEMPERATURE_PATHS, JUNCTION_SLEEP_TIME_IN_BED_MINUTE_PATHS, JUNCTION_SLEEP_TIME_IN_BED_SECOND_PATHS, JUNCTION_SLEEP_TOTAL_MINUTE_PATHS, JUNCTION_SLEEP_TOTAL_SECOND_PATHS, JUNCTION_TIMESERIES_RESOURCE_POLICIES, isJunctionRawDirectIdentityContainerKey, isJunctionRawDirectIdentityKey, normalizeJunctionRawIdentityKey, normalizeJunctionSleepStageValue, normalizeJunctionResourceName, resolveJunctionTimeseriesResourcePolicy, usesJunctionTimeseriesIntervalStartOwnership, } from "./junction-resources.js";
6
+ import { JUNCTION_ALLOWED_SUMMARY_RESOURCES, JUNCTION_ALLOWED_TIMESERIES_RESOURCES, JUNCTION_SLEEP_AVERAGE_HEART_RATE_PATHS, JUNCTION_SLEEP_AWAKE_MINUTE_PATHS, JUNCTION_SLEEP_AWAKE_SECOND_PATHS, JUNCTION_SLEEP_CONSISTENCY_PATHS, JUNCTION_SLEEP_DEEP_MINUTE_PATHS, JUNCTION_SLEEP_DEEP_SECOND_PATHS, JUNCTION_SLEEP_DURATION_MILLISECOND_PATHS, JUNCTION_SLEEP_DURATION_MINUTE_PATHS, JUNCTION_SLEEP_DURATION_SECOND_PATHS, JUNCTION_SLEEP_EFFICIENCY_RATIO_PATHS, JUNCTION_SLEEP_END_TIMESTAMP_PATHS, JUNCTION_SLEEP_HRV_PATHS, JUNCTION_SLEEP_LATENCY_MINUTE_PATHS, JUNCTION_SLEEP_LATENCY_SECOND_PATHS, JUNCTION_SLEEP_LIGHT_MINUTE_PATHS, JUNCTION_SLEEP_LIGHT_SECOND_PATHS, JUNCTION_SLEEP_LOWEST_HEART_RATE_PATHS, JUNCTION_SLEEP_PERFORMANCE_PATHS, JUNCTION_SLEEP_REM_MINUTE_PATHS, JUNCTION_SLEEP_REM_SECOND_PATHS, JUNCTION_SLEEP_RESPIRATORY_RATE_PATHS, JUNCTION_SLEEP_RESTING_HEART_RATE_PATHS, JUNCTION_SLEEP_SCORE_PATHS, JUNCTION_SLEEP_SPO2_PATHS, JUNCTION_SLEEP_STAGE_ARRAY_PATHS, JUNCTION_SLEEP_STAGE_DURATION_MILLISECOND_PATHS, JUNCTION_SLEEP_STAGE_DURATION_MINUTE_PATHS, JUNCTION_SLEEP_STAGE_DURATION_SECOND_PATHS, JUNCTION_SLEEP_STAGE_VALUE_PATHS, JUNCTION_SLEEP_START_TIMESTAMP_PATHS, JUNCTION_SLEEP_TEMPERATURE_DEVIATION_PATHS, JUNCTION_SLEEP_TEMPERATURE_PATHS, JUNCTION_SLEEP_TIME_IN_BED_MINUTE_PATHS, JUNCTION_SLEEP_TIME_IN_BED_SECOND_PATHS, JUNCTION_SLEEP_TOTAL_MINUTE_PATHS, JUNCTION_SLEEP_TOTAL_SECOND_PATHS, isJunctionDailyCanonicalCoverageResource, maxJunctionCanonicalCoverageBoundary, normalizeJunctionCanonicalCoverageBoundary, JUNCTION_TIMESERIES_RESOURCE_POLICIES, isJunctionRawDirectIdentityContainerKey, isJunctionRawDirectIdentityKey, normalizeJunctionRawIdentityKey, normalizeJunctionSleepStageValue, normalizeJunctionResourceName, resolveJunctionTimeseriesResourcePolicy, usesJunctionTimeseriesIntervalStartOwnership, } from "./junction-resources.js";
7
7
  import { assertJunctionTimeseriesResponseBound, assertJunctionTimeseriesSourceDayBound, assertJunctionTimeseriesOutputBounds, deriveJunctionTimeseriesFeatureEnvelope, getJunctionDenseFidelityPolicy, getJunctionSparseIntervalPolicy, isJunctionDenseFidelityResource, isJunctionSparseIntervalResource, } from "./junction-timeseries-fidelity.js";
8
- import { JUNCTION_ECG_VOLTAGE_FEATURE_SCHEMA, JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA, buildJunctionBoundedFeatureIdentity, reduceJunctionElectrocardiogramVoltageRecords, reduceJunctionWorkoutStreamPayload, resolveJunctionBoundedFeatureRecords, selectJunctionWorkoutStreamCandidates, } from "./junction-bounded-features.js";
9
- import { normalizeJunctionSourceProviderSlug, readJunctionSourceProviderSlug, resolveJunctionOrigin, } from "./junction-origin.js";
8
+ import { JUNCTION_ECG_VOLTAGE_FEATURE_SCHEMA, JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA, JunctionWorkoutStreamTimestampCardinalityError, buildJunctionBoundedFeatureIdentity, reduceJunctionElectrocardiogramVoltageRecords, reduceJunctionWorkoutStreamPayload, resolveJunctionBoundedFeatureRecords, selectJunctionWorkoutStreamCandidates, } from "./junction-bounded-features.js";
9
+ import { normalizeKnownJunctionSourceProviderSlug, normalizeJunctionSourceProviderSlug, readJunctionSourceProviderSlug, resolveJunctionOrigin, } from "./junction-origin.js";
10
+ import { buildJunctionTemporalFeatures, isJunctionTemporalFeatureResource, JUNCTION_TEMPORAL_FEATURE_MAX_OBSERVATIONS_PER_DAY, JUNCTION_TEMPORAL_FEATURE_MAX_SAMPLES_PER_DAY, JUNCTION_TEMPORAL_FEATURE_MAX_SAMPLES_PER_IMPORT, } from "./junction-timeseries-features.js";
10
11
  import { JUNCTION_DEVICE_PROVIDER_DESCRIPTOR } from "./provider-descriptors.js";
11
12
  export { JUNCTION_DENSE_FIDELITY_RESOURCES, } from "./junction-timeseries-fidelity.js";
12
13
  export { JUNCTION_ALLOWED_SUMMARY_RESOURCES, JUNCTION_ALLOWED_TIMESERIES_RESOURCES, JUNCTION_DEFAULT_SUMMARY_RESOURCES, JUNCTION_DEFAULT_TIMESERIES_RESOURCES, JUNCTION_KNOWN_TIMESERIES_RESOURCES, JUNCTION_OPT_IN_SUMMARY_RESOURCES, JUNCTION_OPT_IN_TIMESERIES_RESOURCES, JUNCTION_RAW_ONLY_SUMMARY_RESOURCES, normalizeJunctionResourceName, } from "./junction-resources.js";
13
- export { JUNCTION_ECG_VOLTAGE_FEATURE_SCHEMA, JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA, buildJunctionBoundedFeatureIdentity, reduceJunctionElectrocardiogramVoltageRecords, reduceJunctionWorkoutStreamPayload, resolveJunctionBoundedFeatureRecords, selectJunctionWorkoutStreamCandidates, };
14
+ export { JUNCTION_ECG_VOLTAGE_FEATURE_SCHEMA, JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA, JunctionWorkoutStreamTimestampCardinalityError, buildJunctionBoundedFeatureIdentity, reduceJunctionElectrocardiogramVoltageRecords, reduceJunctionWorkoutStreamPayload, resolveJunctionBoundedFeatureRecords, selectJunctionWorkoutStreamCandidates, };
14
15
  const junctionSnapshotSchema = z.object({
15
16
  accountId: z.union([z.string(), z.number()]).optional(),
16
17
  importedAt: z.union([z.string(), z.number(), z.date()]).optional(),
@@ -28,6 +29,11 @@ const junctionSnapshotSchema = z.object({
28
29
  summaries: z.record(z.string(), z.unknown()).optional(),
29
30
  timeseries: z.record(z.string(), z.unknown()).optional(),
30
31
  companionHrvRmssd: z.unknown().optional(),
32
+ canonicalCoverageFence: z.object({
33
+ coverageBoundaryByResource: z.record(z.string(), z.string().nullable()),
34
+ sourceProviderSlug: z.string(),
35
+ }).strict().optional(),
36
+ canonicalCoverageProviderPulledAt: z.string().optional(),
31
37
  }).catchall(z.unknown());
32
38
  const junctionCompanionHrvRmssdSnapshotEntrySchema = z.object({
33
39
  admissionId: z.unknown(),
@@ -41,6 +47,7 @@ const FLOATING_TIMESTAMP_SOURCE_PROVIDER_SLUGS = new Set([
41
47
  "freestyle-libre",
42
48
  "freestyle_libre",
43
49
  ]);
50
+ const JUNCTION_SPARSE_FLOATING_FALLBACK_NORMALIZER_VERSION = "junction-sparse-timeseries.floating-fallback.v2";
44
51
  const RAW_SOURCE_IDENTIFIER_KEYS = new Set([
45
52
  "sourcename",
46
53
  "sourcedeviceid",
@@ -52,6 +59,10 @@ const RAW_SOURCE_NAME_KEYS = new Set([
52
59
  "displayname",
53
60
  "name",
54
61
  ]);
62
+ const JUNCTION_CANONICAL_COVERAGE_RESOURCES = [
63
+ ...JUNCTION_ALLOWED_SUMMARY_RESOURCES.filter((resource) => resource !== "profile"),
64
+ ...JUNCTION_ALLOWED_TIMESERIES_RESOURCES,
65
+ ];
55
66
  const RAW_SOURCE_LINKAGE_KEY_PARTS = [
56
67
  "connectionid",
57
68
  "providerconnectionid",
@@ -103,46 +114,46 @@ const ACTIVITY_METRICS = [
103
114
  { metric: "percent-recorded", unit: "%", title: "Junction activity recording coverage", paths: [], percentRatioPaths: ["percentRecorded", "percent_recorded", "recordingCoverage", "recording_coverage", "recordedRatio", "recorded_ratio", "percentRecordedRatio", "percent_recorded_ratio"] },
104
115
  { metric: "workout-strain", unit: "score", title: "Junction workout strain", paths: ["workoutStrain", "workout_strain"] },
105
116
  { metric: "day-strain", unit: "score", title: "Junction day strain", paths: ["dayStrain", "day_strain", "strain"] },
106
- { metric: "average-heart-rate", unit: "bpm", title: "Junction activity average heart rate", paths: ["heart_rate.avg_bpm"] },
107
- { metric: "walking-average-heart-rate", unit: "bpm", title: "Junction activity walking average heart rate", paths: ["heart_rate.avg_walking_bpm"] },
108
- { metric: "lowest-heart-rate", unit: "bpm", title: "Junction activity lowest heart rate", paths: ["heart_rate.min_bpm"] },
109
- { metric: "max-heart-rate", unit: "bpm", title: "Junction activity max heart rate", paths: ["maxHeartRate", "max_heart_rate", "max_hr", "heart_rate.max_bpm"] },
110
- { metric: "resting-heart-rate", unit: "bpm", title: "Junction activity resting heart rate", paths: ["restingHeartRate", "resting_heart_rate", "resting_hr", "rhr", "heart_rate.resting_bpm"] },
117
+ { metric: "activity-average-heart-rate", unit: "bpm", title: "Junction activity average heart rate", paths: ["averageHeartRate", "average_heart_rate", "average_hr", "avg_hr", "heart_rate.avg_bpm"], nonnegative: true },
118
+ { metric: "walking-average-heart-rate", unit: "bpm", title: "Junction activity walking average heart rate", paths: ["walkingAverageHeartRate", "walking_average_heart_rate", "walking_average_hr", "heart_rate.avg_walking_bpm"], nonnegative: true },
119
+ { metric: "max-heart-rate", unit: "bpm", title: "Junction activity max heart rate", paths: ["maxHeartRate", "max_heart_rate", "max_hr", "heart_rate.max_bpm"], nonnegative: true },
120
+ { metric: "minimum-heart-rate", unit: "bpm", title: "Junction activity minimum heart rate", paths: ["minimumHeartRate", "minimum_heart_rate", "minimum_hr", "min_hr", "heart_rate.min_bpm"], nonnegative: true },
121
+ { metric: "resting-heart-rate", unit: "bpm", title: "Junction activity resting heart rate", paths: ["restingHeartRate", "resting_heart_rate", "resting_hr", "rhr", "heart_rate.resting_bpm"], nonnegative: true },
111
122
  ];
112
123
  const BODY_METRICS = [
113
124
  { metric: "weight", unit: "kg", title: "Junction body weight", paths: ["weightKg", "weight_kg", "weight"] },
114
- { metric: "bmi", unit: "kg_m2", title: "Junction BMI", paths: ["bmi", "body_mass_index"] },
125
+ { metric: "bmi", unit: "kg/m^2", title: "Junction BMI", paths: ["bmi", "body_mass_index"] },
115
126
  { metric: "body-fat-percentage", unit: "%", title: "Junction body fat", paths: ["bodyFatPercentage", "body_fat_percentage", "body_fat_percent", "bodyFat", "body_fat", "fat"] },
127
+ { metric: "lean-body-mass", unit: "kg", title: "Junction lean body mass", paths: ["leanBodyMassKg", "lean_body_mass_kg", "leanBodyMassKilogram", "lean_body_mass_kilogram", "leanMassKg", "lean_mass_kg"] },
128
+ { metric: "waist-circumference", unit: "cm", title: "Junction waist circumference", paths: ["waistCircumference", "waist_circumference", "waistCircumferenceCentimeter", "waist_circumference_centimeter", "waistCircumferenceCm", "waist_circumference_cm"] },
116
129
  {
117
130
  metric: "bone-mass-percentage",
118
131
  unit: "%",
119
132
  title: "Junction bone mass percentage",
120
133
  paths: [],
121
- value: (entry) => firstNumberFromPaths(entry, ["boneMassPercentage", "bone_mass_percentage"]),
134
+ value: (entry) => normalizeBodyCompositionPercent(firstNumberFromPaths(entry, ["boneMassPercentage", "bone_mass_percentage"])),
122
135
  },
123
136
  {
124
137
  metric: "muscle-mass-percentage",
125
138
  unit: "%",
126
139
  title: "Junction muscle mass percentage",
127
140
  paths: [],
128
- value: (entry) => firstNumberFromPaths(entry, ["muscleMassPercentage", "muscle_mass_percentage"]),
141
+ value: (entry) => normalizeBodyCompositionPercent(firstNumberFromPaths(entry, ["muscleMassPercentage", "muscle_mass_percentage"])),
129
142
  },
130
143
  {
131
144
  metric: "visceral-fat-index",
132
145
  unit: "index",
133
146
  title: "Junction visceral fat index",
134
147
  paths: [],
135
- value: (entry) => firstNumberFromPaths(entry, ["visceralFatIndex", "visceral_fat_index"]),
148
+ value: (entry) => normalizeNonNegativeBodyIndex(firstNumberFromPaths(entry, ["visceralFatIndex", "visceral_fat_index"])),
136
149
  },
137
150
  {
138
151
  metric: "body-water-percentage",
139
152
  unit: "%",
140
153
  title: "Junction body water percentage",
141
154
  paths: [],
142
- value: (entry) => firstNumberFromPaths(entry, ["waterPercentage", "water_percentage"]),
155
+ value: (entry) => normalizeBodyCompositionPercent(firstNumberFromPaths(entry, ["waterPercentage", "water_percentage"])),
143
156
  },
144
- { metric: "lean-body-mass", unit: "kg", title: "Junction lean body mass", paths: ["leanBodyMassKg", "lean_body_mass_kg", "leanBodyMassKilogram", "lean_body_mass_kilogram", "leanMassKg", "lean_mass_kg"] },
145
- { metric: "waist-circumference", unit: "cm", title: "Junction waist circumference", paths: ["waistCircumference", "waist_circumference", "waistCircumferenceCentimeter", "waist_circumference_centimeter", "waistCircumferenceCm", "waist_circumference_cm"] },
146
157
  { metric: "temperature", unit: "celsius", title: "Junction body temperature", paths: ["temperature", "bodyTemperature", "body_temperature", "temperatureCelsius", "temperature_celsius", "skin_temperature"] },
147
158
  ];
148
159
  const SLEEP_METRICS = [
@@ -166,6 +177,7 @@ const SLEEP_METRICS = [
166
177
  title: "Junction sleep latency",
167
178
  paths: [],
168
179
  value: resolveJunctionSleepLatencyMinutes,
180
+ nonnegative: true,
169
181
  },
170
182
  { metric: "sleep-consistency", unit: "%", title: "Junction sleep consistency", paths: JUNCTION_SLEEP_CONSISTENCY_PATHS },
171
183
  { metric: "sleep-performance", unit: "%", title: "Junction sleep performance", paths: JUNCTION_SLEEP_PERFORMANCE_PATHS },
@@ -332,6 +344,8 @@ const JUNCTION_LOCAL_CALENDAR_DATE_PATHS = [
332
344
  "calendar_date",
333
345
  "localDate",
334
346
  "local_date",
347
+ "date",
348
+ "day",
335
349
  ];
336
350
  const JUNCTION_MEAL_ITEM_CONTAINER_PATHS = [
337
351
  "data",
@@ -721,6 +735,8 @@ const SLEEP_ASLEEP_STAGE_METRIC_NAMES = new Set([
721
735
  const JUNCTION_SLEEP_STAGE_SUMMARY_NORMALIZER_VERSION = "junction-sleep-stage-summary.v1";
722
736
  const JUNCTION_SLEEP_STAGE_CYCLE_FALLBACK_NORMALIZER_VERSION = "junction-sleep-stage-cycle-fallback.v1";
723
737
  const JUNCTION_SLEEP_UNSPECIFIED_TOTAL_NORMALIZER_VERSION = "junction-sleep-unspecified-total.v1";
738
+ const JUNCTION_NO_ID_PROFILE_NORMALIZER_VERSION = "junction-no-id-profile.v1";
739
+ const JUNCTION_STABLE_PROFILE_CREATED_AT_NORMALIZER_VERSION = "junction-stable-profile-created-at.v1";
724
740
  function parseJunctionSnapshot(snapshot) {
725
741
  const parsed = junctionSnapshotSchema.parse(snapshot);
726
742
  return {
@@ -747,6 +763,7 @@ export function normalizeJunctionSnapshot(snapshot, providerContext = {}) {
747
763
  const windowStart = normalizeTimestamp(snapshot.windowStart);
748
764
  const windowEnd = normalizeTimestamp(snapshot.windowEnd);
749
765
  const evidenceParts = [];
766
+ const accountId = stringId(snapshot.accountId);
750
767
  const events = [];
751
768
  const samples = [];
752
769
  const companionHrvRmssd = snapshot.companionHrvRmssd === undefined
@@ -767,21 +784,30 @@ export function normalizeJunctionSnapshot(snapshot, providerContext = {}) {
767
784
  evidenceParts,
768
785
  events,
769
786
  samples,
787
+ temporalFeatureCurrentFacetsByResource: new Map(),
788
+ temporalFeatureObservationCountsByDay: new Map(),
789
+ temporalFeatureSourceDay: providerContext.completeSourceDay,
790
+ authoritativeEventSets: [],
770
791
  };
771
792
  normalizeSummaries(snapshot.summaries, context);
772
793
  normalizeTimeseries(snapshot.timeseries, context);
773
794
  normalizeCompanionHrvRmssd(companionHrvRmssd, context);
795
+ applyJunctionCanonicalCoverageFence(context, snapshot.canonicalCoverageFence);
796
+ finalizeJunctionTemporalAuthoritativeSets(context);
774
797
  assertJunctionTimeseriesOutputBounds({
775
798
  eventCount: events.length,
776
799
  evidencePartCount: evidenceParts.length,
777
800
  });
778
801
  return makeNormalizedDeviceBatch({
779
802
  provider: "junction",
780
- accountId: stringId(snapshot.accountId),
803
+ accountId,
781
804
  importedAt,
782
805
  events,
783
806
  samples: samples.length > 0 ? samples : undefined,
784
807
  evidenceParts,
808
+ authoritativeEventSets: context.authoritativeEventSets.length > 0
809
+ ? context.authoritativeEventSets
810
+ : undefined,
785
811
  provenance: stripUndefined({
786
812
  schema: "junction.snapshot.v1",
787
813
  normalizerVersion: "junction-normalizer.v1",
@@ -795,6 +821,167 @@ export function normalizeJunctionSnapshot(snapshot, providerContext = {}) {
795
821
  }),
796
822
  });
797
823
  }
824
+ /**
825
+ * Derives provider cutover coverage only from the canonical events returned by
826
+ * the committed vault import. This deliberately does not replay raw Junction
827
+ * normalization: the stored event owns the canonical provider day or interval.
828
+ * Daily coverage becomes final only when a provider pull performed after its
829
+ * local day closed accepts that day. Inline payloads can advance the fence but
830
+ * cannot finalize it. Interval coverage remains anchored to its accepted end.
831
+ */
832
+ export function deriveJunctionCanonicalCoverageEvidence(events, options) {
833
+ const coverageBySourceAndResource = new Map();
834
+ for (const event of events) {
835
+ const evidence = resolveJunctionCanonicalCoverageEvidence(event);
836
+ if (!evidence) {
837
+ continue;
838
+ }
839
+ const providerDayClosedAt = isJunctionDailyCanonicalCoverageResource(evidence.resource)
840
+ ? resolveJunctionProviderDayClosedAt(evidence.coverageBoundary, event)
841
+ : undefined;
842
+ const key = `${evidence.sourceProviderSlug}\u0000${evidence.resource}`;
843
+ const existing = coverageBySourceAndResource.get(key);
844
+ if (!existing
845
+ || maxJunctionCanonicalCoverageBoundary(evidence.resource, existing.evidence.coverageBoundary, evidence.coverageBoundary) === evidence.coverageBoundary) {
846
+ coverageBySourceAndResource.set(key, existing?.evidence.coverageBoundary === evidence.coverageBoundary
847
+ ? {
848
+ evidence,
849
+ providerDayClosedAt: laterOptionalIsoTimestamp(existing.providerDayClosedAt, providerDayClosedAt),
850
+ }
851
+ : { evidence, providerDayClosedAt });
852
+ }
853
+ }
854
+ const providerPulledAt = normalizeJunctionCoverageProviderPulledAt(options?.providerPulledAt);
855
+ return [...coverageBySourceAndResource.values()]
856
+ .map(({ evidence, providerDayClosedAt }) => providerPulledAt
857
+ && providerDayClosedAt
858
+ && providerPulledAt >= providerDayClosedAt
859
+ ? { ...evidence, coverageFinalizedAt: providerPulledAt }
860
+ : evidence)
861
+ .sort((left, right) => left.sourceProviderSlug.localeCompare(right.sourceProviderSlug)
862
+ || left.resource.localeCompare(right.resource));
863
+ }
864
+ const JUNCTION_PROVIDER_DAY_LATEST_UNPROVEN_CLOSE_OFFSET_MINUTES = -12 * 60;
865
+ function resolveJunctionProviderDayClosedAt(dayKey, event) {
866
+ const timeZone = event.timeZone?.trim();
867
+ if (timeZone) {
868
+ const timeZoneClose = resolveJunctionProviderDayClosedAtInTimeZone(dayKey, timeZone);
869
+ if (timeZoneClose) {
870
+ return timeZoneClose;
871
+ }
872
+ }
873
+ const offsetMinutes = event.dataOrigin?.timeZoneOffsetMinutes;
874
+ if (Number.isInteger(offsetMinutes)
875
+ && offsetMinutes !== null
876
+ && offsetMinutes !== undefined
877
+ && Math.abs(offsetMinutes) <= 24 * 60) {
878
+ return resolveJunctionProviderDayClosedAtOffset(dayKey, offsetMinutes);
879
+ }
880
+ // Junction's closed-calendar importer waits until a date has closed at
881
+ // UTC-12. Date-only accepted records therefore converge safely without
882
+ // borrowing the member's mutable vault timezone.
883
+ return resolveJunctionProviderDayClosedAtOffset(dayKey, JUNCTION_PROVIDER_DAY_LATEST_UNPROVEN_CLOSE_OFFSET_MINUTES);
884
+ }
885
+ function resolveJunctionProviderDayClosedAtInTimeZone(dayKey, timeZone) {
886
+ try {
887
+ const nextDayKey = addDaysToIsoDate(dayKey, 1);
888
+ const nominalNextDayMs = Date.parse(`${nextDayKey}T00:00:00.000Z`);
889
+ let lowerBoundMs = nominalNextDayMs - 36 * 60 * 60_000;
890
+ let upperBoundMs = nominalNextDayMs + 36 * 60 * 60_000;
891
+ if (toLocalDayKey(lowerBoundMs, timeZone) > dayKey
892
+ || toLocalDayKey(upperBoundMs, timeZone) <= dayKey) {
893
+ return undefined;
894
+ }
895
+ while (upperBoundMs - lowerBoundMs > 1) {
896
+ const midpointMs = Math.floor((lowerBoundMs + upperBoundMs) / 2);
897
+ if (toLocalDayKey(midpointMs, timeZone) > dayKey) {
898
+ upperBoundMs = midpointMs;
899
+ }
900
+ else {
901
+ lowerBoundMs = midpointMs;
902
+ }
903
+ }
904
+ return new Date(upperBoundMs).toISOString();
905
+ }
906
+ catch {
907
+ return undefined;
908
+ }
909
+ }
910
+ function resolveJunctionProviderDayClosedAtOffset(dayKey, offsetMinutes) {
911
+ const nextDayKey = addDaysToIsoDate(dayKey, 1);
912
+ const nominalNextDayMs = Date.parse(`${nextDayKey}T00:00:00.000Z`);
913
+ return new Date(nominalNextDayMs - offsetMinutes * 60_000).toISOString();
914
+ }
915
+ function normalizeJunctionCoverageProviderPulledAt(providerPulledAt) {
916
+ if (!providerPulledAt) {
917
+ return undefined;
918
+ }
919
+ const providerPulledAtMs = Date.parse(providerPulledAt);
920
+ if (!Number.isFinite(providerPulledAtMs)) {
921
+ return undefined;
922
+ }
923
+ const normalizedProviderPulledAt = new Date(providerPulledAtMs).toISOString();
924
+ return normalizedProviderPulledAt === providerPulledAt
925
+ ? normalizedProviderPulledAt
926
+ : undefined;
927
+ }
928
+ function applyJunctionCanonicalCoverageFence(context, fence) {
929
+ const sourceProviderSlug = normalizeJunctionSourceProviderSlug(fence?.sourceProviderSlug);
930
+ if (!fence || !sourceProviderSlug) {
931
+ return;
932
+ }
933
+ const admittedEvents = context.events.filter((event) => {
934
+ if (normalizeJunctionSourceProviderSlug(event.dataOrigin?.sourceProviderSlug)
935
+ !== sourceProviderSlug) {
936
+ return true;
937
+ }
938
+ const evidence = resolveJunctionCanonicalCoverageEvidence(event);
939
+ if (!evidence) {
940
+ return event.externalRef?.resourceType
941
+ === `junction-${slugify(sourceProviderSlug, "source")}-profile`;
942
+ }
943
+ if (!(evidence.resource in fence.coverageBoundaryByResource)) {
944
+ return true;
945
+ }
946
+ const legacyCoverageBoundary = normalizeJunctionCanonicalCoverageBoundary(evidence.resource, fence.coverageBoundaryByResource[evidence.resource]);
947
+ return legacyCoverageBoundary !== null
948
+ && evidence.coverageBoundary > legacyCoverageBoundary;
949
+ });
950
+ context.events.splice(0, context.events.length, ...admittedEvents);
951
+ }
952
+ function resolveJunctionCanonicalCoverageEvidence(event) {
953
+ const sourceProviderSlug = normalizeJunctionSourceProviderSlug(event.dataOrigin?.sourceProviderSlug);
954
+ const externalRefResourceType = event.externalRef?.resourceType;
955
+ if (!sourceProviderSlug || !externalRefResourceType) {
956
+ return null;
957
+ }
958
+ const resource = JUNCTION_CANONICAL_COVERAGE_RESOURCES.find((candidate) => externalRefResourceType
959
+ === `junction-${slugify(sourceProviderSlug, "source")}-${slugify(candidate, "resource")}`);
960
+ if (!resource) {
961
+ return null;
962
+ }
963
+ const coverageBoundary = normalizeJunctionCanonicalCoverageBoundary(resource, isJunctionDailyCanonicalCoverageResource(resource)
964
+ ? event.dayKey
965
+ : resolveCanonicalEventIntervalEnd(event));
966
+ return coverageBoundary
967
+ ? { coverageBoundary, resource, sourceProviderSlug }
968
+ : null;
969
+ }
970
+ function resolveCanonicalEventIntervalEnd(event) {
971
+ if (event.kind === "sleep_session") {
972
+ return normalizeTimestamp(event.endAt ?? event.fields?.endAt) ?? null;
973
+ }
974
+ if (event.kind === "activity_session") {
975
+ const fieldsWorkout = asPlainObject(event.fields?.workout);
976
+ return normalizeTimestamp(event.workout?.endedAt ?? fieldsWorkout?.endedAt)
977
+ ?? normalizeTimestamp(event.occurredAt)
978
+ ?? null;
979
+ }
980
+ if (event.sample?.endAt) {
981
+ return normalizeTimestamp(event.sample.endAt) ?? null;
982
+ }
983
+ return normalizeTimestamp(event.occurredAt) ?? null;
984
+ }
798
985
  export function classifyJunctionSummaryNormalizationEvidence(snapshot, evidenceWindow) {
799
986
  const evidenceRange = evidenceWindow
800
987
  ? {
@@ -850,6 +1037,9 @@ export function identifyJunctionBloodPressureProviderRecords(snapshot) {
850
1037
  evidenceParts: [],
851
1038
  events: [],
852
1039
  samples: [],
1040
+ temporalFeatureCurrentFacetsByResource: new Map(),
1041
+ temporalFeatureObservationCountsByDay: new Map(),
1042
+ authoritativeEventSets: [],
853
1043
  };
854
1044
  const repairStableExternalRefResourceIds = [];
855
1045
  for (const [resource, payload] of allowedResourceEntries(snapshot.timeseries, TIMESERIES_RESOURCE_ALLOWLIST)) {
@@ -893,6 +1083,9 @@ export function countAcceptedJunctionDailyTimeseriesProviderRecords(snapshot) {
893
1083
  evidenceParts: [],
894
1084
  events: [],
895
1085
  samples: [],
1086
+ temporalFeatureCurrentFacetsByResource: new Map(),
1087
+ temporalFeatureObservationCountsByDay: new Map(),
1088
+ authoritativeEventSets: [],
896
1089
  };
897
1090
  let acceptedProviderRecordCount = 0;
898
1091
  for (const [resource, payload] of allowedResourceEntries(snapshot.timeseries, TIMESERIES_RESOURCE_ALLOWLIST)) {
@@ -987,7 +1180,10 @@ function normalizeSummaries(summaries, context) {
987
1180
  const summaryEntries = allowedResourceEntries(summaries, SUMMARY_RESOURCE_ALLOWLIST);
988
1181
  const sleepSummaryMetricOwners = collectJunctionSleepSummaryMetricOwners(summaryEntries, context);
989
1182
  for (const [resource, payload] of summaryEntries) {
990
- const entries = resourceEntries(payload, resource);
1183
+ const preparedMenstrualCycle = resource === "menstrual_cycle"
1184
+ ? prepareJunctionMenstrualCycleSummary(payload, context.connectionsByKey)
1185
+ : undefined;
1186
+ const entries = preparedMenstrualCycle ? [] : resourceEntries(payload, resource);
991
1187
  const resourceSlug = slugify(resource, "summary");
992
1188
  const evidencePartRole = `junction-summary-${resourceSlug}`;
993
1189
  const fallbackIdentityDisambiguators = resource === "meal"
@@ -999,7 +1195,29 @@ function normalizeSummaries(summaries, context) {
999
1195
  resourceSlug,
1000
1196
  })
1001
1197
  : new Map();
1002
- pushEvidencePart(context.evidenceParts, createEvidencePart(evidencePartRole, `${evidencePartRole}.json`, buildRawResourcePayload(resource, payload, context.connectionsByKey)));
1198
+ pushEvidencePart(context.evidenceParts, createEvidencePart(evidencePartRole, `${evidencePartRole}.json`, preparedMenstrualCycle?.evidence
1199
+ ?? buildRawResourcePayload(resource, payload, context.connectionsByKey)));
1200
+ if (preparedMenstrualCycle) {
1201
+ preparedMenstrualCycle.cycles.forEach((cycle, index) => {
1202
+ const resourceContext = buildResourceContext({
1203
+ entry: cycle.entry,
1204
+ originFallback: cycle.originFallback,
1205
+ resource,
1206
+ resourceSlug,
1207
+ identityKind: "summary",
1208
+ index,
1209
+ fallbackArtifactRole: evidencePartRole,
1210
+ context,
1211
+ });
1212
+ if (!resourceContext) {
1213
+ return;
1214
+ }
1215
+ const firstEventIndex = context.events.length;
1216
+ pushMenstrualCycleSummary(cycle, resourceContext, context);
1217
+ pushJunctionAuthoritativeSummaryEventSet(cycle.entry, resourceContext, context, context.events.slice(firstEventIndex));
1218
+ });
1219
+ continue;
1220
+ }
1003
1221
  const resolvedEntries = entries.flatMap(({ entry, originFallback }, index) => {
1004
1222
  const resourceContext = buildResourceContext({
1005
1223
  entry,
@@ -1022,6 +1240,7 @@ function normalizeSummaries(summaries, context) {
1022
1240
  continue;
1023
1241
  }
1024
1242
  resolvedEntries.forEach(({ entry, resourceContext }) => {
1243
+ const firstEventIndex = context.events.length;
1025
1244
  switch (resource) {
1026
1245
  case "activity":
1027
1246
  pushObservationMetrics(entry, resourceContext, context, ACTIVITY_METRICS);
@@ -1041,13 +1260,11 @@ function normalizeSummaries(summaries, context) {
1041
1260
  case "profile":
1042
1261
  pushProfileSummary(entry, resourceContext, context);
1043
1262
  break;
1044
- case "menstrual_cycle":
1045
- pushMenstrualCycleSummary(entry, resourceContext, context);
1046
- break;
1047
1263
  case "electrocardiogram":
1048
1264
  pushElectrocardiogramSummary(entry, resourceContext, context);
1049
1265
  break;
1050
1266
  }
1267
+ pushJunctionAuthoritativeSummaryEventSet(entry, resourceContext, context, context.events.slice(firstEventIndex));
1051
1268
  });
1052
1269
  }
1053
1270
  }
@@ -1300,6 +1517,8 @@ const JUNCTION_DAILY_TIMESERIES_DESCRIPTOR_ENTRIES = [
1300
1517
  { metric: "glucose", statistic: "mean", title: "Junction glucose average" },
1301
1518
  { metric: "lowest-glucose", statistic: "min", title: "Junction glucose minimum" },
1302
1519
  { metric: "highest-glucose", statistic: "max", title: "Junction glucose maximum" },
1520
+ { metric: "glucose-standard-deviation", statistic: "standard_deviation", title: "Junction glucose standard deviation" },
1521
+ { metric: "glucose-coefficient-of-variation", statistic: "coefficient_of_variation", title: "Junction glucose coefficient of variation", unit: "%" },
1303
1522
  ],
1304
1523
  unit: "mg/dL",
1305
1524
  valuePaths: JUNCTION_GLUCOSE_VALUE_PATHS,
@@ -1435,6 +1654,15 @@ const JUNCTION_FEATURE_TIMESERIES_DESCRIPTORS = new Map(JUNCTION_FEATURE_TIMESER
1435
1654
  const JUNCTION_BLOOD_PRESSURE_RESOURCE = "blood_pressure";
1436
1655
  const JUNCTION_NOTE_RESOURCE = "note";
1437
1656
  const JUNCTION_WEIGHT_RESOURCE = "weight";
1657
+ const JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES = new Set([
1658
+ "fat",
1659
+ ]);
1660
+ const JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES = new Set([
1661
+ "body_mass_index",
1662
+ "lean_body_mass",
1663
+ "waist_circumference",
1664
+ ]);
1665
+ const JUNCTION_CANONICAL_BODY_TIMESTAMP_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|[+-](\d{2}):?(\d{2}))$/iu;
1438
1666
  const JUNCTION_SPARSE_TIMESERIES_DESCRIPTORS = new Map([
1439
1667
  ["body_mass_index", { canonicalUnit: "kg_m2", kind: "observation", maximum: 100, metric: "bmi", minimum: 5, title: "Junction BMI", upstreamUnit: "index" }],
1440
1668
  ["carbohydrates", { canonicalUnit: "g", kind: "observation", maximum: 2_000, metric: "carbohydrates", minimum: 0, title: "Junction carbohydrates", upstreamUnit: "g" }],
@@ -1463,7 +1691,9 @@ const JUNCTION_HEART_ALERT_TYPES = new Set([
1463
1691
  // cannot silently make the raw-snapshot sanitizer retain an unsupported shape.
1464
1692
  const COMPACT_TIMESERIES_RESOURCE_ALLOWLIST = new Set(JUNCTION_TIMESERIES_RESOURCE_POLICIES.map((policy) => policy.resource));
1465
1693
  function normalizeTimeseries(timeseries, context) {
1466
- for (const [resource, payload] of allowedResourceEntries(timeseries, TIMESERIES_RESOURCE_ALLOWLIST)) {
1694
+ const resources = allowedResourceEntries(timeseries, TIMESERIES_RESOURCE_ALLOWLIST)
1695
+ .sort(([left], [right]) => left.localeCompare(right));
1696
+ for (const [resource, payload] of resources) {
1467
1697
  if (resource === JUNCTION_NOTE_RESOURCE) {
1468
1698
  pushJunctionNoteTags(payload, resource, slugify(resource, "timeseries"), context);
1469
1699
  continue;
@@ -1559,18 +1789,28 @@ function assertJunctionSparseCalendarRepairRowsValid(input) {
1559
1789
  && (expectation.sourceInstanceId == null
1560
1790
  || resourceContext.origin.sourceInstanceId === expectation.sourceInstanceId);
1561
1791
  if (!valid) {
1562
- throw new JunctionSparseCalendarRepairNormalizationError();
1792
+ throw junctionCalendarRefreshNormalizationError("sparse_interval.row_incomplete", {
1793
+ rowOrdinal: index + 1,
1794
+ sourceProvider: normalizeKnownJunctionSourceProviderSlug(resourceContext?.sourceProviderSlug),
1795
+ timestampKind: classifyJunctionNormalizationTimestampKind(startRaw),
1796
+ timestampSemantics: timestamp?.timestampSemantics,
1797
+ });
1563
1798
  }
1564
1799
  }
1565
1800
  }
1566
1801
  export class JunctionSparseCalendarRepairNormalizationError extends Error {
1567
1802
  code = "JUNCTION_CALENDAR_REFRESH_INCOMPLETE_NORMALIZATION";
1803
+ diagnostic;
1568
1804
  retryable = true;
1569
- constructor() {
1805
+ constructor(diagnostic) {
1570
1806
  super("Junction calendar refresh contained a row that could not be applied completely.");
1571
1807
  this.name = "JunctionSparseCalendarRepairNormalizationError";
1808
+ this.diagnostic = diagnostic;
1572
1809
  }
1573
1810
  }
1811
+ function junctionCalendarRefreshNormalizationError(reason, diagnostic) {
1812
+ return new JunctionSparseCalendarRepairNormalizationError({ ...diagnostic, reason });
1813
+ }
1574
1814
  function pushJunctionBoundedTimeseriesFeatures(payload, resource, context) {
1575
1815
  const features = normalizeJunctionBoundedTimeseriesFeatures(resource, payload);
1576
1816
  const featureResourceSlug = slugify(resource, "timeseries");
@@ -1609,15 +1849,30 @@ function pushJunctionWorkoutStreamFeature(entry, resourceContext, context) {
1609
1849
  return;
1610
1850
  }
1611
1851
  const baseTimestamp = resolveRecordTimestamp(entry, context, resourceContext.sourceProviderSlug);
1612
- const timestamp = withTimestampOverride(baseTimestamp, {
1852
+ const workoutDayKey = firstStringFromPaths(entry, ["workoutDayKey"]);
1853
+ const timestamp = {
1854
+ ...baseTimestamp,
1613
1855
  occurredAt,
1614
- dayKey: extractIsoDatePrefix(occurredAt) ?? baseTimestamp.dayKey,
1856
+ dayKey: workoutDayKey,
1615
1857
  observedAtRaw: stringId(startRaw) ?? occurredAt,
1616
- });
1858
+ };
1617
1859
  const durationSeconds = firstNonNegativeNumberFromPaths(entry, ["durationSeconds"]);
1618
1860
  const distanceMeters = firstNonNegativeNumberFromPaths(entry, ["distanceMeters"]);
1619
1861
  const averageHeartRate = firstNonNegativeNumberFromPaths(entry, ["averageHeartRate"]);
1620
1862
  const maxHeartRate = firstNonNegativeNumberFromPaths(entry, ["maxHeartRate"]);
1863
+ const firstHalfAverageHeartRate = firstNonNegativeNumberFromPaths(entry, ["firstHalfAverageHeartRate"]);
1864
+ const secondHalfAverageHeartRate = firstNonNegativeNumberFromPaths(entry, ["secondHalfAverageHeartRate"]);
1865
+ const averageCadence = firstNonNegativeNumberFromPaths(entry, ["averageCadence"]);
1866
+ const maxCadence = firstNonNegativeNumberFromPaths(entry, ["maxCadence"]);
1867
+ const cadenceUnit = firstStringFromPaths(entry, ["cadenceUnit"]);
1868
+ const averagePower = firstNonNegativeNumberFromPaths(entry, ["averagePower"]);
1869
+ const maxPower = firstNonNegativeNumberFromPaths(entry, ["maxPower"]);
1870
+ const averageSpeed = firstNonNegativeNumberFromPaths(entry, ["averageSpeed"]);
1871
+ const maxSpeed = firstNonNegativeNumberFromPaths(entry, ["maxSpeed"]);
1872
+ const sport = firstStringFromPaths(entry, ["sport"]);
1873
+ const workoutTags = sport
1874
+ ? [`workout-sport-${trimSlugToLength(slugify(sport, "workout"), 60)}`]
1875
+ : undefined;
1621
1876
  const measurements = [
1622
1877
  ...(durationSeconds === undefined
1623
1878
  ? []
@@ -1631,22 +1886,122 @@ function pushJunctionWorkoutStreamFeature(entry, resourceContext, context) {
1631
1886
  ...(maxHeartRate === undefined
1632
1887
  ? []
1633
1888
  : [{ metric: "max-heart-rate", value: maxHeartRate, unit: "bpm" }]),
1889
+ ...(firstHalfAverageHeartRate === undefined
1890
+ ? []
1891
+ : [{
1892
+ metric: "first-half-average-workout-heart-rate",
1893
+ value: firstHalfAverageHeartRate,
1894
+ unit: "bpm",
1895
+ }]),
1896
+ ...(secondHalfAverageHeartRate === undefined
1897
+ ? []
1898
+ : [{
1899
+ metric: "second-half-average-workout-heart-rate",
1900
+ value: secondHalfAverageHeartRate,
1901
+ unit: "bpm",
1902
+ }]),
1903
+ ...(averageCadence === undefined || !cadenceUnit
1904
+ ? []
1905
+ : [{ metric: "average-workout-cadence", value: averageCadence, unit: cadenceUnit }]),
1906
+ ...(maxCadence === undefined || !cadenceUnit
1907
+ ? []
1908
+ : [{ metric: "max-workout-cadence", value: maxCadence, unit: cadenceUnit }]),
1909
+ ...(averagePower === undefined
1910
+ ? []
1911
+ : [{ metric: "average-workout-power", value: averagePower, unit: "watt" }]),
1912
+ ...(maxPower === undefined
1913
+ ? []
1914
+ : [{ metric: "max-workout-power", value: maxPower, unit: "watt" }]),
1915
+ ...(averageSpeed === undefined
1916
+ ? []
1917
+ : [{ metric: "average-workout-speed", value: averageSpeed, unit: "mps" }]),
1918
+ ...(maxSpeed === undefined
1919
+ ? []
1920
+ : [{ metric: "max-workout-speed", value: maxSpeed, unit: "mps" }]),
1634
1921
  ];
1635
1922
  if (measurements.length === 0) {
1636
1923
  return;
1637
1924
  }
1638
- context.events.push(stripUndefined({
1925
+ const emittedEvents = [];
1926
+ emittedEvents.push(stripUndefined({
1639
1927
  kind: "measurement",
1640
1928
  occurredAt,
1641
1929
  recordedAt: timestamp.recordedAt,
1642
1930
  dayKey: timestamp.dayKey,
1643
1931
  source: "device",
1644
1932
  title: "Junction workout stream features",
1933
+ tags: workoutTags,
1645
1934
  evidenceRoles: resourceContext.evidenceRoles,
1646
1935
  externalRef: makeJunctionExternalRef(resourceContext, entry, timestamp, "workout-stream-feature"),
1647
1936
  dataOrigin: buildDataOrigin(entry, resourceContext, timestamp),
1648
1937
  fields: { measurements },
1649
1938
  }));
1939
+ const splits = Array.isArray(entry.splits)
1940
+ ? entry.splits.flatMap((value) => {
1941
+ const split = asPlainObject(value);
1942
+ return split ? [split] : [];
1943
+ })
1944
+ : [];
1945
+ for (const split of splits) {
1946
+ const splitIndex = finiteNumber(split.index);
1947
+ const splitEndedAt = normalizeTimestamp(firstValueFromPaths(split, ["endedAt"]));
1948
+ const splitDistance = firstNonNegativeNumberFromPaths(split, ["distanceMeters"]);
1949
+ const splitDuration = firstNonNegativeNumberFromPaths(split, ["durationSeconds"]);
1950
+ if (splitIndex === undefined
1951
+ || !Number.isSafeInteger(splitIndex)
1952
+ || !splitEndedAt
1953
+ || splitDistance === undefined
1954
+ || splitDuration === undefined) {
1955
+ continue;
1956
+ }
1957
+ const splitCadence = firstNonNegativeNumberFromPaths(split, ["averageCadence"]);
1958
+ const splitCadenceUnit = firstStringFromPaths(split, ["cadenceUnit"]);
1959
+ const splitHeartRate = firstNonNegativeNumberFromPaths(split, ["averageHeartRate"]);
1960
+ const splitPower = firstNonNegativeNumberFromPaths(split, ["averagePower"]);
1961
+ const splitMeasurements = [
1962
+ { metric: "workout-split-duration", value: splitDuration, unit: "seconds" },
1963
+ { metric: "workout-split-distance", value: splitDistance, unit: "meter" },
1964
+ ...(splitHeartRate === undefined
1965
+ ? []
1966
+ : [{ metric: "average-workout-split-heart-rate", value: splitHeartRate, unit: "bpm" }]),
1967
+ ...(splitCadence === undefined || !splitCadenceUnit
1968
+ ? []
1969
+ : [{
1970
+ metric: "average-workout-split-cadence",
1971
+ value: splitCadence,
1972
+ unit: splitCadenceUnit,
1973
+ }]),
1974
+ ...(splitPower === undefined
1975
+ ? []
1976
+ : [{ metric: "average-workout-split-power", value: splitPower, unit: "watt" }]),
1977
+ ];
1978
+ emittedEvents.push(stripUndefined({
1979
+ kind: "measurement",
1980
+ occurredAt: splitEndedAt,
1981
+ recordedAt: timestamp.recordedAt,
1982
+ dayKey: timestamp.dayKey,
1983
+ source: "device",
1984
+ title: `Junction workout split ${splitIndex}`,
1985
+ tags: workoutTags,
1986
+ evidenceRoles: resourceContext.evidenceRoles,
1987
+ externalRef: makeJunctionExternalRef(resourceContext, entry, timestamp, `workout-stream-split-${splitIndex}`),
1988
+ dataOrigin: buildDataOrigin(entry, resourceContext, timestamp),
1989
+ fields: { measurements: splitMeasurements },
1990
+ }));
1991
+ }
1992
+ context.events.push(...emittedEvents);
1993
+ const version = junctionExternalRefVersion(resourceContext, entry);
1994
+ const identity = emittedEvents[0]?.externalRef;
1995
+ if (version && identity) {
1996
+ context.authoritativeEventSets.push({
1997
+ system: identity.system,
1998
+ resourceType: identity.resourceType,
1999
+ resourceId: identity.resourceId,
2000
+ version,
2001
+ facetPrefixes: ["workout-stream-feature", "workout-stream-split"],
2002
+ currentFacets: emittedEvents.flatMap((event) => event.externalRef?.facet ? [event.externalRef.facet] : []),
2003
+ });
2004
+ }
1650
2005
  }
1651
2006
  function normalizeJunctionBoundedTimeseriesFeatures(resource, payload) {
1652
2007
  const records = timeseriesResourceEntries(payload).map(({ entry }) => entry);
@@ -1669,21 +2024,27 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1669
2024
  if (!resourceContext) {
1670
2025
  continue;
1671
2026
  }
1672
- const record = parseJunctionSparseTimeseriesRecord(entry, resource, descriptor);
1673
- if (!record) {
2027
+ const parsedRecord = parseJunctionSparseTimeseriesRecord(entry, resource, descriptor);
2028
+ if (!parsedRecord) {
1674
2029
  continue;
1675
2030
  }
1676
- const timestamp = resolveRecordTimestamp({ ...entry, observedAtRaw: record.observedAtRaw }, context, resourceContext.sourceProviderSlug);
1677
- const occurredAt = timestamp.occurredAt ?? timestamp.recordedAt;
1678
- const dayKey = resolveJunctionTimeseriesAggregateDayKey(entry, timestamp, occurredAt, context.defaultTimeZone);
1679
- if (!occurredAt || !dayKey) {
2031
+ const resolvedTimestamp = resolveJunctionSparseTimeseriesTimestamp({
2032
+ context,
2033
+ entry,
2034
+ record: parsedRecord,
2035
+ resource,
2036
+ sourceProviderSlug: resourceContext.sourceProviderSlug,
2037
+ });
2038
+ if (!resolvedTimestamp) {
1680
2039
  continue;
1681
2040
  }
2041
+ const { dayKey, occurredAt, record, timeZone, timestamp } = resolvedTimestamp;
1682
2042
  candidates.push({
1683
2043
  dayKey,
1684
2044
  entry,
1685
2045
  identityHash: buildJunctionSparseTimeseriesIdentityHash({
1686
- record,
2046
+ record: parsedRecord,
2047
+ resource,
1687
2048
  resourceContext,
1688
2049
  resourceSlug,
1689
2050
  }),
@@ -1695,16 +2056,19 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1695
2056
  entry,
1696
2057
  occurredAt,
1697
2058
  record,
2059
+ resource,
1698
2060
  resourceContext,
1699
2061
  resourceSlug,
1700
2062
  timestamp,
1701
2063
  }),
2064
+ timeZone,
1702
2065
  timestamp,
1703
2066
  });
1704
2067
  }
1705
2068
  const semanticContentKeysByProviderIdentity = new Map();
1706
2069
  for (const candidate of candidates) {
1707
- if (!candidate.record.providerRowId) {
2070
+ if (!candidate.record.providerRowId
2071
+ && !isJunctionBodyTimeseriesResource(resource)) {
1708
2072
  continue;
1709
2073
  }
1710
2074
  const contentKeys = semanticContentKeysByProviderIdentity.get(candidate.identityHash)
@@ -1716,13 +2080,42 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1716
2080
  .filter(([, contentKeys]) => contentKeys.size > 1)
1717
2081
  .map(([identityHash]) => identityHash));
1718
2082
  const seenRecordIdentityHashes = new Set();
1719
- for (const candidate of candidates) {
1720
- const { dayKey, entry, identityHash, occurredAt, record, resourceContext, timestamp, } = candidate;
1721
- if (conflictingProviderIdentityHashes.has(identityHash)
1722
- || seenRecordIdentityHashes.has(identityHash)) {
1723
- continue;
2083
+ const uniqueCandidates = candidates.filter((candidate) => {
2084
+ if (conflictingProviderIdentityHashes.has(candidate.identityHash)
2085
+ || seenRecordIdentityHashes.has(candidate.identityHash)) {
2086
+ return false;
1724
2087
  }
1725
- seenRecordIdentityHashes.add(identityHash);
2088
+ seenRecordIdentityHashes.add(candidate.identityHash);
2089
+ return true;
2090
+ });
2091
+ const maximumRecords = resolveJunctionTimeseriesResourcePolicy(resource)
2092
+ ?.maxCanonicalRecordsPerWindow;
2093
+ const retainedCandidates = maximumRecords === undefined
2094
+ ? uniqueCandidates
2095
+ : [...uniqueCandidates]
2096
+ .sort((left, right) => left.occurredAt.localeCompare(right.occurredAt)
2097
+ || left.identityHash.localeCompare(right.identityHash)
2098
+ || left.semanticContentKey.localeCompare(right.semanticContentKey))
2099
+ .slice(0, maximumRecords);
2100
+ if (maximumRecords !== undefined && uniqueCandidates.length > maximumRecords) {
2101
+ const role = `${baseArtifactRole}:bounded-overflow`;
2102
+ pushEvidencePart(context.evidenceParts, withJunctionCompactTimeseriesMetadata(resource, createEvidencePart(role, `${role}.json`, {
2103
+ schema: "junction.sparse_timeseries_overflow.v1",
2104
+ provider: "junction",
2105
+ resource,
2106
+ validatedRecordCount: uniqueCandidates.length,
2107
+ retainedRecordCount: retainedCandidates.length,
2108
+ droppedRecordCount: uniqueCandidates.length - retainedCandidates.length,
2109
+ status: "bounded_overflow",
2110
+ }), "timeseries_reading"));
2111
+ }
2112
+ for (const candidate of retainedCandidates) {
2113
+ const { dayKey, entry, identityHash, occurredAt, record, resourceContext, timeZone, timestamp, } = candidate;
2114
+ const usesFloatingFallbackTimeZone = isJunctionSparseFloatingFallbackTimeZone({
2115
+ entry,
2116
+ resource,
2117
+ timestamp,
2118
+ });
1726
2119
  const role = `${baseArtifactRole}:${dayKey}:${identityHash}`;
1727
2120
  pushEvidencePart(context.evidenceParts, withJunctionCompactTimeseriesMetadata(resource, createEvidencePart(role, `${role}.json`, stripUndefined({
1728
2121
  schema: "junction.sparse_timeseries_record.v1",
@@ -1732,12 +2125,13 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1732
2125
  sourceProviderSlug: resourceContext.sourceProviderSlug,
1733
2126
  sourceType: resourceContext.origin.sourceType,
1734
2127
  sourceInstanceId: resourceContext.origin.sourceInstanceId,
1735
- providerRowId: record.providerRowId,
1736
2128
  observedAtRaw: record.observedAtRaw,
1737
- occurredAt,
2129
+ occurredAt: usesFloatingFallbackTimeZone ? undefined : occurredAt,
1738
2130
  recordedAt: timestamp.recordedAt,
1739
- startAt: record.startAt,
1740
- endAt: record.endAt,
2131
+ startAt: usesFloatingFallbackTimeZone ? undefined : record.startAt,
2132
+ endAt: usesFloatingFallbackTimeZone ? undefined : record.endAt,
2133
+ startAtRaw: usesFloatingFallbackTimeZone ? stringId(entry.start) : undefined,
2134
+ endAtRaw: usesFloatingFallbackTimeZone ? stringId(entry.end) : undefined,
1741
2135
  value: record.value,
1742
2136
  unit: record.unit,
1743
2137
  upstreamUnit: record.upstreamUnit,
@@ -1752,7 +2146,11 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1752
2146
  occurredAt,
1753
2147
  dayKey,
1754
2148
  observedAtRaw: record.observedAtRaw,
1755
- }), { normalizerVersion: "junction-sparse-timeseries.v1" });
2149
+ }), {
2150
+ normalizerVersion: usesFloatingFallbackTimeZone
2151
+ ? JUNCTION_SPARSE_FLOATING_FALLBACK_NORMALIZER_VERSION
2152
+ : "junction-sparse-timeseries.v1",
2153
+ });
1756
2154
  if (descriptor.kind === "insulin") {
1757
2155
  const insulinTags = [
1758
2156
  record.bolusPurpose,
@@ -1765,6 +2163,7 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1765
2163
  occurredAt,
1766
2164
  recordedAt: timestamp.recordedAt,
1767
2165
  dayKey,
2166
+ timeZone,
1768
2167
  source: "device",
1769
2168
  title: descriptor.title,
1770
2169
  tags: insulinTags.length > 0 ? [...new Set(insulinTags)] : undefined,
@@ -1799,6 +2198,7 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1799
2198
  occurredAt,
1800
2199
  recordedAt: timestamp.recordedAt,
1801
2200
  dayKey,
2201
+ timeZone,
1802
2202
  source: "device",
1803
2203
  title: descriptor.title,
1804
2204
  tags: alertTag ? [alertTag] : undefined,
@@ -1814,6 +2214,80 @@ function pushJunctionSparseTimeseriesRecords(payload, resource, resourceSlug, co
1814
2214
  }));
1815
2215
  }
1816
2216
  }
2217
+ function resolveJunctionSparseTimeseriesTimestamp(input) {
2218
+ const timestamp = resolveRecordTimestamp({ ...input.entry, observedAtRaw: input.record.observedAtRaw }, input.context, input.sourceProviderSlug);
2219
+ const metabolicInterval = input.resource === "carbohydrates"
2220
+ || input.resource === "insulin_injection";
2221
+ const startRaw = metabolicInterval ? stringId(input.entry.start) : undefined;
2222
+ const endRaw = metabolicInterval ? stringId(input.entry.end) : undefined;
2223
+ const startIsFloating = startRaw
2224
+ ? isJunctionSourceLocalFloatingTimestamp(startRaw, input.sourceProviderSlug)
2225
+ : false;
2226
+ const endIsFloating = endRaw
2227
+ ? isJunctionSourceLocalFloatingTimestamp(endRaw, input.sourceProviderSlug)
2228
+ : false;
2229
+ if (metabolicInterval && (startIsFloating || endIsFloating)) {
2230
+ if (startIsFloating !== endIsFloating) {
2231
+ return null;
2232
+ }
2233
+ const explicitTimeZone = normalizeIanaTimeZone(firstStringFromPaths(input.entry, ["timeZone", "timezone", "time_zone"]));
2234
+ const timeZone = resolveJunctionFloatingTimestampTimeZone(input.entry, input.context.defaultTimeZone);
2235
+ const usesFallbackTimeZone = explicitTimeZone === null;
2236
+ const floatingStart = startRaw && startIsFloating && timeZone
2237
+ ? resolveFloatingIsoTimestampInTimeZone(startRaw, timeZone)
2238
+ : null;
2239
+ const floatingEnd = endRaw && endIsFloating && timeZone
2240
+ ? resolveFloatingIsoTimestampInTimeZone(endRaw, timeZone)
2241
+ : null;
2242
+ const startAt = startIsFloating
2243
+ ? usesFallbackTimeZone
2244
+ ? normalizeTimestamp(startRaw)
2245
+ : floatingStart?.timestamp
2246
+ : resolveSafeTimestamp(startRaw, input.sourceProviderSlug);
2247
+ const endAt = endIsFloating
2248
+ ? usesFallbackTimeZone
2249
+ ? normalizeTimestamp(endRaw)
2250
+ : floatingEnd?.timestamp
2251
+ : resolveSafeTimestamp(endRaw, input.sourceProviderSlug);
2252
+ if (!startAt || !endAt || Date.parse(endAt) < Date.parse(startAt)) {
2253
+ return null;
2254
+ }
2255
+ const dayKey = usesFallbackTimeZone
2256
+ ? extractIsoDatePrefix(startRaw)
2257
+ : floatingStart?.dayKey ?? resolveJunctionTimeseriesAggregateDayKey(input.entry, timestamp, startAt, input.context.defaultTimeZone);
2258
+ if (!dayKey) {
2259
+ return null;
2260
+ }
2261
+ return stripUndefined({
2262
+ dayKey,
2263
+ occurredAt: startAt,
2264
+ record: { ...input.record, startAt, endAt },
2265
+ timeZone,
2266
+ timestamp: withTimestampOverride(timestamp, {
2267
+ occurredAt: startAt,
2268
+ dayKey,
2269
+ observedAtRaw: startRaw,
2270
+ }),
2271
+ });
2272
+ }
2273
+ const occurredAt = timestamp.occurredAt ?? timestamp.recordedAt;
2274
+ const dayKey = resolveJunctionTimeseriesAggregateDayKey(input.entry, timestamp, occurredAt, input.context.defaultTimeZone);
2275
+ if (!occurredAt || !dayKey) {
2276
+ return null;
2277
+ }
2278
+ return stripUndefined({
2279
+ dayKey,
2280
+ occurredAt,
2281
+ record: input.record,
2282
+ timeZone: firstStringFromPaths(input.entry, ["timeZone", "timezone", "time_zone"]),
2283
+ timestamp,
2284
+ });
2285
+ }
2286
+ function isJunctionSparseFloatingFallbackTimeZone(input) {
2287
+ return (input.resource === "carbohydrates" || input.resource === "insulin_injection")
2288
+ && input.timestamp.timestampSemantics === "floating"
2289
+ && normalizeIanaTimeZone(firstStringFromPaths(input.entry, ["timeZone", "timezone", "time_zone"])) === null;
2290
+ }
1817
2291
  function parseJunctionSparseTimeseriesRecord(entry, resource, descriptor) {
1818
2292
  const value = finiteNumber(entry.value);
1819
2293
  const unit = stringId(entry.unit);
@@ -1827,7 +2301,43 @@ function parseJunctionSparseTimeseriesRecord(entry, resource, descriptor) {
1827
2301
  const timestampRaw = stringId(entry.timestamp);
1828
2302
  const startRaw = stringId(entry.start);
1829
2303
  const endRaw = stringId(entry.end);
1830
- const requiresTimestamp = resource !== "carbohydrates"
2304
+ const instantBodyResource = JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES.has(resource);
2305
+ const intervalBodyResource = JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES.has(resource);
2306
+ if (instantBodyResource) {
2307
+ const timestamp = normalizeJunctionCanonicalBodyTimestamp(timestampRaw);
2308
+ if (!timestampRaw || !timestamp) {
2309
+ return null;
2310
+ }
2311
+ return buildJunctionSparseTimeseriesRecord({
2312
+ descriptor,
2313
+ entry,
2314
+ observedAtRaw: timestampRaw,
2315
+ resource,
2316
+ value,
2317
+ });
2318
+ }
2319
+ if (intervalBodyResource) {
2320
+ const startAt = normalizeJunctionCanonicalBodyTimestamp(startRaw);
2321
+ const endAt = normalizeJunctionCanonicalBodyTimestamp(endRaw);
2322
+ if (!startRaw
2323
+ || !endRaw
2324
+ || !startAt
2325
+ || !endAt
2326
+ || Date.parse(endAt) <= Date.parse(startAt)) {
2327
+ return null;
2328
+ }
2329
+ return buildJunctionSparseTimeseriesRecord({
2330
+ descriptor,
2331
+ endAt,
2332
+ entry,
2333
+ observedAtRaw: startRaw,
2334
+ resource,
2335
+ startAt,
2336
+ value,
2337
+ });
2338
+ }
2339
+ const requiresTimestamp = !intervalBodyResource
2340
+ && resource !== "carbohydrates"
1831
2341
  && resource !== "heart_rate_alert"
1832
2342
  && resource !== "insulin_injection";
1833
2343
  const requiresInterval = resource !== "fat";
@@ -1837,7 +2347,7 @@ function parseJunctionSparseTimeseriesRecord(entry, resource, descriptor) {
1837
2347
  const timestamp = timestampRaw ? normalizeTimestamp(timestampRaw) : undefined;
1838
2348
  const startAt = startRaw ? normalizeTimestamp(startRaw) : undefined;
1839
2349
  const endAt = endRaw ? normalizeTimestamp(endRaw) : undefined;
1840
- if ((timestampRaw && !timestamp)
2350
+ if ((!intervalBodyResource && timestampRaw && !timestamp)
1841
2351
  || (startRaw && !startAt)
1842
2352
  || (endRaw && !endAt)
1843
2353
  || (requiresInterval && (!startAt || !endAt))
@@ -1848,6 +2358,51 @@ function parseJunctionSparseTimeseriesRecord(entry, resource, descriptor) {
1848
2358
  if (!observedAtRaw) {
1849
2359
  return null;
1850
2360
  }
2361
+ return buildJunctionSparseTimeseriesRecord({
2362
+ descriptor,
2363
+ endAt,
2364
+ entry,
2365
+ observedAtRaw,
2366
+ resource,
2367
+ startAt,
2368
+ value,
2369
+ });
2370
+ }
2371
+ export function normalizeJunctionCanonicalBodyTimestamp(value) {
2372
+ const normalized = stringId(value);
2373
+ const match = normalized
2374
+ ? JUNCTION_CANONICAL_BODY_TIMESTAMP_PATTERN.exec(normalized)
2375
+ : null;
2376
+ if (!match) {
2377
+ return undefined;
2378
+ }
2379
+ const year = Number(match[1]);
2380
+ const month = Number(match[2]);
2381
+ const day = Number(match[3]);
2382
+ const hour = Number(match[4]);
2383
+ const minute = Number(match[5]);
2384
+ const second = Number(match[6]);
2385
+ const offsetHour = Number(match[7] ?? 0);
2386
+ const offsetMinute = Number(match[8] ?? 0);
2387
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
2388
+ const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] ?? 0;
2389
+ if (day < 1
2390
+ || day > daysInMonth
2391
+ || hour > 23
2392
+ || minute > 59
2393
+ || second > 59
2394
+ || offsetHour > 23
2395
+ || offsetMinute > 59) {
2396
+ return undefined;
2397
+ }
2398
+ return normalizeTimestamp(normalized);
2399
+ }
2400
+ function isJunctionBodyTimeseriesResource(resource) {
2401
+ return JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES.has(resource)
2402
+ || JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES.has(resource);
2403
+ }
2404
+ function buildJunctionSparseTimeseriesRecord(input) {
2405
+ const { descriptor, endAt, entry, observedAtRaw, resource, startAt, value } = input;
1851
2406
  const alertType = descriptor.kind === "alert" && resource === "heart_rate_alert"
1852
2407
  ? normalizeJunctionSemanticToken(entry.type)
1853
2408
  : undefined;
@@ -1903,28 +2458,44 @@ function buildJunctionSparseTimeseriesSemanticIdentityHash(input) {
1903
2458
  return shortHash(junctionSparseTimeseriesSemanticIdentityParts(input));
1904
2459
  }
1905
2460
  function junctionSparseTimeseriesSemanticIdentityParts(input) {
1906
- return [
2461
+ const stableIdentity = [
1907
2462
  input.resourceSlug,
1908
2463
  input.resourceContext.sourceProviderSlug,
1909
2464
  input.resourceContext.origin.sourceType ?? "",
1910
2465
  input.resourceContext.origin.sourceInstanceId ?? "",
1911
2466
  input.record.providerRowId ?? "",
1912
- input.record.observedAtRaw,
1913
- input.record.startAt ?? "",
1914
- input.record.endAt ?? "",
1915
- input.record.value,
1916
- input.record.unit,
1917
- input.record.upstreamUnit,
1918
- input.record.alertType ?? "",
1919
- input.record.bolusPurpose ?? "",
1920
- input.record.deliveryForm ?? "",
1921
- input.record.deliveryMode ?? "",
1922
- input.record.insulinType ?? "",
2467
+ isJunctionBodyTimeseriesResource(input.resource)
2468
+ ? normalizeTimestamp(input.record.observedAtRaw) ?? input.record.observedAtRaw
2469
+ : input.record.observedAtRaw,
1923
2470
  ];
2471
+ return isJunctionBodyTimeseriesResource(input.resource)
2472
+ ? stableIdentity
2473
+ : [
2474
+ ...stableIdentity,
2475
+ input.record.startAt ?? "",
2476
+ input.record.endAt ?? "",
2477
+ input.record.value,
2478
+ input.record.unit,
2479
+ input.record.upstreamUnit,
2480
+ input.record.alertType ?? "",
2481
+ input.record.bolusPurpose ?? "",
2482
+ input.record.deliveryForm ?? "",
2483
+ input.record.deliveryMode ?? "",
2484
+ input.record.insulinType ?? "",
2485
+ ];
1924
2486
  }
1925
2487
  function buildJunctionSparseTimeseriesSemanticContentKey(input) {
1926
2488
  return JSON.stringify([
1927
2489
  ...junctionSparseTimeseriesSemanticIdentityParts(input),
2490
+ ...(isJunctionBodyTimeseriesResource(input.resource)
2491
+ ? [
2492
+ input.record.startAt ?? "",
2493
+ input.record.endAt ?? "",
2494
+ input.record.value,
2495
+ input.record.unit,
2496
+ input.record.upstreamUnit,
2497
+ ]
2498
+ : []),
1928
2499
  input.occurredAt,
1929
2500
  input.timestamp.recordedAt ?? "",
1930
2501
  input.dayKey,
@@ -2072,7 +2643,7 @@ function pushJunctionDailyTimeseriesObservations(payload, resource, resourceSlug
2072
2643
  pushJunctionDailyTimeseriesObservation(context, aggregate, {
2073
2644
  metric: observation.metric,
2074
2645
  title: observation.title,
2075
- unit: descriptor.unit,
2646
+ unit: observation.unit ?? descriptor.unit,
2076
2647
  value: junctionDailyTimeseriesStatisticValue(aggregate, observation.statistic),
2077
2648
  });
2078
2649
  }
@@ -2093,8 +2664,20 @@ function junctionDailyTimeseriesStatisticValue(aggregate, statistic) {
2093
2664
  return aggregate.sum;
2094
2665
  case "mean":
2095
2666
  return aggregate.sum / aggregate.sampleCount;
2667
+ case "standard_deviation":
2668
+ return junctionDailyTimeseriesStandardDeviation(aggregate);
2669
+ case "coefficient_of_variation":
2670
+ return junctionDailyTimeseriesCoefficientOfVariation(aggregate);
2096
2671
  }
2097
2672
  }
2673
+ function junctionDailyTimeseriesStandardDeviation(aggregate) {
2674
+ const mean = aggregate.sum / aggregate.sampleCount;
2675
+ return Math.sqrt(Math.max(0, aggregate.sumSquares / aggregate.sampleCount - mean * mean));
2676
+ }
2677
+ function junctionDailyTimeseriesCoefficientOfVariation(aggregate) {
2678
+ const mean = aggregate.sum / aggregate.sampleCount;
2679
+ return mean === 0 ? 0 : junctionDailyTimeseriesStandardDeviation(aggregate) / mean * 100;
2680
+ }
2098
2681
  function buildJunctionTimeseriesFidelityRecordKey(input) {
2099
2682
  const stableId = firstStringFromPaths(input.entry, JUNCTION_TIMESERIES_RECORD_ID_PATHS);
2100
2683
  const shared = [
@@ -2296,7 +2879,20 @@ function parseJunctionFloatingDateTime(value) {
2296
2879
  function buildJunctionDailyTimeseriesAggregates(input) {
2297
2880
  const evidencePartRole = `junction-timeseries-daily-${input.resourceSlug}`;
2298
2881
  const aggregates = new Map();
2299
- const entries = timeseriesResourceEntries(input.payload);
2882
+ const temporalAggregates = new Map();
2883
+ const temporalFeatureResource = isJunctionTemporalFeatureResource(input.resource)
2884
+ ? input.resource
2885
+ : undefined;
2886
+ const ownsTemporalFeatures = temporalFeatureResource !== undefined
2887
+ && input.context.temporalFeatureSourceDay?.resources.includes(input.resource) === true;
2888
+ let temporalFeatureInputCount = 0;
2889
+ let temporalFeatureImportSuppressed = false;
2890
+ // A complete-source-day payload is always grouped; its rows are exactly the
2891
+ // grouped rows, so an empty grouped envelope or an explicitly empty group
2892
+ // data array is a valid authoritative empty rather than a lossy row.
2893
+ const entries = ownsTemporalFeatures && asPlainObject(asPlainObject(input.payload)?.groups)
2894
+ ? groupedTimeseriesResourceEntries(input.payload).filter(({ entry }) => !Array.isArray(entry.data) || entry.data.length > 0)
2895
+ : timeseriesResourceEntries(input.payload);
2300
2896
  const fidelityResource = isJunctionDenseFidelityResource(input.resource)
2301
2897
  ? input.resource
2302
2898
  : isJunctionSparseIntervalResource(input.resource)
@@ -2317,6 +2913,15 @@ function buildJunctionDailyTimeseriesAggregates(input) {
2317
2913
  : undefined;
2318
2914
  const seenFidelityRecords = new Set();
2319
2915
  for (const [index, { entry, originFallback }] of entries.entries()) {
2916
+ const timestampPaths = isJunctionSparseIntervalResource(input.resource)
2917
+ ? JUNCTION_INTERVAL_START_TIMESTAMP_PATHS
2918
+ : JUNCTION_INTERVAL_START_OWNED_TIMESTAMP_PATHS;
2919
+ const providerTimestamp = firstStringFromPaths(entry, timestampPaths)
2920
+ ?? firstValueFromPaths(entry, timestampPaths);
2921
+ const rowDiagnostic = {
2922
+ rowOrdinal: index + 1,
2923
+ timestampKind: classifyJunctionNormalizationTimestampKind(providerTimestamp),
2924
+ };
2320
2925
  const resourceContext = buildResourceContext({
2321
2926
  entry,
2322
2927
  originFallback,
@@ -2328,10 +2933,21 @@ function buildJunctionDailyTimeseriesAggregates(input) {
2328
2933
  context: input.context,
2329
2934
  });
2330
2935
  if (!resourceContext) {
2936
+ // A complete-source-day replacement may not silently drop a delivered
2937
+ // row; an unowned row invalidates the day's replacement authority.
2938
+ if (ownsTemporalFeatures) {
2939
+ throw junctionCalendarRefreshNormalizationError("source_context.unresolved", {
2940
+ ...rowDiagnostic,
2941
+ timestampSemantics: typeof providerTimestamp === "string"
2942
+ ? inferTimestampSemantics(providerTimestamp)
2943
+ : undefined,
2944
+ });
2945
+ }
2331
2946
  continue;
2332
2947
  }
2333
2948
  const providerValue = firstValueFromPaths(entry, input.valuePaths);
2334
- const value = input.normalizeValue(firstNumberFromPaths(entry, input.valuePaths), entry);
2949
+ const numericProviderValue = firstNumberFromPaths(entry, input.valuePaths);
2950
+ const value = input.normalizeValue(numericProviderValue, entry);
2335
2951
  const sparseStartRaw = isJunctionSparseIntervalResource(input.resource)
2336
2952
  ? firstStringFromPaths(entry, JUNCTION_INTERVAL_START_TIMESTAMP_PATHS)
2337
2953
  : undefined;
@@ -2352,6 +2968,29 @@ function buildJunctionDailyTimeseriesAggregates(input) {
2352
2968
  && !hasValidJunctionExplicitTimeseriesTimestamp(timestamp))
2353
2969
  || !sampleAt
2354
2970
  || !dayKey) {
2971
+ // Rows without a usable value, timestamp semantics, or target day cannot
2972
+ // certify a complete source day; fail the import instead of certifying a
2973
+ // lossy response as authoritative.
2974
+ if (ownsTemporalFeatures) {
2975
+ let reason;
2976
+ if (numericProviderValue === undefined) {
2977
+ reason = providerValue === undefined || providerValue === null
2978
+ ? "daily.value_missing"
2979
+ : "daily.value_non_numeric";
2980
+ }
2981
+ else if (value === undefined) {
2982
+ reason = "daily.value_out_of_range";
2983
+ }
2984
+ else {
2985
+ reason = "daily.timestamp_or_day_unresolved";
2986
+ }
2987
+ throw junctionCalendarRefreshNormalizationError(reason, {
2988
+ ...rowDiagnostic,
2989
+ sourceProvider: normalizeKnownJunctionSourceProviderSlug(resourceContext.sourceProviderSlug),
2990
+ timestampKind: classifyJunctionNormalizationTimestampKind(timestamp.observedAtRaw ?? providerTimestamp),
2991
+ timestampSemantics: timestamp.timestampSemantics,
2992
+ });
2993
+ }
2355
2994
  continue;
2356
2995
  }
2357
2996
  const key = [
@@ -2437,42 +3076,177 @@ function buildJunctionDailyTimeseriesAggregates(input) {
2437
3076
  resourceContext,
2438
3077
  sampleCount: 1,
2439
3078
  sum: value,
3079
+ sumSquares: value * value,
2440
3080
  timestamp,
2441
3081
  timeZone,
2442
3082
  });
2443
3083
  if (fidelityResource) {
2444
3084
  assertJunctionTimeseriesSourceDayBound(fidelityResource, 1);
2445
3085
  }
2446
- continue;
2447
- }
2448
- existing.sampleCount += 1;
2449
- existing.authoritativeEmptyCalendarSet ||= entry.authoritativeEmptyCalendarSet === true;
2450
- if (fidelitySample) {
2451
- existing.fidelitySamples.push(fidelitySample);
2452
- }
2453
- if (fidelityResource) {
2454
- assertJunctionTimeseriesSourceDayBound(fidelityResource, existing.sampleCount);
2455
- }
2456
- existing.sum += value;
2457
- if (legacyDayKey && legacyDayKey !== dayKey) {
2458
- existing.legacyDayKeys.add(legacyDayKey);
2459
- }
2460
- if (sampleAt < existing.firstSampleAt) {
2461
- existing.firstSampleAt = sampleAt;
2462
3086
  }
2463
- if (sampleAt >= existing.lastSampleAt) {
2464
- existing.lastSampleAt = sampleAt;
2465
- existing.lastRecordedAt = recordedAt;
2466
- existing.timestamp = timestamp;
2467
- }
2468
- if (value < existing.minValue) {
2469
- existing.minValue = value;
3087
+ else {
3088
+ existing.sampleCount += 1;
3089
+ existing.authoritativeEmptyCalendarSet ||= entry.authoritativeEmptyCalendarSet === true;
3090
+ if (fidelitySample) {
3091
+ existing.fidelitySamples.push(fidelitySample);
3092
+ }
3093
+ if (fidelityResource) {
3094
+ assertJunctionTimeseriesSourceDayBound(fidelityResource, existing.sampleCount);
3095
+ }
3096
+ existing.sum += value;
3097
+ existing.sumSquares += value * value;
3098
+ if (legacyDayKey && legacyDayKey !== dayKey) {
3099
+ existing.legacyDayKeys.add(legacyDayKey);
3100
+ }
3101
+ if (sampleAt < existing.firstSampleAt) {
3102
+ existing.firstSampleAt = sampleAt;
3103
+ }
3104
+ if (sampleAt >= existing.lastSampleAt) {
3105
+ existing.lastSampleAt = sampleAt;
3106
+ existing.lastRecordedAt = recordedAt;
3107
+ existing.timestamp = timestamp;
3108
+ }
3109
+ if (value < existing.minValue) {
3110
+ existing.minValue = value;
3111
+ }
3112
+ if (value > existing.maxValue) {
3113
+ existing.maxValue = value;
3114
+ }
2470
3115
  }
2471
- if (value > existing.maxValue) {
2472
- existing.maxValue = value;
3116
+ const temporalSourceDay = ownsTemporalFeatures && !temporalFeatureImportSuppressed
3117
+ ? input.context.temporalFeatureSourceDay
3118
+ : undefined;
3119
+ const temporalSampleAt = temporalSourceDay
3120
+ ? resolveJunctionTemporalFeatureInstant(timestamp, temporalSourceDay.timeZone, {
3121
+ ...rowDiagnostic,
3122
+ sourceProvider: normalizeKnownJunctionSourceProviderSlug(resourceContext.sourceProviderSlug),
3123
+ timestampKind: classifyJunctionNormalizationTimestampKind(timestamp.observedAtRaw ?? providerTimestamp),
3124
+ timestampSemantics: timestamp.timestampSemantics,
3125
+ })
3126
+ : null;
3127
+ if (temporalSourceDay && temporalSampleAt !== null) {
3128
+ temporalFeatureInputCount += 1;
3129
+ if (temporalFeatureInputCount > JUNCTION_TEMPORAL_FEATURE_MAX_SAMPLES_PER_IMPORT) {
3130
+ temporalFeatureImportSuppressed = true;
3131
+ for (const current of temporalAggregates.values()) {
3132
+ delete current.temporalFeatureSamples;
3133
+ }
3134
+ }
3135
+ else {
3136
+ const sourceDay = temporalSourceDay;
3137
+ const vaultDayKey = toLocalDayKey(temporalSampleAt, sourceDay.timeZone);
3138
+ if (vaultDayKey !== sourceDay.dayKey) {
3139
+ // The provider fetched the exact authorized window, so a row that
3140
+ // normalizes outside the target vault day (for example a fallback
3141
+ // timestamp) is a lossy normalization, not out-of-scope data.
3142
+ throw junctionCalendarRefreshNormalizationError("source_day.outside_authorized_day", {
3143
+ ...rowDiagnostic,
3144
+ sourceProvider: normalizeKnownJunctionSourceProviderSlug(resourceContext.sourceProviderSlug),
3145
+ timestampKind: classifyJunctionNormalizationTimestampKind(timestamp.observedAtRaw ?? providerTimestamp),
3146
+ timestampSemantics: timestamp.timestampSemantics,
3147
+ });
3148
+ }
3149
+ const temporalKey = [
3150
+ resourceContext.externalRefResourceType,
3151
+ resourceContext.origin.sourceType ?? "",
3152
+ resourceContext.origin.sourceInstanceId ?? "",
3153
+ sourceDay.dayKey,
3154
+ ].join("\u0000");
3155
+ let temporalAggregate = temporalAggregates.get(temporalKey);
3156
+ if (!temporalAggregate) {
3157
+ const sourceFacet = shortHash([
3158
+ resourceContext.sourceProviderSlug,
3159
+ resourceContext.origin.sourceType ?? "",
3160
+ resourceContext.origin.sourceInstanceId ?? "",
3161
+ ]);
3162
+ temporalAggregate = {
3163
+ authoritativeEmptyCalendarSet: false,
3164
+ dayKey: sourceDay.dayKey,
3165
+ duplicateSampleCount: 0,
3166
+ entry,
3167
+ fidelitySamples: [],
3168
+ firstSampleAt: temporalSampleAt,
3169
+ lastRecordedAt: recordedAt,
3170
+ lastSampleAt: temporalSampleAt,
3171
+ legacyDayKeys: new Set(),
3172
+ maxValue: value,
3173
+ minValue: value,
3174
+ evidencePartRole: `junction-timeseries-temporal-${input.resourceSlug}:${sourceDay.dayKey}:${sourceFacet}`,
3175
+ resourceContext,
3176
+ sampleCount: 0,
3177
+ sum: 0,
3178
+ sumSquares: 0,
3179
+ timestamp,
3180
+ timeZone: sourceDay.timeZone,
3181
+ };
3182
+ temporalAggregates.set(temporalKey, temporalAggregate);
3183
+ }
3184
+ temporalAggregate.sampleCount += 1;
3185
+ temporalAggregate.sum += value;
3186
+ if (temporalSampleAt < temporalAggregate.firstSampleAt) {
3187
+ temporalAggregate.firstSampleAt = temporalSampleAt;
3188
+ }
3189
+ if (temporalSampleAt >= temporalAggregate.lastSampleAt) {
3190
+ temporalAggregate.lastSampleAt = temporalSampleAt;
3191
+ temporalAggregate.lastRecordedAt = recordedAt;
3192
+ temporalAggregate.timestamp = timestamp;
3193
+ }
3194
+ temporalAggregate.minValue = Math.min(temporalAggregate.minValue, value);
3195
+ temporalAggregate.maxValue = Math.max(temporalAggregate.maxValue, value);
3196
+ const temporalFeatureSamples = temporalAggregate.temporalFeatureSamples ?? [];
3197
+ if (temporalFeatureSamples.length >= JUNCTION_TEMPORAL_FEATURE_MAX_SAMPLES_PER_DAY) {
3198
+ temporalAggregate.temporalFeatureInputSuppressed = true;
3199
+ delete temporalAggregate.temporalFeatureSamples;
3200
+ }
3201
+ else {
3202
+ temporalFeatureSamples.push(stripUndefined({
3203
+ recordedAt: temporalSampleAt,
3204
+ value,
3205
+ localMinuteOfDay: resolveJunctionTemporalFeatureVaultLocalMinuteOfDay(temporalSampleAt, sourceDay.timeZone),
3206
+ }));
3207
+ temporalAggregate.temporalFeatureSamples = temporalFeatureSamples;
3208
+ }
3209
+ }
2473
3210
  }
2474
3211
  }
2475
3212
  const sortedAggregates = [...aggregates.values()].sort(compareJunctionDailyTimeseriesAggregates);
3213
+ if (temporalFeatureResource && ownsTemporalFeatures) {
3214
+ for (const aggregate of [...temporalAggregates.values()].sort(compareJunctionDailyTimeseriesAggregates)) {
3215
+ const result = temporalFeatureImportSuppressed || aggregate.temporalFeatureInputSuppressed
3216
+ ? { observations: [], status: "suppressed_input_cap" }
3217
+ : buildJunctionTemporalFeatures({
3218
+ resource: temporalFeatureResource,
3219
+ samples: aggregate.temporalFeatureSamples ?? [],
3220
+ });
3221
+ const existingObservationCount = input.context.temporalFeatureObservationCountsByDay.get(aggregate.dayKey) ?? 0;
3222
+ if (result.status === "complete"
3223
+ && existingObservationCount + result.observations.length
3224
+ > JUNCTION_TEMPORAL_FEATURE_MAX_OBSERVATIONS_PER_DAY) {
3225
+ aggregate.temporalFeatureResult = {
3226
+ observations: [],
3227
+ status: "suppressed_output_cap",
3228
+ };
3229
+ }
3230
+ else {
3231
+ aggregate.temporalFeatureResult = result;
3232
+ input.context.temporalFeatureObservationCountsByDay.set(aggregate.dayKey, existingObservationCount + result.observations.length);
3233
+ }
3234
+ pushJunctionTemporalFeatureArtifact(input.context, input.resource, aggregate);
3235
+ if (aggregate.temporalFeatureResult?.status === "complete") {
3236
+ for (const observation of aggregate.temporalFeatureResult.observations) {
3237
+ pushJunctionDailyTimeseriesObservation(input.context, aggregate, observation);
3238
+ }
3239
+ }
3240
+ delete aggregate.temporalFeatureSamples;
3241
+ }
3242
+ }
3243
+ // A complete-source-day import owns only temporal facets. Its vault-window
3244
+ // samples cover partial provider days, so emitting ordinary observations,
3245
+ // aggregate artifacts, or dense envelopes would supersede the calendar-day
3246
+ // owner's full-day facts with partial revisions.
3247
+ if (ownsTemporalFeatures) {
3248
+ return [];
3249
+ }
2476
3250
  if (sortedAggregates.length === 0) {
2477
3251
  pushJunctionEmptyDailyTimeseriesAggregateArtifact(input.context, input.resource, input.resourceSlug);
2478
3252
  return sortedAggregates;
@@ -2502,7 +3276,9 @@ function pushJunctionDailyTimeseriesAggregateArtifacts(context, resource, aggreg
2502
3276
  ])}`;
2503
3277
  aggregate.evidencePartRole = role;
2504
3278
  pushEvidencePart(context.evidenceParts, withJunctionCompactTimeseriesMetadata(resource, createEvidencePart(role, `${role}.json`, stripUndefined({
2505
- schema: "junction.timeseries_daily_aggregate.v1",
3279
+ schema: aggregate.temporalFeatureResult
3280
+ ? "junction.timeseries_daily_aggregate.v2"
3281
+ : "junction.timeseries_daily_aggregate.v1",
2506
3282
  provider: "junction",
2507
3283
  resource,
2508
3284
  dayKey: aggregate.dayKey,
@@ -2531,14 +3307,45 @@ function pushJunctionDailyTimeseriesAggregateArtifacts(context, resource, aggreg
2531
3307
  maxValue: aggregate.authoritativeEmptyCalendarSet
2532
3308
  ? undefined
2533
3309
  : roundJunctionTimeseriesAggregateValue(resource, aggregate.maxValue),
3310
+ standardDeviationValue: resource === "glucose"
3311
+ && !aggregate.authoritativeEmptyCalendarSet
3312
+ ? roundJunctionDailyAggregateValue(junctionDailyTimeseriesStandardDeviation(aggregate))
3313
+ : undefined,
3314
+ coefficientOfVariationPercent: resource === "glucose"
3315
+ && !aggregate.authoritativeEmptyCalendarSet
3316
+ ? roundJunctionDailyAggregateValue(junctionDailyTimeseriesCoefficientOfVariation(aggregate))
3317
+ : undefined,
2534
3318
  sumValue: JUNCTION_DAILY_TIMESERIES_DESCRIPTORS.get(resource)?.retainSumValue === true
2535
3319
  && !aggregate.authoritativeEmptyCalendarSet
2536
3320
  ? roundJunctionTimeseriesAggregateValue(resource, aggregate.sum)
2537
3321
  : undefined,
2538
3322
  unit: junctionDailyTimeseriesAggregateUnit(resource),
3323
+ temporalFeatureStatus: aggregate.temporalFeatureResult?.status,
3324
+ temporalFeatures: aggregate.temporalFeatureResult?.status === "complete"
3325
+ ? aggregate.temporalFeatureResult.envelope
3326
+ : undefined,
2539
3327
  }))));
2540
3328
  }
2541
3329
  }
3330
+ function pushJunctionTemporalFeatureArtifact(context, resource, aggregate) {
3331
+ pushEvidencePart(context.evidenceParts, withJunctionCompactTimeseriesMetadata(resource, createEvidencePart(aggregate.evidencePartRole, `${aggregate.evidencePartRole}.json`, stripUndefined({
3332
+ schema: "junction.timeseries_temporal_features.v2",
3333
+ provider: "junction",
3334
+ resource,
3335
+ dayKey: aggregate.dayKey,
3336
+ timeZone: aggregate.timeZone,
3337
+ sourceProviderSlug: aggregate.resourceContext.sourceProviderSlug,
3338
+ sourceType: aggregate.resourceContext.origin.sourceType,
3339
+ sourceInstanceId: aggregate.resourceContext.origin.sourceInstanceId,
3340
+ sampleCount: aggregate.sampleCount,
3341
+ firstSampleAt: aggregate.firstSampleAt,
3342
+ lastSampleAt: aggregate.lastSampleAt,
3343
+ status: aggregate.temporalFeatureResult?.status,
3344
+ features: aggregate.temporalFeatureResult?.status === "complete"
3345
+ ? aggregate.temporalFeatureResult.envelope
3346
+ : undefined,
3347
+ }))));
3348
+ }
2542
3349
  function roundJunctionTimeseriesAggregateValue(resource, value) {
2543
3350
  return JUNCTION_DAILY_TIMESERIES_DESCRIPTORS.has(resource)
2544
3351
  ? roundJunctionDailyAggregateValue(value)
@@ -2547,6 +3354,174 @@ function roundJunctionTimeseriesAggregateValue(resource, value) {
2547
3354
  function junctionDailyTimeseriesAggregateUnit(resource) {
2548
3355
  return JUNCTION_DAILY_TIMESERIES_DESCRIPTORS.get(resource)?.unit;
2549
3356
  }
3357
+ function resolveJunctionTemporalFeatureVaultLocalMinuteOfDay(sampleAt, timeZone) {
3358
+ const sampleEpochMs = Date.parse(sampleAt);
3359
+ if (!Number.isFinite(sampleEpochMs)) {
3360
+ return undefined;
3361
+ }
3362
+ try {
3363
+ const parts = new Intl.DateTimeFormat("en-US", {
3364
+ hour: "2-digit",
3365
+ hourCycle: "h23",
3366
+ minute: "2-digit",
3367
+ timeZone,
3368
+ }).formatToParts(sampleEpochMs);
3369
+ const hour = Number(parts.find((part) => part.type === "hour")?.value);
3370
+ const minute = Number(parts.find((part) => part.type === "minute")?.value);
3371
+ return Number.isInteger(hour) && Number.isInteger(minute)
3372
+ ? hour * 60 + minute
3373
+ : undefined;
3374
+ }
3375
+ catch {
3376
+ return undefined;
3377
+ }
3378
+ }
3379
+ // Temporal samples require a genuine provider clock. Date-only rows prove raw
3380
+ // source-day membership for ordinary daily facts but return null here so they
3381
+ // never become temporal observations. A supplied clock that cannot resolve in
3382
+ // the retained authority timezone (a daylight-saving gap or malformed clock)
3383
+ // fails the complete-day import retryably instead of inventing an instant.
3384
+ // The single accepted timestamp language for complete-source-day authority.
3385
+ // Every pattern is anchored: the full raw value must be consumed, so a valid
3386
+ // prefix followed by unsupported text can never certify a temporal instant or
3387
+ // day membership.
3388
+ const JUNCTION_COMPLETE_DAY_DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
3389
+ const JUNCTION_COMPLETE_DAY_FLOATING_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/u;
3390
+ const JUNCTION_COMPLETE_DAY_ABSOLUTE_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:[Zz]|([+-])(\d{2}):?(\d{2}))$/u;
3391
+ function readJunctionCompleteDayClock(match) {
3392
+ const hour = Number(match[2]);
3393
+ const minute = Number(match[3]);
3394
+ const second = match[4] === undefined ? 0 : Number(match[4]);
3395
+ if (hour > 23 || minute > 59 || second > 59) {
3396
+ return null;
3397
+ }
3398
+ const fraction = match[5];
3399
+ const millisecond = fraction === undefined
3400
+ ? 0
3401
+ : Number(fraction.slice(0, 3).padEnd(3, "0"));
3402
+ return { hour, minute, second, millisecond };
3403
+ }
3404
+ // The one strict acceptance parse for the complete-source-day timestamp
3405
+ // language. It consumes the entire raw value, proves the actual Gregorian
3406
+ // calendar date and clock/offset field ranges, and preserves fractional
3407
+ // seconds, so a lexically plausible but impossible value (a non-leap
3408
+ // February 29, an April 31, a 24:00 clock) can never be normalized onto a
3409
+ // neighboring valid day. Both the importing owner and the provider's
3410
+ // authorized-day filter consume this one result; a value this parse rejects
3411
+ // must be retained for the importer to fail closed, never reinterpreted.
3412
+ export function parseJunctionCompleteDayTimestamp(value) {
3413
+ const raw = value.trim();
3414
+ if (JUNCTION_COMPLETE_DAY_DATE_ONLY_PATTERN.test(raw)) {
3415
+ return isStrictIsoDate(raw) ? { kind: "date-only", dayKey: raw } : null;
3416
+ }
3417
+ const floatingMatch = raw.match(JUNCTION_COMPLETE_DAY_FLOATING_PATTERN);
3418
+ if (floatingMatch) {
3419
+ const dayKey = floatingMatch[1] ?? "";
3420
+ const clock = readJunctionCompleteDayClock(floatingMatch);
3421
+ if (!clock || !isStrictIsoDate(dayKey)) {
3422
+ return null;
3423
+ }
3424
+ return { kind: "floating", dayKey, ...clock };
3425
+ }
3426
+ const absoluteMatch = raw.match(JUNCTION_COMPLETE_DAY_ABSOLUTE_PATTERN);
3427
+ if (!absoluteMatch) {
3428
+ return null;
3429
+ }
3430
+ const dayKey = absoluteMatch[1] ?? "";
3431
+ const clock = readJunctionCompleteDayClock(absoluteMatch);
3432
+ if (!clock || !isStrictIsoDate(dayKey)) {
3433
+ return null;
3434
+ }
3435
+ const offsetHours = absoluteMatch[7] === undefined ? 0 : Number(absoluteMatch[7]);
3436
+ const offsetMinutesPart = absoluteMatch[8] === undefined ? 0 : Number(absoluteMatch[8]);
3437
+ if (offsetHours > 23 || offsetMinutesPart > 59) {
3438
+ return null;
3439
+ }
3440
+ const offsetMinutes = (absoluteMatch[6] === "-" ? -1 : 1) * (offsetHours * 60 + offsetMinutesPart);
3441
+ const [year, month, day] = dayKey.split("-").map(Number);
3442
+ const local = new Date(Date.UTC(year, month - 1, day, clock.hour, clock.minute, clock.second, clock.millisecond));
3443
+ local.setUTCFullYear(year);
3444
+ return {
3445
+ kind: "absolute",
3446
+ instant: new Date(local.getTime() - offsetMinutes * 60_000).toISOString(),
3447
+ };
3448
+ }
3449
+ function classifyJunctionNormalizationTimestampKind(value) {
3450
+ if (value === undefined || value === null || value === "") {
3451
+ return "missing";
3452
+ }
3453
+ if (typeof value !== "string") {
3454
+ return "invalid";
3455
+ }
3456
+ const parsed = parseJunctionCompleteDayTimestamp(value);
3457
+ if (!parsed) {
3458
+ return value.trim() ? "invalid" : "missing";
3459
+ }
3460
+ return parsed.kind === "date-only" ? "date_only" : parsed.kind;
3461
+ }
3462
+ // The importer's complete-day boundary is the one acceptance owner: the
3463
+ // single strict parse above yields semantics agreement, day membership, and
3464
+ // temporal-instant eligibility together. Date-only rows return null (day
3465
+ // membership, zero temporal coverage); floating clocks resolve in the
3466
+ // retained authority timezone with omitted seconds as zero and fractional
3467
+ // seconds preserved; absolute forms use the exact instant derived from their
3468
+ // validated parts, never from permissive runtime normalization. Anything
3469
+ // else — trailing unsupported text, an impossible calendar or clock value,
3470
+ // or semantics that contradict the raw shape — fails the import retryably
3471
+ // before any canonical write.
3472
+ function resolveJunctionTemporalFeatureInstant(timestamp, timeZone, diagnostic) {
3473
+ const raw = timestamp.observedAtRaw?.trim();
3474
+ if (!raw) {
3475
+ throw junctionCalendarRefreshNormalizationError("temporal.timestamp_missing_or_non_string", diagnostic);
3476
+ }
3477
+ const parsed = parseJunctionCompleteDayTimestamp(raw);
3478
+ if (!parsed) {
3479
+ throw junctionCalendarRefreshNormalizationError("temporal.timestamp_invalid", diagnostic);
3480
+ }
3481
+ if (parsed.kind === "date-only") {
3482
+ if (timestamp.timestampSemantics !== "floating") {
3483
+ throw junctionCalendarRefreshNormalizationError("temporal.timestamp_semantics_mismatch", diagnostic);
3484
+ }
3485
+ return null;
3486
+ }
3487
+ if (parsed.kind === "absolute") {
3488
+ if (timestamp.timestampSemantics === "floating") {
3489
+ throw junctionCalendarRefreshNormalizationError("temporal.timestamp_semantics_mismatch", diagnostic);
3490
+ }
3491
+ return parsed.instant;
3492
+ }
3493
+ if (timestamp.timestampSemantics !== "floating") {
3494
+ throw junctionCalendarRefreshNormalizationError("temporal.timestamp_semantics_mismatch", diagnostic);
3495
+ }
3496
+ const [year, month, day] = parsed.dayKey.split("-").map(Number);
3497
+ const { hour, minute, second } = parsed;
3498
+ const targetPseudoMs = Date.UTC(year, month - 1, day, hour, minute, second);
3499
+ // Enumerate the bounded candidate instants implied by the zone's offsets
3500
+ // around the target wall clock and require exactly one to display it: a
3501
+ // spring-forward gap yields zero matches and a fall-back overlap yields
3502
+ // two, and both fail retryably instead of silently picking one DST fold.
3503
+ const candidateOffsets = new Set();
3504
+ for (const probeMs of [targetPseudoMs - 86_400_000, targetPseudoMs, targetPseudoMs + 86_400_000]) {
3505
+ const parts = formatTimeZoneDateTimeParts(probeMs, timeZone);
3506
+ candidateOffsets.add(Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second)
3507
+ - probeMs);
3508
+ }
3509
+ const matches = [...candidateOffsets]
3510
+ .map((offsetMs) => targetPseudoMs - offsetMs)
3511
+ .filter((candidateMs) => {
3512
+ const resolved = formatTimeZoneDateTimeParts(candidateMs, timeZone);
3513
+ return resolved.year === year
3514
+ && resolved.month === month
3515
+ && resolved.day === day
3516
+ && resolved.hour === hour
3517
+ && resolved.minute === minute
3518
+ && resolved.second === second;
3519
+ });
3520
+ if (matches.length !== 1 || matches[0] === undefined) {
3521
+ throw junctionCalendarRefreshNormalizationError("temporal.local_time_ambiguous_or_nonexistent", diagnostic);
3522
+ }
3523
+ return new Date(matches[0] + parsed.millisecond).toISOString();
3524
+ }
2550
3525
  function withJunctionCompactTimeseriesMetadata(resource, artifact, resourceCategory = "timeseries_daily_aggregate") {
2551
3526
  if (!artifact) {
2552
3527
  return null;
@@ -2575,6 +3550,7 @@ function pushJunctionDailyTimeseriesObservation(context, aggregate, observation)
2575
3550
  observedAtRaw: `${aggregate.dayKey}:${aggregate.resourceContext.resource}:daily`,
2576
3551
  });
2577
3552
  const legacyExternalRefs = legacyJunctionDailyTimeseriesAggregateExternalRefs(aggregate, observation.metric);
3553
+ const dataOrigin = buildDataOrigin(aggregate.entry, aggregate.resourceContext, timestamp);
2578
3554
  context.events.push(stripUndefined({
2579
3555
  kind: "observation",
2580
3556
  occurredAt: aggregate.lastSampleAt,
@@ -2584,17 +3560,77 @@ function pushJunctionDailyTimeseriesObservation(context, aggregate, observation)
2584
3560
  source: "device",
2585
3561
  title: resolveJunctionHrvTitle(metric, observation.title),
2586
3562
  evidenceRoles: [aggregate.evidencePartRole],
2587
- externalRef: makeJunctionExternalRef(aggregate.resourceContext, aggregate.entry, timestamp, observation.metric),
3563
+ externalRef: observation.qualifiers
3564
+ ? buildJunctionTemporalFeatureExternalRef(context, aggregate, observation.metric)
3565
+ : makeJunctionExternalRef(aggregate.resourceContext, aggregate.entry, timestamp, observation.metric),
2588
3566
  legacyExternalRefs: legacyExternalRefs.length > 0 ? legacyExternalRefs : undefined,
2589
- dataOrigin: buildDataOrigin(aggregate.entry, aggregate.resourceContext, timestamp),
3567
+ dataOrigin: observation.confidence
3568
+ ? { ...dataOrigin, originConfidence: observation.confidence }
3569
+ : dataOrigin,
2590
3570
  fields: {
2591
3571
  metric,
2592
3572
  observationGrain: "summary",
3573
+ ...(observation.qualifiers ? { qualifiers: observation.qualifiers } : {}),
2593
3574
  value: roundJunctionDailyAggregateValue(observation.value),
2594
3575
  unit: observation.unit,
2595
3576
  },
2596
3577
  }));
2597
3578
  }
3579
+ function buildJunctionTemporalFeatureExternalRef(context, aggregate, metric) {
3580
+ const sourceDay = context.temporalFeatureSourceDay;
3581
+ if (!sourceDay) {
3582
+ throw new TypeError("Junction temporal feature authority is missing.");
3583
+ }
3584
+ const resource = aggregate.resourceContext.resource;
3585
+ const sourceFacet = shortHash([
3586
+ aggregate.resourceContext.sourceProviderSlug,
3587
+ aggregate.resourceContext.origin.sourceType ?? "",
3588
+ aggregate.resourceContext.origin.sourceInstanceId ?? "",
3589
+ ]);
3590
+ const facet = `temporal-${sourceFacet}-${metric}`;
3591
+ const currentFacets = context.temporalFeatureCurrentFacetsByResource.get(resource)
3592
+ ?? new Set();
3593
+ currentFacets.add(facet);
3594
+ context.temporalFeatureCurrentFacetsByResource.set(resource, currentFacets);
3595
+ // Temporal facets follow the repository's unversioned complete-set
3596
+ // reconciliation: the import wall clock is not a source version, so an
3597
+ // unchanged replacement set collapses as a no-op on the canonical event
3598
+ // spine while growth, change, removal, and reassertion reconcile through
3599
+ // the serialized authoritative-set seam in arrival order.
3600
+ return {
3601
+ system: "junction",
3602
+ resourceType: "junction-timeseries-temporal-day",
3603
+ resourceId: junctionTemporalFeatureResourceId(context, resource, sourceDay.dayKey),
3604
+ facet,
3605
+ };
3606
+ }
3607
+ function finalizeJunctionTemporalAuthoritativeSets(context) {
3608
+ const sourceDay = context.temporalFeatureSourceDay;
3609
+ if (!sourceDay) {
3610
+ return;
3611
+ }
3612
+ for (const resource of [...new Set(sourceDay.resources)].sort()) {
3613
+ if (!isJunctionTemporalFeatureResource(resource)) {
3614
+ continue;
3615
+ }
3616
+ context.authoritativeEventSets.push({
3617
+ system: "junction",
3618
+ resourceType: "junction-timeseries-temporal-day",
3619
+ resourceId: junctionTemporalFeatureResourceId(context, resource, sourceDay.dayKey),
3620
+ version: sourceDay.revisionAt,
3621
+ facetPrefixes: ["temporal"],
3622
+ currentFacets: [...(context.temporalFeatureCurrentFacetsByResource.get(resource) ?? [])]
3623
+ .sort(),
3624
+ });
3625
+ }
3626
+ }
3627
+ function junctionTemporalFeatureResourceId(context, resource, dayKey) {
3628
+ const connectionId = context.temporalFeatureSourceDay?.connectionId;
3629
+ if (!connectionId) {
3630
+ throw new TypeError("Junction temporal feature connection identity is missing.");
3631
+ }
3632
+ return shortHash([connectionId, resource, dayKey]);
3633
+ }
2598
3634
  function legacyJunctionDailyTimeseriesAggregateExternalRefs(aggregate, metric) {
2599
3635
  if (aggregate.legacyDayKeys.size === 0) {
2600
3636
  return [];
@@ -3184,12 +4220,20 @@ function pushJunctionWeightReadings(payload, resource, resourceSlug, context) {
3184
4220
  continue;
3185
4221
  }
3186
4222
  const { providerReadingId, weightKilograms } = resolveJunctionWeightProviderRecordIdentity(entry);
3187
- const rawOccurredAt = firstValueFromPaths(entry, JUNCTION_RECORD_TIMESTAMP_PATHS);
4223
+ const rawOccurredAt = firstStringFromPaths(entry, ["timestamp"]);
4224
+ const timestampSemantics = inferTimestampSemantics(rawOccurredAt);
3188
4225
  const occurredAt = resolveSafeTimestamp(rawOccurredAt, resourceContext.sourceProviderSlug);
3189
- if (weightKilograms === undefined || !occurredAt) {
4226
+ if (weightKilograms === undefined
4227
+ || !occurredAt
4228
+ || (timestampSemantics !== "utc" && timestampSemantics !== "offset")) {
3190
4229
  continue;
3191
4230
  }
3192
- const timestamp = withTimestampOverride(resolveRecordTimestamp(entry, context, resourceContext.sourceProviderSlug), { occurredAt });
4231
+ const timestamp = withTimestampOverride(resolveRecordTimestamp(entry, context, resourceContext.sourceProviderSlug), {
4232
+ occurredAt,
4233
+ dayKey: resolveJunctionLocalDayKey(entry, rawOccurredAt, occurredAt, timestampSemantics),
4234
+ observedAtRaw: rawOccurredAt,
4235
+ timestampSemantics,
4236
+ });
3193
4237
  const dayKey = resolveJunctionTimeseriesAggregateDayKey(entry, timestamp, occurredAt, context.defaultTimeZone);
3194
4238
  if (!dayKey) {
3195
4239
  continue;
@@ -3207,8 +4251,7 @@ function pushJunctionWeightReadings(payload, resource, resourceSlug, context) {
3207
4251
  resourceContext.sourceProviderSlug,
3208
4252
  resourceContext.origin.sourceType ?? "",
3209
4253
  resourceContext.origin.sourceInstanceId ?? "",
3210
- timestamp.observedAtRaw ?? occurredAt,
3211
- weightKilograms,
4254
+ occurredAt,
3212
4255
  ]);
3213
4256
  if (seenReadingIdentityHashes.has(readingIdentityHash)) {
3214
4257
  continue;
@@ -3288,6 +4331,9 @@ function buildJunctionBloodPressureReadingIdentityHash(input) {
3288
4331
  ]);
3289
4332
  }
3290
4333
  function buildRawResourcePayload(resource, payload, connectionsByKey) {
4334
+ if (resource === "menstrual_cycle") {
4335
+ return prepareJunctionMenstrualCycleSummary(payload, connectionsByKey).evidence;
4336
+ }
3291
4337
  if (resource === JUNCTION_NOTE_RESOURCE) {
3292
4338
  return sanitizeJunctionNoteRawValue(sanitizeJunctionRawPayload(payload));
3293
4339
  }
@@ -3391,7 +4437,6 @@ function sanitizeJunctionRawSnapshot(snapshot) {
3391
4437
  if (!sanitized) {
3392
4438
  return sanitized;
3393
4439
  }
3394
- const sanitizedSnapshot = Object.fromEntries(Object.entries(sanitized).filter(([key]) => key !== "strictSparseCalendarRepair"));
3395
4440
  const connections = asArray(snapshot.connections).flatMap((connection) => {
3396
4441
  const normalized = asPlainObject(connection);
3397
4442
  return normalized ? [normalized] : [];
@@ -3402,6 +4447,10 @@ function sanitizeJunctionRawSnapshot(snapshot) {
3402
4447
  if (!summaries && !timeseries) {
3403
4448
  return {};
3404
4449
  }
4450
+ const { canonicalCoverageFence: _canonicalCoverageFence, canonicalCoverageProviderPulledAt: _canonicalCoverageProviderPulledAt, strictSparseCalendarRepair: _strictSparseCalendarRepair, ...sanitizedSnapshot } = sanitized;
4451
+ void _canonicalCoverageFence;
4452
+ void _canonicalCoverageProviderPulledAt;
4453
+ void _strictSparseCalendarRepair;
3405
4454
  return stripUndefined({
3406
4455
  ...sanitizedSnapshot,
3407
4456
  connections: sanitizeJunctionRawConnections(snapshot.connections),
@@ -3471,10 +4520,12 @@ function sanitizeProfilePayload(payload, connection) {
3471
4520
  const gender = firstStringFromPaths(profile, JUNCTION_PROFILE_GENDER_PATHS);
3472
4521
  const sourceProviderSlug = readJunctionSourceProviderSlug(profile, connection)
3473
4522
  ?? origin.sourceProviderSlug;
4523
+ const createdAt = resolveSafeTimestamp(firstValueFromPaths(profile, ["createdAt", "created_at", "updatedAt", "updated_at"]), origin.sourceProviderSlug);
3474
4524
  const updatedAt = resolveSafeTimestamp(firstValueFromPaths(profile, ["updatedAt", "updated_at", "createdAt", "created_at"]), origin.sourceProviderSlug);
3475
4525
  const sanitized = stripUndefined({
4526
+ createdAt,
3476
4527
  gender: gender ? trimToLength(gender, 80) : undefined,
3477
- stableResourceId: buildStableProfileResourceId(profile, sourceProviderSlug, origin.sourceType, origin.sourceInstanceId, updatedAt),
4528
+ stableResourceId: buildStableProfileResourceId(profile, sourceProviderSlug, origin.sourceType, origin.sourceInstanceId, createdAt ?? updatedAt),
3478
4529
  sourceProviderSlug,
3479
4530
  sourceInstanceId: origin.sourceInstanceId,
3480
4531
  sourceType: origin.sourceType,
@@ -4362,15 +5413,67 @@ const JUNCTION_PROFILE_BIRTH_DATE_PATHS = [
4362
5413
  "date_of_birth",
4363
5414
  "dob",
4364
5415
  ];
5416
+ const JUNCTION_PROFILE_GENDER_PATHS = ["gender", "reportedGender"];
4365
5417
  // `gender` is deliberately not a fallback: Junction documents it as a
4366
- // distinct enum from biological sex and the note segment is labeled
4367
- // "Biological sex", so a gender value here would be mislabeled.
5418
+ // distinct enum from biological sex, so each lands under its own label and
5419
+ // the reported gender also receives its own typed canonical field.
4368
5420
  const JUNCTION_PROFILE_SEX_PATHS = [
4369
5421
  "sex",
4370
5422
  "biologicalSex",
4371
5423
  "biological_sex",
4372
5424
  ];
4373
- const JUNCTION_PROFILE_GENDER_PATHS = ["gender"];
5425
+ const JUNCTION_PROFILE_AUTHORITATIVE_FACET_PREFIXES = [
5426
+ "height",
5427
+ "gender",
5428
+ "profile-demographics",
5429
+ ];
5430
+ const JUNCTION_MENSTRUAL_AUTHORITATIVE_FACET_PREFIXES = [
5431
+ "period-length-days",
5432
+ "cycle-length-days",
5433
+ "menstrual-flow",
5434
+ "ovulation-test",
5435
+ "pregnancy-test",
5436
+ "cervical-mucus",
5437
+ "intermenstrual-bleeding",
5438
+ "home-progesterone-test",
5439
+ "progesterone-test",
5440
+ "contraceptive",
5441
+ "sexual-activity",
5442
+ "menstrual-cycle-deviation",
5443
+ ];
5444
+ function pushJunctionAuthoritativeSummaryEventSet(entry, resourceContext, context, emittedEvents) {
5445
+ const facetPrefixes = resourceContext.resource === "profile"
5446
+ ? JUNCTION_PROFILE_AUTHORITATIVE_FACET_PREFIXES
5447
+ : resourceContext.resource === "menstrual_cycle"
5448
+ && firstValueFromPaths(entry, ["isPredicted", "is_predicted"]) !== true
5449
+ ? JUNCTION_MENSTRUAL_AUTHORITATIVE_FACET_PREFIXES
5450
+ : undefined;
5451
+ const explicitId = firstStringFromPaths(entry, JUNCTION_GENERIC_SUMMARY_ID_PATHS);
5452
+ const version = junctionAuthoritativeSummaryVersion(resourceContext, entry);
5453
+ if (!facetPrefixes || !explicitId || !version) {
5454
+ return;
5455
+ }
5456
+ const timestamp = resolveRecordTimestamp(entry, context, resourceContext.sourceProviderSlug);
5457
+ const identity = makeJunctionExternalRef(resourceContext, entry, timestamp, facetPrefixes[0]);
5458
+ const currentFacets = emittedEvents.flatMap((event) => {
5459
+ const externalRef = event.externalRef;
5460
+ return externalRef?.system === identity.system
5461
+ && externalRef.resourceType === identity.resourceType
5462
+ && externalRef.resourceId === identity.resourceId
5463
+ && externalRef.version === version
5464
+ && externalRef.facet
5465
+ ? [externalRef.facet]
5466
+ : [];
5467
+ });
5468
+ context.authoritativeEventSets.push({
5469
+ system: identity.system,
5470
+ resourceType: identity.resourceType,
5471
+ resourceId: identity.resourceId,
5472
+ version,
5473
+ facetPrefixes: [...facetPrefixes],
5474
+ currentFacets: [...new Set(currentFacets)].sort(),
5475
+ });
5476
+ }
4374
5477
  // Junction profile is a single current-state snapshot per source. Height
4375
5478
  // follows the body-summary observation pattern; gender is a distinct
4376
5479
  // categorical measurement, while birth date, biological sex, and wheelchair
@@ -4381,13 +5484,15 @@ function pushProfileSummary(entry, resourceContext, context) {
4381
5484
  // Profile entries carry no observed-at timestamp, so the generic resolver
4382
5485
  // falls back to the sync window. Pin the FULL event time (occurredAt,
4383
5486
  // recordedAt, dayKey) plus the identity-bearing observedAtRaw to the
4384
- // provider's updated/created timestamps: a window-drifting occurredAt
4385
- // would revise the event spine on every reconcile and duplicate the
4386
- // profile across month shards (cross-shard reconcile only indexes the
4387
- // target shard). Junction documents created_at/updated_at as REQUIRED on
5487
+ // provider's stable creation timestamp (falling back to updated-at for
5488
+ // legacy rows that omit it): a window-drifting or mutable updated-at
5489
+ // occurredAt would revise the event spine on every reconcile and duplicate
5490
+ // the profile across month shards. Version ordering remains updated-at-first
5491
+ // in junctionAuthoritativeSummaryVersion below.
5492
+ // Junction documents created_at/updated_at as REQUIRED on
4388
5493
  // ClientFacingProfile, so a row without them is malformed input and
4389
5494
  // deliberately stays raw-only rather than getting an invented time.
4390
- const providerTimestampRaw = firstValueFromPaths(entry, ["updatedAt", "updated_at", "createdAt", "created_at"]);
5495
+ const providerTimestampRaw = firstValueFromPaths(entry, ["createdAt", "created_at", "updatedAt", "updated_at"]);
4391
5496
  const providerTimestamp = resolveSafeTimestamp(providerTimestampRaw, resourceContext.sourceProviderSlug);
4392
5497
  const pinnedOccurredAt = baseTimestamp.observedAtRaw ? baseTimestamp.occurredAt : providerTimestamp;
4393
5498
  if (!pinnedOccurredAt) {
@@ -4399,13 +5504,16 @@ function pushProfileSummary(entry, resourceContext, context) {
4399
5504
  dayKey: extractIsoDatePrefix(pinnedOccurredAt) ?? baseTimestamp.dayKey,
4400
5505
  observedAtRaw: pinnedOccurredAt,
4401
5506
  });
4402
- const legacyExternalRefs = (facet) => buildJunctionProfileLegacyExternalRefs(resourceContext, entry, timestamp, providerTimestampRaw, facet);
4403
- pushObservationMetrics(entry, resourceContext, context, JUNCTION_PROFILE_METRICS, timestamp, legacyExternalRefs);
5507
+ const profileNormalizerVersion = firstStringFromPaths(entry, ["stableResourceId"])
5508
+ || firstStringFromPaths(entry, JUNCTION_GENERIC_SUMMARY_ID_PATHS)
5509
+ ? JUNCTION_STABLE_PROFILE_CREATED_AT_NORMALIZER_VERSION
5510
+ : JUNCTION_NO_ID_PROFILE_NORMALIZER_VERSION;
5511
+ pushObservationMetrics(entry, resourceContext, context, JUNCTION_PROFILE_METRICS, timestamp, { normalizerVersion: profileNormalizerVersion });
4404
5512
  if (!timestamp.occurredAt) {
4405
5513
  return;
4406
5514
  }
4407
- const gender = firstStringFromPaths(entry, JUNCTION_PROFILE_GENDER_PATHS);
4408
- if (gender) {
5515
+ const reportedGender = readJunctionProfileGender(entry);
5516
+ if (reportedGender) {
4409
5517
  context.events.push(stripUndefined({
4410
5518
  kind: "measurement",
4411
5519
  occurredAt: timestamp.occurredAt,
@@ -4415,14 +5523,15 @@ function pushProfileSummary(entry, resourceContext, context) {
4415
5523
  title: "Junction gender",
4416
5524
  evidenceRoles: resourceContext.evidenceRoles,
4417
5525
  externalRef: makeJunctionExternalRef(resourceContext, entry, timestamp, "gender"),
4418
- legacyExternalRefs: legacyExternalRefs("gender"),
4419
- dataOrigin: buildDataOrigin(entry, resourceContext, timestamp),
5526
+ dataOrigin: buildDataOrigin(entry, resourceContext, timestamp, {
5527
+ normalizerVersion: profileNormalizerVersion,
5528
+ }),
4420
5529
  fields: {
4421
5530
  measurements: [{
4422
5531
  metric: "gender",
4423
5532
  value: 1,
4424
5533
  unit: "recording",
4425
- qualifiers: { gender: trimToLength(gender, 80) },
5534
+ qualifiers: { gender: reportedGender },
4426
5535
  }],
4427
5536
  },
4428
5537
  }));
@@ -4448,10 +5557,17 @@ function pushProfileSummary(entry, resourceContext, context) {
4448
5557
  note: trimToLength(segments.join(" "), 4000),
4449
5558
  evidenceRoles: resourceContext.evidenceRoles,
4450
5559
  externalRef: makeJunctionExternalRef(resourceContext, entry, timestamp, "profile-demographics"),
4451
- legacyExternalRefs: legacyExternalRefs("profile-demographics"),
4452
- dataOrigin: buildDataOrigin(entry, resourceContext, timestamp),
5560
+ dataOrigin: buildDataOrigin(entry, resourceContext, timestamp, {
5561
+ normalizerVersion: profileNormalizerVersion,
5562
+ }),
4453
5563
  }));
4454
5564
  }
5565
+ function readJunctionProfileGender(entry) {
5566
+ const value = firstStringFromPaths(entry, JUNCTION_PROFILE_GENDER_PATHS)?.trim().toLowerCase();
5567
+ return value === "female" || value === "male" || value === "other"
5568
+ ? value
5569
+ : undefined;
5570
+ }
4455
5571
  function readJunctionProfileSex(entry) {
4456
5572
  const value = firstStringFromPaths(entry, JUNCTION_PROFILE_SEX_PATHS);
4457
5573
  if (!value || value.trim().toLowerCase() === "unknown") {
@@ -4467,50 +5583,298 @@ const JUNCTION_MENSTRUAL_FLOW_ORDINALS = Object.freeze({
4467
5583
  medium: 2,
4468
5584
  heavy: 3,
4469
5585
  });
4470
- // Surge results are positive detections; indeterminate results carry no
4471
- // usable value and stay raw-only.
4472
- const JUNCTION_FERTILITY_TEST_RESULT_VALUES = Object.freeze({
5586
+ const JUNCTION_CERVICAL_MUCUS_QUALITIES = new Set([
5587
+ "dry",
5588
+ "sticky",
5589
+ "creamy",
5590
+ "watery",
5591
+ "egg_white",
5592
+ ]);
5593
+ const JUNCTION_CONTRACEPTIVE_TYPES = new Set([
5594
+ "implant",
5595
+ "injection",
5596
+ "iud",
5597
+ "intravaginal_ring",
5598
+ "oral",
5599
+ "patch",
5600
+ ]);
5601
+ const JUNCTION_MENSTRUAL_DEVIATIONS = new Set([
5602
+ "persistent_intermenstrual_bleeding",
5603
+ "prolonged_menstrual_periods",
5604
+ "irregular_menstrual_cycles",
5605
+ "infrequent_menstrual_cycles",
5606
+ ]);
5607
+ const JUNCTION_OVULATION_TEST_RESULT_VALUES = Object.freeze({
4473
5608
  negative: 0,
4474
5609
  positive: 1,
4475
5610
  luteinizing_hormone_surge: 1,
4476
5611
  estrogen_surge: 1,
4477
5612
  });
4478
- // One Junction menstrual cycle summary per cycle (~13/year), with small dated
4479
- // sub-arrays (docs.junction.com/api-reference/data/menstrual-cycle/get-summary).
4480
- // Predicted cycles are upstream forecasts, not facts, and stay raw-only.
4481
- function pushMenstrualCycleSummary(entry, resourceContext, context) {
4482
- if (firstValueFromPaths(entry, ["isPredicted", "is_predicted"]) === true) {
4483
- return;
5613
+ const JUNCTION_PREGNANCY_TEST_RESULT_VALUES = Object.freeze({
5614
+ negative: 0,
5615
+ positive: 1,
5616
+ });
5617
+ const JUNCTION_PROGESTERONE_TEST_RESULT_VALUES = Object.freeze({
5618
+ negative: 0,
5619
+ positive: 1,
5620
+ });
5621
+ const JUNCTION_MENSTRUAL_CYCLE_LIMIT = 64;
5622
+ const JUNCTION_MENSTRUAL_FACT_LIMIT = 512;
5623
+ function prepareJunctionMenstrualCycleSummary(payload, connectionsByKey) {
5624
+ const sortedSourceCycles = resourceEntries(payload, "menstrual_cycle")
5625
+ .map(({ entry, originFallback }) => {
5626
+ const connection = connectionsByKey
5627
+ ? resolveEntryConnection(entry, connectionsByKey)
5628
+ : undefined;
5629
+ return prepareJunctionMenstrualCycle(entry, buildJunctionOriginFallback(connection, originFallback));
5630
+ })
5631
+ .sort(comparePreparedJunctionMenstrualCycles);
5632
+ const sourceCyclesByHash = new Map();
5633
+ for (const cycle of sortedSourceCycles) {
5634
+ const existing = sourceCyclesByHash.get(cycle.recordHash);
5635
+ if (!existing) {
5636
+ sourceCyclesByHash.set(cycle.recordHash, cycle);
5637
+ continue;
5638
+ }
5639
+ existing.facts.push(...cycle.facts);
4484
5640
  }
4485
- const baseTimestamp = resolveRecordTimestamp(entry, context, resourceContext.sourceProviderSlug);
5641
+ const sourceCycles = [...sourceCyclesByHash.values()];
5642
+ const admittedCycles = sourceCycles.slice(0, JUNCTION_MENSTRUAL_CYCLE_LIMIT);
5643
+ const uniqueFacts = new Map();
5644
+ for (const fact of admittedCycles.flatMap((cycle) => cycle.facts).sort(compareJunctionMenstrualFacts)) {
5645
+ const key = junctionMenstrualFactSortKey(fact);
5646
+ if (!uniqueFacts.has(key)) {
5647
+ uniqueFacts.set(key, fact);
5648
+ }
5649
+ }
5650
+ const allFacts = [...uniqueFacts.values()];
5651
+ const admittedFacts = allFacts.slice(0, JUNCTION_MENSTRUAL_FACT_LIMIT);
5652
+ const factsByCycle = new Map();
5653
+ for (const fact of admittedFacts) {
5654
+ const facts = factsByCycle.get(fact.cycleRecordHash) ?? [];
5655
+ facts.push(fact);
5656
+ factsByCycle.set(fact.cycleRecordHash, facts);
5657
+ }
5658
+ return {
5659
+ cycles: admittedCycles.map((cycle) => ({
5660
+ ...cycle,
5661
+ facts: factsByCycle.get(cycle.recordHash) ?? [],
5662
+ })),
5663
+ evidence: {
5664
+ schema: "junction.menstrual_cycle_evidence.v1",
5665
+ provider: "junction",
5666
+ resource: "menstrual_cycle",
5667
+ cycleCount: admittedCycles.length,
5668
+ factCount: admittedFacts.length,
5669
+ omittedCycleCount: Math.max(0, sourceCycles.length - admittedCycles.length),
5670
+ omittedFactCount: Math.max(0, allFacts.length - admittedFacts.length),
5671
+ cycles: admittedCycles.map((cycle) => ({
5672
+ ...cycle.evidence,
5673
+ factCount: factsByCycle.get(cycle.recordHash)?.length ?? 0,
5674
+ })),
5675
+ facts: admittedFacts.map(({ canonical: _canonical, isPredicted: _isPredicted, ...fact }) => fact),
5676
+ },
5677
+ };
5678
+ }
5679
+ function prepareJunctionMenstrualCycle(entry, originFallback) {
5680
+ const isPredicted = firstValueFromPaths(entry, ["isPredicted", "is_predicted"]) === true;
5681
+ const drafts = collectJunctionMenstrualFactDrafts(entry);
5682
+ const origin = resolveJunctionOrigin(entry, originFallback);
5683
+ const sourceProviderSlug = origin.sourceProviderSlug;
5684
+ const sourceType = origin.sourceType;
5685
+ const explicitId = firstStringFromPaths(entry, JUNCTION_GENERIC_SUMMARY_ID_PATHS);
4486
5686
  const periodStart = firstIsoDateFromPaths(entry, ["periodStart", "period_start"]);
4487
5687
  const periodEnd = firstIsoDateFromPaths(entry, ["periodEnd", "period_end"]);
4488
- const cycleStart = firstIsoDateFromPaths(entry, ["cycleStart", "cycle_start"]);
4489
5688
  const cycleEnd = firstIsoDateFromPaths(entry, ["cycleEnd", "cycle_end"]);
4490
- const cycleAnchorDate = periodStart ?? cycleStart;
4491
- if (cycleAnchorDate) {
4492
- const cycleTimestamp = junctionDateOnlyTimestamp(baseTimestamp, cycleAnchorDate);
4493
- // Date-derived lengths win; explicit provider length fields are the
4494
- // fallback (the backfill completion predicate already treats them as
4495
- // useful, so a record carrying only the explicit fields must still
4496
- // land its observation). Both paths share the 1..120-day guard.
5689
+ const createdAt = trimOptionalToLength(firstStringFromPaths(entry, ["createdAt", "created_at"]), 80);
5690
+ const updatedAt = trimOptionalToLength(firstStringFromPaths(entry, ["updatedAt", "updated_at"]), 80);
5691
+ const legacyCycleStart = firstIsoDateFromPaths(entry, ["cycleStart", "cycle_start"]);
5692
+ const legacyCycleDay = firstNumberFromPaths(entry, ["cycleDay", "cycle_day"]);
5693
+ const legacyPeriodLengthDays = firstNumberFromPaths(entry, ["periodLengthDays", "period_length_days"]);
5694
+ const legacyCycleLengthDays = firstNumberFromPaths(entry, ["cycleLengthDays", "cycle_length_days"]);
5695
+ const recordHash = shortHash([
5696
+ "menstrual-cycle",
5697
+ sourceProviderSlug,
5698
+ sourceType,
5699
+ origin.sourceInstanceId,
5700
+ explicitId,
5701
+ periodStart,
5702
+ periodEnd,
5703
+ cycleEnd,
5704
+ createdAt,
5705
+ updatedAt,
5706
+ isPredicted,
5707
+ legacyCycleStart,
5708
+ legacyCycleDay,
5709
+ legacyPeriodLengthDays,
5710
+ legacyCycleLengthDays,
5711
+ ]);
5712
+ const facts = drafts.map((fact) => ({
5713
+ ...fact,
5714
+ cycleRecordHash: recordHash,
5715
+ isPredicted,
5716
+ }));
5717
+ const evidence = stripUndefined({
5718
+ recordHash,
5719
+ sourceProviderSlug,
5720
+ sourceType,
5721
+ isPredicted,
5722
+ periodStart,
5723
+ periodEnd,
5724
+ cycleEnd,
5725
+ createdAt,
5726
+ updatedAt,
5727
+ legacyCycleStart,
5728
+ legacyCycleDay,
5729
+ legacyPeriodLengthDays,
5730
+ legacyCycleLengthDays,
5731
+ });
5732
+ const scalarEntry = stripUndefined({
5733
+ id: explicitId,
5734
+ period_start: periodStart,
5735
+ period_end: periodEnd,
5736
+ cycle_end: cycleEnd,
5737
+ created_at: createdAt,
5738
+ updated_at: updatedAt,
5739
+ is_predicted: isPredicted,
5740
+ });
5741
+ return {
5742
+ entry: scalarEntry,
5743
+ evidence,
5744
+ facts,
5745
+ isPredicted,
5746
+ originFallback: { ...origin },
5747
+ recordHash,
5748
+ sortDate: firstStrictIsoDateFromPaths(entry, [
5749
+ "periodStart",
5750
+ "period_start",
5751
+ "cycleEnd",
5752
+ "cycle_end",
5753
+ "periodEnd",
5754
+ "period_end",
5755
+ ]),
5756
+ };
5757
+ }
5758
+ function collectJunctionMenstrualFactDrafts(entry) {
5759
+ const facts = [];
5760
+ const pushEnumFacts = (paths, kind, valuePaths, isCanonical) => {
5761
+ for (const sub of junctionDatedEvidenceSubEntries(entry, paths)) {
5762
+ const value = normalizeJunctionMenstrualEvidenceLabel(firstStringFromPaths(sub.entry, valuePaths));
5763
+ if (value) {
5764
+ facts.push({
5765
+ canonical: isStrictIsoDate(sub.date) && isCanonical(value),
5766
+ date: sub.date,
5767
+ kind,
5768
+ value,
5769
+ });
5770
+ }
5771
+ }
5772
+ };
5773
+ pushEnumFacts(["menstrualFlow", "menstrual_flow"], "menstrual_flow", ["flow"], (value) => Object.hasOwn(JUNCTION_MENSTRUAL_FLOW_ORDINALS, value));
5774
+ pushEnumFacts(["cervicalMucus", "cervical_mucus"], "cervical_mucus", ["quality"], (value) => JUNCTION_CERVICAL_MUCUS_QUALITIES.has(value));
5775
+ pushEnumFacts(["contraceptive"], "contraceptive", ["type"], (value) => JUNCTION_CONTRACEPTIVE_TYPES.has(value));
5776
+ pushEnumFacts(["detectedDeviations", "detected_deviations"], "detected_deviation", ["deviation"], (value) => JUNCTION_MENSTRUAL_DEVIATIONS.has(value));
5777
+ pushEnumFacts(["ovulationTest", "ovulation_test"], "ovulation_test", ["testResult", "test_result"], (value) => Object.hasOwn(JUNCTION_OVULATION_TEST_RESULT_VALUES, value));
5778
+ pushEnumFacts(["homePregnancyTest", "home_pregnancy_test"], "home_pregnancy_test", ["testResult", "test_result"], (value) => Object.hasOwn(JUNCTION_PREGNANCY_TEST_RESULT_VALUES, value));
5779
+ pushEnumFacts(["homeProgesteroneTest", "home_progesterone_test"], "home_progesterone_test", ["testResult", "test_result"], (value) => Object.hasOwn(JUNCTION_PROGESTERONE_TEST_RESULT_VALUES, value));
5780
+ for (const sub of junctionDatedEvidenceSubEntries(entry, ["intermenstrualBleeding", "intermenstrual_bleeding"])) {
5781
+ facts.push({
5782
+ canonical: isStrictIsoDate(sub.date),
5783
+ date: sub.date,
5784
+ kind: "intermenstrual_bleeding",
5785
+ });
5786
+ }
5787
+ for (const sub of junctionDatedEvidenceSubEntries(entry, ["basalBodyTemperature", "basal_body_temperature"])) {
5788
+ const value = finiteNumber(firstValueFromPaths(sub.entry, ["value"]));
5789
+ if (value !== undefined) {
5790
+ facts.push({ canonical: false, date: sub.date, kind: "basal_body_temperature", value });
5791
+ }
5792
+ }
5793
+ for (const sub of junctionDatedEvidenceSubEntries(entry, ["sexualActivity", "sexual_activity"])) {
5794
+ const rawProtectionUsed = firstValueFromPaths(sub.entry, ["protectionUsed", "protection_used"]);
5795
+ if (rawProtectionUsed === undefined || rawProtectionUsed === null || typeof rawProtectionUsed === "boolean") {
5796
+ facts.push(stripUndefined({
5797
+ canonical: isStrictIsoDate(sub.date),
5798
+ date: sub.date,
5799
+ kind: "sexual_activity",
5800
+ value: typeof rawProtectionUsed === "boolean" ? rawProtectionUsed : undefined,
5801
+ }));
5802
+ continue;
5803
+ }
5804
+ const invalidValue = normalizeJunctionMenstrualEvidenceLabel(String(rawProtectionUsed));
5805
+ if (invalidValue) {
5806
+ facts.push({
5807
+ canonical: false,
5808
+ date: sub.date,
5809
+ kind: "sexual_activity",
5810
+ value: invalidValue,
5811
+ });
5812
+ }
5813
+ }
5814
+ return facts;
5815
+ }
5816
+ function comparePreparedJunctionMenstrualCycles(left, right) {
5817
+ return Number(left.isPredicted) - Number(right.isPredicted)
5818
+ || compareNewestOptionalIsoDates(left.sortDate, right.sortDate)
5819
+ || left.recordHash.localeCompare(right.recordHash);
5820
+ }
5821
+ function compareJunctionMenstrualFacts(left, right) {
5822
+ return Number(left.isPredicted) - Number(right.isPredicted)
5823
+ || Number(!left.canonical) - Number(!right.canonical)
5824
+ || compareNewestOptionalIsoDates(isStrictIsoDate(left.date) ? left.date : undefined, isStrictIsoDate(right.date) ? right.date : undefined)
5825
+ || junctionMenstrualFactSortKey(left).localeCompare(junctionMenstrualFactSortKey(right));
5826
+ }
5827
+ function compareNewestOptionalIsoDates(left, right) {
5828
+ if (left && right) {
5829
+ return right.localeCompare(left);
5830
+ }
5831
+ return left ? -1 : right ? 1 : 0;
5832
+ }
5833
+ function junctionMenstrualFactSortKey(fact) {
5834
+ return JSON.stringify([
5835
+ fact.cycleRecordHash,
5836
+ fact.date,
5837
+ fact.kind,
5838
+ fact.value ?? null,
5839
+ ]);
5840
+ }
5841
+ function normalizeJunctionMenstrualEvidenceLabel(value) {
5842
+ const normalized = value?.trim().toLowerCase();
5843
+ return normalized ? trimToLength(normalized, 80) : undefined;
5844
+ }
5845
+ // One Junction menstrual cycle summary per cycle (~13/year). The caller has
5846
+ // already selected the deterministic admitted flat fact set. Predicted cycles
5847
+ // are upstream forecasts, not canonical facts.
5848
+ function pushMenstrualCycleSummary(cycle, resourceContext, context) {
5849
+ if (cycle.isPredicted) {
5850
+ return;
5851
+ }
5852
+ const entry = cycle.entry;
5853
+ const baseTimestamp = resolveRecordTimestamp(entry, context, resourceContext.sourceProviderSlug);
5854
+ const periodStart = firstStrictIsoDateFromPaths(entry, ["periodStart", "period_start"]);
5855
+ const periodEnd = firstStrictIsoDateFromPaths(entry, ["periodEnd", "period_end"]);
5856
+ const cycleEnd = firstStrictIsoDateFromPaths(entry, ["cycleEnd", "cycle_end"]);
5857
+ const dailyFactFacet = (prefix, fact) => `${prefix}-${fact.date}-${shortHash([
5858
+ "menstrual-fact",
5859
+ prefix,
5860
+ fact.date,
5861
+ fact.kind,
5862
+ fact.value ?? null,
5863
+ ])}`;
5864
+ if (periodStart) {
5865
+ const cycleTimestamp = junctionDateOnlyTimestamp(baseTimestamp, periodStart);
5866
+ // Junction currently documents period_start, period_end, and cycle_end.
5867
+ // Legacy cycle_start and scalar length aliases remain evidence-only.
4497
5868
  const cycleObservations = [
4498
5869
  {
4499
5870
  metric: "period-length-days",
4500
5871
  title: "Junction period length",
4501
- value: (periodStart ? inclusiveDaysBetween(periodStart, periodEnd) : undefined)
4502
- ?? plausibleCycleLengthDays(firstNumberFromPaths(entry, ["periodLengthDays", "period_length_days"])),
5872
+ value: inclusiveDaysBetween(periodStart, periodEnd),
4503
5873
  },
4504
5874
  {
4505
5875
  metric: "cycle-length-days",
4506
5876
  title: "Junction cycle length",
4507
- // Cycle length anchors on the cycle start when present (period
4508
- // start is the fallback anchor — cycles begin with the period in
4509
- // Junction's model, but explicit cycle_start wins if they differ).
4510
- value: (cycleStart ?? periodStart
4511
- ? inclusiveDaysBetween(cycleStart ?? periodStart ?? "", cycleEnd)
4512
- : undefined)
4513
- ?? plausibleCycleLengthDays(firstNumberFromPaths(entry, ["cycleLengthDays", "cycle_length_days"])),
5877
+ value: inclusiveDaysBetween(periodStart, cycleEnd),
4514
5878
  },
4515
5879
  ];
4516
5880
  for (const observation of cycleObservations) {
@@ -4536,169 +5900,112 @@ function pushMenstrualCycleSummary(entry, resourceContext, context) {
4536
5900
  }));
4537
5901
  }
4538
5902
  }
4539
- // Basal body temperature deliberately stays raw-only here: the dedicated
4540
- // `basal_body_temperature` daily timeseries (a default resource) is the
4541
- // canonical seam for the `basal-body-temperature` metric, and mapping the
4542
- // cycle sub-array too would land duplicate same-day observations under a
4543
- // different resourceType that dedupe cannot collapse.
4544
- for (const sub of junctionDatedSubEntries(entry, ["menstrualFlow", "menstrual_flow"])) {
4545
- const flow = firstStringFromPaths(sub.entry, ["flow"]);
4546
- const value = flow ? JUNCTION_MENSTRUAL_FLOW_ORDINALS[flow.trim().toLowerCase()] : undefined;
4547
- if (!flow || value === undefined) {
4548
- continue;
4549
- }
4550
- // Result-bearing facet (same invariant as ovulation/pregnancy tests):
4551
- // a same-day flow change, e.g. light then heavy, keeps both facts.
4552
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4553
- date: sub.date,
4554
- facet: `menstrual-flow-${slugify(flow, "flow")}-${sub.date}`,
4555
- measurement: {
4556
- metric: "menstrual-flow",
4557
- value,
4558
- unit: "score",
4559
- qualifiers: { flow: trimToLength(flow, 80) },
4560
- },
4561
- title: "Junction menstrual flow",
4562
- });
4563
- }
4564
- for (const sub of junctionDatedSubEntries(entry, ["cervicalMucus", "cervical_mucus"])) {
4565
- const quality = firstStringFromPaths(sub.entry, ["quality"]);
4566
- if (!quality) {
4567
- continue;
4568
- }
4569
- const boundedQuality = trimToLength(quality, 80);
4570
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4571
- date: sub.date,
4572
- facet: `cervical-mucus-quality-${trimSlugToLength(slugify(boundedQuality, "quality"), 80)}-${sub.date}`,
4573
- measurement: {
4574
- metric: "cervical-mucus-quality",
4575
- value: 1,
4576
- unit: "recording",
4577
- qualifiers: { quality: boundedQuality },
4578
- },
4579
- title: "Junction cervical mucus quality",
4580
- });
4581
- }
4582
- for (const sub of junctionDatedSubEntries(entry, ["intermenstrualBleeding", "intermenstrual_bleeding"])) {
4583
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4584
- date: sub.date,
4585
- facet: `intermenstrual-bleeding-${sub.date}`,
4586
- measurement: {
4587
- metric: "intermenstrual-bleeding",
4588
- value: 1,
4589
- unit: "flag",
4590
- },
4591
- title: "Junction intermenstrual bleeding",
4592
- });
4593
- }
4594
- for (const sub of junctionDatedSubEntries(entry, ["contraceptive"])) {
4595
- const contraceptiveType = firstStringFromPaths(sub.entry, ["type"]);
4596
- if (!contraceptiveType) {
5903
+ // Basal temperature stays evidence-only here because the dedicated sparse
5904
+ // daily resource is its canonical owner. Every other event is emitted from
5905
+ // the same admitted fact used by bounded evidence and receipt hashing.
5906
+ for (const fact of cycle.facts) {
5907
+ if (!fact.canonical || fact.kind === "basal_body_temperature") {
4597
5908
  continue;
4598
5909
  }
4599
- const boundedType = trimToLength(contraceptiveType, 80);
4600
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4601
- date: sub.date,
4602
- facet: `contraceptive-type-${trimSlugToLength(slugify(boundedType, "type"), 80)}-${sub.date}`,
4603
- measurement: {
4604
- metric: "contraceptive-type",
4605
- value: 1,
4606
- unit: "recording",
4607
- qualifiers: { type: boundedType },
4608
- },
4609
- title: "Junction contraceptive type",
4610
- });
4611
- }
4612
- for (const test of [
4613
- { metric: "ovulation-test", paths: ["ovulationTest", "ovulation_test"], title: "Junction ovulation test" },
4614
- { metric: "pregnancy-test", paths: ["homePregnancyTest", "home_pregnancy_test"], title: "Junction pregnancy test" },
4615
- ]) {
4616
- for (const sub of junctionDatedSubEntries(entry, test.paths)) {
4617
- const result = firstStringFromPaths(sub.entry, ["testResult", "test_result"]);
4618
- const value = result ? JUNCTION_FERTILITY_TEST_RESULT_VALUES[result.trim().toLowerCase()] : undefined;
4619
- if (!result || value === undefined) {
4620
- continue;
5910
+ const value = typeof fact.value === "string" ? fact.value : undefined;
5911
+ let input;
5912
+ switch (fact.kind) {
5913
+ case "menstrual_flow": {
5914
+ const ordinal = value ? JUNCTION_MENSTRUAL_FLOW_ORDINALS[value] : undefined;
5915
+ if (value && ordinal !== undefined) {
5916
+ input = {
5917
+ date: fact.date,
5918
+ facet: dailyFactFacet("menstrual-flow", fact),
5919
+ measurement: { metric: "menstrual-flow", value: ordinal, unit: "score", qualifiers: { flow: value } },
5920
+ title: "Junction menstrual flow",
5921
+ };
5922
+ }
5923
+ break;
4621
5924
  }
4622
- // The facet carries the normalized result so two same-day tests with
4623
- // different outcomes (e.g. negative then surge) keep distinct
4624
- // identities; repeated identical (date, result) rows still merge.
4625
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4626
- date: sub.date,
4627
- facet: `${test.metric}-${slugify(result, "result")}-${sub.date}`,
4628
- measurement: {
4629
- metric: test.metric,
4630
- value,
4631
- unit: "result",
4632
- qualifiers: { result: trimToLength(result, 80) },
4633
- },
4634
- title: test.title,
4635
- });
4636
- }
4637
- }
4638
- for (const sub of junctionDatedSubEntries(entry, ["homeProgesteroneTest", "home_progesterone_test"])) {
4639
- const result = firstStringFromPaths(sub.entry, ["testResult", "test_result"]);
4640
- if (!result) {
4641
- continue;
5925
+ case "ovulation_test":
5926
+ case "home_pregnancy_test":
5927
+ case "home_progesterone_test": {
5928
+ const definition = fact.kind === "ovulation_test"
5929
+ ? { metric: "ovulation-test", title: "Junction ovulation test", values: JUNCTION_OVULATION_TEST_RESULT_VALUES }
5930
+ : fact.kind === "home_pregnancy_test"
5931
+ ? { metric: "pregnancy-test", title: "Junction pregnancy test", values: JUNCTION_PREGNANCY_TEST_RESULT_VALUES }
5932
+ : { metric: "progesterone-test", title: "Junction progesterone test", values: JUNCTION_PROGESTERONE_TEST_RESULT_VALUES };
5933
+ const ordinal = value ? definition.values[value] : undefined;
5934
+ if (value && ordinal !== undefined) {
5935
+ input = {
5936
+ date: fact.date,
5937
+ facet: dailyFactFacet(definition.metric, fact),
5938
+ measurement: { metric: definition.metric, value: ordinal, unit: "result", qualifiers: { result: value } },
5939
+ title: definition.title,
5940
+ };
5941
+ }
5942
+ break;
5943
+ }
5944
+ case "cervical_mucus":
5945
+ if (value) {
5946
+ input = {
5947
+ date: fact.date,
5948
+ facet: dailyFactFacet("cervical-mucus", fact),
5949
+ measurement: { metric: "cervical-mucus", value: 1, unit: "observation", qualifiers: { quality: value } },
5950
+ title: "Junction cervical mucus",
5951
+ };
5952
+ }
5953
+ break;
5954
+ case "intermenstrual_bleeding":
5955
+ input = {
5956
+ date: fact.date,
5957
+ facet: dailyFactFacet("intermenstrual-bleeding", fact),
5958
+ measurement: { metric: "intermenstrual-bleeding", value: 1, unit: "event", qualifiers: { bleeding: "intermenstrual" } },
5959
+ title: "Junction intermenstrual bleeding",
5960
+ };
5961
+ break;
5962
+ case "contraceptive":
5963
+ if (value) {
5964
+ input = {
5965
+ date: fact.date,
5966
+ facet: dailyFactFacet("contraceptive", fact),
5967
+ measurement: { metric: "contraceptive-use", value: 1, unit: "event", qualifiers: { type: value } },
5968
+ title: "Junction contraceptive use",
5969
+ };
5970
+ }
5971
+ break;
5972
+ case "sexual_activity":
5973
+ input = {
5974
+ date: fact.date,
5975
+ facet: dailyFactFacet("sexual-activity", fact),
5976
+ measurement: {
5977
+ metric: "sexual-activity",
5978
+ value: 1,
5979
+ unit: "event",
5980
+ qualifiers: typeof fact.value === "boolean" ? { "protection-used": fact.value } : {},
5981
+ },
5982
+ title: "Junction sexual activity",
5983
+ };
5984
+ break;
5985
+ case "detected_deviation":
5986
+ if (value) {
5987
+ input = {
5988
+ date: fact.date,
5989
+ facet: dailyFactFacet("menstrual-cycle-deviation", fact),
5990
+ measurement: { metric: "menstrual-cycle-deviation", value: 1, unit: "flag", qualifiers: { deviation: value } },
5991
+ title: "Junction cycle deviation",
5992
+ };
5993
+ }
5994
+ break;
4642
5995
  }
4643
- const boundedResult = trimToLength(result, 80);
4644
- // This records the provider's categorical result rather than coercing it
4645
- // into a positive/negative scalar, so indeterminate and unknown remain
4646
- // truthful queryable values instead of being dropped or guessed.
4647
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4648
- date: sub.date,
4649
- facet: `home-progesterone-test-${trimSlugToLength(slugify(boundedResult, "result"), 80)}-${sub.date}`,
4650
- measurement: {
4651
- metric: "home-progesterone-test",
4652
- value: 1,
4653
- unit: "recording",
4654
- qualifiers: { result: boundedResult },
4655
- },
4656
- title: "Junction home progesterone test",
4657
- });
4658
- }
4659
- for (const sub of junctionDatedSubEntries(entry, ["sexualActivity", "sexual_activity"])) {
4660
- const protectionUsedRaw = firstValueFromPaths(sub.entry, ["protectionUsed", "protection_used"]);
4661
- const protectionUsed = typeof protectionUsedRaw === "boolean" ? protectionUsedRaw : undefined;
4662
- const protectionFacet = protectionUsed === true
4663
- ? "protected"
4664
- : protectionUsed === false
4665
- ? "unprotected"
4666
- : "protection-unspecified";
4667
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4668
- date: sub.date,
4669
- facet: `sexual-activity-${protectionFacet}-${sub.date}`,
4670
- measurement: {
4671
- metric: "sexual-activity",
4672
- value: 1,
4673
- unit: "recording",
4674
- qualifiers: protectionUsed === undefined ? undefined : { "protection-used": protectionUsed },
4675
- },
4676
- title: "Junction sexual activity",
4677
- });
4678
- }
4679
- for (const sub of junctionDatedSubEntries(entry, ["detectedDeviations", "detected_deviations"])) {
4680
- const deviation = firstStringFromPaths(sub.entry, ["deviation"]);
4681
- if (!deviation) {
4682
- continue;
5996
+ if (input) {
5997
+ pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, input);
4683
5998
  }
4684
- pushJunctionCycleDailyMeasurement(entry, resourceContext, context, baseTimestamp, {
4685
- date: sub.date,
4686
- facet: `menstrual-cycle-deviation-${trimSlugToLength(slugify(deviation, "deviation"), 80)}-${sub.date}`,
4687
- measurement: {
4688
- metric: "menstrual-cycle-deviation",
4689
- value: 1,
4690
- unit: "flag",
4691
- qualifiers: { deviation: trimToLength(deviation, 80) },
4692
- },
4693
- title: "Junction cycle deviation",
4694
- });
4695
5999
  }
4696
6000
  }
4697
6001
  function junctionDatedSubEntries(entry, paths) {
6002
+ return junctionDatedEvidenceSubEntries(entry, paths).filter((subEntry) => isStrictIsoDate(subEntry.date));
6003
+ }
6004
+ function junctionDatedEvidenceSubEntries(entry, paths) {
4698
6005
  const subEntries = [];
4699
6006
  for (const value of asArray(firstValueFromPaths(entry, paths))) {
4700
6007
  const subEntry = asPlainObject(value);
4701
- const date = subEntry ? firstIsoDateFromPaths(subEntry, ["date"]) : undefined;
6008
+ const date = trimOptionalToLength(subEntry ? firstStringFromPaths(subEntry, ["date"]) : undefined, 40);
4702
6009
  if (subEntry && date) {
4703
6010
  subEntries.push({ date, entry: subEntry });
4704
6011
  }
@@ -4730,11 +6037,6 @@ function junctionDateOnlyTimestamp(baseTimestamp, date) {
4730
6037
  observedAtRaw: date,
4731
6038
  });
4732
6039
  }
4733
- function plausibleCycleLengthDays(value) {
4734
- return value !== undefined && Number.isInteger(value) && value >= 1 && value <= 120
4735
- ? value
4736
- : undefined;
4737
- }
4738
6040
  function inclusiveDaysBetween(startDate, endDate) {
4739
6041
  if (!endDate) {
4740
6042
  return undefined;
@@ -5177,7 +6479,7 @@ function buildWorkoutRouteMetadata(entry) {
5177
6479
  });
5178
6480
  return Object.keys(route).length > 0 ? route : undefined;
5179
6481
  }
5180
- function pushObservationMetrics(entry, resourceContext, context, metrics, timestampOverride, legacyExternalRefs) {
6482
+ function pushObservationMetrics(entry, resourceContext, context, metrics, timestampOverride, options = {}) {
5181
6483
  const timestamp = timestampOverride ?? resolveRecordTimestamp(entry, context, resourceContext.sourceProviderSlug);
5182
6484
  const occurredAt = timestamp.occurredAt;
5183
6485
  if (!occurredAt) {
@@ -5202,8 +6504,7 @@ function pushObservationMetrics(entry, resourceContext, context, metrics, timest
5202
6504
  // supersedes an existing generic Apple HRV event instead of duplicating
5203
6505
  // it under the corrected SDNN metric.
5204
6506
  externalRef: makeJunctionExternalRef(resourceContext, entry, timestamp, metric.metric),
5205
- legacyExternalRefs: legacyExternalRefs?.(metric.metric),
5206
- dataOrigin: buildDataOrigin(entry, resourceContext, timestamp),
6507
+ dataOrigin: buildDataOrigin(entry, resourceContext, timestamp, options),
5207
6508
  fields: {
5208
6509
  metric: metricKey,
5209
6510
  observationGrain: "summary",
@@ -5256,18 +6557,18 @@ function pushJunctionRecoveryReadinessScore(entry, resourceContext, context, tim
5256
6557
  }
5257
6558
  function resolveMetricDescriptorValue(entry, metric) {
5258
6559
  const computedValue = metric.value ? finiteNumber(metric.value(entry)) : undefined;
5259
- if (computedValue !== undefined) {
6560
+ if (computedValue !== undefined && (!metric.nonnegative || computedValue >= 0)) {
5260
6561
  return { value: computedValue, unit: metric.unit };
5261
6562
  }
5262
6563
  const directValue = firstNumberFromPaths(entry, metric.paths);
5263
- if (directValue !== undefined) {
6564
+ if (directValue !== undefined && (!metric.nonnegative || directValue >= 0)) {
5264
6565
  return {
5265
6566
  value: directValue,
5266
6567
  unit: firstStringFromPaths(entry, ["unit"]) ?? metric.unit,
5267
6568
  };
5268
6569
  }
5269
6570
  const secondsValue = secondsToMinutes(firstNumberFromPaths(entry, metric.secondsPaths ?? []));
5270
- if (secondsValue !== undefined) {
6571
+ if (secondsValue !== undefined && (!metric.nonnegative || secondsValue >= 0)) {
5271
6572
  return { value: secondsValue, unit: metric.unit };
5272
6573
  }
5273
6574
  const metersValue = metersToKilometers(firstNumberFromPaths(entry, metric.metersPaths ?? []));
@@ -5288,18 +6589,14 @@ function resolveJunctionSleepLatencyMinutes(entry) {
5288
6589
  return secondsToMinutes(firstNumberFromPaths(entry, JUNCTION_SLEEP_LATENCY_SECOND_PATHS));
5289
6590
  }
5290
6591
  function resolveJunctionDailyActivityMinutes(entry) {
5291
- // Get Summary reports these buckets in minutes and `daily_movement` as
5292
- // deprecated equivalent-walking meters. Sense's `*_second` query columns
5293
- // are a separate normalized representation, not summary payload aliases.
5294
- const lowActivityMinutes = firstNumberFromPaths(entry, ["low"]);
5295
- const mediumActivityMinutes = firstNumberFromPaths(entry, ["medium"]);
5296
- const highActivityMinutes = firstNumberFromPaths(entry, ["high"]);
6592
+ // Junction activity summary intensity buckets are already minutes. Keeping
6593
+ // the shared resolver here also applies the same per-bucket and daily caps.
6594
+ const lowActivityMinutes = resolveJunctionActivityIntensityMinutes(entry, "low");
6595
+ const mediumActivityMinutes = resolveJunctionActivityIntensityMinutes(entry, "medium");
6596
+ const highActivityMinutes = resolveJunctionActivityIntensityMinutes(entry, "high");
5297
6597
  if (lowActivityMinutes === undefined
5298
6598
  || mediumActivityMinutes === undefined
5299
- || highActivityMinutes === undefined
5300
- || lowActivityMinutes < 0
5301
- || mediumActivityMinutes < 0
5302
- || highActivityMinutes < 0) {
6599
+ || highActivityMinutes === undefined) {
5303
6600
  return undefined;
5304
6601
  }
5305
6602
  const totalActivityMinutes = lowActivityMinutes + mediumActivityMinutes + highActivityMinutes;
@@ -5380,18 +6677,30 @@ function withTimestampOverride(timestamp, override) {
5380
6677
  };
5381
6678
  }
5382
6679
  function makeJunctionExternalRef(resourceContext, entry, timestamp, facet, sourceVersion) {
5383
- return makeProviderExternalRef("junction", resourceContext.externalRefResourceType, buildStableResourceId(resourceContext, entry, timestamp), sourceVersion ?? junctionCompanionExternalRefVersion(resourceContext, entry), slugify(facet, "value"));
6680
+ return makeProviderExternalRef("junction", resourceContext.externalRefResourceType, buildStableResourceId(resourceContext, entry, timestamp), sourceVersion ?? junctionExternalRefVersion(resourceContext, entry), slugify(facet, "value"));
5384
6681
  }
5385
- function junctionCompanionExternalRefVersion(resourceContext, entry) {
6682
+ function junctionExternalRefVersion(resourceContext, entry) {
6683
+ if (resourceContext.identityKind === "timeseries"
6684
+ && resourceContext.resource === "workout_stream"
6685
+ && entry.schema === JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA) {
6686
+ return normalizeTimestamp(firstValueFromPaths(entry, ["version"]));
6687
+ }
5386
6688
  if (resourceContext.sourceProviderSlug !== "apple-health-kit"
5387
6689
  || resourceContext.origin.sourceType !== "companion-whoop-metadata-unverified") {
5388
- return undefined;
6690
+ return junctionAuthoritativeSummaryVersion(resourceContext, entry);
5389
6691
  }
5390
6692
  const version = entry.companionSyncVersion;
5391
6693
  return typeof version === "number" && Number.isSafeInteger(version) && version >= 0
5392
6694
  ? String(version)
5393
6695
  : undefined;
5394
6696
  }
6697
+ function junctionAuthoritativeSummaryVersion(resourceContext, entry) {
6698
+ if (resourceContext.identityKind !== "summary"
6699
+ || (resourceContext.resource !== "profile" && resourceContext.resource !== "menstrual_cycle")) {
6700
+ return undefined;
6701
+ }
6702
+ return resolveSafeTimestamp(firstValueFromPaths(entry, ["updatedAt", "updated_at", "createdAt", "created_at"]), resourceContext.sourceProviderSlug);
6703
+ }
5395
6704
  function buildJunctionResourceType(sourceProviderSlug, resourceSlug) {
5396
6705
  return `junction-${slugify(sourceProviderSlug, "source")}-${slugify(resourceSlug, "resource")}`;
5397
6706
  }
@@ -5433,7 +6742,7 @@ function buildStableSummaryResourceId(resourceContext, entry, timestamp) {
5433
6742
  : []),
5434
6743
  ])}`;
5435
6744
  }
5436
- function buildStableProfileResourceId(entry, sourceProviderSlug, sourceType, sourceInstanceId, updatedAt) {
6745
+ function buildStableProfileResourceId(entry, sourceProviderSlug, sourceType, sourceInstanceId, identityTimestamp) {
5437
6746
  const retainedResourceId = firstStringFromPaths(entry, ["stableResourceId"]);
5438
6747
  if (retainedResourceId && /^profile-[a-f0-9]{16}$/u.test(retainedResourceId)) {
5439
6748
  return retainedResourceId;
@@ -5447,37 +6756,16 @@ function buildStableProfileResourceId(entry, sourceProviderSlug, sourceType, sou
5447
6756
  explicitId,
5448
6757
  ])}`;
5449
6758
  }
5450
- return updatedAt
6759
+ return identityTimestamp
5451
6760
  ? `profile-${shortHash([
5452
6761
  "profile",
5453
6762
  sourceProviderSlug,
5454
6763
  sourceType,
5455
6764
  sourceInstanceId,
5456
- updatedAt,
6765
+ identityTimestamp,
5457
6766
  ])}`
5458
6767
  : undefined;
5459
6768
  }
5460
- function buildJunctionProfileLegacyExternalRefs(resourceContext, entry, timestamp, providerTimestampRaw, facet) {
5461
- if (firstStringFromPaths(entry, ["stableResourceId"])
5462
- || firstStringFromPaths(entry, JUNCTION_GENERIC_SUMMARY_ID_PATHS)) {
5463
- return undefined;
5464
- }
5465
- const rawTimestamp = stringId(providerTimestampRaw);
5466
- if (!rawTimestamp) {
5467
- return undefined;
5468
- }
5469
- const primary = makeJunctionExternalRef(resourceContext, entry, timestamp, facet);
5470
- const legacyResourceId = `profile-${shortHash([
5471
- "profile",
5472
- resourceContext.sourceProviderSlug,
5473
- resourceContext.origin.sourceType,
5474
- resourceContext.origin.sourceInstanceId,
5475
- rawTimestamp,
5476
- ])}`;
5477
- return legacyResourceId === primary.resourceId
5478
- ? undefined
5479
- : [{ ...primary, resourceId: legacyResourceId }];
5480
- }
5481
6769
  function buildStableTimeseriesResourceId(resourceContext, timestamp) {
5482
6770
  if (resourceContext.fallbackIdentityDisambiguator) {
5483
6771
  return `${resourceContext.resourceSlug}-${shortHash([
@@ -5522,7 +6810,9 @@ function resolveRecordTimestamp(entry, context, sourceProviderSlug, timestampPat
5522
6810
  const rawObservedAt = firstStringFromPaths(entry, timestampPaths);
5523
6811
  const localCalendarDayKey = firstIsoDateFromPaths(entry, JUNCTION_LOCAL_CALENDAR_DATE_PATHS);
5524
6812
  const explicitSemantics = firstTimestampSemantics(entry);
5525
- const hasSourceSpecificFloatingTime = hasFloatingTimestampSourceProvider(sourceProviderSlug);
6813
+ const hasSourceSpecificFloatingTime = rawObservedAt
6814
+ ? isJunctionSourceLocalFloatingTimestamp(rawObservedAt, sourceProviderSlug)
6815
+ : hasFloatingTimestampSourceProvider(sourceProviderSlug);
5526
6816
  const timestampSemantics = hasSourceSpecificFloatingTime
5527
6817
  ? "floating"
5528
6818
  : explicitSemantics ?? inferTimestampSemantics(rawObservedAt);
@@ -5877,9 +7167,13 @@ function normalizeTimestamp(value) {
5877
7167
  const time = Date.parse(value);
5878
7168
  return Number.isFinite(time) ? new Date(time).toISOString() : undefined;
5879
7169
  }
7170
+ function resolveJunctionFloatingTimestampTimeZone(entry, defaultTimeZone) {
7171
+ return normalizeIanaTimeZone(firstStringFromPaths(entry, ["timeZone", "timezone", "time_zone"])) ?? normalizeIanaTimeZone(defaultTimeZone) ?? undefined;
7172
+ }
5880
7173
  function resolveSafeTimestamp(value, sourceProviderSlug) {
5881
7174
  const raw = typeof value === "string" ? value.trim() : value;
5882
- if (typeof raw === "string" && hasFloatingTimestampSourceProvider(sourceProviderSlug)) {
7175
+ if (typeof raw === "string"
7176
+ && isJunctionSourceLocalFloatingTimestamp(raw, sourceProviderSlug)) {
5883
7177
  return undefined;
5884
7178
  }
5885
7179
  if (typeof raw === "string" && inferTimestampSemantics(raw) === "floating") {
@@ -5891,6 +7185,13 @@ function hasFloatingTimestampSourceProvider(sourceProviderSlug) {
5891
7185
  const normalized = normalizeJunctionSourceProviderSlug(sourceProviderSlug);
5892
7186
  return normalized ? FLOATING_TIMESTAMP_SOURCE_PROVIDER_SLUGS.has(normalized) : false;
5893
7187
  }
7188
+ function isJunctionSourceLocalFloatingTimestamp(value, sourceProviderSlug) {
7189
+ if (!hasFloatingTimestampSourceProvider(sourceProviderSlug)) {
7190
+ return false;
7191
+ }
7192
+ const semantics = inferTimestampSemantics(value);
7193
+ return semantics === "floating" || /(?:z|[+-]00:?00)$/iu.test(value.trim());
7194
+ }
5894
7195
  function inferTimestampSemantics(value) {
5895
7196
  if (!value) {
5896
7197
  return undefined;
@@ -5913,6 +7214,11 @@ function firstTimestampSemantics(entry) {
5913
7214
  ? value
5914
7215
  : undefined;
5915
7216
  }
7217
+ function isImplicitJunctionCanonicalUtcTimestamp(entry, timestamp) {
7218
+ return firstTimestampSemantics(entry) === undefined
7219
+ && timestamp.timestampSemantics !== "floating"
7220
+ && /\+00:?00$/u.test(timestamp.observedAtRaw?.trim() ?? "");
7221
+ }
5916
7222
  function resolveJunctionTimeseriesAggregateDayKey(entry, timestamp, sampleAt, defaultTimeZone) {
5917
7223
  if (timestamp.timestampSemantics === "offset") {
5918
7224
  return timestamp.dayKey ?? extractIsoDatePrefix(sampleAt) ?? undefined;
@@ -5965,6 +7271,13 @@ function resolveVaultLocalDayKey(timestamp, defaultTimeZone) {
5965
7271
  }
5966
7272
  }
5967
7273
  function resolveLegacyJunctionTimeseriesAggregateDayKey(entry, timestamp, sampleAt) {
7274
+ // Before canonical +00:00 timestamps were recognized as UTC, the importer
7275
+ // treated their raw calendar date as authoritative. Keep that old day as a
7276
+ // replay alias when the corrected source-local day crosses midnight.
7277
+ if (timestamp.timestampSemantics === "utc"
7278
+ && isImplicitJunctionCanonicalUtcTimestamp(entry, timestamp)) {
7279
+ return extractIsoDatePrefix(timestamp.observedAtRaw) ?? undefined;
7280
+ }
5968
7281
  const offsetSeconds = readJunctionTimeZoneOffsetSeconds(entry);
5969
7282
  if (offsetSeconds !== null
5970
7283
  && offsetSeconds !== undefined
@@ -6382,6 +7695,18 @@ function normalizePercentRatio(value) {
6382
7695
  }
6383
7696
  return numeric >= 0 && numeric <= 1 ? numeric * 100 : numeric;
6384
7697
  }
7698
+ function normalizeBodyCompositionPercent(value) {
7699
+ const numeric = finiteNumber(value);
7700
+ return numeric !== undefined && numeric >= 0 && numeric <= 100
7701
+ ? roundJunctionDailyAggregateValue(numeric)
7702
+ : undefined;
7703
+ }
7704
+ function normalizeNonNegativeBodyIndex(value) {
7705
+ const numeric = finiteNumber(value);
7706
+ return numeric !== undefined && numeric >= 0
7707
+ ? roundJunctionDailyAggregateValue(numeric)
7708
+ : undefined;
7709
+ }
6385
7710
  function normalizeBloodOxygenPercent(value) {
6386
7711
  const numeric = normalizePercentRatio(value);
6387
7712
  if (numeric === undefined || numeric <= 0 || numeric > 100) {
@@ -6581,6 +7906,10 @@ function firstIsoDateFromPaths(source, paths) {
6581
7906
  }
6582
7907
  return undefined;
6583
7908
  }
7909
+ function firstStrictIsoDateFromPaths(source, paths) {
7910
+ const date = firstIsoDateFromPaths(source, paths);
7911
+ return date && isStrictIsoDate(date) ? date : undefined;
7912
+ }
6584
7913
  function trimOptionalToLength(value, maxLength) {
6585
7914
  return value ? trimToLength(value, maxLength) : undefined;
6586
7915
  }