-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathservice.go
More file actions
108 lines (99 loc) · 3.03 KB
/
service.go
File metadata and controls
108 lines (99 loc) · 3.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package review
import (
"context"
"fmt"
"strings"
)
// GraphContext is the small graph-side dep ReviewService needs. The
// concrete implementation lives in mcp/cli code; this interface keeps the
// review package decoupled from the graph.Store. nil is acceptable —
// review will just send the diff without graph evidence.
type GraphContext interface {
// EvidenceForFile returns a short, deterministic textual summary of
// the graph context for a path: nodes-in-file, callers/dependents,
// frameworks detected. Free-form; whatever the LLM finds useful.
EvidenceForFile(path string) string
}
// Service orchestrates diff → graph evidence → LLM call. Stateless;
// pass-by-value safe.
type Service struct {
Client *Client
Graph GraphContext // may be nil
}
// NewService wires the Service. Graph may be nil for non-enriched
// invocations (the LLM gets just the diff).
func NewService(client *Client, graph GraphContext) *Service {
return &Service{Client: client, Graph: graph}
}
// Review runs the full pipeline against the given working tree and refs.
// Pass an empty base to compare against HEAD~1; empty head defaults to HEAD.
// FocusFiles, if non-empty, limits the review to those file paths.
func (s *Service) Review(ctx context.Context, cwd, base, head string, focusFiles []string) (*Report, error) {
if base == "" {
base = "HEAD~1"
}
if head == "" {
head = "HEAD"
}
files, err := GitDiff(cwd, base, head)
if err != nil {
return nil, err
}
files = filterFocus(files, focusFiles)
if maxF := maxFilesFromClient(s.Client); maxF > 0 && len(files) > maxF {
files = files[:maxF]
}
prompt := s.buildPrompt(base, head, files)
return s.Client.Review(ctx, prompt)
}
func filterFocus(files []ChangedFile, focus []string) []ChangedFile {
if len(focus) == 0 {
return files
}
want := make(map[string]struct{}, len(focus))
for _, f := range focus {
want[f] = struct{}{}
}
var out []ChangedFile
for _, f := range files {
if _, ok := want[f.Path]; ok {
out = append(out, f)
}
}
return out
}
func maxFilesFromClient(c *Client) int {
if c == nil {
return 0
}
return c.Config.MaxFiles
}
// buildPrompt assembles the user message: a header with base..head, then
// per-file blocks containing the diff hunks plus graph evidence (when
// Graph is wired). The whole prompt is plain text — Markdown-ish for
// readability but not strictly formatted.
func (s *Service) buildPrompt(base, head string, files []ChangedFile) string {
var b strings.Builder
fmt.Fprintf(&b, "Reviewing %s..%s (%d changed file%s).\n\n", base, head, len(files), plural(len(files)))
for _, f := range files {
fmt.Fprintf(&b, "## File: %s\n", f.Path)
fmt.Fprintf(&b, "+%d / -%d lines\n", f.AddedLines, f.RemovedLines)
if s.Graph != nil {
if ev := s.Graph.EvidenceForFile(f.Path); ev != "" {
fmt.Fprintf(&b, "\nGraph evidence:\n%s\n", ev)
}
}
b.WriteString("\nDiff:\n```\n")
for _, h := range f.Hunks {
b.WriteString(h)
}
b.WriteString("```\n\n")
}
return b.String()
}
func plural(n int) string {
if n == 1 {
return ""
}
return "s"
}