github.com/soypat/gitaligned@v0.3.4-0.20221228122414-e435aab44fbc/align.go (about)

     1  package main
     2  
     3  import (
     4  	"math"
     5  
     6  	"github.com/jdkato/prose/v2"
     7  )
     8  
     9  const alignmentThreshold = 0.3
    10  
    11  type alignment struct {
    12  	// These are axes on our alignment chart.
    13  	// that go from -1 to 1
    14  	Licitness float64 `json:"licitness"`
    15  	Morality  float64 `json:"morality"`
    16  }
    17  
    18  // Format returns human readable alignment.
    19  // i.e. "Neutral Evil".
    20  //
    21  // The threshold is set by a global variable called `alignmentThreshold`
    22  func (a alignment) Format() (format string) {
    23  	var good, lawful, evil, chaotic bool
    24  	good = a.Morality > alignmentThreshold
    25  	lawful = a.Licitness > alignmentThreshold
    26  	evil = a.Morality < -alignmentThreshold
    27  	chaotic = a.Licitness < -alignmentThreshold
    28  	if !evil && !good && !lawful && !chaotic {
    29  		return "True Neutral"
    30  	}
    31  	switch {
    32  	case lawful:
    33  		format = "Lawful "
    34  	case chaotic:
    35  		format = "Chaotic "
    36  	default:
    37  		format = "Neutral "
    38  	}
    39  	if good {
    40  		format += "Good"
    41  	} else if evil {
    42  		format += "Evil"
    43  	} else {
    44  		format += "Neutral"
    45  	}
    46  	return format
    47  }
    48  
    49  // SetCommitAlignments processes commits and assigns them an alignment
    50  func SetCommitAlignments(commits []commit, authors []author) error {
    51  	return walkCommits(commits, func(c *commit, t []prose.Token) {
    52  		c.alignment = getAlignment(c, t)
    53  	})
    54  }
    55  
    56  // SetAuthorAlignments processes authors and sets their alignment
    57  func SetAuthorAlignments(commits []commit, authors []author) error {
    58  	walkCommits(commits, func(c *commit, t []prose.Token) {
    59  		c.User.Commits++
    60  		a := getAlignment(c, t)
    61  		c.User.accumulator.Morality += a.Morality
    62  		c.User.accumulator.Licitness += a.Licitness
    63  	})
    64  	for i := range authors {
    65  		if authors[i].Commits > 0 {
    66  			authors[i].alignment.Licitness = math.Erf(authors[i].accumulator.Licitness)
    67  			authors[i].alignment.Morality = math.Erf(authors[i].accumulator.Morality)
    68  		}
    69  	}
    70  	return nil
    71  }
    72  
    73  func getAlignment(c *commit, t []prose.Token) (a alignment) {
    74  	tlen := len(t)
    75  	if edgeCases(t, &a) {
    76  		return a
    77  	}
    78  	var adjectives, determiners, interjections int
    79  	for i := 1; i < tlen; i++ {
    80  		switch t[i].Tag {
    81  		case "NN", "NNP", "NNS":
    82  			continue // NN (noun, singular or mass) could be just about anything
    83  		case "JJ":
    84  			adjectives++
    85  		case "DT", "WDT":
    86  			determiners++
    87  		case "UH":
    88  			interjections++
    89  		}
    90  	}
    91  	// interjections: uh, oops, ah
    92  	a.Licitness -= float64(interjections)
    93  	//determiners are just noise in small messages: an, a, one, my, the
    94  	a.Morality -= float64(determiners) * 0.1 * (10 - float64(min(tlen, 10)))
    95  	// adjectives
    96  	a.Morality += math.Min(float64(adjectives)*0.4, 1)
    97  	// if adjectives > 1 {
    98  	a.Licitness -= (float64(adjectives)/float64(tlen) - 0.2) * 3
    99  	// }
   100  
   101  	// normalize values so that it is within alignment chart values: [-1,1]
   102  	a.Morality = capNorm(1, a.Morality)
   103  	a.Licitness = capNorm(1, a.Licitness)
   104  	return
   105  }
   106  
   107  func capNorm(c, f float64) float64 {
   108  	if math.Signbit(f) {
   109  		return math.Max(-c, f)
   110  	}
   111  	return math.Min(c, f)
   112  }
   113  
   114  // edgeCases handles the edge cases of a git commit message
   115  // without worrying much about NLP aspects of it. If it finds
   116  // an edge case `a` should be modified accordingly.
   117  //
   118  // Returned bool indicates if the resulting alignment is final
   119  // (no more processing needed).
   120  func edgeCases(t []prose.Token, a *alignment) (finalAlignment bool) {
   121  	tlen := len(t)
   122  	if tlen <= 2 {
   123  		a.Morality = -1
   124  		return true
   125  	}
   126  	switch t[0].Tag {
   127  	// first word is verb. nice to read these commits
   128  	case "VB", "VBZ":
   129  		a.Morality = 1
   130  	}
   131  	switch t[0].Text {
   132  	// branch merging demonstrates organized development
   133  	case "merge":
   134  		a.Licitness = 1
   135  	}
   136  	return false
   137  }