@kaiord/fit 7.1.1 → 8.0.0

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { sportSchema, durationTypeSchema, createConsoleLogger, createFitParsingError, fileTypeSchema, workoutSchema, isRepetitionBlock, convertLengthToMeters, subSportSchema, lengthUnitSchema, equipmentSchema, targetTypeSchema, FIT_TO_SWIM_STROKE, targetUnitSchema, intensitySchema } from '@kaiord/core';
1
+ import { fileTypeSchema, sportSchema, durationTypeSchema, createConsoleLogger, createFitParsingError, workoutSchema, isRepetitionBlock, convertLengthToMeters, subSportSchema, lengthUnitSchema, equipmentSchema, targetTypeSchema, FIT_TO_SWIM_STROKE, targetUnitSchema, intensitySchema } from '@kaiord/core';
2
2
  import { Stream, Decoder, Encoder, Profile } from '@garmin/fitsdk';
3
3
  import { z } from 'zod';
4
4
 
@@ -8,8 +8,7 @@ import { z } from 'zod';
8
8
  var FIT_MESSAGE_NUMBERS = {
9
9
  FILE_ID: 0,
10
10
  WORKOUT: 26,
11
- WORKOUT_STEP: 27
12
- };
11
+ WORKOUT_STEP: 27};
13
12
  z.enum([
14
13
  "device",
15
14
  "settings",
@@ -195,8 +194,6 @@ var mapSubSportToFit = (krdSubSport) => {
195
194
  }
196
195
  return KRD_TO_FIT_SUB_SPORT_MAP[result.data] || fitSubSportSchema.enum.generic;
197
196
  };
198
-
199
- // src/adapters/krd-to-fit/krd-to-fit-metadata.mapper.ts
200
197
  var DEFAULT_MANUFACTURER = "garmin";
201
198
  var mapManufacturer = (manufacturer, logger) => {
202
199
  if (!manufacturer) {
@@ -215,25 +212,44 @@ var mapManufacturer = (manufacturer, logger) => {
215
212
  );
216
213
  return DEFAULT_MANUFACTURER;
217
214
  };
215
+ var countValidSteps = (steps) => {
216
+ let count = 0;
217
+ for (const step of steps) {
218
+ if (isRepetitionBlock(step)) {
219
+ count += countValidSteps(step.steps) + 1;
220
+ } else {
221
+ count += 1;
222
+ }
223
+ }
224
+ return count;
225
+ };
226
+
227
+ // src/adapters/krd-to-fit/krd-to-fit-metadata.converter.ts
228
+ var resolveFitFileType = (krdType) => {
229
+ if (krdType === "structured_workout") return "workout";
230
+ if (krdType === "recorded_activity") return "activity";
231
+ if (krdType === "course") return "course";
232
+ return "workout";
233
+ };
234
+ var assignParsedNumber = (target, key, raw) => {
235
+ const parsed = parseInt(raw, 10);
236
+ if (!isNaN(parsed)) {
237
+ target[key] = parsed;
238
+ }
239
+ };
218
240
  var convertMetadataToFileId = (krd, logger) => {
219
241
  logger.debug("Converting metadata to file_id message");
220
- const fileType = krd.type === "structured_workout" ? "workout" : krd.type === "recorded_activity" ? "activity" : krd.type === "course" ? "course" : "workout";
242
+ const fileType = resolveFitFileType(krd.type);
221
243
  const fileId = {
222
- type: FIT_FILE_TYPE_TO_NUMBER[fileType] ?? FIT_FILE_TYPE_TO_NUMBER.workout,
244
+ type: FIT_FILE_TYPE_TO_NUMBER[fileType],
223
245
  timeCreated: new Date(krd.metadata.created),
224
246
  manufacturer: mapManufacturer(krd.metadata.manufacturer, logger)
225
247
  };
226
248
  if (krd.metadata.product !== void 0) {
227
- const productNumber = parseInt(krd.metadata.product, 10);
228
- if (!isNaN(productNumber)) {
229
- fileId.product = productNumber;
230
- }
249
+ assignParsedNumber(fileId, "product", krd.metadata.product);
231
250
  }
232
251
  if (krd.metadata.serialNumber) {
233
- const serialNumber = parseInt(krd.metadata.serialNumber, 10);
234
- if (!isNaN(serialNumber)) {
235
- fileId.serialNumber = serialNumber;
236
- }
252
+ assignParsedNumber(fileId, "serialNumber", krd.metadata.serialNumber);
237
253
  }
238
254
  return fileId;
239
255
  };
@@ -254,17 +270,6 @@ var convertWorkoutMetadata = (workout, logger) => {
254
270
  }
255
271
  return workoutMesg;
256
272
  };
257
- var countValidSteps = (steps) => {
258
- let count = 0;
259
- for (const step of steps) {
260
- if (isRepetitionBlock(step)) {
261
- count += step.steps.length + 1;
262
- } else {
263
- count += 1;
264
- }
265
- }
266
- return count;
267
- };
268
273
  var fitDurationTypeSchema = z.enum([
269
274
  "time",
270
275
  "distance",
@@ -490,7 +495,7 @@ var convertStrokeTarget = (step, message) => {
490
495
  message.targetValue = strokeValue.value;
491
496
  };
492
497
 
493
- // src/adapters/krd-to-fit/krd-to-fit-target.mapper.ts
498
+ // src/adapters/krd-to-fit/krd-to-fit-target.converter.ts
494
499
  var convertTarget = (step, message) => {
495
500
  if (step.target.type === targetTypeSchema.enum.open) {
496
501
  message.targetType = fitTargetTypeSchema.enum.open;
@@ -671,9 +676,468 @@ var fitMessageKeySchema = z.enum([
671
676
  "sessionMesgs",
672
677
  "recordMesgs",
673
678
  "eventMesgs",
674
- "lapMesgs"
679
+ "lapMesgs",
680
+ // Health domain (KRD v2.0)
681
+ "sleepLevelMesgs",
682
+ "monitoringMesgs",
683
+ "monitoringInfoMesgs",
684
+ "weightScaleMesgs",
685
+ "bodyCompositionMesgs",
686
+ "hrvStatusSummaryMesgs",
687
+ "hrvValueMesgs",
688
+ "stressLevelMesgs"
675
689
  ]);
676
690
 
691
+ // src/adapters/health/shared/health-metadata.builder.ts
692
+ var isString = (value) => typeof value === "string";
693
+ var isNumber = (value) => typeof value === "number";
694
+ var MS_PER_S = 1e3;
695
+ var convertFitTimeCreatedToIso = (timeCreated) => {
696
+ if (timeCreated instanceof Date) return timeCreated.toISOString();
697
+ if (isNumber(timeCreated))
698
+ return new Date(timeCreated * MS_PER_S).toISOString();
699
+ return (/* @__PURE__ */ new Date()).toISOString();
700
+ };
701
+ var buildHealthMetadata = (fileId) => ({
702
+ created: convertFitTimeCreatedToIso(fileId?.timeCreated),
703
+ manufacturer: isString(fileId?.manufacturer) ? fileId.manufacturer : void 0,
704
+ product: isNumber(fileId?.product) ? String(fileId.product) : void 0
705
+ });
706
+ var fitBodyCompositionSchema = z.object({
707
+ timestamp: z.union([z.date(), z.string(), z.number()]),
708
+ percentFat: z.number().optional(),
709
+ percentHydration: z.number().optional(),
710
+ visceralFatRating: z.number().optional(),
711
+ boneMass: z.number().optional(),
712
+ muscleMass: z.number().optional(),
713
+ basalMet: z.number().optional(),
714
+ bmi: z.number().optional()
715
+ });
716
+
717
+ // src/adapters/health/body-composition/health-body-composition.converter.ts
718
+ var HEALTH_VERSION = "2.0";
719
+ var toIsoString = (value) => {
720
+ if (value instanceof Date) return value.toISOString();
721
+ if (typeof value === "number") return new Date(value * 1e3).toISOString();
722
+ return new Date(value).toISOString();
723
+ };
724
+ var FIT_KG_SCALE = 100;
725
+ var fromScaledKg = (value) => value === void 0 ? void 0 : value / FIT_KG_SCALE;
726
+ var collectMeasurements = (fit) => {
727
+ const out = {};
728
+ if (fit.percentFat !== void 0) out.bodyFatPercent = fit.percentFat;
729
+ if (fit.percentHydration !== void 0)
730
+ out.bodyWaterPercent = fit.percentHydration;
731
+ const leanMassKilograms = fromScaledKg(fit.muscleMass);
732
+ if (leanMassKilograms !== void 0)
733
+ out.leanMassKilograms = leanMassKilograms;
734
+ const boneMassKilograms = fromScaledKg(fit.boneMass);
735
+ if (boneMassKilograms !== void 0)
736
+ out.boneMassKilograms = boneMassKilograms;
737
+ if (fit.bmi !== void 0) out.bmi = fit.bmi;
738
+ return out;
739
+ };
740
+ var mapFitBodyCompositionToKrd = (fit) => {
741
+ const measurements = collectMeasurements(fit);
742
+ if (Object.keys(measurements).length === 0) return void 0;
743
+ return {
744
+ kind: "bodyComposition",
745
+ version: HEALTH_VERSION,
746
+ measuredAt: toIsoString(fit.timestamp),
747
+ ...measurements
748
+ };
749
+ };
750
+
751
+ // src/adapters/health/body-composition/fit-to-krd-health-body-composition.converter.ts
752
+ var KRD_VERSION = "2.0";
753
+ var parseFirstBodyComposition = (raw, logger) => {
754
+ for (const entry of raw) {
755
+ const result = fitBodyCompositionSchema.safeParse(entry);
756
+ if (result.success) return result.data;
757
+ logger.warn("Skipping malformed body_composition message", {
758
+ issues: result.error.issues
759
+ });
760
+ }
761
+ return void 0;
762
+ };
763
+ var convertFitToKrdHealthBodyComposition = (messages, logger) => {
764
+ const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
765
+ const rawBody = messages[fitMessageKeySchema.enum.bodyCompositionMesgs] ?? [];
766
+ const fitBody = parseFirstBodyComposition(rawBody, logger);
767
+ const body = fitBody ? mapFitBodyCompositionToKrd(fitBody) : void 0;
768
+ return {
769
+ version: KRD_VERSION,
770
+ type: fileTypeSchema.enum.body_composition,
771
+ metadata: buildHealthMetadata(fileId),
772
+ extensions: body ? { health: { bodyComposition: body } } : void 0
773
+ };
774
+ };
775
+ var fitMonitoringInfoSchema = z.object({
776
+ timestamp: z.union([z.date(), z.string(), z.number()]),
777
+ restingMetabolicRate: z.number().optional()
778
+ });
779
+ var fitMonitoringSchema = z.object({
780
+ timestamp: z.union([z.date(), z.string(), z.number()]).optional(),
781
+ steps: z.number().int().nonnegative().optional(),
782
+ activeCalories: z.number().int().nonnegative().optional(),
783
+ durationMin: z.number().int().nonnegative().optional(),
784
+ cycles: z.number().nonnegative().optional(),
785
+ distance: z.number().nonnegative().optional(),
786
+ activeTime: z.number().int().nonnegative().optional(),
787
+ activityType: z.string().optional()
788
+ });
789
+
790
+ // src/adapters/health/daily/health-daily.converter.ts
791
+ var HEALTH_VERSION2 = "2.0";
792
+ var toIsoDate = (value) => {
793
+ if (!value) return void 0;
794
+ const date = value instanceof Date ? value : typeof value === "number" ? new Date(value * 1e3) : new Date(value);
795
+ return date.toISOString().slice(0, 10);
796
+ };
797
+ var sumSteps = (samples) => samples.reduce((sum, s) => sum + (s.steps ?? 0), 0);
798
+ var sumActiveCalories = (samples) => samples.reduce((sum, s) => sum + (s.activeCalories ?? 0), 0);
799
+ var pickDate = (info, samples) => {
800
+ if (info) return toIsoDate(info.timestamp);
801
+ const firstWithTs = samples.find((s) => s.timestamp);
802
+ return firstWithTs ? toIsoDate(firstWithTs.timestamp) : void 0;
803
+ };
804
+ var mapFitMonitoringToKrdDaily = (info, monitoring) => {
805
+ const date = pickDate(info, monitoring);
806
+ if (!date) return void 0;
807
+ return {
808
+ kind: "daily",
809
+ version: HEALTH_VERSION2,
810
+ date,
811
+ steps: sumSteps(monitoring),
812
+ activeCalories: sumActiveCalories(monitoring),
813
+ restingCalories: info?.restingMetabolicRate ?? 0,
814
+ intensityMinutes: { moderate: 0, vigorous: 0 }
815
+ };
816
+ };
817
+
818
+ // src/adapters/health/daily/fit-to-krd-health-daily.converter.ts
819
+ var KRD_VERSION2 = "2.0";
820
+ var parseMonitoringInfo = (raw, logger) => {
821
+ for (const entry of raw) {
822
+ const result = fitMonitoringInfoSchema.safeParse(entry);
823
+ if (result.success) return result.data;
824
+ logger.warn("Skipping malformed monitoring_info message", {
825
+ issues: result.error.issues
826
+ });
827
+ }
828
+ return void 0;
829
+ };
830
+ var parseMonitoring = (raw, logger) => {
831
+ const parsed = [];
832
+ for (const entry of raw) {
833
+ const result = fitMonitoringSchema.safeParse(entry);
834
+ if (result.success) parsed.push(result.data);
835
+ else
836
+ logger.warn("Skipping malformed monitoring message", {
837
+ issues: result.error.issues
838
+ });
839
+ }
840
+ return parsed;
841
+ };
842
+ var convertFitToKrdHealthDaily = (messages, logger) => {
843
+ const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
844
+ const info = parseMonitoringInfo(
845
+ messages[fitMessageKeySchema.enum.monitoringInfoMesgs] ?? [],
846
+ logger
847
+ );
848
+ const monitoring = parseMonitoring(
849
+ messages[fitMessageKeySchema.enum.monitoringMesgs] ?? [],
850
+ logger
851
+ );
852
+ const daily = mapFitMonitoringToKrdDaily(info, monitoring);
853
+ return {
854
+ version: KRD_VERSION2,
855
+ type: fileTypeSchema.enum.daily_wellness,
856
+ metadata: buildHealthMetadata(fileId),
857
+ extensions: daily ? { health: { daily } } : void 0
858
+ };
859
+ };
860
+ var fitHrvStatusSummarySchema = z.object({
861
+ timestamp: z.union([z.date(), z.string(), z.number()]),
862
+ weeklyAverage: z.number().optional(),
863
+ lastNightAverage: z.number().optional(),
864
+ lastNight5MinHigh: z.number().optional(),
865
+ baselineLowUpper: z.number().optional(),
866
+ baselineBalancedLower: z.number().optional(),
867
+ baselineBalancedUpper: z.number().optional(),
868
+ status: z.enum(["none", "poor", "low", "unbalanced", "balanced"]).optional()
869
+ });
870
+ var fitHrvValueSchema = z.object({
871
+ timestamp: z.union([z.date(), z.string(), z.number()]),
872
+ value: z.number()
873
+ });
874
+
875
+ // src/adapters/health/hrv/health-hrv.converter.ts
876
+ var HEALTH_VERSION3 = "2.0";
877
+ var toIsoString2 = (value) => {
878
+ if (value instanceof Date) return value.toISOString();
879
+ if (typeof value === "number") return new Date(value * 1e3).toISOString();
880
+ return new Date(value).toISOString();
881
+ };
882
+ var mapFitHrvToKrd = (summary, firstValue) => {
883
+ if (summary && summary.lastNightAverage !== void 0) {
884
+ return {
885
+ kind: "hrv",
886
+ version: HEALTH_VERSION3,
887
+ measuredAt: toIsoString2(summary.timestamp),
888
+ rMSSD: summary.lastNightAverage,
889
+ measurementWindow: "overnight"
890
+ };
891
+ }
892
+ if (firstValue) {
893
+ return {
894
+ kind: "hrv",
895
+ version: HEALTH_VERSION3,
896
+ measuredAt: toIsoString2(firstValue.timestamp),
897
+ rMSSD: firstValue.value,
898
+ measurementWindow: "spot"
899
+ };
900
+ }
901
+ return void 0;
902
+ };
903
+
904
+ // src/adapters/health/hrv/fit-to-krd-health-hrv.converter.ts
905
+ var KRD_VERSION3 = "2.0";
906
+ var parseFirstSummary = (raw, logger) => {
907
+ for (const entry of raw) {
908
+ const result = fitHrvStatusSummarySchema.safeParse(entry);
909
+ if (result.success) return result.data;
910
+ logger.warn("Skipping malformed hrv_status_summary message", {
911
+ issues: result.error.issues
912
+ });
913
+ }
914
+ return void 0;
915
+ };
916
+ var parseFirstValue = (raw, logger) => {
917
+ for (const entry of raw) {
918
+ const result = fitHrvValueSchema.safeParse(entry);
919
+ if (result.success) return result.data;
920
+ logger.warn("Skipping malformed hrv_value message", {
921
+ issues: result.error.issues
922
+ });
923
+ }
924
+ return void 0;
925
+ };
926
+ var convertFitToKrdHealthHrv = (messages, logger) => {
927
+ const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
928
+ const summary = parseFirstSummary(
929
+ messages[fitMessageKeySchema.enum.hrvStatusSummaryMesgs] ?? [],
930
+ logger
931
+ );
932
+ const firstValue = parseFirstValue(
933
+ messages[fitMessageKeySchema.enum.hrvValueMesgs] ?? [],
934
+ logger
935
+ );
936
+ const hrv = mapFitHrvToKrd(summary, firstValue);
937
+ return {
938
+ version: KRD_VERSION3,
939
+ type: fileTypeSchema.enum.hrv_summary,
940
+ metadata: buildHealthMetadata(fileId),
941
+ extensions: hrv ? { health: { hrv } } : void 0
942
+ };
943
+ };
944
+ var fitSleepLevelSchema = z.object({
945
+ timestamp: z.union([z.date(), z.string(), z.number()]),
946
+ sleepLevel: z.enum(["awake", "light", "deep", "rem", "unmeasurable"])
947
+ });
948
+
949
+ // src/adapters/health/sleep/health-sleep.converter.ts
950
+ var HEALTH_VERSION4 = "2.0";
951
+ var toIsoString3 = (value) => {
952
+ if (value instanceof Date) return value.toISOString();
953
+ if (typeof value === "number") return new Date(value * 1e3).toISOString();
954
+ return new Date(value).toISOString();
955
+ };
956
+ var isKnownSleepStage = (level) => level === "awake" || level === "light" || level === "deep" || level === "rem";
957
+ var mapFitSleepLevelsToKrdSleep = (fitLevels) => {
958
+ if (fitLevels.length < 2) return void 0;
959
+ const sorted = [...fitLevels].sort(
960
+ (a, b) => new Date(toIsoString3(a.timestamp)).getTime() - new Date(toIsoString3(b.timestamp)).getTime()
961
+ );
962
+ const startTime = toIsoString3(sorted[0].timestamp);
963
+ const endTime = toIsoString3(sorted[sorted.length - 1].timestamp);
964
+ const stages = [];
965
+ for (let i = 0; i < sorted.length - 1; i += 1) {
966
+ const current = sorted[i];
967
+ if (!isKnownSleepStage(current.sleepLevel)) continue;
968
+ const startMs = new Date(toIsoString3(current.timestamp)).getTime();
969
+ const nextMs = new Date(toIsoString3(sorted[i + 1].timestamp)).getTime();
970
+ const durationSeconds = Math.max(0, Math.round((nextMs - startMs) / 1e3));
971
+ stages.push({
972
+ stage: current.sleepLevel,
973
+ startTime: toIsoString3(current.timestamp),
974
+ durationSeconds
975
+ });
976
+ }
977
+ const totalDurationSeconds = stages.reduce(
978
+ (sum, stage) => sum + stage.durationSeconds,
979
+ 0
980
+ );
981
+ return {
982
+ kind: "sleep",
983
+ version: HEALTH_VERSION4,
984
+ startTime,
985
+ endTime,
986
+ totalDurationSeconds,
987
+ stages
988
+ };
989
+ };
990
+
991
+ // src/adapters/health/sleep/fit-to-krd-health-sleep.converter.ts
992
+ var KRD_VERSION4 = "2.0";
993
+ var parseSleepLevels = (raw, logger) => {
994
+ const parsed = [];
995
+ for (const entry of raw) {
996
+ const result = fitSleepLevelSchema.safeParse(entry);
997
+ if (result.success) {
998
+ parsed.push(result.data);
999
+ } else {
1000
+ logger.warn("Skipping malformed sleep_level message", {
1001
+ issues: result.error.issues
1002
+ });
1003
+ }
1004
+ }
1005
+ return parsed;
1006
+ };
1007
+ var convertFitToKrdHealthSleep = (messages, logger) => {
1008
+ const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
1009
+ const rawLevels = messages[fitMessageKeySchema.enum.sleepLevelMesgs] ?? [];
1010
+ const fitLevels = parseSleepLevels(rawLevels, logger);
1011
+ const sleep = mapFitSleepLevelsToKrdSleep(fitLevels);
1012
+ return {
1013
+ version: KRD_VERSION4,
1014
+ type: fileTypeSchema.enum.sleep_record,
1015
+ metadata: buildHealthMetadata(fileId),
1016
+ extensions: sleep ? { health: { sleep } } : void 0
1017
+ };
1018
+ };
1019
+ var fitStressLevelSchema = z.object({
1020
+ stressLevelTime: z.union([z.date(), z.string(), z.number()]),
1021
+ stressLevelValue: z.number().int()
1022
+ });
1023
+
1024
+ // src/adapters/health/stress/health-stress.converter.ts
1025
+ var HEALTH_VERSION5 = "2.0";
1026
+ var MIN_VALID = 0;
1027
+ var MAX_VALID = 100;
1028
+ var MS_PER_S2 = 1e3;
1029
+ var toEpochMs = (value) => {
1030
+ if (value instanceof Date) return value.getTime();
1031
+ if (typeof value === "number") return value * MS_PER_S2;
1032
+ return new Date(value).getTime();
1033
+ };
1034
+ var isValid = (sample) => sample.stressLevelValue >= MIN_VALID && sample.stressLevelValue <= MAX_VALID;
1035
+ var mean = (values) => Math.round(values.reduce((s, v) => s + v, 0) / values.length);
1036
+ var mapFitStressToKrd = (samples) => {
1037
+ const valid = samples.filter(isValid);
1038
+ if (valid.length === 0) return void 0;
1039
+ const epochs = valid.map((s) => toEpochMs(s.stressLevelTime));
1040
+ const values = valid.map((s) => s.stressLevelValue);
1041
+ return {
1042
+ kind: "stress",
1043
+ version: HEALTH_VERSION5,
1044
+ startTime: new Date(Math.min(...epochs)).toISOString(),
1045
+ endTime: new Date(Math.max(...epochs)).toISOString(),
1046
+ averageLevel: mean(values),
1047
+ peakLevel: Math.max(...values)
1048
+ };
1049
+ };
1050
+
1051
+ // src/adapters/health/stress/fit-to-krd-health-stress.converter.ts
1052
+ var KRD_VERSION5 = "2.0";
1053
+ var parseSamples = (raw, logger) => {
1054
+ const out = [];
1055
+ for (const entry of raw) {
1056
+ const result = fitStressLevelSchema.safeParse(entry);
1057
+ if (result.success) {
1058
+ out.push(result.data);
1059
+ continue;
1060
+ }
1061
+ logger.warn("Skipping malformed stress_level message", {
1062
+ issues: result.error.issues
1063
+ });
1064
+ }
1065
+ return out;
1066
+ };
1067
+ var convertFitToKrdHealthStress = (messages, logger) => {
1068
+ const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
1069
+ const samples = parseSamples(
1070
+ messages[fitMessageKeySchema.enum.stressLevelMesgs] ?? [],
1071
+ logger
1072
+ );
1073
+ const stress = mapFitStressToKrd(samples);
1074
+ return {
1075
+ version: KRD_VERSION5,
1076
+ type: fileTypeSchema.enum.stress_episode,
1077
+ metadata: buildHealthMetadata(fileId),
1078
+ extensions: stress ? { health: { stress } } : void 0
1079
+ };
1080
+ };
1081
+ var fitWeightScaleSchema = z.object({
1082
+ timestamp: z.union([z.date(), z.string(), z.number()]),
1083
+ weight: z.number(),
1084
+ percentFat: z.number().optional(),
1085
+ percentHydration: z.number().optional(),
1086
+ visceralFatMass: z.number().optional(),
1087
+ boneMass: z.number().optional(),
1088
+ muscleMass: z.number().optional(),
1089
+ bmi: z.number().optional(),
1090
+ userProfileIndex: z.number().int().optional()
1091
+ });
1092
+
1093
+ // src/adapters/health/weight/health-weight.converter.ts
1094
+ var HEALTH_VERSION6 = "2.0";
1095
+ var FIT_WEIGHT_SCALE = 100;
1096
+ var toIsoString4 = (value) => {
1097
+ if (value instanceof Date) return value.toISOString();
1098
+ if (typeof value === "number") return new Date(value * 1e3).toISOString();
1099
+ return new Date(value).toISOString();
1100
+ };
1101
+ var mapFitWeightScaleToKrd = (fit) => {
1102
+ if (fit.weight <= 0) return void 0;
1103
+ return {
1104
+ kind: "weight",
1105
+ version: HEALTH_VERSION6,
1106
+ measuredAt: toIsoString4(fit.timestamp),
1107
+ weightKilograms: fit.weight / FIT_WEIGHT_SCALE
1108
+ };
1109
+ };
1110
+
1111
+ // src/adapters/health/weight/fit-to-krd-health-weight.converter.ts
1112
+ var KRD_VERSION6 = "2.0";
1113
+ var parseFirstWeight = (raw, logger) => {
1114
+ for (const entry of raw) {
1115
+ const result = fitWeightScaleSchema.safeParse(entry);
1116
+ if (result.success) return result.data;
1117
+ logger.warn("Skipping malformed weight_scale message", {
1118
+ issues: result.error.issues
1119
+ });
1120
+ }
1121
+ return void 0;
1122
+ };
1123
+ var convertFitToKrdHealthWeight = (messages, logger) => {
1124
+ const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
1125
+ const rawWeights = messages[fitMessageKeySchema.enum.weightScaleMesgs] ?? [];
1126
+ if (rawWeights.length > 1) {
1127
+ logger.warn("Multiple weight_scale messages \u2014 keeping the first only", {
1128
+ count: rawWeights.length
1129
+ });
1130
+ }
1131
+ const fitWeight = parseFirstWeight(rawWeights, logger);
1132
+ const weight = fitWeight ? mapFitWeightScaleToKrd(fitWeight) : void 0;
1133
+ return {
1134
+ version: KRD_VERSION6,
1135
+ type: fileTypeSchema.enum.weight_measurement,
1136
+ metadata: buildHealthMetadata(fileId),
1137
+ extensions: weight ? { health: { weight } } : void 0
1138
+ };
1139
+ };
1140
+
677
1141
  // src/adapters/event/event-type-maps.ts
678
1142
  var FIT_EVENT_TO_KRD_TYPE = {
679
1143
  timer: "event_timer",
@@ -1152,7 +1616,7 @@ var convertFitToKrdSession = (data) => {
1152
1616
  };
1153
1617
 
1154
1618
  // src/adapters/messages/activity.mapper.ts
1155
- var KRD_VERSION = "1.0";
1619
+ var KRD_VERSION7 = "1.0";
1156
1620
  var convertTimeCreated = (timeCreated) => {
1157
1621
  if (timeCreated instanceof Date) return timeCreated.toISOString();
1158
1622
  if (typeof timeCreated === "number")
@@ -1184,7 +1648,7 @@ var mapActivityFileToKRD = (messages, logger) => {
1184
1648
  const laps = convertFitToKrdLaps(lapMsgs);
1185
1649
  const fitExtensions = extractFitExtensions(messages, logger);
1186
1650
  return {
1187
- version: KRD_VERSION,
1651
+ version: KRD_VERSION7,
1188
1652
  type: fileTypeSchema.enum.recorded_activity,
1189
1653
  metadata: buildKrdMetadata(
1190
1654
  fileId,
@@ -1197,6 +1661,30 @@ var mapActivityFileToKRD = (messages, logger) => {
1197
1661
  extensions: { fit: fitExtensions }
1198
1662
  };
1199
1663
  };
1664
+ var HEALTH_DETECTORS = [
1665
+ ["sleepLevelMesgs", fileTypeSchema.enum.sleep_record],
1666
+ ["bodyCompositionMesgs", fileTypeSchema.enum.body_composition],
1667
+ ["weightScaleMesgs", fileTypeSchema.enum.weight_measurement],
1668
+ ["hrvStatusSummaryMesgs", fileTypeSchema.enum.hrv_summary],
1669
+ ["hrvValueMesgs", fileTypeSchema.enum.hrv_summary],
1670
+ ["monitoringMesgs", fileTypeSchema.enum.daily_wellness],
1671
+ ["stressLevelMesgs", fileTypeSchema.enum.stress_episode]
1672
+ ];
1673
+ var detectFileType = (messages) => {
1674
+ for (const [key, type] of HEALTH_DETECTORS) {
1675
+ const mesgs = messages[key];
1676
+ if (mesgs && mesgs.length > 0) return type;
1677
+ }
1678
+ const workoutMsgs = messages[fitMessageKeySchema.enum.workoutMesgs];
1679
+ if (workoutMsgs && workoutMsgs.length > 0)
1680
+ return fileTypeSchema.enum.structured_workout;
1681
+ const sessionMsgs = messages[fitMessageKeySchema.enum.sessionMesgs];
1682
+ const recordMsgs = messages[fitMessageKeySchema.enum.recordMesgs];
1683
+ if (sessionMsgs && sessionMsgs.length > 0 || recordMsgs && recordMsgs.length > 0) {
1684
+ return fileTypeSchema.enum.recorded_activity;
1685
+ }
1686
+ return fileTypeSchema.enum.structured_workout;
1687
+ };
1200
1688
  var DEFAULT_SPORT = sportSchema.enum.cycling;
1201
1689
  var mapSportType = (fitSport) => {
1202
1690
  if (!fitSport) {
@@ -1873,7 +2361,7 @@ var validateMessages = (...[fileId, workoutMsg, messages, logger, options = {}])
1873
2361
  };
1874
2362
 
1875
2363
  // src/adapters/messages/workout.mapper.ts
1876
- var KRD_VERSION2 = "1.0";
2364
+ var KRD_VERSION8 = "1.0";
1877
2365
  var mapWorkoutFileToKRD = (messages, logger) => {
1878
2366
  const fileId = messages[fitMessageKeySchema.enum.fileIdMesgs]?.[0];
1879
2367
  const workoutMsg = messages[fitMessageKeySchema.enum.workoutMesgs]?.[0];
@@ -1883,7 +2371,7 @@ var mapWorkoutFileToKRD = (messages, logger) => {
1883
2371
  const workout = mapWorkout(workoutMsg, workoutSteps, logger);
1884
2372
  const fitExtensions = extractFitExtensions(messages, logger);
1885
2373
  return {
1886
- version: KRD_VERSION2,
2374
+ version: KRD_VERSION8,
1887
2375
  type: fileTypeSchema.enum.structured_workout,
1888
2376
  metadata,
1889
2377
  extensions: { structured_workout: workout, fit: fitExtensions }
@@ -1891,17 +2379,6 @@ var mapWorkoutFileToKRD = (messages, logger) => {
1891
2379
  };
1892
2380
 
1893
2381
  // src/adapters/messages/messages.mapper.ts
1894
- var detectFileType = (messages) => {
1895
- const workoutMsgs = messages[fitMessageKeySchema.enum.workoutMesgs];
1896
- if (workoutMsgs && workoutMsgs.length > 0)
1897
- return fileTypeSchema.enum.structured_workout;
1898
- const sessionMsgs = messages[fitMessageKeySchema.enum.sessionMesgs];
1899
- const recordMsgs = messages[fitMessageKeySchema.enum.recordMesgs];
1900
- if (sessionMsgs && sessionMsgs.length > 0 || recordMsgs && recordMsgs.length > 0) {
1901
- return fileTypeSchema.enum.recorded_activity;
1902
- }
1903
- return fileTypeSchema.enum.structured_workout;
1904
- };
1905
2382
  var mapMessagesToKRD = (messages, logger) => {
1906
2383
  logger.debug("Mapping FIT messages to KRD", {
1907
2384
  messageCount: Object.keys(messages).length
@@ -1909,6 +2386,18 @@ var mapMessagesToKRD = (messages, logger) => {
1909
2386
  const fileType = detectFileType(messages);
1910
2387
  logger.debug("Detected file type", { fileType });
1911
2388
  switch (fileType) {
2389
+ case fileTypeSchema.enum.sleep_record:
2390
+ return convertFitToKrdHealthSleep(messages, logger);
2391
+ case fileTypeSchema.enum.weight_measurement:
2392
+ return convertFitToKrdHealthWeight(messages, logger);
2393
+ case fileTypeSchema.enum.body_composition:
2394
+ return convertFitToKrdHealthBodyComposition(messages, logger);
2395
+ case fileTypeSchema.enum.hrv_summary:
2396
+ return convertFitToKrdHealthHrv(messages, logger);
2397
+ case fileTypeSchema.enum.daily_wellness:
2398
+ return convertFitToKrdHealthDaily(messages, logger);
2399
+ case fileTypeSchema.enum.stress_episode:
2400
+ return convertFitToKrdHealthStress(messages, logger);
1912
2401
  case fileTypeSchema.enum.recorded_activity:
1913
2402
  return mapActivityFileToKRD(messages, logger);
1914
2403
  case fileTypeSchema.enum.structured_workout: