@sunsteel/contracts 0.69.0 → 0.71.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.cjs CHANGED
@@ -37,6 +37,8 @@ __export(index_exports, {
37
37
  ACTIVITY_REACTIONS: () => ACTIVITY_REACTIONS,
38
38
  ACTIVITY_TYPES: () => ACTIVITY_TYPES,
39
39
  ACTIVITY_TYPE_SECTIONS: () => ACTIVITY_TYPE_SECTIONS,
40
+ BAR_CHOICES_KG: () => BAR_CHOICES_KG,
41
+ BAR_WARM_UP_STEPS: () => BAR_WARM_UP_STEPS,
40
42
  BLOCKED_MEMBERS_MAX: () => BLOCKED_MEMBERS_MAX,
41
43
  CLONE_ROUTINE_REFUSALS: () => CLONE_ROUTINE_REFUSALS,
42
44
  COMEBACK_MIN_INACTIVE_DAYS: () => COMEBACK_MIN_INACTIVE_DAYS,
@@ -64,6 +66,8 @@ __export(index_exports, {
64
66
  FOLLOW_SUGGESTIONS_DEFAULT_LIMIT: () => FOLLOW_SUGGESTIONS_DEFAULT_LIMIT,
65
67
  FOLLOW_SUGGESTIONS_MAX_LIMIT: () => FOLLOW_SUGGESTIONS_MAX_LIMIT,
66
68
  FOLLOW_SUGGESTION_REASONS: () => FOLLOW_SUGGESTION_REASONS,
69
+ LIMITED_SHORTFALL_KG: () => LIMITED_SHORTFALL_KG,
70
+ MAX_SETS_PER_EXERCISE: () => MAX_SETS_PER_EXERCISE,
67
71
  MEASURABLE_GOALS_MAX: () => MEASURABLE_GOALS_MAX,
68
72
  MEASURABLE_GOAL_DIRECTIONS: () => MEASURABLE_GOAL_DIRECTIONS,
69
73
  MEASURABLE_GOAL_TYPES: () => MEASURABLE_GOAL_TYPES,
@@ -79,12 +83,14 @@ __export(index_exports, {
79
83
  NOTIFICATIONS_RETENTION_DAYS: () => NOTIFICATIONS_RETENTION_DAYS,
80
84
  NOTIFICATION_CATEGORIES: () => NOTIFICATION_CATEGORIES,
81
85
  NOTIFICATION_KINDS: () => NOTIFICATION_KINDS,
86
+ OTHER_WARM_UP_STEPS: () => OTHER_WARM_UP_STEPS,
82
87
  PLATEAU_MIN_DAYS_SINCE_BEST: () => PLATEAU_MIN_DAYS_SINCE_BEST,
83
88
  PLATEAU_MIN_SESSIONS: () => PLATEAU_MIN_SESSIONS,
84
89
  PLATEAU_MIN_SESSIONS_MAX: () => PLATEAU_MIN_SESSIONS_MAX,
85
90
  PLATEAU_MIN_SESSIONS_MIN: () => PLATEAU_MIN_SESSIONS_MIN,
86
91
  PLATEAU_RECENT_DAYS: () => PLATEAU_RECENT_DAYS,
87
92
  PLATEAU_WINDOW_DAYS: () => PLATEAU_WINDOW_DAYS,
93
+ PLATE_SETS: () => PLATE_SETS,
88
94
  PREFERRED_TRAINING_STYLE_VALUES: () => PREFERRED_TRAINING_STYLE_VALUES,
89
95
  PROFILE_BIO_MAX_LENGTH: () => PROFILE_BIO_MAX_LENGTH,
90
96
  PROFILE_FAVORITE_EXERCISES_MAX: () => PROFILE_FAVORITE_EXERCISES_MAX,
@@ -140,6 +146,8 @@ __export(index_exports, {
140
146
  SESSION_SHARE_DEFAULT_FIELDS: () => SESSION_SHARE_DEFAULT_FIELDS,
141
147
  SESSION_SHARE_FIELDS: () => SESSION_SHARE_FIELDS,
142
148
  SESSION_SHARE_MAX_ACTIVE_LINKS: () => SESSION_SHARE_MAX_ACTIVE_LINKS,
149
+ SET_KINDS: () => SET_KINDS,
150
+ SET_KIND_LABELS: () => SET_KIND_LABELS,
143
151
  SEXES: () => SEXES,
144
152
  SHARED_ROUTINE_SOURCES: () => SHARED_ROUTINE_SOURCES,
145
153
  STARRED_EXERCISES_MAX: () => STARRED_EXERCISES_MAX,
@@ -175,15 +183,27 @@ __export(index_exports, {
175
183
  WORKOUT_SESSION_STATUSES: () => WORKOUT_SESSION_STATUSES,
176
184
  accountExportFileName: () => accountExportFileName,
177
185
  applyDeload: () => applyDeload,
186
+ buildWarmUpRamp: () => buildWarmUpRamp,
187
+ calculatePlateLoading: () => calculatePlateLoading,
188
+ countsAsWork: () => countsAsWork,
189
+ countsForProgression: () => countsForProgression,
190
+ defaultWarmUpEquipment: () => defaultWarmUpEquipment,
178
191
  deloadLengthDays: () => deloadLengthDays,
192
+ followWorkingLoad: () => followWorkingLoad,
193
+ isBarLoaded: () => isBarLoaded,
194
+ isSetKind: () => isSetKind,
179
195
  isWithinQuietHours: () => isWithinQuietHours,
196
+ leadWorkingWeight: () => leadWorkingWeight,
180
197
  matchesDeletionConfirmation: () => matchesDeletionConfirmation,
198
+ requiredToFinish: () => requiredToFinish,
181
199
  resolveActiveTrainingBlock: () => resolveActiveTrainingBlock,
182
200
  resolveRoutinePlan: () => resolveRoutinePlan,
183
201
  routineDayLabel: () => routineDayLabel,
184
202
  sessionCorrectionWindow: () => sessionCorrectionWindow,
203
+ setKindOf: () => setKindOf,
185
204
  setLogValuesDiffer: () => setLogValuesDiffer,
186
- trainingBlockCovers: () => trainingBlockCovers
205
+ trainingBlockCovers: () => trainingBlockCovers,
206
+ warmUpLoad: () => warmUpLoad
187
207
  });
188
208
  module.exports = __toCommonJS(index_exports);
189
209
 
@@ -511,6 +531,283 @@ var COMEBACK_WINDOW_DAYS = 14;
511
531
  var COMEBACK_SESSION_LOOKBACK = 500;
512
532
  var COMEBACK_RECOGNITION_LIMIT = 20;
513
533
 
534
+ // src/set-kinds.ts
535
+ var SET_KINDS = ["WORKING", "WARMUP", "DROP", "OPTIONAL"];
536
+ var SET_KIND_LABELS = {
537
+ WORKING: "Working",
538
+ WARMUP: "Warm-up",
539
+ DROP: "Drop",
540
+ OPTIONAL: "Optional"
541
+ };
542
+ function isSetKind(value) {
543
+ return SET_KINDS.includes(value);
544
+ }
545
+ function setKindOf(item) {
546
+ return item?.kind ?? "WORKING";
547
+ }
548
+ function countsAsWork(kind) {
549
+ return (kind ?? "WORKING") !== "WARMUP";
550
+ }
551
+ function countsForProgression(kind) {
552
+ const k = kind ?? "WORKING";
553
+ return k === "WORKING" || k === "OPTIONAL";
554
+ }
555
+ function requiredToFinish(kind) {
556
+ const k = kind ?? "WORKING";
557
+ return k === "WORKING" || k === "DROP";
558
+ }
559
+
560
+ // src/plate-loading.ts
561
+ var CANONICAL_SCALE = 1e4;
562
+ var DENSE_CAPACITY_LIMIT = 2e5;
563
+ var toCanonicalUnits = (weightKg) => Math.round(weightKg * CANONICAL_SCALE);
564
+ var fromCanonicalUnits = (units) => units / CANONICAL_SCALE;
565
+ var greatestCommonDivisor = (left, right) => {
566
+ let a = Math.abs(left);
567
+ let b = Math.abs(right);
568
+ while (b > 0) {
569
+ const remainder = a % b;
570
+ a = b;
571
+ b = remainder;
572
+ }
573
+ return a;
574
+ };
575
+ var buildChunks = (plates, divisor) => {
576
+ const chunks = [];
577
+ plates.forEach((plate, plateIndex) => {
578
+ const pairValue = Math.round(
579
+ toCanonicalUnits(plate.weightKg) * 2 / divisor
580
+ );
581
+ let remaining = plate.pairCount;
582
+ let chunkSize = 1;
583
+ while (remaining > 0) {
584
+ const quantity = Math.min(chunkSize, remaining);
585
+ chunks.push({
586
+ plateIndex,
587
+ quantity,
588
+ value: pairValue * quantity
589
+ });
590
+ remaining -= quantity;
591
+ chunkSize *= 2;
592
+ }
593
+ });
594
+ return chunks;
595
+ };
596
+ var reconstructSelection = (best, chunks, previousSum, previousChunk, plateCount) => {
597
+ const selection = Array.from({ length: plateCount }, () => 0);
598
+ let current = best;
599
+ while (current > 0) {
600
+ const chunkIndex = previousChunk(current);
601
+ if (chunkIndex < 0) break;
602
+ const chunk = chunks[chunkIndex];
603
+ selection[chunk.plateIndex] += chunk.quantity;
604
+ current = previousSum(current);
605
+ }
606
+ return selection;
607
+ };
608
+ var solveDense = (capacity, chunks, plateCount) => {
609
+ const reachable = new Uint8Array(capacity + 1);
610
+ const previousSum = new Int32Array(capacity + 1);
611
+ const previousChunk = new Int16Array(capacity + 1);
612
+ previousSum.fill(-1);
613
+ previousChunk.fill(-1);
614
+ reachable[0] = 1;
615
+ chunks.forEach((chunk, chunkIndex) => {
616
+ for (let sum = capacity; sum >= chunk.value; sum -= 1) {
617
+ if (!reachable[sum] && reachable[sum - chunk.value]) {
618
+ reachable[sum] = 1;
619
+ previousSum[sum] = sum - chunk.value;
620
+ previousChunk[sum] = chunkIndex;
621
+ }
622
+ }
623
+ });
624
+ let best = capacity;
625
+ while (best > 0 && !reachable[best]) best -= 1;
626
+ return {
627
+ best,
628
+ selection: reconstructSelection(
629
+ best,
630
+ chunks,
631
+ (sum) => previousSum[sum],
632
+ (sum) => previousChunk[sum],
633
+ plateCount
634
+ )
635
+ };
636
+ };
637
+ var solveSparse = (capacity, chunks, plateCount) => {
638
+ const reachable = /* @__PURE__ */ new Map();
639
+ reachable.set(0, { previous: -1, chunkIndex: -1 });
640
+ chunks.forEach((chunk, chunkIndex) => {
641
+ const currentSums = Array.from(reachable.keys());
642
+ for (const sum of currentSums) {
643
+ const next = sum + chunk.value;
644
+ if (next <= capacity && !reachable.has(next)) {
645
+ reachable.set(next, { previous: sum, chunkIndex });
646
+ }
647
+ }
648
+ });
649
+ let best = 0;
650
+ for (const sum of reachable.keys()) {
651
+ if (sum > best) best = sum;
652
+ }
653
+ return {
654
+ best,
655
+ selection: reconstructSelection(
656
+ best,
657
+ chunks,
658
+ (sum) => reachable.get(sum)?.previous ?? -1,
659
+ (sum) => reachable.get(sum)?.chunkIndex ?? -1,
660
+ plateCount
661
+ )
662
+ };
663
+ };
664
+ var heaviestFirst = (target, pairValues, counts) => {
665
+ let remaining = target;
666
+ const selection = pairValues.map((value, index) => {
667
+ const take = Math.min(counts[index], Math.floor(remaining / value));
668
+ remaining -= take * value;
669
+ return take;
670
+ });
671
+ return remaining === 0 ? selection : null;
672
+ };
673
+ var plateTotal = (selection) => selection.reduce((total, count) => total + count, 0);
674
+ var calculatePlateLoading = (targetWeightKg, barWeightKg, availablePlatePairs) => {
675
+ const targetUnits = Math.max(0, toCanonicalUnits(targetWeightKg));
676
+ const barUnits = Math.max(0, toCanonicalUnits(barWeightKg));
677
+ if (targetUnits < barUnits) {
678
+ return {
679
+ targetWeightKg: fromCanonicalUnits(targetUnits),
680
+ loadedWeightKg: fromCanonicalUnits(barUnits),
681
+ differenceKg: fromCanonicalUnits(barUnits - targetUnits),
682
+ status: "over",
683
+ platesPerSide: []
684
+ };
685
+ }
686
+ const plates = availablePlatePairs.filter((plate) => plate.weightKg > 0 && plate.pairCount > 0).sort((left, right) => right.weightKg - left.weightKg);
687
+ const pairValues = plates.map((plate) => toCanonicalUnits(plate.weightKg) * 2);
688
+ const divisor = pairValues.reduce(greatestCommonDivisor, 0) || 1;
689
+ const residualUnits = targetUnits - barUnits;
690
+ const requestedCapacity = Math.floor(residualUnits / divisor);
691
+ const inventoryCapacity = pairValues.reduce(
692
+ (total, value, index) => total + value / divisor * plates[index].pairCount,
693
+ 0
694
+ );
695
+ const capacity = Math.min(requestedCapacity, inventoryCapacity);
696
+ const chunks = buildChunks(plates, divisor);
697
+ const solution = capacity <= DENSE_CAPACITY_LIMIT ? solveDense(capacity, chunks, plates.length) : solveSparse(capacity, chunks, plates.length);
698
+ const greedy = heaviestFirst(
699
+ solution.best,
700
+ pairValues.map((value) => value / divisor),
701
+ plates.map((plate) => plate.pairCount)
702
+ );
703
+ const selection = greedy && plateTotal(greedy) <= plateTotal(solution.selection) ? greedy : solution.selection;
704
+ const plateUnits = solution.best * divisor;
705
+ const loadedUnits = barUnits + plateUnits;
706
+ const differenceUnits = targetUnits - loadedUnits;
707
+ return {
708
+ targetWeightKg: fromCanonicalUnits(targetUnits),
709
+ loadedWeightKg: fromCanonicalUnits(loadedUnits),
710
+ differenceKg: fromCanonicalUnits(differenceUnits),
711
+ status: differenceUnits === 0 ? "exact" : "short",
712
+ platesPerSide: plates.flatMap(
713
+ (plate, index) => selection[index] > 0 ? [
714
+ {
715
+ weightKg: plate.weightKg,
716
+ platesPerSide: selection[index]
717
+ }
718
+ ] : []
719
+ )
720
+ };
721
+ };
722
+
723
+ // src/warm-ups.ts
724
+ var MAX_SETS_PER_EXERCISE = 10;
725
+ var BAR_WARM_UP_STEPS = [
726
+ { share: 0, reps: 10 },
727
+ { share: 0.4, reps: 5 },
728
+ { share: 0.6, reps: 3 },
729
+ { share: 0.8, reps: 2 }
730
+ ];
731
+ var OTHER_WARM_UP_STEPS = [
732
+ { share: 0.5, reps: 8 },
733
+ { share: 0.75, reps: 3 }
734
+ ];
735
+ var BAR_LOADED = /* @__PURE__ */ new Set(["barbell", "ez-bar", "smith-machine"]);
736
+ var isBarLoaded = (equipmentRequired) => (equipmentRequired ?? []).some((item) => BAR_LOADED.has(item));
737
+ var POUNDS_PER_KILOGRAM = 2.2046226218;
738
+ var lb = (pounds) => pounds / POUNDS_PER_KILOGRAM;
739
+ var BAR_CHOICES_KG = {
740
+ KG: [20, 15, 10],
741
+ LB: [lb(45), lb(35), lb(15)]
742
+ };
743
+ var pairs = (weights, heaviestPairs) => weights.map((weightKg, index) => ({
744
+ weightKg,
745
+ pairCount: index === 0 ? heaviestPairs : 2
746
+ }));
747
+ var PLATE_SETS = {
748
+ KG: {
749
+ STANDARD: pairs([25, 20, 15, 10, 5, 2.5, 1.25], 4),
750
+ LIGHT: pairs([20, 15, 10, 5, 2.5, 1.25], 2)
751
+ },
752
+ LB: {
753
+ STANDARD: pairs([45, 35, 25, 10, 5, 2.5].map(lb), 4),
754
+ LIGHT: pairs([25, 10, 5, 2.5].map(lb), 2)
755
+ }
756
+ };
757
+ var LIMITED_SHORTFALL_KG = 2.5;
758
+ var defaultWarmUpEquipment = (unit) => ({
759
+ barWeightKg: BAR_CHOICES_KG[unit][0],
760
+ platePairs: PLATE_SETS[unit].STANDARD
761
+ });
762
+ var round = (value) => Math.round(value * 1e4) / 1e4;
763
+ function warmUpLoad(share, workingWeightKg, equipment) {
764
+ const target = share * workingWeightKg;
765
+ if (equipment.barLoaded) {
766
+ const loading = calculatePlateLoading(
767
+ Math.max(target, equipment.barWeightKg),
768
+ equipment.barWeightKg,
769
+ [...equipment.platePairs]
770
+ );
771
+ return {
772
+ weightKg: round(loading.loadedWeightKg),
773
+ platesPerSide: loading.platesPerSide,
774
+ limited: share > 0 && loading.status === "short" && loading.differenceKg >= LIMITED_SHORTFALL_KG - 1e-9
775
+ };
776
+ }
777
+ const increment = equipment.incrementKg > 0 ? equipment.incrementKg : 2.5;
778
+ return {
779
+ weightKg: round(Math.floor(target / increment + 1e-9) * increment),
780
+ limited: false
781
+ };
782
+ }
783
+ function buildWarmUpRamp(input) {
784
+ const work = input.workingWeightKg;
785
+ if (!(work > 0)) return { sets: [], leftOut: 0 };
786
+ const steps = input.barLoaded ? BAR_WARM_UP_STEPS : OTHER_WARM_UP_STEPS;
787
+ const sets = [];
788
+ for (const step of steps) {
789
+ const load = warmUpLoad(step.share, work, input);
790
+ const previous = sets[sets.length - 1];
791
+ if (load.weightKg <= 0 || load.weightKg >= work - 1e-9) continue;
792
+ if (previous && load.weightKg <= previous.weightKg + 1e-9) continue;
793
+ sets.push({ ...load, reps: step.reps, share: step.share });
794
+ }
795
+ const room = Math.max(0, input.room);
796
+ const kept = sets.slice(Math.max(0, sets.length - room));
797
+ return { sets: kept, leftOut: sets.length - kept.length };
798
+ }
799
+ function leadWorkingWeight(sets) {
800
+ const lead = sets.find((set) => countsForProgression(set.kind));
801
+ return typeof lead?.weight === "number" && lead.weight > 0 ? lead.weight : null;
802
+ }
803
+ function followWorkingLoad(sets, equipment) {
804
+ const work = leadWorkingWeight(sets);
805
+ if (work === null) return [...sets];
806
+ return sets.map(
807
+ (set) => set.kind === "WARMUP" && typeof set.warmUpShare === "number" ? { ...set, weight: warmUpLoad(set.warmUpShare, work, equipment).weightKg } : set
808
+ );
809
+ }
810
+
514
811
  // src/routine.ts
515
812
  var ROUTINE_SCHEDULE_MODES = ["WEEKLY", "ROTATION"];
516
813
  var ROUTINE_DAYS_MAX = 7;
@@ -632,13 +929,18 @@ var lighterOnGrid = (original, target, increment) => {
632
929
  const stepped = Math.round(Math.round(target / step) * step * 1e3) / 1e3;
633
930
  return stepped > 0 && stepped < original ? stepped : original;
634
931
  };
932
+ function halveSets(sets) {
933
+ const work = sets.filter((set) => countsAsWork(set.kind));
934
+ const keep = new Set(work.slice(0, Math.ceil(work.length / 2)));
935
+ return sets.filter((set) => !countsAsWork(set.kind) || keep.has(set)).map((set, index) => ({ ...set, setNumber: index + 1 }));
936
+ }
635
937
  function applyDeload(setup, options) {
636
938
  const factor = 1 - options.loadReductionPercent / 100;
637
939
  let lighter = false;
638
940
  const days = setup.days.map((day) => ({
639
941
  ...day,
640
942
  exercises: day.exercises.map((exercise) => {
641
- const kept = options.setMode === "HALF" ? exercise.sets.slice(0, Math.ceil(exercise.sets.length / 2)) : exercise.sets;
943
+ const kept = options.setMode === "HALF" ? halveSets(exercise.sets) : exercise.sets;
642
944
  if (kept.length < exercise.sets.length) lighter = true;
643
945
  return {
644
946
  ...exercise,
@@ -931,6 +1233,8 @@ function accountExportFileName(username, exportedAt) {
931
1233
  ACTIVITY_REACTIONS,
932
1234
  ACTIVITY_TYPES,
933
1235
  ACTIVITY_TYPE_SECTIONS,
1236
+ BAR_CHOICES_KG,
1237
+ BAR_WARM_UP_STEPS,
934
1238
  BLOCKED_MEMBERS_MAX,
935
1239
  CLONE_ROUTINE_REFUSALS,
936
1240
  COMEBACK_MIN_INACTIVE_DAYS,
@@ -958,6 +1262,8 @@ function accountExportFileName(username, exportedAt) {
958
1262
  FOLLOW_SUGGESTIONS_DEFAULT_LIMIT,
959
1263
  FOLLOW_SUGGESTIONS_MAX_LIMIT,
960
1264
  FOLLOW_SUGGESTION_REASONS,
1265
+ LIMITED_SHORTFALL_KG,
1266
+ MAX_SETS_PER_EXERCISE,
961
1267
  MEASURABLE_GOALS_MAX,
962
1268
  MEASURABLE_GOAL_DIRECTIONS,
963
1269
  MEASURABLE_GOAL_TYPES,
@@ -973,12 +1279,14 @@ function accountExportFileName(username, exportedAt) {
973
1279
  NOTIFICATIONS_RETENTION_DAYS,
974
1280
  NOTIFICATION_CATEGORIES,
975
1281
  NOTIFICATION_KINDS,
1282
+ OTHER_WARM_UP_STEPS,
976
1283
  PLATEAU_MIN_DAYS_SINCE_BEST,
977
1284
  PLATEAU_MIN_SESSIONS,
978
1285
  PLATEAU_MIN_SESSIONS_MAX,
979
1286
  PLATEAU_MIN_SESSIONS_MIN,
980
1287
  PLATEAU_RECENT_DAYS,
981
1288
  PLATEAU_WINDOW_DAYS,
1289
+ PLATE_SETS,
982
1290
  PREFERRED_TRAINING_STYLE_VALUES,
983
1291
  PROFILE_BIO_MAX_LENGTH,
984
1292
  PROFILE_FAVORITE_EXERCISES_MAX,
@@ -1034,6 +1342,8 @@ function accountExportFileName(username, exportedAt) {
1034
1342
  SESSION_SHARE_DEFAULT_FIELDS,
1035
1343
  SESSION_SHARE_FIELDS,
1036
1344
  SESSION_SHARE_MAX_ACTIVE_LINKS,
1345
+ SET_KINDS,
1346
+ SET_KIND_LABELS,
1037
1347
  SEXES,
1038
1348
  SHARED_ROUTINE_SOURCES,
1039
1349
  STARRED_EXERCISES_MAX,
@@ -1069,13 +1379,25 @@ function accountExportFileName(username, exportedAt) {
1069
1379
  WORKOUT_SESSION_STATUSES,
1070
1380
  accountExportFileName,
1071
1381
  applyDeload,
1382
+ buildWarmUpRamp,
1383
+ calculatePlateLoading,
1384
+ countsAsWork,
1385
+ countsForProgression,
1386
+ defaultWarmUpEquipment,
1072
1387
  deloadLengthDays,
1388
+ followWorkingLoad,
1389
+ isBarLoaded,
1390
+ isSetKind,
1073
1391
  isWithinQuietHours,
1392
+ leadWorkingWeight,
1074
1393
  matchesDeletionConfirmation,
1394
+ requiredToFinish,
1075
1395
  resolveActiveTrainingBlock,
1076
1396
  resolveRoutinePlan,
1077
1397
  routineDayLabel,
1078
1398
  sessionCorrectionWindow,
1399
+ setKindOf,
1079
1400
  setLogValuesDiffer,
1080
- trainingBlockCovers
1401
+ trainingBlockCovers,
1402
+ warmUpLoad
1081
1403
  });