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 }