@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.
- package/CHANGELOG.md +808 -0
- package/config.schema.json +91 -40
- package/dist/bin.js +6 -4
- package/dist/bin.js.map +1 -1
- package/dist/cli-entry.d.ts +8 -1
- package/dist/cli-entry.d.ts.map +1 -1
- package/dist/cli-entry.js +79 -3
- package/dist/cli-entry.js.map +1 -1
- package/dist/commands/audit-command-helpers.d.ts.map +1 -1
- package/dist/commands/audit-command-helpers.js +7 -5
- package/dist/commands/audit-command-helpers.js.map +1 -1
- package/dist/commands/batch.d.ts +5 -5
- package/dist/commands/batch.d.ts.map +1 -1
- package/dist/commands/batch.js +2 -21
- package/dist/commands/batch.js.map +1 -1
- package/dist/commands/commons.d.ts.map +1 -1
- package/dist/commands/commons.js +2 -4
- package/dist/commands/commons.js.map +1 -1
- package/dist/commands/exercise.d.ts.map +1 -1
- package/dist/commands/exercise.js +16 -3
- package/dist/commands/exercise.js.map +1 -1
- package/dist/commands/experiment-progress-card-image.d.ts.map +1 -1
- package/dist/commands/experiment-progress-card-image.js +63 -12
- package/dist/commands/experiment-progress-card-image.js.map +1 -1
- package/dist/commands/experiment.d.ts.map +1 -1
- package/dist/commands/experiment.js +45 -5
- package/dist/commands/experiment.js.map +1 -1
- package/dist/commands/export-intake-read-helpers.d.ts.map +1 -1
- package/dist/commands/export-intake-read-helpers.js +2 -3
- package/dist/commands/export-intake-read-helpers.js.map +1 -1
- package/dist/commands/food.d.ts.map +1 -1
- package/dist/commands/food.js +1 -7
- package/dist/commands/food.js.map +1 -1
- package/dist/commands/habitat.d.ts.map +1 -1
- package/dist/commands/habitat.js +120 -9
- package/dist/commands/habitat.js.map +1 -1
- package/dist/commands/health-blood-test-save.d.ts.map +1 -1
- package/dist/commands/health-blood-test-save.js +42 -8
- package/dist/commands/health-blood-test-save.js.map +1 -1
- package/dist/commands/meal.d.ts.map +1 -1
- package/dist/commands/meal.js +4 -1
- package/dist/commands/meal.js.map +1 -1
- package/dist/commands/measurement.d.ts.map +1 -1
- package/dist/commands/measurement.js +3 -0
- package/dist/commands/measurement.js.map +1 -1
- package/dist/commands/murph-age.d.ts +9 -1
- package/dist/commands/murph-age.d.ts.map +1 -1
- package/dist/commands/murph-age.js +132 -7
- package/dist/commands/murph-age.js.map +1 -1
- package/dist/commands/public-validation-issue.d.ts +7 -0
- package/dist/commands/public-validation-issue.d.ts.map +1 -0
- package/dist/commands/public-validation-issue.js +7 -0
- package/dist/commands/public-validation-issue.js.map +1 -0
- package/dist/commands/scheduled-log.d.ts +12 -0
- package/dist/commands/scheduled-log.d.ts.map +1 -1
- package/dist/commands/scheduled-log.js +456 -106
- package/dist/commands/scheduled-log.js.map +1 -1
- package/dist/commands/wearables.d.ts +286 -0
- package/dist/commands/wearables.d.ts.map +1 -1
- package/dist/commands/wearables.js +33 -1
- package/dist/commands/wearables.js.map +1 -1
- package/dist/commands/workout-live.d.ts.map +1 -1
- package/dist/commands/workout-live.js +123 -42
- package/dist/commands/workout-live.js.map +1 -1
- package/dist/commands/workout-typed-options.d.ts +38 -0
- package/dist/commands/workout-typed-options.d.ts.map +1 -0
- package/dist/commands/workout-typed-options.js +90 -0
- package/dist/commands/workout-typed-options.js.map +1 -0
- package/dist/commands/workout.d.ts.map +1 -1
- package/dist/commands/workout.js +80 -73
- package/dist/commands/workout.js.map +1 -1
- package/dist/hosted-data-api-labels.d.ts.map +1 -1
- package/dist/hosted-data-api-labels.js +161 -30
- package/dist/hosted-data-api-labels.js.map +1 -1
- package/dist/incur-error-bridge.d.ts.map +1 -1
- package/dist/incur-error-bridge.js +7 -15
- package/dist/incur-error-bridge.js.map +1 -1
- package/dist/incur.generated.d.ts +22 -13
- package/dist/incur.generated.d.ts.map +1 -1
- package/dist/mapbox-address.d.ts.map +1 -1
- package/dist/mapbox-address.js +3 -4
- package/dist/mapbox-address.js.map +1 -1
- package/dist/mapbox-route-client.d.ts +2 -2
- package/dist/mapbox-route-client.d.ts.map +1 -1
- package/dist/mapbox-route-client.js +49 -12
- package/dist/mapbox-route-client.js.map +1 -1
- package/dist/mapbox-route-directions.d.ts.map +1 -1
- package/dist/mapbox-route-directions.js +0 -1
- package/dist/mapbox-route-directions.js.map +1 -1
- package/dist/mapbox-route-elevation.js +0 -1
- package/dist/mapbox-route-elevation.js.map +1 -1
- package/dist/mapbox-route-points.js +0 -2
- package/dist/mapbox-route-points.js.map +1 -1
- package/dist/mapbox-route.d.ts.map +1 -1
- package/dist/mapbox-route.js +1 -1
- package/dist/mapbox-route.js.map +1 -1
- package/dist/vault-cli-command-manifest.d.ts +304 -6
- package/dist/vault-cli-command-manifest.d.ts.map +1 -1
- package/dist/vault-cli-skill-hash.generated.d.ts +1 -1
- package/dist/vault-cli-skill-hash.generated.js +1 -1
- package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js +6 -2
- package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/README.md +4 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts +15 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js +121 -24
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js +2 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js +55 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js +9 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cli-surface-contract.generated.json +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js +7 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js +123 -22
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js +48 -19
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js +3 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts +5 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js +24 -10
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts +2 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js +12 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts +5 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js +9 -10
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts +41 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js +9 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js +2 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts +3 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js +8 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts +3 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js +26 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js +687 -370
- package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js +54 -18
- package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts +2 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js +10 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js +146 -18
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts +25 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js +72 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js +14 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js +8 -5
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js +8 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js +25 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts +11 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js +67 -176
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts +17 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js +10 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts +19 -4
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js +76 -40
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts +3 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts +4 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js +8 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/store.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/store.js +6 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/store.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts +3 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js +57 -29
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js +17 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts +4 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js +21 -6
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js +6 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js +8 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts +49 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js +387 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js +12 -13
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js +16 -5
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js +2 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js +26 -13
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts +1024 -1091
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js +354 -330
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts +50 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js +79 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js +153 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts +4 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js +29 -35
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts +4 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js +43 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts +6 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts +60 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js +91 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts +41 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js +85 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts +25 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js +280 -89
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/failures.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js +1 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts +19 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js +203 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts +9 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js +220 -130
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/index.d.ts +2 -1
- package/node_modules/@murphai/assistant-engine/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/index.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/index.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/skills/automatic-meal-capture/SKILL.md +111 -68
- package/node_modules/@murphai/assistant-engine/skills/behavior-followthrough/SKILL.md +0 -2
- package/node_modules/@murphai/assistant-engine/skills/daily-activity/SKILL.md +11 -4
- package/node_modules/@murphai/assistant-engine/skills/food-journal/SKILL.md +83 -22
- package/node_modules/@murphai/assistant-engine/skills/hosted-low-usage/SKILL.md +3 -3
- package/node_modules/@murphai/assistant-engine/skills/murph-family/SKILL.md +1 -1
- package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/SKILL.md +17 -11
- package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/references/daily-nutrition-card-goals.md +62 -34
- package/node_modules/@murphai/assistant-engine/skills/physical-notes/SKILL.md +47 -0
- package/node_modules/@murphai/assistant-engine/skills/shared/exercise-catalog-runtime.md +6 -0
- package/node_modules/@murphai/assistant-engine/skills/signup-link/SKILL.md +1 -1
- package/node_modules/@murphai/assistant-engine/skills/strength-training/SKILL.md +12 -5
- package/node_modules/@murphai/assistant-engine/skills/tracked-table/SKILL.md +156 -66
- package/node_modules/@murphai/core/dist/automation.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/automation.js +17 -9
- package/node_modules/@murphai/core/dist/automation.js.map +1 -1
- package/node_modules/@murphai/core/dist/bank/habitat.d.ts +2 -0
- package/node_modules/@murphai/core/dist/bank/habitat.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/bank/habitat.js +27 -0
- package/node_modules/@murphai/core/dist/bank/habitat.js.map +1 -1
- package/node_modules/@murphai/core/dist/bank/regimens.js +1 -1
- package/node_modules/@murphai/core/dist/bank/regimens.js.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts +21 -0
- package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events/ledger.js +102 -8
- package/node_modules/@murphai/core/dist/domains/events/ledger.js.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events.d.ts +2 -2
- package/node_modules/@murphai/core/dist/domains/events.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events.js +1 -1
- package/node_modules/@murphai/core/dist/domains/events.js.map +1 -1
- package/node_modules/@murphai/core/dist/history/api.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/history/api.js +18 -3
- package/node_modules/@murphai/core/dist/history/api.js.map +1 -1
- package/node_modules/@murphai/core/dist/index.d.ts +4 -2
- package/node_modules/@murphai/core/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/index.js +3 -1
- package/node_modules/@murphai/core/dist/index.js.map +1 -1
- package/node_modules/@murphai/core/dist/mutations.d.ts +25 -2
- package/node_modules/@murphai/core/dist/mutations.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/mutations.js +1588 -78
- package/node_modules/@murphai/core/dist/mutations.js.map +1 -1
- package/node_modules/@murphai/core/dist/operations/index.d.ts +1 -1
- package/node_modules/@murphai/core/dist/operations/index.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/operations/index.js +1 -1
- package/node_modules/@murphai/core/dist/operations/index.js.map +1 -1
- package/node_modules/@murphai/core/dist/operations/write-batch.d.ts +2 -0
- package/node_modules/@murphai/core/dist/operations/write-batch.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/operations/write-batch.js +7 -0
- package/node_modules/@murphai/core/dist/operations/write-batch.js.map +1 -1
- package/node_modules/@murphai/core/dist/protocols.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/protocols.js +52 -9
- package/node_modules/@murphai/core/dist/protocols.js.map +1 -1
- package/node_modules/@murphai/core/dist/public-mutations.d.ts +1 -1
- package/node_modules/@murphai/core/dist/public-mutations.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/public-mutations.js +2 -2
- package/node_modules/@murphai/core/dist/public-mutations.js.map +1 -1
- package/node_modules/@murphai/core/dist/scheduled-logs.d.ts +3 -0
- package/node_modules/@murphai/core/dist/scheduled-logs.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/scheduled-logs.js +65 -29
- package/node_modules/@murphai/core/dist/scheduled-logs.js.map +1 -1
- package/node_modules/@murphai/device-syncd/README.md +140 -46
- package/node_modules/@murphai/device-syncd/dist/client.d.ts +14 -0
- package/node_modules/@murphai/device-syncd/dist/client.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/client.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts +4 -1
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js +24 -2
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js +2 -2
- package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts +53 -0
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js +13 -0
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.js +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts +57 -0
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js +166 -0
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts +43 -0
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js +129 -5
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts +3 -0
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js +36 -16
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts +17 -2
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js +99 -40
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts +17 -0
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js +45 -0
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts +8 -1
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js +12 -1
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts +7 -0
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js +285 -46
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts +9 -0
- package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction.js +2045 -367
- package/node_modules/@murphai/device-syncd/dist/providers/junction.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-account.d.ts +2 -0
- package/node_modules/@murphai/device-syncd/dist/public-account.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-account.js +96 -0
- package/node_modules/@murphai/device-syncd/dist/public-account.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-ingress.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-ingress.js +6 -4
- package/node_modules/@murphai/device-syncd/dist/public-ingress.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/service.d.ts +6 -2
- package/node_modules/@murphai/device-syncd/dist/service.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/service.js +385 -44
- package/node_modules/@murphai/device-syncd/dist/service.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts +15 -0
- package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/source-staleness.js +25 -1
- package/node_modules/@murphai/device-syncd/dist/source-staleness.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts +10 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.js +95 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/schema.js +25 -3
- package/node_modules/@murphai/device-syncd/dist/store/schema.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts +24 -2
- package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/sources.js +223 -14
- package/node_modules/@murphai/device-syncd/dist/store/sources.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store.d.ts +13 -3
- package/node_modules/@murphai/device-syncd/dist/store.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store.js +104 -22
- package/node_modules/@murphai/device-syncd/dist/store.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/types.d.ts +96 -9
- package/node_modules/@murphai/device-syncd/dist/types.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/types.js +4 -0
- package/node_modules/@murphai/device-syncd/dist/types.js.map +1 -1
- package/node_modules/@murphai/device-syncd/package.json +8 -0
- package/node_modules/@murphai/health-metrics/dist/definitions/activity.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/definitions/activity.js +66 -0
- package/node_modules/@murphai/health-metrics/dist/definitions/activity.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/definitions/body.js +2 -2
- package/node_modules/@murphai/health-metrics/dist/definitions/body.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/normalize.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/normalize.js +3 -0
- package/node_modules/@murphai/health-metrics/dist/normalize.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts +3 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js +5 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts +79 -0
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js +19 -0
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js.map +1 -1
- package/node_modules/@murphai/importers/dist/core-port.d.ts +15 -1
- package/node_modules/@murphai/importers/dist/core-port.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/core-port.js.map +1 -1
- package/node_modules/@murphai/importers/dist/create-importers.d.ts +9 -1
- package/node_modules/@murphai/importers/dist/create-importers.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/create-importers.js +12 -2
- package/node_modules/@murphai/importers/dist/create-importers.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts +17 -3
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js +102 -8
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/index.d.ts +5 -4
- package/node_modules/@murphai/importers/dist/device-providers/index.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/index.js +4 -3
- package/node_modules/@murphai/importers/dist/device-providers/index.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts +11 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js +420 -27
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js +7 -2
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts +4 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js +42 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts +79 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts.map +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js +356 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js.map +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts +76 -3
- package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction.js +1676 -347
- package/node_modules/@murphai/importers/dist/device-providers/junction.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/types.d.ts +9 -1
- package/node_modules/@murphai/importers/dist/device-providers/types.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/index.d.ts +2 -2
- package/node_modules/@murphai/importers/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/index.js +1 -1
- package/node_modules/@murphai/importers/dist/index.js.map +1 -1
- package/node_modules/@murphai/inboxd/README.md +1 -1
- package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts +6 -0
- package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts.map +1 -1
- package/node_modules/@murphai/inboxd/dist/indexing/retention.js +64 -18
- package/node_modules/@murphai/inboxd/dist/indexing/retention.js.map +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.d.ts +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.d.ts.map +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.js +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts +1 -0
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js +5 -0
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts +175 -152
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js +62 -41
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/device-sync-client.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/device-sync-client.js +1 -5
- package/node_modules/@murphai/operator-config/dist/device-sync-client.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts +66 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js +58 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts +24 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts.map +1 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js +219 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js.map +1 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-errors.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-errors.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts +9 -0
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.js +48 -5
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.js.map +1 -1
- package/node_modules/@murphai/operator-config/package.json +5 -0
- package/node_modules/@murphai/query/README.md +20 -5
- package/node_modules/@murphai/query/dist/browser-replica/build.d.ts +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/build.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/build.js +25 -16
- package/node_modules/@murphai/query/dist/browser-replica/build.js.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/json.d.ts +6 -0
- package/node_modules/@murphai/query/dist/browser-replica/json.d.ts.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/json.js +103 -0
- package/node_modules/@murphai/query/dist/browser-replica/json.js.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/source.d.ts +13 -0
- package/node_modules/@murphai/query/dist/browser-replica/source.d.ts.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/source.js +45 -0
- package/node_modules/@murphai/query/dist/browser-replica/source.js.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica-server.d.ts +2 -0
- package/node_modules/@murphai/query/dist/browser-replica-server.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica-server.js +2 -0
- package/node_modules/@murphai/query/dist/browser-replica-server.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/blood-tests.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/blood-tests.js +3 -2
- package/node_modules/@murphai/query/dist/health/blood-tests.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts +11 -1
- package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/canonical-collector.js +39 -16
- package/node_modules/@murphai/query/dist/health/canonical-collector.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/entity-slices.d.ts +9 -3
- package/node_modules/@murphai/query/dist/health/entity-slices.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/entity-slices.js +6 -6
- package/node_modules/@murphai/query/dist/health/entity-slices.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/immunizations.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/immunizations.js +14 -11
- package/node_modules/@murphai/query/dist/health/immunizations.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/loaders.d.ts +21 -7
- package/node_modules/@murphai/query/dist/health/loaders.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/loaders.js +53 -15
- package/node_modules/@murphai/query/dist/health/loaders.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/registries.d.ts +1 -0
- package/node_modules/@murphai/query/dist/health/registries.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/registries.js.map +1 -1
- package/node_modules/@murphai/query/dist/index.d.ts +4 -3
- package/node_modules/@murphai/query/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/index.js +4 -3
- package/node_modules/@murphai/query/dist/index.js.map +1 -1
- package/node_modules/@murphai/query/dist/meal-nutrition.d.ts +1 -1
- package/node_modules/@murphai/query/dist/meal-nutrition.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/meal-nutrition.js +10 -3
- package/node_modules/@murphai/query/dist/meal-nutrition.js.map +1 -1
- package/node_modules/@murphai/query/dist/metrics/projection.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/metrics/projection.js +19 -6
- package/node_modules/@murphai/query/dist/metrics/projection.js.map +1 -1
- package/node_modules/@murphai/query/dist/murph-age.d.ts +5 -1
- package/node_modules/@murphai/query/dist/murph-age.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/murph-age.js +22 -8
- package/node_modules/@murphai/query/dist/murph-age.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/entity-store.js +4 -4
- package/node_modules/@murphai/query/dist/projection/entity-store.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/schema.d.ts +1 -1
- package/node_modules/@murphai/query/dist/projection/schema.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/projection/schema.js +3 -1
- package/node_modules/@murphai/query/dist/projection/schema.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js +4 -0
- package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js +4 -0
- package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js.map +1 -1
- package/node_modules/@murphai/query/dist/query-visibility.d.ts +5 -0
- package/node_modules/@murphai/query/dist/query-visibility.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/query-visibility.js +11 -5
- package/node_modules/@murphai/query/dist/query-visibility.js.map +1 -1
- package/node_modules/@murphai/query/dist/read-model.d.ts +8 -0
- package/node_modules/@murphai/query/dist/read-model.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/read-model.js +11 -1
- package/node_modules/@murphai/query/dist/read-model.js.map +1 -1
- package/node_modules/@murphai/query/dist/scheduled-logs.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/scheduled-logs.js +55 -79
- package/node_modules/@murphai/query/dist/scheduled-logs.js.map +1 -1
- package/node_modules/@murphai/query/dist/vault-source.d.ts +22 -4
- package/node_modules/@murphai/query/dist/vault-source.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/vault-source.js +188 -59
- package/node_modules/@murphai/query/dist/vault-source.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/candidates.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/candidates.js +26 -15
- package/node_modules/@murphai/query/dist/wearables/candidates.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/public-output.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/public-output.js +6 -0
- package/node_modules/@murphai/query/dist/wearables/public-output.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/source-health.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/source-health.js +4 -0
- package/node_modules/@murphai/query/dist/wearables/source-health.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/types.d.ts +41 -4
- package/node_modules/@murphai/query/dist/wearables/types.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/types.js +2 -0
- package/node_modules/@murphai/query/dist/wearables/types.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts +5 -0
- package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts.map +1 -0
- package/node_modules/@murphai/query/dist/wearables/workout-features.js +179 -0
- package/node_modules/@murphai/query/dist/wearables/workout-features.js.map +1 -0
- package/node_modules/@murphai/query/dist/wearables.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables.js +23 -6
- package/node_modules/@murphai/query/dist/wearables.js.map +1 -1
- package/node_modules/@murphai/query/package.json +5 -0
- package/node_modules/@murphai/runtime-state/README.md +1 -1
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts +4 -0
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js +16 -3
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts +1 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js +31 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts +5 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-email.js +10 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-email.js.map +1 -1
- package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.d.ts.map +1 -1
- package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js +6 -15
- package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/README.md +9 -0
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts +6 -0
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js +49 -1
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/capture.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js +22 -12
- package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js +1 -3
- package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js +24 -18
- package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js +1 -3
- package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts +8 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js +56 -5
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts +20 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts.map +1 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js +72 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js.map +1 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js +42 -9
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js +158 -24
- package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/food.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/food.js +98 -5
- package/node_modules/@murphai/vault-usecases/dist/usecases/food.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js +20 -4
- package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js +49 -24
- package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js +13 -11
- package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js +18 -11
- package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js +43 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/shared.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js +1 -3
- package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts +2 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js +24 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts +2 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts +36 -4
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js +48 -12
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts +14 -14
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js +84 -82
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts +16 -4
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js +316 -79
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js +19 -17
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts +20 -7
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js +56 -31
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js.map +1 -1
- package/node_modules/incur/dist/Cli.js +7 -32
- package/node_modules/incur/dist/Errors.d.ts +17 -0
- package/node_modules/incur/dist/Errors.js +23 -0
- package/node_modules/incur/dist/internal/command.d.ts +2 -0
- package/node_modules/incur/dist/internal/command.js +2 -6
- package/node_modules/incur/src/Cli.ts +11 -32
- package/node_modules/incur/src/Errors.ts +36 -0
- package/node_modules/incur/src/internal/command.ts +4 -6
- package/package.json +4 -4
- 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: "
|
|
109
|
-
{ metric: "
|
|
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: "
|
|
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
|
|
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
|
|
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`,
|
|
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
|
-
|
|
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
|
|
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
|
|
1852
|
+
const workoutDayKey = firstStringFromPaths(entry, ["workoutDayKey"]);
|
|
1853
|
+
const timestamp = {
|
|
1854
|
+
...baseTimestamp,
|
|
1613
1855
|
occurredAt,
|
|
1614
|
-
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
|
-
|
|
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
|
|
1673
|
-
if (!
|
|
2027
|
+
const parsedRecord = parseJunctionSparseTimeseriesRecord(entry, resource, descriptor);
|
|
2028
|
+
if (!parsedRecord) {
|
|
1674
2029
|
continue;
|
|
1675
2030
|
}
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
-
}), {
|
|
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
|
|
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
|
-
|
|
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.
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2464
|
-
existing.
|
|
2465
|
-
existing.
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
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
|
-
|
|
2472
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
4223
|
+
const rawOccurredAt = firstStringFromPaths(entry, ["timestamp"]);
|
|
4224
|
+
const timestampSemantics = inferTimestampSemantics(rawOccurredAt);
|
|
3188
4225
|
const occurredAt = resolveSafeTimestamp(rawOccurredAt, resourceContext.sourceProviderSlug);
|
|
3189
|
-
if (weightKilograms === undefined
|
|
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), {
|
|
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
|
-
|
|
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
|
|
4367
|
-
//
|
|
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
|
|
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
|
|
4385
|
-
//
|
|
4386
|
-
//
|
|
4387
|
-
//
|
|
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, ["
|
|
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
|
|
4403
|
-
|
|
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
|
|
4408
|
-
if (
|
|
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
|
-
|
|
4419
|
-
|
|
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:
|
|
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
|
-
|
|
4452
|
-
|
|
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
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
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
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
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
|
|
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
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
4540
|
-
//
|
|
4541
|
-
//
|
|
4542
|
-
|
|
4543
|
-
|
|
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
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
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
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
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
|
-
|
|
4644
|
-
|
|
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 ?
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
5292
|
-
//
|
|
5293
|
-
|
|
5294
|
-
const
|
|
5295
|
-
const
|
|
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 ??
|
|
6680
|
+
return makeProviderExternalRef("junction", resourceContext.externalRefResourceType, buildStableResourceId(resourceContext, entry, timestamp), sourceVersion ?? junctionExternalRefVersion(resourceContext, entry), slugify(facet, "value"));
|
|
5384
6681
|
}
|
|
5385
|
-
function
|
|
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
|
|
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,
|
|
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
|
|
6759
|
+
return identityTimestamp
|
|
5451
6760
|
? `profile-${shortHash([
|
|
5452
6761
|
"profile",
|
|
5453
6762
|
sourceProviderSlug,
|
|
5454
6763
|
sourceType,
|
|
5455
6764
|
sourceInstanceId,
|
|
5456
|
-
|
|
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 =
|
|
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"
|
|
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
|
}
|