diff --git a/internal/parser/clangtidy/score.go b/internal/parser/clangtidy/score.go
index 3689edb..62721a0 100644
--- a/internal/parser/clangtidy/score.go
+++ b/internal/parser/clangtidy/score.go
@@ -2,47 +2,53 @@ package clangtidy
 
 import (
 	"fmt"
+	"sort"
 	"strings"
-
-	"github.com/joint-online-judge/JOJ3/pkg/utils"
 )
 
 func GetResult(jsonMessages []JsonMessage, conf Conf) (int, string) {
 	score := conf.Score
 	comment := "### Test results summary\n\n"
-	categoryCount := map[string]int{}
+	matchCount := make(map[string]int)
+	scoreChange := make(map[string]int)
 	for _, jsonMessage := range jsonMessages {
 		// checkName is commas separated string here
 		checkName := jsonMessage.CheckName
 		for _, match := range conf.Matches {
 			for _, keyword := range match.Keywords {
 				if strings.Contains(checkName, keyword) {
-					score -= match.Score
+					matchCount[keyword] += 1
+					scoreChange[keyword] += -match.Score
+					score += -match.Score
 				}
 			}
 		}
-		checkNames := strings.Split(checkName, ",")
-		for _, checkName := range checkNames {
-			parts := strings.Split(checkName, "-")
-			if len(parts) > 0 {
-				category := parts[0]
-				// checkName might be: -warnings-as-errors
-				if category == "" {
-					continue
-				}
-				categoryCount[category] += 1
-			}
-		}
 	}
-	sortedMap := utils.SortMap(categoryCount,
-		func(i, j utils.Pair[string, int]) bool {
-			if i.Value == j.Value {
-				return i.Key < j.Key
-			}
-			return i.Value > j.Value
+	type Result struct {
+		Keyword     string
+		Count       int
+		ScoreChange int
+	}
+	var results []Result
+	for keyword, count := range matchCount {
+		results = append(results, Result{
+			Keyword:     keyword,
+			Count:       count,
+			ScoreChange: scoreChange[keyword],
 		})
-	for i, kv := range sortedMap {
-		comment += fmt.Sprintf("%d. %s: %d\n", i+1, kv.Key, kv.Value)
+	}
+	sort.Slice(results, func(i, j int) bool {
+		if results[i].ScoreChange != results[j].ScoreChange {
+			return results[i].ScoreChange < results[j].ScoreChange
+		}
+		if results[i].Count != results[j].Count {
+			return results[i].Count > results[j].Count
+		}
+		return results[i].Keyword < results[j].Keyword
+	})
+	for i, result := range results {
+		comment += fmt.Sprintf("%d. `%s`: %d occurrence(s), %d point(s)\n",
+			i+1, result.Keyword, result.Count, result.ScoreChange)
 	}
 	return score, comment
 }
diff --git a/internal/parser/cppcheck/score.go b/internal/parser/cppcheck/score.go
index c777198..4cb26b4 100644
--- a/internal/parser/cppcheck/score.go
+++ b/internal/parser/cppcheck/score.go
@@ -3,6 +3,7 @@ package cppcheck
 import (
 	"fmt"
 	"log/slog"
+	"sort"
 	"strings"
 )
 
@@ -41,9 +42,9 @@ func severityFromString(severityString string) (Severity, error) {
 }
 
 func GetResult(records []Record, conf Conf) (string, int, error) {
-	result := "### Test results summary\n\n"
-	var severityCounts [UNKNOWN + 1]int
 	score := conf.Score
+	comment := "### Test results summary\n\n"
+	var severityCounts [UNKNOWN + 1]int
 	// TODO: remove me
 	var severityScore [UNKNOWN + 1]int
 	for _, match := range conf.Matches {
@@ -73,22 +74,52 @@ func GetResult(records []Record, conf Conf) (string, int, error) {
 			severityCounts[int(severity)] += 1
 			score -= severityScore[int(severity)]
 		}
+		comment += fmt.Sprintf("1. error: %d\n", severityCounts[0])
+		comment += fmt.Sprintf("2. warning: %d\n", severityCounts[1])
+		comment += fmt.Sprintf("3. portability: %d\n", severityCounts[2])
+		comment += fmt.Sprintf("4. performance: %d\n", severityCounts[3])
+		comment += fmt.Sprintf("5. style: %d\n", severityCounts[4])
+		comment += fmt.Sprintf("6. information: %d\n", severityCounts[5])
+		comment += fmt.Sprintf("7. debug: %d\n", severityCounts[6])
 	}
+	matchCount := make(map[string]int)
+	scoreChange := make(map[string]int)
 	for _, record := range records {
 		for _, match := range conf.Matches {
 			for _, keyword := range match.Keywords {
 				if strings.Contains(record.Id, keyword) {
-					score -= match.Score
+					matchCount[keyword] += 1
+					scoreChange[keyword] += -match.Score
+					score += -match.Score
 				}
 			}
 		}
 	}
-	result += fmt.Sprintf("1. error: %d\n", severityCounts[0])
-	result += fmt.Sprintf("2. warning: %d\n", severityCounts[1])
-	result += fmt.Sprintf("3. portability: %d\n", severityCounts[2])
-	result += fmt.Sprintf("4. performance: %d\n", severityCounts[3])
-	result += fmt.Sprintf("5. style: %d\n", severityCounts[4])
-	result += fmt.Sprintf("6. information: %d\n", severityCounts[5])
-	result += fmt.Sprintf("7. debug: %d\n", severityCounts[6])
-	return result, score, nil
+	type Result struct {
+		Keyword     string
+		Count       int
+		ScoreChange int
+	}
+	var results []Result
+	for keyword, count := range matchCount {
+		results = append(results, Result{
+			Keyword:     keyword,
+			Count:       count,
+			ScoreChange: scoreChange[keyword],
+		})
+	}
+	sort.Slice(results, func(i, j int) bool {
+		if results[i].ScoreChange != results[j].ScoreChange {
+			return results[i].ScoreChange < results[j].ScoreChange
+		}
+		if results[i].Count != results[j].Count {
+			return results[i].Count > results[j].Count
+		}
+		return results[i].Keyword < results[j].Keyword
+	})
+	for i, result := range results {
+		comment += fmt.Sprintf("%d. `%s`: %d occurrence(s), %d point(s)\n",
+			i+1, result.Keyword, result.Count, result.ScoreChange)
+	}
+	return comment, score, nil
 }
diff --git a/internal/parser/cpplint/parser.go b/internal/parser/cpplint/parser.go
index d3a0c3f..a6beac9 100644
--- a/internal/parser/cpplint/parser.go
+++ b/internal/parser/cpplint/parser.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"log/slog"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -30,13 +31,15 @@ func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
 	stderr := executorResult.Files[conf.Stderr]
 	pattern := `(.+):(\d+):  (.+)  \[(.+)\] \[(\d)]\n`
 	re := regexp.MustCompile(pattern)
-	matches := re.FindAllStringSubmatch(stderr, -1)
+	regexMatches := re.FindAllStringSubmatch(stderr, -1)
 	score := conf.Score
 	comment := "### Test results summary\n\n"
-	categoryCount := map[string]int{}
-	for _, match := range matches {
-		// fileName := match[1]
-		// lineNum, err := strconv.Atoi(match[2])
+	categoryCount := make(map[string]int)
+	matchCount := make(map[string]int)
+	scoreChange := make(map[string]int)
+	for _, regexMatch := range regexMatches {
+		// fileName := regexMatch[1]
+		// lineNum, err := strconv.Atoi(regexMatch[2])
 		// if err != nil {
 		// 	slog.Error("parse lineNum", "error", err)
 		// 	return stage.ParserResult{
@@ -44,11 +47,11 @@ func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
 		// 		Comment: fmt.Sprintf("Unexpected parser error: %s.", err),
 		// 	}
 		// }
-		// message := match[3]
-		category := match[4]
+		// message := regexMatch[3]
+		category := regexMatch[4]
 		// TODO: remove me
 		if len(conf.Matches) == 0 {
-			confidence, err := strconv.Atoi(match[5])
+			confidence, err := strconv.Atoi(regexMatch[5])
 			if err != nil {
 				slog.Error("parse confidence", "error", err)
 				return stage.ParserResult{
@@ -58,19 +61,23 @@ func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
 			}
 			score -= confidence
 		}
-		for _, match := range conf.Matches {
-			for _, keyword := range match.Keywords {
-				if strings.Contains(category, keyword) {
-					score -= match.Score
-				}
-			}
-		}
 		parts := strings.Split(category, "/")
 		if len(parts) > 0 {
 			category := parts[0]
 			categoryCount[category] += 1
 		}
+		// TODO: remove me ends
+		for _, match := range conf.Matches {
+			for _, keyword := range match.Keywords {
+				if strings.Contains(category, keyword) {
+					matchCount[keyword] += 1
+					scoreChange[keyword] += -match.Score
+					score += -match.Score
+				}
+			}
+		}
 	}
+	// TODO: remove me
 	sortedMap := utils.SortMap(categoryCount,
 		func(i, j utils.Pair[string, int]) bool {
 			if i.Value == j.Value {
@@ -81,6 +88,33 @@ func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
 	for i, kv := range sortedMap {
 		comment += fmt.Sprintf("%d. %s: %d\n", i+1, kv.Key, kv.Value)
 	}
+	// TODO: remove me ends
+	type Result struct {
+		Keyword     string
+		Count       int
+		ScoreChange int
+	}
+	var results []Result
+	for keyword, count := range matchCount {
+		results = append(results, Result{
+			Keyword:     keyword,
+			Count:       count,
+			ScoreChange: scoreChange[keyword],
+		})
+	}
+	sort.Slice(results, func(i, j int) bool {
+		if results[i].ScoreChange != results[j].ScoreChange {
+			return results[i].ScoreChange < results[j].ScoreChange
+		}
+		if results[i].Count != results[j].Count {
+			return results[i].Count > results[j].Count
+		}
+		return results[i].Keyword < results[j].Keyword
+	})
+	for i, result := range results {
+		comment += fmt.Sprintf("%d. `%s`: %d occurrence(s), %d point(s)\n",
+			i+1, result.Keyword, result.Count, result.ScoreChange)
+	}
 	return stage.ParserResult{
 		Score:   score,
 		Comment: comment,
diff --git a/internal/parser/keyword/parser.go b/internal/parser/keyword/parser.go
index f7a3913..57c992e 100644
--- a/internal/parser/keyword/parser.go
+++ b/internal/parser/keyword/parser.go
@@ -2,6 +2,7 @@ package keyword
 
 import (
 	"fmt"
+	"sort"
 	"strings"
 
 	"github.com/joint-online-judge/JOJ3/internal/stage"
@@ -27,24 +28,48 @@ type Keyword struct{}
 func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
 	score := conf.Score
 	comment := ""
+	matchCount := make(map[string]int)
+	scoreChange := make(map[string]int)
 	for _, match := range conf.Matches {
 		for _, keyword := range match.Keywords {
-			keywordMatchCount := 0
 			for _, file := range conf.Files {
 				content := executorResult.Files[file]
-				keywordMatchCount += strings.Count(content, keyword)
+				matchCount[keyword] += strings.Count(content, keyword)
 			}
 			if match.MaxMatchCount > 0 {
-				keywordMatchCount = min(keywordMatchCount, match.MaxMatchCount)
-			}
-			if keywordMatchCount > 0 {
-				score -= keywordMatchCount * match.Score
-				comment += fmt.Sprintf(
-					"Matched keyword %d time(s): %s\n",
-					keywordMatchCount, keyword)
+				matchCount[keyword] = min(
+					matchCount[keyword], match.MaxMatchCount)
 			}
+			score += -match.Score * matchCount[keyword]
+			scoreChange[keyword] = -match.Score * matchCount[keyword]
 		}
 	}
+	type Result struct {
+		Keyword     string
+		Count       int
+		ScoreChange int
+	}
+	var results []Result
+	for keyword, count := range matchCount {
+		results = append(results, Result{
+			Keyword:     keyword,
+			Count:       count,
+			ScoreChange: scoreChange[keyword],
+		})
+	}
+	sort.Slice(results, func(i, j int) bool {
+		if results[i].ScoreChange != results[j].ScoreChange {
+			return results[i].ScoreChange < results[j].ScoreChange
+		}
+		if results[i].Count != results[j].Count {
+			return results[i].Count > results[j].Count
+		}
+		return results[i].Keyword < results[j].Keyword
+	})
+	for i, result := range results {
+		comment += fmt.Sprintf("%d. `%s`: %d occurrence(s), %d point(s)\n",
+			i+1, result.Keyword, result.Count, result.ScoreChange)
+	}
 	return stage.ParserResult{
 		Score:   score,
 		Comment: comment,