@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.js CHANGED
@@ -322,6 +322,283 @@ var COMEBACK_WINDOW_DAYS = 14;
322
322
  var COMEBACK_SESSION_LOOKBACK = 500;
323
323
  var COMEBACK_RECOGNITION_LIMIT = 20;
324
324
 
325
+ // src/set-kinds.ts
326
+ var SET_KINDS = ["WORKING", "WARMUP", "DROP", "OPTIONAL"];
327
+ var SET_KIND_LABELS = {
328
+ WORKING: "Working",
329
+ WARMUP: "Warm-up",
330
+ DROP: "Drop",
331
+ OPTIONAL: "Optional"
332
+ };
333
+ function isSetKind(value) {
334
+ return SET_KINDS.includes(value);
335
+ }
336
+ function setKindOf(item) {
337
+ return item?.kind ?? "WORKING";
338
+ }
339
+ function countsAsWork(kind) {
340
+ return (kind ?? "WORKING") !== "WARMUP";
341
+ }
342
+ function countsForProgression(kind) {
343
+ const k = kind ?? "WORKING";
344
+ return k === "WORKING" || k === "OPTIONAL";
345
+ }
346
+ function requiredToFinish(kind) {
347
+ const k = kind ?? "WORKING";
348
+ return k === "WORKING" || k === "DROP";
349
+ }
350
+
351
+ // src/plate-loading.ts
352
+ var CANONICAL_SCALE = 1e4;
353
+ var DENSE_CAPACITY_LIMIT = 2e5;
354
+ var toCanonicalUnits = (weightKg) => Math.round(weightKg * CANONICAL_SCALE);
355
+ var fromCanonicalUnits = (units) => units / CANONICAL_SCALE;
356
+ var greatestCommonDivisor = (left, right) => {
357
+ let a = Math.abs(left);
358
+ let b = Math.abs(right);
359
+ while (b > 0) {
360
+ const remainder = a % b;
361
+ a = b;
362
+ b = remainder;
363
+ }
364
+ return a;
365
+ };
366
+ var buildChunks = (plates, divisor) => {
367
+ const chunks = [];
368
+ plates.forEach((plate, plateIndex) => {
369
+ const pairValue = Math.round(
370
+ toCanonicalUnits(plate.weightKg) * 2 / divisor
371
+ );
372
+ let remaining = plate.pairCount;
373
+ let chunkSize = 1;
374
+ while (remaining > 0) {
375
+ const quantity = Math.min(chunkSize, remaining);
376
+ chunks.push({
377
+ plateIndex,
378
+ quantity,
379
+ value: pairValue * quantity
380
+ });
381
+ remaining -= quantity;
382
+ chunkSize *= 2;
383
+ }
384
+ });
385
+ return chunks;
386
+ };
387
+ var reconstructSelection = (best, chunks, previousSum, previousChunk, plateCount) => {
388
+ const selection = Array.from({ length: plateCount }, () => 0);
389
+ let current = best;
390
+ while (current > 0) {
391
+ const chunkIndex = previousChunk(current);
392
+ if (chunkIndex < 0) break;
393
+ const chunk = chunks[chunkIndex];
394
+ selection[chunk.plateIndex] += chunk.quantity;
395
+ current = previousSum(current);
396
+ }
397
+ return selection;
398
+ };
399
+ var solveDense = (capacity, chunks, plateCount) => {
400
+ const reachable = new Uint8Array(capacity + 1);
401
+ const previousSum = new Int32Array(capacity + 1);
402
+ const previousChunk = new Int16Array(capacity + 1);
403
+ previousSum.fill(-1);
404
+ previousChunk.fill(-1);
405
+ reachable[0] = 1;
406
+ chunks.forEach((chunk, chunkIndex) => {
407
+ for (let sum = capacity; sum >= chunk.value; sum -= 1) {
408
+ if (!reachable[sum] && reachable[sum - chunk.value]) {
409
+ reachable[sum] = 1;
410
+ previousSum[sum] = sum - chunk.value;
411
+ previousChunk[sum] = chunkIndex;
412
+ }
413
+ }
414
+ });
415
+ let best = capacity;
416
+ while (best > 0 && !reachable[best]) best -= 1;
417
+ return {
418
+ best,
419
+ selection: reconstructSelection(
420
+ best,
421
+ chunks,
422
+ (sum) => previousSum[sum],
423
+ (sum) => previousChunk[sum],
424
+ plateCount
425
+ )
426
+ };
427
+ };
428
+ var solveSparse = (capacity, chunks, plateCount) => {
429
+ const reachable = /* @__PURE__ */ new Map();
430
+ reachable.set(0, { previous: -1, chunkIndex: -1 });
431
+ chunks.forEach((chunk, chunkIndex) => {
432
+ const currentSums = Array.from(reachable.keys());
433
+ for (const sum of currentSums) {
434
+ const next = sum + chunk.value;
435
+ if (next <= capacity && !reachable.has(next)) {
436
+ reachable.set(next, { previous: sum, chunkIndex });
437
+ }
438
+ }
439
+ });
440
+ let best = 0;
441
+ for (const sum of reachable.keys()) {
442
+ if (sum > best) best = sum;
443
+ }
444
+ return {
445
+ best,
446
+ selection: reconstructSelection(
447
+ best,
448
+ chunks,
449
+ (sum) => reachable.get(sum)?.previous ?? -1,
450
+ (sum) => reachable.get(sum)?.chunkIndex ?? -1,
451
+ plateCount
452
+ )
453
+ };
454
+ };
455
+ var heaviestFirst = (target, pairValues, counts) => {
456
+ let remaining = target;
457
+ const selection = pairValues.map((value, index) => {
458
+ const take = Math.min(counts[index], Math.floor(remaining / value));
459
+ remaining -= take * value;
460
+ return take;
461
+ });
462
+ return remaining === 0 ? selection : null;
463
+ };
464
+ var plateTotal = (selection) => selection.reduce((total, count) => total + count, 0);
465
+ var calculatePlateLoading = (targetWeightKg, barWeightKg, availablePlatePairs) => {
466
+ const targetUnits = Math.max(0, toCanonicalUnits(targetWeightKg));
467
+ const barUnits = Math.max(0, toCanonicalUnits(barWeightKg));
468
+ if (targetUnits < barUnits) {
469
+ return {
470
+ targetWeightKg: fromCanonicalUnits(targetUnits),
471
+ loadedWeightKg: fromCanonicalUnits(barUnits),
472
+ differenceKg: fromCanonicalUnits(barUnits - targetUnits),
473
+ status: "over",
474
+ platesPerSide: []
475
+ };
476
+ }
477
+ const plates = availablePlatePairs.filter((plate) => plate.weightKg > 0 && plate.pairCount > 0).sort((left, right) => right.weightKg - left.weightKg);
478
+ const pairValues = plates.map((plate) => toCanonicalUnits(plate.weightKg) * 2);
479
+ const divisor = pairValues.reduce(greatestCommonDivisor, 0) || 1;
480
+ const residualUnits = targetUnits - barUnits;
481
+ const requestedCapacity = Math.floor(residualUnits / divisor);
482
+ const inventoryCapacity = pairValues.reduce(
483
+ (total, value, index) => total + value / divisor * plates[index].pairCount,
484
+ 0
485
+ );
486
+ const capacity = Math.min(requestedCapacity, inventoryCapacity);
487
+ const chunks = buildChunks(plates, divisor);
488
+ const solution = capacity <= DENSE_CAPACITY_LIMIT ? solveDense(capacity, chunks, plates.length) : solveSparse(capacity, chunks, plates.length);
489
+ const greedy = heaviestFirst(
490
+ solution.best,
491
+ pairValues.map((value) => value / divisor),
492
+ plates.map((plate) => plate.pairCount)
493
+ );
494
+ const selection = greedy && plateTotal(greedy) <= plateTotal(solution.selection) ? greedy : solution.selection;
495
+ const plateUnits = solution.best * divisor;
496
+ const loadedUnits = barUnits + plateUnits;
497
+ const differenceUnits = targetUnits - loadedUnits;
498
+ return {
499
+ targetWeightKg: fromCanonicalUnits(targetUnits),
500
+ loadedWeightKg: fromCanonicalUnits(loadedUnits),
501
+ differenceKg: fromCanonicalUnits(differenceUnits),
502
+ status: differenceUnits === 0 ? "exact" : "short",
503
+ platesPerSide: plates.flatMap(
504
+ (plate, index) => selection[index] > 0 ? [
505
+ {
506
+ weightKg: plate.weightKg,
507
+ platesPerSide: selection[index]
508
+ }
509
+ ] : []
510
+ )
511
+ };
512
+ };
513
+
514
+ // src/warm-ups.ts
515
+ var MAX_SETS_PER_EXERCISE = 10;
516
+ var BAR_WARM_UP_STEPS = [
517
+ { share: 0, reps: 10 },
518
+ { share: 0.4, reps: 5 },
519
+ { share: 0.6, reps: 3 },
520
+ { share: 0.8, reps: 2 }
521
+ ];
522
+ var OTHER_WARM_UP_STEPS = [
523
+ { share: 0.5, reps: 8 },
524
+ { share: 0.75, reps: 3 }
525
+ ];
526
+ var BAR_LOADED = /* @__PURE__ */ new Set(["barbell", "ez-bar", "smith-machine"]);
527
+ var isBarLoaded = (equipmentRequired) => (equipmentRequired ?? []).some((item) => BAR_LOADED.has(item));
528
+ var POUNDS_PER_KILOGRAM = 2.2046226218;
529
+ var lb = (pounds) => pounds / POUNDS_PER_KILOGRAM;
530
+ var BAR_CHOICES_KG = {
531
+ KG: [20, 15, 10],
532
+ LB: [lb(45), lb(35), lb(15)]
533
+ };
534
+ var pairs = (weights, heaviestPairs) => weights.map((weightKg, index) => ({
535
+ weightKg,
536
+ pairCount: index === 0 ? heaviestPairs : 2
537
+ }));
538
+ var PLATE_SETS = {
539
+ KG: {
540
+ STANDARD: pairs([25, 20, 15, 10, 5, 2.5, 1.25], 4),
541
+ LIGHT: pairs([20, 15, 10, 5, 2.5, 1.25], 2)
542
+ },
543
+ LB: {
544
+ STANDARD: pairs([45, 35, 25, 10, 5, 2.5].map(lb), 4),
545
+ LIGHT: pairs([25, 10, 5, 2.5].map(lb), 2)
546
+ }
547
+ };
548
+ var LIMITED_SHORTFALL_KG = 2.5;
549
+ var defaultWarmUpEquipment = (unit) => ({
550
+ barWeightKg: BAR_CHOICES_KG[unit][0],
551
+ platePairs: PLATE_SETS[unit].STANDARD
552
+ });
553
+ var round = (value) => Math.round(value * 1e4) / 1e4;
554
+ function warmUpLoad(share, workingWeightKg, equipment) {
555
+ const target = share * workingWeightKg;
556
+ if (equipment.barLoaded) {
557
+ const loading = calculatePlateLoading(
558
+ Math.max(target, equipment.barWeightKg),
559
+ equipment.barWeightKg,
560
+ [...equipment.platePairs]
561
+ );
562
+ return {
563
+ weightKg: round(loading.loadedWeightKg),
564
+ platesPerSide: loading.platesPerSide,
565
+ limited: share > 0 && loading.status === "short" && loading.differenceKg >= LIMITED_SHORTFALL_KG - 1e-9
566
+ };
567
+ }
568
+ const increment = equipment.incrementKg > 0 ? equipment.incrementKg : 2.5;
569
+ return {
570
+ weightKg: round(Math.floor(target / increment + 1e-9) * increment),
571
+ limited: false
572
+ };
573
+ }
574
+ function buildWarmUpRamp(input) {
575
+ const work = input.workingWeightKg;
576
+ if (!(work > 0)) return { sets: [], leftOut: 0 };
577
+ const steps = input.barLoaded ? BAR_WARM_UP_STEPS : OTHER_WARM_UP_STEPS;
578
+ const sets = [];
579
+ for (const step of steps) {
580
+ const load = warmUpLoad(step.share, work, input);
581
+ const previous = sets[sets.length - 1];
582
+ if (load.weightKg <= 0 || load.weightKg >= work - 1e-9) continue;
583
+ if (previous && load.weightKg <= previous.weightKg + 1e-9) continue;
584
+ sets.push({ ...load, reps: step.reps, share: step.share });
585
+ }
586
+ const room = Math.max(0, input.room);
587
+ const kept = sets.slice(Math.max(0, sets.length - room));
588
+ return { sets: kept, leftOut: sets.length - kept.length };
589
+ }
590
+ function leadWorkingWeight(sets) {
591
+ const lead = sets.find((set) => countsForProgression(set.kind));
592
+ return typeof lead?.weight === "number" && lead.weight > 0 ? lead.weight : null;
593
+ }
594
+ function followWorkingLoad(sets, equipment) {
595
+ const work = leadWorkingWeight(sets);
596
+ if (work === null) return [...sets];
597
+ return sets.map(
598
+ (set) => set.kind === "WARMUP" && typeof set.warmUpShare === "number" ? { ...set, weight: warmUpLoad(set.warmUpShare, work, equipment).weightKg } : set
599
+ );
600
+ }
601
+
325
602
  // src/routine.ts
326
603
  var ROUTINE_SCHEDULE_MODES = ["WEEKLY", "ROTATION"];
327
604
  var ROUTINE_DAYS_MAX = 7;
@@ -443,13 +720,18 @@ var lighterOnGrid = (original, target, increment) => {
443
720
  const stepped = Math.round(Math.round(target / step) * step * 1e3) / 1e3;
444
721
  return stepped > 0 && stepped < original ? stepped : original;
445
722
  };
723
+ function halveSets(sets) {
724
+ const work = sets.filter((set) => countsAsWork(set.kind));
725
+ const keep = new Set(work.slice(0, Math.ceil(work.length / 2)));
726
+ return sets.filter((set) => !countsAsWork(set.kind) || keep.has(set)).map((set, index) => ({ ...set, setNumber: index + 1 }));
727
+ }
446
728
  function applyDeload(setup, options) {
447
729
  const factor = 1 - options.loadReductionPercent / 100;
448
730
  let lighter = false;
449
731
  const days = setup.days.map((day) => ({
450
732
  ...day,
451
733
  exercises: day.exercises.map((exercise) => {
452
- const kept = options.setMode === "HALF" ? exercise.sets.slice(0, Math.ceil(exercise.sets.length / 2)) : exercise.sets;
734
+ const kept = options.setMode === "HALF" ? halveSets(exercise.sets) : exercise.sets;
453
735
  if (kept.length < exercise.sets.length) lighter = true;
454
736
  return {
455
737
  ...exercise,
@@ -741,6 +1023,8 @@ export {
741
1023
  ACTIVITY_REACTIONS,
742
1024
  ACTIVITY_TYPES,
743
1025
  ACTIVITY_TYPE_SECTIONS,
1026
+ BAR_CHOICES_KG,
1027
+ BAR_WARM_UP_STEPS,
744
1028
  BLOCKED_MEMBERS_MAX,
745
1029
  CLONE_ROUTINE_REFUSALS,
746
1030
  COMEBACK_MIN_INACTIVE_DAYS,
@@ -768,6 +1052,8 @@ export {
768
1052
  FOLLOW_SUGGESTIONS_DEFAULT_LIMIT,
769
1053
  FOLLOW_SUGGESTIONS_MAX_LIMIT,
770
1054
  FOLLOW_SUGGESTION_REASONS,
1055
+ LIMITED_SHORTFALL_KG,
1056
+ MAX_SETS_PER_EXERCISE,
771
1057
  MEASURABLE_GOALS_MAX,
772
1058
  MEASURABLE_GOAL_DIRECTIONS,
773
1059
  MEASURABLE_GOAL_TYPES,
@@ -783,12 +1069,14 @@ export {
783
1069
  NOTIFICATIONS_RETENTION_DAYS,
784
1070
  NOTIFICATION_CATEGORIES,
785
1071
  NOTIFICATION_KINDS,
1072
+ OTHER_WARM_UP_STEPS,
786
1073
  PLATEAU_MIN_DAYS_SINCE_BEST,
787
1074
  PLATEAU_MIN_SESSIONS,
788
1075
  PLATEAU_MIN_SESSIONS_MAX,
789
1076
  PLATEAU_MIN_SESSIONS_MIN,
790
1077
  PLATEAU_RECENT_DAYS,
791
1078
  PLATEAU_WINDOW_DAYS,
1079
+ PLATE_SETS,
792
1080
  PREFERRED_TRAINING_STYLE_VALUES,
793
1081
  PROFILE_BIO_MAX_LENGTH,
794
1082
  PROFILE_FAVORITE_EXERCISES_MAX,
@@ -844,6 +1132,8 @@ export {
844
1132
  SESSION_SHARE_DEFAULT_FIELDS,
845
1133
  SESSION_SHARE_FIELDS,
846
1134
  SESSION_SHARE_MAX_ACTIVE_LINKS,
1135
+ SET_KINDS,
1136
+ SET_KIND_LABELS,
847
1137
  SEXES,
848
1138
  SHARED_ROUTINE_SOURCES,
849
1139
  STARRED_EXERCISES_MAX,
@@ -879,13 +1169,25 @@ export {
879
1169
  WORKOUT_SESSION_STATUSES,
880
1170
  accountExportFileName,
881
1171
  applyDeload,
1172
+ buildWarmUpRamp,
1173
+ calculatePlateLoading,
1174
+ countsAsWork,
1175
+ countsForProgression,
1176
+ defaultWarmUpEquipment,
882
1177
  deloadLengthDays,
1178
+ followWorkingLoad,
1179
+ isBarLoaded,
1180
+ isSetKind,
883
1181
  isWithinQuietHours,
1182
+ leadWorkingWeight,
884
1183
  matchesDeletionConfirmation,
1184
+ requiredToFinish,
885
1185
  resolveActiveTrainingBlock,
886
1186
  resolveRoutinePlan,
887
1187
  routineDayLabel,
888
1188
  sessionCorrectionWindow,
1189
+ setKindOf,
889
1190
  setLogValuesDiffer,
890
- trainingBlockCovers
1191
+ trainingBlockCovers,
1192
+ warmUpLoad
891
1193
  };
package/package.json CHANGED
@@ -1,40 +1,40 @@
1
- {
2
- "name": "@sunsteel/contracts",
3
- "version": "0.69.0",
4
- "repository": {
5
- "type": "git",
6
- "url": "git+https://github.com/ezee969/sunsteel-contracts.git"
7
- },
8
- "private": false,
9
- "engines": {
10
- "node": ">=20.11"
11
- },
12
- "type": "module",
13
- "sideEffects": false,
14
- "main": "./dist/index.cjs",
15
- "module": "./dist/index.js",
16
- "types": "./dist/index.d.ts",
17
- "exports": {
18
- ".": {
19
- "types": "./dist/index.d.ts",
20
- "import": "./dist/index.js",
21
- "require": "./dist/index.cjs"
22
- }
23
- },
24
- "files": [
25
- "dist"
26
- ],
27
- "scripts": {
28
- "build": "tsup src/index.ts --format esm,cjs --dts --clean",
29
- "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
30
- "typecheck": "tsc --noEmit",
31
- "lint": "npm run typecheck",
32
- "verify": "npm run typecheck && npm run build",
33
- "verify:security": "npm audit",
34
- "prepublishOnly": "npm run build"
35
- },
36
- "devDependencies": {
37
- "tsup": "^8.3.5",
38
- "typescript": "^5.7.3"
39
- }
40
- }
1
+ {
2
+ "name": "@sunsteel/contracts",
3
+ "version": "0.71.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/ezee969/sunsteel-contracts.git"
7
+ },
8
+ "private": false,
9
+ "engines": {
10
+ "node": ">=20.11"
11
+ },
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
29
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "npm run typecheck",
32
+ "verify": "npm run typecheck && npm run build",
33
+ "verify:security": "npm audit",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "devDependencies": {
37
+ "tsup": "^8.3.5",
38
+ "typescript": "^5.7.3"
39
+ }
40
+ }