Skip to content

Commit 5eb84eb

Browse files
authored
Merge pull request #173 from SimplyLiz/fix/review-feedback
feat: Address 5 items from external technical review
2 parents 54c5107 + d505dd5 commit 5eb84eb

4 files changed

Lines changed: 104 additions & 14 deletions

File tree

internal/query/review.go

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,24 @@ type GeneratedFileInfo struct {
165165
// DefaultReviewPolicy returns sensible defaults.
166166
func DefaultReviewPolicy() *ReviewPolicy {
167167
return &ReviewPolicy{
168-
BlockBreakingChanges: true,
169-
BlockSecrets: true,
170-
FailOnLevel: "error",
171-
HoldTheLine: true,
172-
SplitThreshold: 50,
173-
GeneratedPatterns: []string{"*.generated.*", "*.pb.go", "*.pb.cc", "parser.tab.c", "lex.yy.c"},
174-
GeneratedMarkers: []string{"DO NOT EDIT", "Generated by", "AUTO-GENERATED", "This file is generated"},
168+
BlockBreakingChanges: true,
169+
BlockSecrets: true,
170+
FailOnLevel: "error",
171+
HoldTheLine: true,
172+
SplitThreshold: 50,
173+
GeneratedPatterns: []string{
174+
"*.generated.*", "*.pb.go", "*.pb.cc", "*.pb.h",
175+
"parser.tab.c", "lex.yy.c",
176+
"*.swagger.json", "*.openapi.json",
177+
"*_generated.go", "*_gen.go",
178+
"*.min.js", "*.min.css",
179+
},
180+
GeneratedMarkers: []string{
181+
"DO NOT EDIT", "Generated by", "AUTO-GENERATED", "This file is generated",
182+
"Code generated", "Automatically generated",
183+
"eslint-disable", "swagger-codegen", "openapi-generator",
184+
"@generated", "protoc-gen", "graphql-codegen",
185+
},
175186
CriticalSeverity: "error",
176187
DeadCodeMinConfidence: 0.8,
177188
TestGapMinLines: 5,
@@ -790,10 +801,6 @@ func (e *Engine) checkBreakingChanges(ctx context.Context, opts ReviewPROptions)
790801
}
791802

792803
var findings []ReviewFinding
793-
breakingCount := 0
794-
if resp.Summary != nil {
795-
breakingCount = resp.Summary.BreakingChanges
796-
}
797804

798805
for _, change := range resp.Changes {
799806
if change.Severity == "breaking" || change.Severity == "error" {
@@ -808,6 +815,11 @@ func (e *Engine) checkBreakingChanges(ctx context.Context, opts ReviewPROptions)
808815
}
809816
}
810817

818+
// Filter out rename pairs — a removed + added symbol in the same file
819+
// with the same kind is likely a rename, not a breaking change.
820+
findings = filterRenamePairs(findings)
821+
breakingCount := len(findings)
822+
811823
status := "pass"
812824
severity := "error"
813825
summary := "No breaking API changes"
@@ -825,6 +837,43 @@ func (e *Engine) checkBreakingChanges(ctx context.Context, opts ReviewPROptions)
825837
}, findings
826838
}
827839

840+
// filterRenamePairs removes findings that are likely renames rather than
841+
// breaking changes. A rename produces "removed X" + "added Y" in the same
842+
// file with the same kind — not a real API break.
843+
func filterRenamePairs(findings []ReviewFinding) []ReviewFinding {
844+
// Group by file
845+
byFile := make(map[string][]ReviewFinding)
846+
for _, f := range findings {
847+
byFile[f.File] = append(byFile[f.File], f)
848+
}
849+
850+
var filtered []ReviewFinding
851+
for _, fileFindings := range byFile {
852+
// Count removed and added per kind
853+
removedByKind := make(map[string]int)
854+
addedByKind := make(map[string]int)
855+
for _, f := range fileFindings {
856+
if strings.Contains(f.Message, "removed") || strings.Contains(f.Message, "Removed") {
857+
removedByKind[f.RuleID]++
858+
} else if strings.Contains(f.Message, "added") || strings.Contains(f.Message, "Added") || strings.Contains(f.Message, "new") {
859+
addedByKind[f.RuleID]++
860+
}
861+
}
862+
863+
for _, f := range fileFindings {
864+
kind := f.RuleID
865+
isRemoved := strings.Contains(f.Message, "removed") || strings.Contains(f.Message, "Removed")
866+
// If there's a matching add for this remove in the same file+kind, skip it
867+
if isRemoved && addedByKind[kind] > 0 {
868+
addedByKind[kind]--
869+
continue // Likely a rename
870+
}
871+
filtered = append(filtered, f)
872+
}
873+
}
874+
return filtered
875+
}
876+
828877
func (e *Engine) checkSecrets(ctx context.Context, files []string) (ReviewCheck, []ReviewFinding) {
829878
start := time.Now()
830879

internal/query/review_coupling.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,27 @@ package query
33
import (
44
"context"
55
"fmt"
6+
"os/exec"
67
"strings"
78
"time"
89

910
"github.com/SimplyLiz/CodeMCP/internal/backends/git"
1011
"github.com/SimplyLiz/CodeMCP/internal/coupling"
1112
)
1213

14+
const maxCouplingAge = 180 * 24 * time.Hour
15+
16+
// fileLastModified returns the last modification date of a file according to git.
17+
func (e *Engine) fileLastModified(ctx context.Context, file string) time.Time {
18+
cmd := exec.CommandContext(ctx, "git", "-C", e.repoRoot, "log", "-1", "--format=%aI", "--", file)
19+
out, err := cmd.Output()
20+
if err != nil {
21+
return time.Time{}
22+
}
23+
t, _ := time.Parse(time.RFC3339, strings.TrimSpace(string(out)))
24+
return t
25+
}
26+
1327
// CouplingGap represents a missing co-changed file.
1428
type CouplingGap struct {
1529
ChangedFile string `json:"changedFile"`
@@ -76,10 +90,18 @@ func (e *Engine) checkCouplingGaps(ctx context.Context, changedFiles []string, d
7690
missing = corr.File
7791
}
7892
if corr.Correlation >= minCorrelation && !changedSet[missing] && !isCouplingNoiseFile(missing) {
93+
// Skip stale couplings — if the coupled file hasn't been
94+
// modified in the last 180 days, the co-change relationship
95+
// is historical noise (e.g., test written once alongside source).
96+
lastMod := e.fileLastModified(ctx, missing)
97+
if !lastMod.IsZero() && time.Since(lastMod) > maxCouplingAge {
98+
continue
99+
}
79100
gaps = append(gaps, CouplingGap{
80101
ChangedFile: file,
81102
MissingFile: missing,
82103
CoChangeRate: corr.Correlation,
104+
LastCoChange: lastMod.Format(time.RFC3339),
83105
})
84106
}
85107
}

internal/query/review_health.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ type CodeHealthReport struct {
5151
// Coverage was removed because no coverage data source is available yet.
5252
// When coverage is added, reduce churn and cyclomatic by 0.05 each.
5353
const (
54-
weightCyclomatic = 0.25
55-
weightCognitive = 0.15
54+
weightCyclomatic = 0.15
55+
weightCognitive = 0.25
5656
weightFileSize = 0.10
5757
weightChurn = 0.15
5858
weightCoupling = 0.10

internal/query/review_split.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func (e *Engine) suggestPRSplit(ctx context.Context, diffStats []git.DiffStats,
4343
statsMap[ds.FilePath] = ds
4444
}
4545

46+
// For very large PRs, skip coupling analysis (O(n) git calls)
47+
// and rely on module-based clustering only
48+
skipCoupling := len(diffStats) > 200
49+
4650
// Build adjacency graph: files are connected if they share a module
4751
// or have high coupling correlation
4852
adj := make(map[string]map[string]bool)
@@ -70,7 +74,9 @@ func (e *Engine) suggestPRSplit(ctx context.Context, diffStats []git.DiffStats,
7074
}
7175

7276
// Connect files with high coupling
73-
e.addCouplingEdges(ctx, files, adj)
77+
if !skipCoupling {
78+
e.addCouplingEdges(ctx, files, adj)
79+
}
7480

7581
// Find connected components using BFS
7682
visited := make(map[string]bool)
@@ -84,6 +90,19 @@ func (e *Engine) suggestPRSplit(ctx context.Context, diffStats []git.DiffStats,
8490
components = append(components, component)
8591
}
8692

93+
const maxClusters = 20
94+
if len(components) > maxClusters {
95+
// Merge smallest clusters into an "other" bucket
96+
sort.Slice(components, func(i, j int) bool {
97+
return len(components[i]) > len(components[j])
98+
})
99+
var other []string
100+
for i := maxClusters - 1; i < len(components); i++ {
101+
other = append(other, components[i]...)
102+
}
103+
components = append(components[:maxClusters-1], other)
104+
}
105+
87106
if len(components) <= 1 {
88107
return &PRSplitSuggestion{
89108
ShouldSplit: false,

0 commit comments

Comments
 (0)