Skip to content

Commit 9fe04f0

Browse files
committed
refactor: satisfy scalable contract linters
1 parent f8d6d13 commit 9fe04f0

12 files changed

Lines changed: 346 additions & 220 deletions

internal/app/hydration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type fakeHydrationReader struct {
2828
failWith error
2929
}
3030

31-
func (f *fakeHydrationReader) ListIssueTimeline(ctx context.Context, owner, name string, number int, opts github.PageOptions) (github.ListResult[github.IssueTimelineEvent], error) {
31+
func (f *fakeHydrationReader) ListIssueTimeline(_ context.Context, _, _ string, _ int, opts github.PageOptions) (github.ListResult[github.IssueTimelineEvent], error) {
3232
idx := f.issueTimelineCalls
3333
if idx >= len(f.issueTimelinePages) {
3434
return github.ListResult[github.IssueTimelineEvent]{Page: github.PageInfo{Page: opts.Page, PerPage: opts.PerPage}}, nil

internal/app/mcp.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,11 @@ func (r *MCPReader) GetCoverage(ctx context.Context, in mcpserver.GetCoverageInp
610610
key := coverageTargetKey(target)
611611
item := mcpserver.BatchItem[mcpserver.CoverageOutput]{Key: key, Status: "complete"}
612612
value, reason, err := readCoverageTarget(ctx, c, target)
613-
if err != nil {
613+
if errors.Is(err, errInvalidCoverageTarget) {
614+
item.Status, item.Reason = "unavailable", "invalid_reference"
615+
item.Message = "owner/repo and optional kind/number must identify a repository or exact thread"
616+
out.Status = "partial"
617+
} else if err != nil {
614618
item.Status, item.Reason, item.Message = "failed", "read_failed", err.Error()
615619
out.Status = "partial"
616620
} else if reason != "" {
@@ -638,14 +642,16 @@ func coverageTargetKey(target mcpserver.CoverageTarget) string {
638642
return key
639643
}
640644

645+
var errInvalidCoverageTarget = errors.New("invalid coverage target")
646+
641647
func readCoverageTarget(ctx context.Context, c *corpus.Corpus, target mcpserver.CoverageTarget) (mcpserver.CoverageOutput, string, error) {
642648
ref := domain.RepoRef{Owner: target.Owner, Repo: target.Repo}
643649
if err := ref.Validate(); err != nil {
644-
return mcpserver.CoverageOutput{}, "invalid_reference", nil
650+
return mcpserver.CoverageOutput{}, "invalid_reference", fmt.Errorf("%w: %v", errInvalidCoverageTarget, err)
645651
}
646652
isThread := target.Kind != "" || target.Number != 0
647653
if isThread && ((target.Kind != "issue" && target.Kind != "pull_request") || target.Number < 1) {
648-
return mcpserver.CoverageOutput{}, "invalid_reference", nil
654+
return mcpserver.CoverageOutput{}, "invalid_reference", errInvalidCoverageTarget
649655
}
650656
repo, err := c.GetRepository(ctx, ref.Owner, ref.Repo)
651657
if err != nil {

internal/app/mcp_jobs.go

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,69 +31,70 @@ func (r *MCPReader) CancelJobs(ctx context.Context, in mcpserver.CancelJobInput)
3131
if err := ctx.Err(); err != nil {
3232
return out, err
3333
}
34-
id := strings.TrimSpace(inputID)
35-
item := mcpserver.BatchItem[mcpserver.GetJobOutput]{Key: id, Status: "complete"}
36-
if id == "" {
37-
item.Status, item.Reason, item.Message = "failed", "invalid_id", "job ID must not be empty"
38-
out.Status = "partial"
39-
out.Items[i] = item
40-
continue
41-
}
42-
current, err := r.Service.GetJob(ctx, id)
43-
if err != nil {
44-
var cliErr *cli.CLIError
45-
if errors.As(err, &cliErr) && cliErr.Code == cli.ExitNotFound {
46-
item.Status, item.Reason = "unavailable", "not_found"
47-
} else {
48-
item.Status, item.Reason = "failed", "read_failed"
49-
}
50-
item.Message = err.Error()
51-
out.Status = "partial"
52-
out.Items[i] = item
53-
continue
54-
}
55-
if current.Status == "cancelled" {
56-
value, err := jobResultToMCP(current)
57-
if err != nil {
58-
return mcpserver.GetJobsOutput{}, err
59-
}
60-
item.Value = &value
61-
out.Items[i] = item
62-
continue
63-
}
64-
if current.Status == "succeeded" || current.Status == "failed" {
65-
item.Status, item.Reason, item.Message = "unavailable", "terminal", "job is already "+current.Status
66-
out.Status = "partial"
67-
out.Items[i] = item
68-
continue
69-
}
70-
job, err := r.Service.CancelJob(ctx, id)
71-
if err != nil {
72-
// A terminal transition can race the request; report its latest durable
73-
// state as unavailable rather than failing unrelated cancellations.
74-
latest, getErr := r.Service.GetJob(ctx, id)
75-
if getErr == nil && (latest.Status == "succeeded" || latest.Status == "failed") {
76-
item.Status, item.Reason, item.Message = "unavailable", "terminal", "job is already "+latest.Status
77-
} else {
78-
item.Status, item.Reason, item.Message = "failed", "cancellation_failed", err.Error()
79-
}
80-
out.Status = "partial"
81-
out.Items[i] = item
82-
continue
83-
}
84-
value, err := jobResultToMCP(job)
34+
item, err := r.cancelJobItem(ctx, inputID)
8535
if err != nil {
8636
return mcpserver.GetJobsOutput{}, err
8737
}
88-
item.Value = &value
89-
if value.Status == "running" {
90-
item.NextAction = "Poll jobs.get until this job reaches a terminal state."
38+
if item.Status != "complete" {
39+
out.Status = "partial"
9140
}
9241
out.Items[i] = item
9342
}
9443
return out, nil
9544
}
9645

46+
func (r *MCPReader) cancelJobItem(ctx context.Context, inputID string) (mcpserver.BatchItem[mcpserver.GetJobOutput], error) {
47+
id := strings.TrimSpace(inputID)
48+
item := mcpserver.BatchItem[mcpserver.GetJobOutput]{Key: id, Status: "complete"}
49+
if id == "" {
50+
item.Status, item.Reason, item.Message = "failed", "invalid_id", "job ID must not be empty"
51+
return item, nil
52+
}
53+
current, err := r.Service.GetJob(ctx, id)
54+
if err != nil {
55+
var cliErr *cli.CLIError
56+
if errors.As(err, &cliErr) && cliErr.Code == cli.ExitNotFound {
57+
item.Status, item.Reason = "unavailable", "not_found"
58+
} else {
59+
item.Status, item.Reason = "failed", "read_failed"
60+
}
61+
item.Message = err.Error()
62+
return item, nil
63+
}
64+
if current.Status == "cancelled" {
65+
return jobResultItem(item, current)
66+
}
67+
if current.Status == "succeeded" || current.Status == "failed" {
68+
item.Status, item.Reason, item.Message = "unavailable", "terminal", "job is already "+current.Status
69+
return item, nil
70+
}
71+
job, err := r.CancelJob(ctx, id)
72+
if err != nil {
73+
// A terminal transition can race the request; report its latest durable
74+
// state as unavailable rather than failing unrelated cancellations.
75+
latest, getErr := r.Service.GetJob(ctx, id)
76+
if getErr == nil && (latest.Status == "succeeded" || latest.Status == "failed") {
77+
item.Status, item.Reason, item.Message = "unavailable", "terminal", "job is already "+latest.Status
78+
} else {
79+
item.Status, item.Reason, item.Message = "failed", "cancellation_failed", err.Error()
80+
}
81+
return item, nil
82+
}
83+
return jobResultItem(item, job)
84+
}
85+
86+
func jobResultItem(item mcpserver.BatchItem[mcpserver.GetJobOutput], job *cli.JobResult) (mcpserver.BatchItem[mcpserver.GetJobOutput], error) {
87+
value, err := jobResultToMCP(job)
88+
if err != nil {
89+
return item, err
90+
}
91+
item.Value = &value
92+
if value.Status == "running" {
93+
item.NextAction = "Poll jobs.get until this job reaches a terminal state."
94+
}
95+
return item, nil
96+
}
97+
9798
func jobResultToMCP(job *cli.JobResult) (mcpserver.GetJobOutput, error) {
9899
request, err := decodeJobJSON("request", job.Request)
99100
if err != nil {

internal/app/mcp_portfolio_relationships.go

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,38 +26,10 @@ func (r *MCPReader) FindPortfolioOverlaps(ctx context.Context, in mcpserver.Find
2626
return mcpserver.FindPortfolioOverlapsOutput{}, err
2727
}
2828
out := mcpserver.FindPortfolioOverlapsOutput{Status: "complete", Items: make([]mcpserver.BatchItem[mcpserver.PortfolioOverlapOutput], len(in.Candidates))}
29-
var candidates []corpus.PortfolioSubject
30-
var candidateIndexes []int
31-
for i, candidate := range in.Candidates {
32-
item := mcpserver.BatchItem[mcpserver.PortfolioOverlapOutput]{Key: candidate.Kind + ":" + candidate.Ref}
33-
if !validPortfolioSubjectInput(candidate) {
34-
item.Status, item.Reason, item.Message = "failed", "invalid_candidate", "kind must be opportunity, workspace, or pull_request and ref must be a valid local ID"
35-
out.Status, out.Items[i] = "partial", item
36-
continue
37-
}
38-
candidates = append(candidates, corpus.PortfolioSubject{Kind: candidate.Kind, Ref: strings.TrimSpace(candidate.Ref)})
39-
candidateIndexes = append(candidateIndexes, i)
40-
}
41-
var prIDs []int64
42-
missingPullRequests := false
43-
for _, ref := range in.PullRequests {
44-
repo, err := c.GetRepository(ctx, ref.Owner, ref.Repo)
45-
if err != nil {
46-
return mcpserver.FindPortfolioOverlapsOutput{}, err
47-
}
48-
if repo == nil {
49-
missingPullRequests = true
50-
continue
51-
}
52-
thread, err := c.GetThreadByNumber(ctx, repo.ID, ref.Number)
53-
if err != nil {
54-
return mcpserver.FindPortfolioOverlapsOutput{}, err
55-
}
56-
if thread == nil || thread.Kind != corpus.ThreadKindPullRequest {
57-
missingPullRequests = true
58-
continue
59-
}
60-
prIDs = append(prIDs, thread.ID)
29+
candidates, candidateIndexes := collectPortfolioCandidates(in.Candidates, &out)
30+
prIDs, missingPullRequests, err := resolvePortfolioPullRequests(ctx, c, in.PullRequests)
31+
if err != nil {
32+
return mcpserver.FindPortfolioOverlapsOutput{}, err
6133
}
6234
if len(candidates) == 0 {
6335
return out, nil
@@ -75,18 +47,7 @@ func (r *MCPReader) FindPortfolioOverlaps(ctx context.Context, in mcpserver.Find
7547
}
7648
for resultIndex, result := range results {
7749
i := candidateIndexes[resultIndex]
78-
value := mcpserver.PortfolioOverlapOutput{Candidate: mcpserver.PortfolioSubjectInput{Kind: result.Candidate.Kind, Ref: result.Candidate.Ref}, Status: result.Status, Coverage: result.Coverage}
79-
for _, match := range result.Matches {
80-
converted := mcpserver.PortfolioOverlapMatchOutput{PullRequestThreadID: match.PullRequestThreadID}
81-
for _, evidence := range match.Evidence {
82-
item := mcpserver.PortfolioOverlapEvidenceOutput{Kind: evidence.Kind, Value: evidence.Value, Score: evidence.Score}
83-
for _, ref := range evidence.SourceObservationRefs {
84-
item.SourceRefs = append(item.SourceRefs, ref.Kind+":"+strconv.FormatInt(ref.ID, 10))
85-
}
86-
converted.Evidence = append(converted.Evidence, item)
87-
}
88-
value.Matches = append(value.Matches, converted)
89-
}
50+
value := portfolioOverlapOutput(result)
9051
batch := mcpserver.BatchItem[mcpserver.PortfolioOverlapOutput]{Key: result.Candidate.Kind + ":" + result.Candidate.Ref, Status: "complete", Value: &value}
9152
if missingPullRequests {
9253
out.Status = "partial"
@@ -100,6 +61,63 @@ func (r *MCPReader) FindPortfolioOverlaps(ctx context.Context, in mcpserver.Find
10061
return out, nil
10162
}
10263

64+
func collectPortfolioCandidates(inputs []mcpserver.PortfolioSubjectInput, out *mcpserver.FindPortfolioOverlapsOutput) ([]corpus.PortfolioSubject, []int) {
65+
var candidates []corpus.PortfolioSubject
66+
var indexes []int
67+
for i, candidate := range inputs {
68+
item := mcpserver.BatchItem[mcpserver.PortfolioOverlapOutput]{Key: candidate.Kind + ":" + candidate.Ref}
69+
if !validPortfolioSubjectInput(candidate) {
70+
item.Status, item.Reason, item.Message = "failed", "invalid_candidate", "kind must be opportunity, workspace, or pull_request and ref must be a valid local ID"
71+
out.Status, out.Items[i] = "partial", item
72+
continue
73+
}
74+
candidates = append(candidates, corpus.PortfolioSubject{Kind: candidate.Kind, Ref: strings.TrimSpace(candidate.Ref)})
75+
indexes = append(indexes, i)
76+
}
77+
return candidates, indexes
78+
}
79+
80+
func resolvePortfolioPullRequests(ctx context.Context, c *corpus.Corpus, refs []mcpserver.ThreadRef) ([]int64, bool, error) {
81+
var ids []int64
82+
missing := false
83+
for _, ref := range refs {
84+
repo, err := c.GetRepository(ctx, ref.Owner, ref.Repo)
85+
if err != nil {
86+
return nil, false, err
87+
}
88+
if repo == nil {
89+
missing = true
90+
continue
91+
}
92+
thread, err := c.GetThreadByNumber(ctx, repo.ID, ref.Number)
93+
if err != nil {
94+
return nil, false, err
95+
}
96+
if thread == nil || thread.Kind != corpus.ThreadKindPullRequest {
97+
missing = true
98+
continue
99+
}
100+
ids = append(ids, thread.ID)
101+
}
102+
return ids, missing, nil
103+
}
104+
105+
func portfolioOverlapOutput(result corpus.PortfolioOverlapResult) mcpserver.PortfolioOverlapOutput {
106+
value := mcpserver.PortfolioOverlapOutput{Candidate: mcpserver.PortfolioSubjectInput{Kind: result.Candidate.Kind, Ref: result.Candidate.Ref}, Status: result.Status, Coverage: result.Coverage}
107+
for _, match := range result.Matches {
108+
converted := mcpserver.PortfolioOverlapMatchOutput{PullRequestThreadID: match.PullRequestThreadID}
109+
for _, evidence := range match.Evidence {
110+
item := mcpserver.PortfolioOverlapEvidenceOutput{Kind: evidence.Kind, Value: evidence.Value, Score: evidence.Score}
111+
for _, ref := range evidence.SourceObservationRefs {
112+
item.SourceRefs = append(item.SourceRefs, ref.Kind+":"+strconv.FormatInt(ref.ID, 10))
113+
}
114+
converted.Evidence = append(converted.Evidence, item)
115+
}
116+
value.Matches = append(value.Matches, converted)
117+
}
118+
return value
119+
}
120+
103121
func validPortfolioSubjectInput(candidate mcpserver.PortfolioSubjectInput) bool {
104122
ref := strings.TrimSpace(candidate.Ref)
105123
if ref == "" {

0 commit comments

Comments
 (0)