@sunsteel/contracts 0.86.0 → 0.87.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 +292 -2
- package/dist/index.d.cts +294 -4
- package/dist/index.d.ts +294 -4
- package/dist/index.js +262 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,9 @@ var REP_TYPES = ["FIXED", "RANGE"];
|
|
|
10
10
|
var PROGRESSION_SCHEMES = [
|
|
11
11
|
"NONE",
|
|
12
12
|
"DOUBLE_PROGRESSION",
|
|
13
|
-
"DYNAMIC_DOUBLE_PROGRESSION"
|
|
13
|
+
"DYNAMIC_DOUBLE_PROGRESSION",
|
|
14
|
+
// ROUT-17: an 8-step block by percentage of a reference max and target RIR.
|
|
15
|
+
"LINEAR_PERIODIZATION"
|
|
14
16
|
];
|
|
15
17
|
var MUSCLE_GROUPS = [
|
|
16
18
|
"PECTORAL",
|
|
@@ -593,6 +595,224 @@ function requiredToFinish(kind) {
|
|
|
593
595
|
return k === "WORKING" || k === "DROP";
|
|
594
596
|
}
|
|
595
597
|
|
|
598
|
+
// src/linear-periodization.ts
|
|
599
|
+
var LP_BLOCK_STEPS = 8;
|
|
600
|
+
var LP_WORKING_SETS = 3;
|
|
601
|
+
var LP_REST_SECONDS = 180;
|
|
602
|
+
var LP_REFERENCE_MAX_KG = 1e3;
|
|
603
|
+
var EASY = { rirMin: 2, rirMax: 3, amrap: false };
|
|
604
|
+
var AMRAP = { rirMin: 0, rirMax: 0, amrap: true };
|
|
605
|
+
var HARD = { rirMin: 1, rirMax: 2, amrap: false };
|
|
606
|
+
var LP_STEPS = [
|
|
607
|
+
{ step: 1, percentage: 0.63, sets: [EASY, EASY, AMRAP] },
|
|
608
|
+
{ step: 2, percentage: 0.66, sets: [EASY, EASY, AMRAP] },
|
|
609
|
+
{ step: 3, percentage: 0.69, sets: [EASY, EASY, AMRAP] },
|
|
610
|
+
{ step: 4, percentage: 0.72, sets: [EASY, EASY, AMRAP] },
|
|
611
|
+
{ step: 5, percentage: 0.75, sets: [HARD, HARD, HARD] },
|
|
612
|
+
{ step: 6, percentage: 0.78, sets: [HARD, HARD, HARD] },
|
|
613
|
+
{ step: 7, percentage: 0.81, sets: [HARD, HARD, HARD] },
|
|
614
|
+
{ step: 8, percentage: 0.85, sets: [HARD, HARD, HARD] }
|
|
615
|
+
];
|
|
616
|
+
function lpStep(step) {
|
|
617
|
+
const index = Math.min(Math.max(Math.trunc(step) || 1, 1), LP_BLOCK_STEPS);
|
|
618
|
+
return LP_STEPS[index - 1];
|
|
619
|
+
}
|
|
620
|
+
var LP_RECOVERY = { percentage: 0.7, sets: 2, reps: 5 };
|
|
621
|
+
var tidy = (value) => Math.round(value * 1e6) / 1e6;
|
|
622
|
+
function roundToIncrement(value, increment) {
|
|
623
|
+
if (!(increment > 0)) return tidy(value);
|
|
624
|
+
const steps = value / increment;
|
|
625
|
+
const floor = Math.floor(steps + 1e-9);
|
|
626
|
+
const fraction = steps - floor;
|
|
627
|
+
return tidy((fraction > 0.5 + 1e-9 ? floor + 1 : floor) * increment);
|
|
628
|
+
}
|
|
629
|
+
function roundDownToIncrement(value, increment) {
|
|
630
|
+
if (!(increment > 0)) return tidy(value);
|
|
631
|
+
return tidy(Math.floor(value / increment + 1e-9) * increment);
|
|
632
|
+
}
|
|
633
|
+
function lpLoadKg(referenceMaxKg, percentage, incrementKg) {
|
|
634
|
+
return roundToIncrement(referenceMaxKg * percentage, incrementKg);
|
|
635
|
+
}
|
|
636
|
+
var LP_PHASES = ["BLOCK", "RECOVERY", "FINISHED"];
|
|
637
|
+
function startLinearPeriodization(referenceMaxKg, cycle = 1) {
|
|
638
|
+
return {
|
|
639
|
+
referenceMaxKg,
|
|
640
|
+
phase: "BLOCK",
|
|
641
|
+
step: 1,
|
|
642
|
+
cycle,
|
|
643
|
+
samples: [],
|
|
644
|
+
finishedAt: null,
|
|
645
|
+
estimatedMaxKg: null,
|
|
646
|
+
nextReferenceMaxKg: null
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function linearPeriodizationProblem(state) {
|
|
650
|
+
if (!Number.isFinite(state.referenceMaxKg) || state.referenceMaxKg <= 0 || state.referenceMaxKg > LP_REFERENCE_MAX_KG) {
|
|
651
|
+
return "The reference max must be above 0";
|
|
652
|
+
}
|
|
653
|
+
if (!LP_PHASES.includes(state.phase)) {
|
|
654
|
+
return "Unknown block phase";
|
|
655
|
+
}
|
|
656
|
+
if (!Number.isInteger(state.step) || state.step < 1 || state.step > LP_BLOCK_STEPS) {
|
|
657
|
+
return "The block step must be between 1 and 8";
|
|
658
|
+
}
|
|
659
|
+
if (!Number.isInteger(state.cycle) || state.cycle < 1) {
|
|
660
|
+
return "The block number must be 1 or more";
|
|
661
|
+
}
|
|
662
|
+
if (state.phase === "RECOVERY" && !((state.nextReferenceMaxKg ?? 0) > 0)) {
|
|
663
|
+
return "A recovery step needs the next block's reference max";
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
function lpPrescription(state, incrementKg) {
|
|
668
|
+
if (state.phase === "FINISHED") return [];
|
|
669
|
+
if (state.phase === "RECOVERY") {
|
|
670
|
+
const loadKg2 = lpLoadKg(
|
|
671
|
+
state.referenceMaxKg,
|
|
672
|
+
LP_RECOVERY.percentage,
|
|
673
|
+
incrementKg
|
|
674
|
+
);
|
|
675
|
+
return Array.from({ length: LP_RECOVERY.sets }, () => ({
|
|
676
|
+
loadKg: loadKg2,
|
|
677
|
+
target: null,
|
|
678
|
+
reps: LP_RECOVERY.reps
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
const step = lpStep(state.step);
|
|
682
|
+
const loadKg = lpLoadKg(state.referenceMaxKg, step.percentage, incrementKg);
|
|
683
|
+
return step.sets.map((target) => ({ loadKg, target, reps: null }));
|
|
684
|
+
}
|
|
685
|
+
function lpWorkingSets(sets) {
|
|
686
|
+
return sets.filter((set) => setKindOf(set) !== "WARMUP");
|
|
687
|
+
}
|
|
688
|
+
var LP_ESTIMATE_STEPS = [7, 8];
|
|
689
|
+
var LP_ESTIMATE_MAX_REPS_TO_FAILURE = 10;
|
|
690
|
+
var LP_ESTIMATE_MIN_SETS = 3;
|
|
691
|
+
function lpRirMidpoint(target) {
|
|
692
|
+
return (target.rirMin + target.rirMax) / 2;
|
|
693
|
+
}
|
|
694
|
+
function lpSampleRir(sample) {
|
|
695
|
+
const target = lpStep(sample.step).sets[sample.set - 1] ?? HARD;
|
|
696
|
+
return lpRirMidpoint(target);
|
|
697
|
+
}
|
|
698
|
+
function lpSetEstimateKg(loadKg, reps, rir) {
|
|
699
|
+
return loadKg * (1 + (reps + rir) / 30);
|
|
700
|
+
}
|
|
701
|
+
function lpEstimate(samples, incrementKg) {
|
|
702
|
+
const sets = samples.filter((sample) => LP_ESTIMATE_STEPS.includes(sample.step)).map((sample) => ({ ...sample, rir: lpSampleRir(sample) })).filter(
|
|
703
|
+
(sample) => sample.reps >= 1 && sample.loadKg > 0 && sample.reps + sample.rir <= LP_ESTIMATE_MAX_REPS_TO_FAILURE
|
|
704
|
+
).map((sample) => ({
|
|
705
|
+
...sample,
|
|
706
|
+
estimateKg: tidy(lpSetEstimateKg(sample.loadKg, sample.reps, sample.rir))
|
|
707
|
+
})).sort((a, b) => a.step - b.step || a.set - b.set);
|
|
708
|
+
if (sets.length < LP_ESTIMATE_MIN_SETS) {
|
|
709
|
+
return { estimatedMaxKg: null, sets };
|
|
710
|
+
}
|
|
711
|
+
const values = sets.map((set) => set.estimateKg).sort((a, b) => a - b);
|
|
712
|
+
const middle = Math.floor(values.length / 2);
|
|
713
|
+
const median = values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
|
|
714
|
+
return { estimatedMaxKg: roundDownToIncrement(median, incrementKg), sets };
|
|
715
|
+
}
|
|
716
|
+
var LP_PROGRESS_THRESHOLD = 0.025;
|
|
717
|
+
var LP_PROGRESS_CAP = 0.05;
|
|
718
|
+
var LP_NEXT_BLOCK_KINDS = ["PROGRESS", "REPEAT", "RESET"];
|
|
719
|
+
function lpRecommendation(referenceMaxKg, estimatedMaxKg, incrementKg) {
|
|
720
|
+
if (!(estimatedMaxKg && estimatedMaxKg > 0) || !(referenceMaxKg > 0)) {
|
|
721
|
+
return { kind: "REPEAT", nextReferenceMaxKg: referenceMaxKg, change: null };
|
|
722
|
+
}
|
|
723
|
+
const change = estimatedMaxKg / referenceMaxKg - 1;
|
|
724
|
+
if (change >= LP_PROGRESS_THRESHOLD - 1e-9) {
|
|
725
|
+
const capped = Math.min(
|
|
726
|
+
estimatedMaxKg,
|
|
727
|
+
referenceMaxKg * (1 + LP_PROGRESS_CAP)
|
|
728
|
+
);
|
|
729
|
+
const next = roundDownToIncrement(capped, incrementKg);
|
|
730
|
+
return {
|
|
731
|
+
kind: "PROGRESS",
|
|
732
|
+
nextReferenceMaxKg: next > referenceMaxKg ? next : referenceMaxKg,
|
|
733
|
+
change
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
if (change < -LP_PROGRESS_THRESHOLD - 1e-9) {
|
|
737
|
+
return {
|
|
738
|
+
kind: "RESET",
|
|
739
|
+
nextReferenceMaxKg: roundDownToIncrement(estimatedMaxKg, incrementKg),
|
|
740
|
+
change
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return { kind: "REPEAT", nextReferenceMaxKg: referenceMaxKg, change };
|
|
744
|
+
}
|
|
745
|
+
function sameLpPosition(a, b) {
|
|
746
|
+
if (!a || !b) return !a && !b;
|
|
747
|
+
return a.phase === b.phase && a.step === b.step && a.cycle === b.cycle && Math.abs(a.referenceMaxKg - b.referenceMaxKg) < 1e-6;
|
|
748
|
+
}
|
|
749
|
+
function advanceLinearPeriodization(state, logged, context) {
|
|
750
|
+
if (state.phase === "FINISHED") return null;
|
|
751
|
+
const prescribed = lpPrescription(state, context.incrementKg).length;
|
|
752
|
+
for (let set = 1; set <= prescribed; set += 1) {
|
|
753
|
+
if (!logged.some((entry) => entry.set === set && entry.completed)) {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (state.phase === "RECOVERY") {
|
|
758
|
+
return startLinearPeriodization(
|
|
759
|
+
state.nextReferenceMaxKg ?? state.referenceMaxKg,
|
|
760
|
+
state.cycle + 1
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
const samples = LP_ESTIMATE_STEPS.includes(state.step) ? [
|
|
764
|
+
...state.samples.filter((sample) => sample.step !== state.step),
|
|
765
|
+
...logged.filter((entry) => entry.completed && entry.set <= prescribed).map((entry) => ({
|
|
766
|
+
sessionId: context.sessionId,
|
|
767
|
+
step: state.step,
|
|
768
|
+
set: entry.set,
|
|
769
|
+
loadKg: entry.loadKg,
|
|
770
|
+
reps: entry.reps
|
|
771
|
+
}))
|
|
772
|
+
] : state.samples;
|
|
773
|
+
if (state.step < LP_BLOCK_STEPS) {
|
|
774
|
+
return { ...state, step: state.step + 1, samples };
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
...state,
|
|
778
|
+
phase: "FINISHED",
|
|
779
|
+
samples,
|
|
780
|
+
finishedAt: context.finishedAt,
|
|
781
|
+
estimatedMaxKg: lpEstimate(samples, context.incrementKg).estimatedMaxKg,
|
|
782
|
+
nextReferenceMaxKg: null
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function continueLinearPeriodization(state, choice) {
|
|
786
|
+
if (state.phase !== "FINISHED") return null;
|
|
787
|
+
if (choice.recovery) {
|
|
788
|
+
return {
|
|
789
|
+
...state,
|
|
790
|
+
phase: "RECOVERY",
|
|
791
|
+
nextReferenceMaxKg: choice.referenceMaxKg
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return startLinearPeriodization(choice.referenceMaxKg, state.cycle + 1);
|
|
795
|
+
}
|
|
796
|
+
function lpStoredSets(state, incrementKg) {
|
|
797
|
+
if (!state) {
|
|
798
|
+
return Array.from({ length: LP_WORKING_SETS }, () => ({
|
|
799
|
+
repType: "FIXED",
|
|
800
|
+
reps: null,
|
|
801
|
+
weight: null,
|
|
802
|
+
rir: null,
|
|
803
|
+
kind: "WORKING"
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
const prescription = state.phase === "FINISHED" ? lpPrescription({ ...state, phase: "BLOCK", step: LP_BLOCK_STEPS }, incrementKg) : lpPrescription(state, incrementKg);
|
|
807
|
+
return prescription.map((set) => ({
|
|
808
|
+
repType: "FIXED",
|
|
809
|
+
reps: set.reps,
|
|
810
|
+
weight: set.loadKg,
|
|
811
|
+
rir: set.target ? set.target.rirMin : null,
|
|
812
|
+
kind: "WORKING"
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
|
|
596
816
|
// src/plate-loading.ts
|
|
597
817
|
var CANONICAL_SCALE = 1e4;
|
|
598
818
|
var DENSE_CAPACITY_LIMIT = 2e5;
|
|
@@ -1225,7 +1445,9 @@ var NOTIFICATION_KINDS = [
|
|
|
1225
1445
|
"TRAINING_PARTNER_ENCOURAGEMENT",
|
|
1226
1446
|
// NOTIF-07: selected activity from a partner who currently shares it.
|
|
1227
1447
|
"TRAINING_PARTNER_SESSION",
|
|
1228
|
-
"TRAINING_PARTNER_ACHIEVEMENT"
|
|
1448
|
+
"TRAINING_PARTNER_ACHIEVEMENT",
|
|
1449
|
+
// ROUT-18: an LP exercise finished its 8-step block.
|
|
1450
|
+
"LINEAR_BLOCK_FINISHED"
|
|
1229
1451
|
];
|
|
1230
1452
|
var NOTIFICATIONS_LOOKBACK_DAYS = 30;
|
|
1231
1453
|
var NOTIFICATIONS_RETENTION_DAYS = 90;
|
|
@@ -1412,6 +1634,14 @@ var API_ERROR_MESSAGES = {
|
|
|
1412
1634
|
SWAP_SESSION_NOT_ACTIVE: "Exercises can only be swapped in an active session",
|
|
1413
1635
|
SWAP_SETS_COMPLETED: "This exercise already has completed sets. Swap it before completing any.",
|
|
1414
1636
|
NOTES_DISCARDED_SESSION: "A discarded workout cannot take notes",
|
|
1637
|
+
// ── Linear periodization (ROUT-17 to ROUT-19) ──────────────────────────
|
|
1638
|
+
LINEAR_BLOCK_INVALID: "This exercise's block is not valid: {reason}",
|
|
1639
|
+
LINEAR_BLOCK_EXTRA_SET: "An exercise on an 8-week block takes no extra sets",
|
|
1640
|
+
LINEAR_BLOCK_KIND_FIXED: "The working sets of an 8-week block cannot change kind",
|
|
1641
|
+
LINEAR_BLOCK_LOAD_FIXED: "The load of an 8-week block is prescribed and cannot be changed",
|
|
1642
|
+
LINEAR_BLOCK_SWAP_ROUTINE: "An exercise on an 8-week block can be swapped for this workout only",
|
|
1643
|
+
LINEAR_BLOCK_NOT_LINEAR: "This exercise is not on an 8-week block",
|
|
1644
|
+
LINEAR_BLOCK_NOT_FINISHED: "This exercise's block has not finished, so there is nothing to choose yet",
|
|
1415
1645
|
// ── Corrections (LIVE-17) ──────────────────────────────────────────────
|
|
1416
1646
|
CORRECTION_NOT_COMPLETED: "Only a completed workout can be corrected",
|
|
1417
1647
|
CORRECTION_NOT_LATEST: "Only your most recent workout can be corrected",
|
|
@@ -1641,6 +1871,19 @@ export {
|
|
|
1641
1871
|
INCHES_PER_FOOT,
|
|
1642
1872
|
LENGTH_UNITS,
|
|
1643
1873
|
LIMITED_SHORTFALL_KG,
|
|
1874
|
+
LP_BLOCK_STEPS,
|
|
1875
|
+
LP_ESTIMATE_MAX_REPS_TO_FAILURE,
|
|
1876
|
+
LP_ESTIMATE_MIN_SETS,
|
|
1877
|
+
LP_ESTIMATE_STEPS,
|
|
1878
|
+
LP_NEXT_BLOCK_KINDS,
|
|
1879
|
+
LP_PHASES,
|
|
1880
|
+
LP_PROGRESS_CAP,
|
|
1881
|
+
LP_PROGRESS_THRESHOLD,
|
|
1882
|
+
LP_RECOVERY,
|
|
1883
|
+
LP_REFERENCE_MAX_KG,
|
|
1884
|
+
LP_REST_SECONDS,
|
|
1885
|
+
LP_STEPS,
|
|
1886
|
+
LP_WORKING_SETS,
|
|
1644
1887
|
MAX_SETS_PER_EXERCISE,
|
|
1645
1888
|
MEASURABLE_GOALS_MAX,
|
|
1646
1889
|
MEASURABLE_GOAL_DIRECTIONS,
|
|
@@ -1782,6 +2025,7 @@ export {
|
|
|
1782
2025
|
WEIGHT_UNITS,
|
|
1783
2026
|
WORKOUT_SESSION_STATUSES,
|
|
1784
2027
|
accountExportFileName,
|
|
2028
|
+
advanceLinearPeriodization,
|
|
1785
2029
|
apiError,
|
|
1786
2030
|
apiErrorMessage,
|
|
1787
2031
|
applyDeload,
|
|
@@ -1792,6 +2036,7 @@ export {
|
|
|
1792
2036
|
canLinkToNext,
|
|
1793
2037
|
cmToFeetInches,
|
|
1794
2038
|
cmToInches,
|
|
2039
|
+
continueLinearPeriodization,
|
|
1795
2040
|
countsAsWork,
|
|
1796
2041
|
countsForProgression,
|
|
1797
2042
|
customExerciseProblem,
|
|
@@ -1821,6 +2066,17 @@ export {
|
|
|
1821
2066
|
isWithinQuietHours,
|
|
1822
2067
|
latestBodyWeight,
|
|
1823
2068
|
leadWorkingWeight,
|
|
2069
|
+
linearPeriodizationProblem,
|
|
2070
|
+
lpEstimate,
|
|
2071
|
+
lpLoadKg,
|
|
2072
|
+
lpPrescription,
|
|
2073
|
+
lpRecommendation,
|
|
2074
|
+
lpRirMidpoint,
|
|
2075
|
+
lpSampleRir,
|
|
2076
|
+
lpSetEstimateKg,
|
|
2077
|
+
lpStep,
|
|
2078
|
+
lpStoredSets,
|
|
2079
|
+
lpWorkingSets,
|
|
1824
2080
|
matchesDeletionConfirmation,
|
|
1825
2081
|
messageBodyLength,
|
|
1826
2082
|
normalizeDashboardLayout,
|
|
@@ -1833,10 +2089,14 @@ export {
|
|
|
1833
2089
|
requiredToFinish,
|
|
1834
2090
|
resolveActiveTrainingBlock,
|
|
1835
2091
|
resolveRoutinePlan,
|
|
2092
|
+
roundDownToIncrement,
|
|
2093
|
+
roundToIncrement,
|
|
1836
2094
|
routineDayLabel,
|
|
2095
|
+
sameLpPosition,
|
|
1837
2096
|
sessionCorrectionWindow,
|
|
1838
2097
|
setKindOf,
|
|
1839
2098
|
setLogValuesDiffer,
|
|
2099
|
+
startLinearPeriodization,
|
|
1840
2100
|
summarizeBodyProgress,
|
|
1841
2101
|
trainingBlockCovers,
|
|
1842
2102
|
warmUpLoad,
|