Compare commits

..

1 Commits

Author SHA1 Message Date
u80864958
9f6c6830a6 feat: guidelines 2026-02-13 18:20:57 +01:00
6 changed files with 10 additions and 69 deletions

View File

@@ -45,11 +45,10 @@ type ReviewConfig struct {
MaxChunkSize int `help:"Maximum diff chunk size in bytes" default:"60000"` MaxChunkSize int `help:"Maximum diff chunk size in bytes" default:"60000"`
Guidelines []string `help:"Project guidelines to prepend" sep:","` Guidelines []string `help:"Project guidelines to prepend" sep:","`
DisableComments bool `help:"Disable posting comments (dry run)"` DisableComments bool `help:"Disable posting comments (dry run)"`
SanityCheck bool `help:"Run sanitycheck LLM prompts per comment" default:"true"`
} }
type Config struct { type Config struct {
LogLevel string `help:"Log verbosity: debug, info, warn, error" default:"info"` LogLevel string `help:"Log verbosity: debug, info, warn, error"`
// Embedding ReviewConfig with a prefix changes flag names to `--review-…`. // Embedding ReviewConfig with a prefix changes flag names to `--review-…`.
// Existing configuration files using the old flag names will need to be updated. // Existing configuration files using the old flag names will need to be updated.
@@ -153,7 +152,6 @@ func main() {
} }
pierreService := pierre.New(ai, git, cfg.Review.MaxChunkSize, cfg.Review.Guidelines, cfg.Review.DisableComments) pierreService := pierre.New(ai, git, cfg.Review.MaxChunkSize, cfg.Review.Guidelines, cfg.Review.DisableComments)
pierreService.SetSanityCheck(cfg.Review.SanityCheck)
if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil { if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil {
log.Fatalf("Error during review: %v", err) log.Fatalf("Error during review: %v", err)
} }

View File

@@ -31,8 +31,6 @@ func (b *BitbucketAdapter) GetFileContent(ctx context.Context, projectKey, repos
if err != nil { if err != nil {
return "", err return "", err
} }
// Ensure raw file retrieval
r.Header.Set("Accept", "application/octet-stream")
if ref != "" { if ref != "" {
q := r.URL.Query() q := r.URL.Query()
q.Set("at", ref) q.Set("at", ref)

View File

@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"net/http"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/pierre" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/pierre"
@@ -51,14 +50,11 @@ func (g *Adapter) AddComment(ctx context.Context, owner, repo string, prID int,
}, },
}, },
} }
_, resp, err := g.client.CreatePullReview(owner, repo, int64(prID), opts) _, _, err := g.client.CreatePullReview(owner, repo, int64(prID), opts)
if err != nil { if err != nil {
slog.Error("Gitea AddComment failed", "err", err) slog.Error("Gitea AddComment failed", "err", err)
return err return err
} }
if resp != nil && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("unexpected status %d creating comment", resp.StatusCode)
}
slog.Info("Gitea AddComment succeeded", "pr", prID) slog.Info("Gitea AddComment succeeded", "pr", prID)
return nil return nil
} }

View File

@@ -1,44 +0,0 @@
package pierre
import (
"context"
"testing"
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
)
type overlapChat struct{ callCount int }
func (m *overlapChat) GenerateStructured(ctx context.Context, msgs []chatter.Message, target interface{}) error {
m.callCount++
if cSlice, ok := target.(*[]Comment); ok {
// Return two comments with same file and line to test deduplication
*cSlice = []Comment{{File: "dup.go", Line: 10, Message: "first"}, {File: "dup.go", Line: 10, Message: "second"}}
return nil
}
return nil
}
func (m *overlapChat) GetProviderName() string { return "mock" }
func TestJudgePR_DeduplicationOverlap(t *testing.T) {
chat := &overlapChat{}
svc := &Service{
maxChunkSize: 1000,
guidelines: nil,
git: &mockGit{},
chat: chat,
}
diffReader, err := svc.git.GetDiff(context.Background(), "", "", 0)
if err != nil {
t.Fatalf("failed to get diff: %v", err)
}
defer diffReader.Close()
comments, err := svc.judgePR(context.Background(), diffReader)
if err != nil {
t.Fatalf("judgePR error: %v", err)
}
if len(comments) != 1 {
t.Fatalf("expected 1 deduplicated comment, got %d", len(comments))
}
}

View File

@@ -17,7 +17,6 @@ import (
type Service struct { type Service struct {
maxChunkSize int maxChunkSize int
guidelines []string // stored as slice of lines; legacy, see WithGuidelines guidelines []string // stored as slice of lines; legacy, see WithGuidelines
skipSanityCheck bool // if true, skip LLM sanitycheck prompts per comment
disableComments bool disableComments bool
git GitAdapter git GitAdapter
chat ChatAdapter chat ChatAdapter
@@ -30,7 +29,6 @@ func New(chat ChatAdapter, git GitAdapter, maxChunkSize int, guidelines []string
chat: chat, chat: chat,
maxChunkSize: maxChunkSize, maxChunkSize: maxChunkSize,
guidelines: guidelines, guidelines: guidelines,
skipSanityCheck: false,
disableComments: disableComments, disableComments: disableComments,
} }
} }
@@ -51,11 +49,6 @@ func (s *Service) WithGuidelines(md string) error {
// parseGuidelinesFromString splits a markdown string into trimmed, nonempty // parseGuidelinesFromString splits a markdown string into trimmed, nonempty
// lines and ensures the total number of lines does not exceed 1000. // lines and ensures the total number of lines does not exceed 1000.
func (s *Service) SetSanityCheck(enabled bool) {
s.skipSanityCheck = !enabled
}
// parseGuidelinesFromString splits a markdown string into trimmed, nonempty
func parseGuidelinesFromString(md string) ([]string, error) { func parseGuidelinesFromString(md string) ([]string, error) {
var result []string var result []string
// Split on newline. Handles both \n and \r\n because TrimSpace removes \r. // Split on newline. Handles both \n and \r\n because TrimSpace removes \r.

View File

@@ -3,7 +3,7 @@ package pierre
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog" "log"
"git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter"
) )
@@ -22,17 +22,17 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
return fmt.Errorf("error judging PR: %w", err) return fmt.Errorf("error judging PR: %w", err)
} }
// ---------- Sanitycheck step ---------- // ---------- Sanitycheck step (always enabled) ----------
headSHA, err := s.git.GetPRHeadSHA(ctx, organisation, repo, prID) headSHA, err := s.git.GetPRHeadSHA(ctx, organisation, repo, prID)
if err != nil { if err != nil {
slog.Warn("could not fetch PR head SHA", "error", err) log.Printf("warning: could not fetch PR head SHA (%v); skipping sanity check", err)
} else if !s.skipSanityCheck { } else {
filtered := []Comment{} filtered := []Comment{}
for _, c := range comments { for _, c := range comments {
// Retrieve full file content at the PR head // Retrieve full file content at the PR head
fileContent, fErr := s.git.GetFileContent(ctx, organisation, repo, c.File, headSHA) fileContent, fErr := s.git.GetFileContent(ctx, organisation, repo, c.File, headSHA)
if fErr != nil { if fErr != nil {
slog.Warn("failed to fetch file", "path", c.File, "error", fErr) log.Printf("failed to fetch file %s: %v keeping original comment", c.File, fErr)
filtered = append(filtered, c) filtered = append(filtered, c)
continue continue
} }
@@ -47,7 +47,7 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
} }
var res sanityResult var res sanityResult
if err := s.chat.GenerateStructured(ctx, []chatter.Message{{Role: chatter.RoleSystem, Content: systemPrompt}, {Role: chatter.RoleUser, Content: userPrompt}}, &res); err != nil { if err := s.chat.GenerateStructured(ctx, []chatter.Message{{Role: chatter.RoleSystem, Content: systemPrompt}, {Role: chatter.RoleUser, Content: userPrompt}}, &res); err != nil {
slog.Error("sanity check error", "file", c.File, "line", c.Line, "error", err) log.Printf("sanity check error for %s:%d: %v keeping comment", c.File, c.Line, err)
filtered = append(filtered, c) filtered = append(filtered, c)
continue continue
} }
@@ -56,7 +56,7 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
c.Message = fmt.Sprintf("%s (Reason: %s)", c.Message, res.Reason) c.Message = fmt.Sprintf("%s (Reason: %s)", c.Message, res.Reason)
filtered = append(filtered, c) filtered = append(filtered, c)
} else { } else {
slog.Info("comment discarded", "file", c.File, "line", c.Line, "reason", res.Reason) log.Printf("comment on %s:%d discarded: %s", c.File, c.Line, res.Reason)
} }
} }
comments = filtered comments = filtered
@@ -75,7 +75,7 @@ func (s *Service) MakeReview(ctx context.Context, organisation string, repo stri
if !s.disableComments { if !s.disableComments {
if err := s.git.AddComment(ctx, organisation, repo, prID, c); err != nil { if err := s.git.AddComment(ctx, organisation, repo, prID, c); err != nil {
slog.Error("failed to add comment", "error", err) log.Printf("Failed to add comment: %v", err)
} }
} }
} }