Skip to content

Feature/opportunity ranking#436

Open
saidai-bhuvanesh wants to merge 2 commits into
agnivo988:mainfrom
saidai-bhuvanesh:feature/opportunity-ranking
Open

Feature/opportunity ranking#436
saidai-bhuvanesh wants to merge 2 commits into
agnivo988:mainfrom
saidai-bhuvanesh:feature/opportunity-ranking

Conversation

@saidai-bhuvanesh

@saidai-bhuvanesh saidai-bhuvanesh commented Jun 3, 2026

Copy link
Copy Markdown

Advanced scoring utilities (score.go), weight & age‑bonus logic, contributor‑count contribution. Will later host difficulty estimation and a ranked “Top N” list.

Summary by CodeRabbit

  • New Features
    • Added opportunities command to analyze GitHub repositories and calculate a Contribution Opportunity Score for the specified owner/repo
    • Classifies and evaluates open issues across multiple categories (good-first-issue, help-wanted, bug, enhancement, documentation)
    • Scores repositories based on issue classifications and contributor metrics to identify contribution opportunities

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a complete new feature for analyzing GitHub repositories and computing contribution opportunity scores. The system adds a CLI command that fetches open issues and contributor statistics from a specified repository, classifies issues by label (good-first-issue, help-wanted, bug, etc.), and computes a weighted opportunity score combining issue classification, issue age, and contributor count.

Changes

Opportunity Analysis Feature

Layer / File(s) Summary
Issue Classification Contract
internal/opportunity/classifier.go
Exports IssueClassification type with constants for categorizing GitHub issues (good-first-issue, help-wanted, bug, enhancement, documentation, other) and provides a Classify function that maps issue labels to classifications with nil-safety.
GitHub API Service
internal/opportunity/github_service.go
Implements GitHubService wrapper with ListIssues (paginated, 100 items per page), ListPullRequests (paginated), and ListContributors (with HTTP 202 retry and 2-second polling for stats completion).
Opportunity Scoring Logic
internal/opportunity/score.go
Defines IssueScore struct and calculateWeight (maps classification to base weight) and computeScore (sums issue weights plus age bonus plus capped contributor count) functions.
Opportunity Engine Orchestration
internal/opportunity/engine.go
Implements Engine type with Run method that fetches open issues and contributors via GitHubService, classifies issues, computes total score as goodFirst*2 + helpWanted + len(contributors), and returns a populated Report.
CLI Command Integration
cmd/opportunities.go
Adds opportunitiesCmd Cobra subcommand accepting owner/repo argument format, invokes Engine.Run, and prints Contribution Opportunity Score for owner/repo: <score> to stdout.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

ECWoC26, ECWoC26-L1

Poem

🐰 A rabbit hops through GitHub's flow,
Classifying issues high and low,
With scores computed, weighted right,
New opportunities come to light! ✨
Opportunities bloom where contributors play,
Your CLI leads them the right way!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Feature/opportunity ranking" is partially related to the changeset. It refers to a real aspect (opportunity ranking/scoring), but the main additions are a complete contributor opportunity analyzer with a new CLI command, GitHub service integration, issue classification, and scoring logic. The title is somewhat vague and doesn't capture that this is a new feature introducing both the command and the underlying infrastructure. Consider a more specific title that reflects the primary change, such as "Add contributor opportunity analyzer command with scoring" to better represent the new command, classification system, and scoring infrastructure being introduced.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/opportunities.go`:
- Around line 3-8: The build fails because strings.SplitN is used in this file
but the "strings" package is not imported; update the import block in
cmd/opportunities.go to include "strings" so functions referencing
strings.SplitN (used near the code that parses opportunity identifiers) compile
successfully—add "strings" alongside "context", "fmt", and the other imports.
- Around line 19-23: The current validation only checks parts length and allows
empty segments like "owner/" or "/repo"; update the validation around the parts
parsing (variables parts, owner, repo) to also ensure parts[0] and parts[1] are
non-empty and return a clear error (e.g., fmt.Errorf("invalid repository format,
expected owner/repo with non-empty owner and repo")) when either is empty so
invalid inputs are rejected early in the CLI path where ctx :=
context.Background() is created.

In `@internal/opportunity/classifier.go`:
- Around line 21-38: The current loop in classifier.go returns on the first
matching label which makes classification order-dependent; instead scan all
issue.Labels and compute a deterministic result by applying a precedence list
(good-first > help-wanted > bug > enhancement > docs), e.g., track which of the
known classifications are present while iterating (use issue.Labels and the
ClassificationGoodFirstIssue, ClassificationHelpWanted, ClassificationBug,
ClassificationEnhancement, ClassificationDocumentation symbols), then after the
loop return the highest-precedence classification (or default) so an issue with
multiple labels always yields the same classification.
- Around line 25-37: Normalize the label string before the switch in classify
logic: replace name := *lbl.Name with a normalized value (e.g.,
strings.TrimSpace(strings.ToLower(*lbl.Name))) so matches like "Good First
Issue" or "help wanted " hit the expected cases; add the strings import if
missing and keep the existing switch cases (ClassificationGoodFirstIssue,
ClassificationHelpWanted, ClassificationBug, ClassificationEnhancement,
ClassificationDocumentation) using the normalized name.
- Line 3: The build breaks because the package import
"github.com/google/go-github/v55/github" (used in
internal/opportunity/classifier.go, score.go, github_service.go, engine.go) is
not declared in go.mod/go.sum; fix by adding the module to go.mod and updating
go.sum—run a module fetch such as `go get github.com/google/go-github/v55` and
then `go mod tidy` (or equivalent tooling) so the dependency and matching v55
version are recorded and resolved before merging.

In `@internal/opportunity/engine.go`:
- Around line 3-8: Remove the unused time import from the import block in
internal/opportunity/engine.go: locate the import list that currently includes
"time" alongside "fmt", "context", and "github.com/google/go-github/v55/github"
and delete the "time" entry so the package builds cleanly (no other code changes
needed unless you actually need time elsewhere in functions within this file
such as in any function in the same package).
- Around line 41-52: The local ad-hoc scoring (variables goodFirst, helpWanted
and the formula score := goodFirst*2 + helpWanted + len(contributors)) must be
replaced with the package's shared scorer from score.go so we use the canonical
weights, age bonus, and contributor cap; remove the manual Classify/count loop
and instead call the shared scoring function defined in
internal/opportunity/score.go (the scorer used elsewhere in the package) passing
the same issues and contributors, and assign its return to score so this code
uses the same scoring contract as the rest of the package.
- Line 5: Remove the unused time import in internal/opportunity/engine.go; add a
require line for github.com/google/go-github v55 to go.mod and run go mod tidy
so the module is recorded (and update go.sum). In engine.go update Run to call
the existing computeScore function from internal/opportunity/score.go (instead
of computing the score inline) and ensure it supplies any required parameters;
also either use the Engine.e.client inside Run (e.g., to fetch GitHub data) or
remove the unused e.client field/parameter from NewEngine so there are no unused
fields.

In `@internal/opportunity/github_service.go`:
- Around line 53-61: In ListContributors (the loop calling
s.client.Repositories.ListContributorsStats) currently returns immediately on
err != nil, which prevents retrying for GitHub's 202 case; change the error
handling to detect github.AcceptedError (type *github.AcceptedError) and treat
it like a non-fatal retry: if err is an AcceptedError (or resp.StatusCode ==
202) then wait (respecting ctx.Done()) and continue the loop, otherwise return
the error; ensure the wait honors ctx cancellation (select on ctx.Done() vs
time.After) so the method exits promptly when context is cancelled.
- Around line 17-31: ListIssues is returning both issues and PRs (because
github.Issue objects for PRs have PullRequestLinks set), which inflates scoring;
modify internal/opportunity/github_service.go inside GitHubService.ListIssues to
skip any issue where issue.PullRequestLinks != nil before appending to all,
keeping pagination logic intact. Also add the missing time import in
internal/opportunity/score.go (ensure the file imports "time") so calls to
time.Since compile. Reference: GitHubService.ListIssues and the score.go usage
of time.Since.
- Around line 13-15: NewGitHubService currently constructs a github client with
no timeout; change NewGitHubService to create the client with an http.Client
that sets a reasonable Timeout (or accept/require a context with deadline) so
API calls cannot hang indefinitely. When listing issues with Issues.ListByRepo
(used in GitHubService methods that enumerate issues), filter out pull requests
by checking each issue's PullRequest field (skip if iss.PullRequest != nil)
before classifying/scoping. For contributor stats handling, stop relying on
resp.StatusCode==202 after err==nil and instead detect GitHub "accepted"
responses by checking the error type using errors.As(err,
new(*github.AcceptedError)) (and retry on that condition); apply this change in
the method that fetches contributor stats in GitHubService. Ensure you update
any callers (e.g., functions that pass context.Background()) to pass a context
with timeout if you choose the context-deadline approach.

In `@internal/opportunity/score.go`:
- Around line 27-52: The score logic in engine.go is bypassing computeScore;
replace the inline calculation (goodFirst*2 + helpWanted + len(contributors))
with a call to computeScore, passing the issues slice and the contributors slice
to computeScore(issues, contributors), and remove the duplicated weighting/age
logic there; ensure the types match (issues as []*github.Issue and contributors
as []*github.ContributorStats) or adapt computeScore signature accordingly and
import/update any packages needed so the CLI uses the centralized computeScore
implementation.
- Around line 30-36: Guard against nil entries in the issues slice before
dereferencing fields: in the loop over issues (for _, iss := range issues) add a
nil-check (if iss == nil { continue }) before calling calculateWeight(iss) and
before accessing iss.CreatedAt so you don't panic on nil pointers; update
references around calculateWeight, iss, and the CreatedAt ageDays calculation to
early-continue when iss is nil.
- Line 3: The file is missing the time package import used by computeScore (it
calls time.Since); update the import block to include "time" (either add a
separate import or include it in the existing import group with
"github.com/google/go-github/v55/github") so computeScore can call time.Since
and the package will compile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 695b8af3-73cc-45d3-bef0-68b5cc125c26

📥 Commits

Reviewing files that changed from the base of the PR and between 6d6fa89 and d2a58ee.

📒 Files selected for processing (5)
  • cmd/opportunities.go
  • internal/opportunity/classifier.go
  • internal/opportunity/engine.go
  • internal/opportunity/github_service.go
  • internal/opportunity/score.go

Comment thread cmd/opportunities.go
Comment on lines +3 to +8
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/agnivo988/Repo-lyzer/internal/opportunity"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing strings import causes build failure.

strings.SplitN is used on Line 18, but strings is not imported in Lines 3-8, so this file won’t compile.

Proposed fix
 import (
     "context"
     "fmt"
+    "strings"
     "github.com/spf13/cobra"
     "github.com/agnivo988/Repo-lyzer/internal/opportunity"
 )

Also applies to: 18-18

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/opportunities.go` around lines 3 - 8, The build fails because
strings.SplitN is used in this file but the "strings" package is not imported;
update the import block in cmd/opportunities.go to include "strings" so
functions referencing strings.SplitN (used near the code that parses opportunity
identifiers) compile successfully—add "strings" alongside "context", "fmt", and
the other imports.

Comment thread cmd/opportunities.go
Comment on lines +19 to +23
if len(parts) != 2 {
return fmt.Errorf("invalid repository format, expected owner/repo")
}
owner, repo = parts[0], parts[1]
ctx := context.Background()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject empty owner/repo segments during CLI validation.

The current check accepts inputs like owner/ or /repo. This passes invalid values downstream and fails later with noisier errors; validate non-empty owner and repo at this boundary.

Proposed fix
         parts := strings.SplitN(ownerRepo, "/", 2)
-        if len(parts) != 2 {
+        if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
             return fmt.Errorf("invalid repository format, expected owner/repo")
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/opportunities.go` around lines 19 - 23, The current validation only
checks parts length and allows empty segments like "owner/" or "/repo"; update
the validation around the parts parsing (variables parts, owner, repo) to also
ensure parts[0] and parts[1] are non-empty and return a clear error (e.g.,
fmt.Errorf("invalid repository format, expected owner/repo with non-empty owner
and repo")) when either is empty so invalid inputs are rejected early in the CLI
path where ctx := context.Background() is created.

@@ -0,0 +1,40 @@
package opportunity

import "github.com/google/go-github/v55/github"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify missing module declaration for go-github v55 in manifest files.
set -euo pipefail

fd -a '^go\.mod$' | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,220p" "{}"'
fd -a '^go\.sum$' | xargs -I{} sh -c 'echo "== {} (go-github entries) =="; rg -n "github.com/google/go-github" "{}" || true'

Repository: agnivo988/Repo-lyzer

Length of output: 2515


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact import in the referenced file
if [ -f internal/opportunity/classifier.go ]; then
  echo "== internal/opportunity/classifier.go =="
  sed -n '1,120p' internal/opportunity/classifier.go | nl -ba | sed -n '1,80p'
else
  echo "Missing file: internal/opportunity/classifier.go"
fi

# Check whether any go-github (any version) is referenced anywhere in the repo
echo "== Searching for github.com/google/go-github =="
rg -n "github\.com/google/go-github" . || true

# Check go.mod/go.sum for any go-github versions (not just v55)
echo "== Checking go.mod for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.mod || true
echo "== Checking go.sum for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.sum || true

Repository: agnivo988/Repo-lyzer

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show referenced import lines (no nl dependency)
if [ -f internal/opportunity/classifier.go ]; then
  echo "== internal/opportunity/classifier.go =="
  cat -n internal/opportunity/classifier.go | sed -n '1,120p'
else
  echo "Missing file: internal/opportunity/classifier.go"
fi

echo "== Searching for github.com/google/go-github =="
rg -n "github\.com/google/go-github" . || true

echo "== Checking go.mod for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.mod || true

echo "== Checking go.sum for github.com/google/go-github =="
rg -n "github\.com/google/go-github" go.sum || true

Repository: agnivo988/Repo-lyzer

Length of output: 2195


Build is blocked: github.com/google/go-github/v55 is imported but not declared in go.mod / go.sum

  • internal/opportunity/classifier.go imports github.com/google/go-github/v55/github (same v55 import is also used in internal/opportunity/score.go, internal/opportunity/github_service.go, and internal/opportunity/engine.go).
  • go.mod and go.sum contain no github.com/google/go-github entries, so module resolution/typecheck will fail.

Add github.com/google/go-github/v55 to go.mod and update go.sum (e.g., via go get ... and go mod tidy) before merge.

🧰 Tools
🪛 GitHub Actions: CI/CD Pipeline / test

[error] 3-3: go test ./... failed during package setup: no required module provides package github.com/google/go-github/v55/github. Run go get github.com/google/go-github/v55/github and/or add the dependency to go.mod.

🪛 GitHub Check: test

[failure] 3-3:
no required module provides package github.com/google/go-github/v55/github; to add it:


[failure] 3-3:
no required module provides package github.com/google/go-github/v55/github; to add it:


[failure] 3-3:
no required module provides package github.com/google/go-github/v55/github; to add it:

🪛 golangci-lint (2.12.2)

[error] 3-3: could not import github.com/google/go-github/v55/github (internal/opportunity/classifier.go:3:8: no required module provides package github.com/google/go-github/v55/github; to add it:
go get github.com/google/go-github/v55/github)

(typecheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/classifier.go` at line 3, The build breaks because the
package import "github.com/google/go-github/v55/github" (used in
internal/opportunity/classifier.go, score.go, github_service.go, engine.go) is
not declared in go.mod/go.sum; fix by adding the module to go.mod and updating
go.sum—run a module fetch such as `go get github.com/google/go-github/v55` and
then `go mod tidy` (or equivalent tooling) so the dependency and matching v55
version are recorded and resolved before merging.

Comment on lines +21 to +38
for _, lbl := range issue.Labels {
if lbl == nil || lbl.Name == nil {
continue
}
name := *lbl.Name
switch name {
case "good first issue", "good-first-issue":
return ClassificationGoodFirstIssue
case "help wanted", "help-wanted":
return ClassificationHelpWanted
case "bug":
return ClassificationBug
case "enhancement", "feature":
return ClassificationEnhancement
case "documentation", "docs":
return ClassificationDocumentation
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid order-dependent classification when multiple labels are present.

Returning on first matched label makes classification depend on GitHub label order. An issue with both bug and good first issue may be classified as bug and lose opportunity scoring weight. Prefer deterministic precedence (e.g., good-first > help-wanted > bug > enhancement > docs) across all labels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/classifier.go` around lines 21 - 38, The current loop in
classifier.go returns on the first matching label which makes classification
order-dependent; instead scan all issue.Labels and compute a deterministic
result by applying a precedence list (good-first > help-wanted > bug >
enhancement > docs), e.g., track which of the known classifications are present
while iterating (use issue.Labels and the ClassificationGoodFirstIssue,
ClassificationHelpWanted, ClassificationBug, ClassificationEnhancement,
ClassificationDocumentation symbols), then after the loop return the
highest-precedence classification (or default) so an issue with multiple labels
always yields the same classification.

Comment on lines +25 to +37
name := *lbl.Name
switch name {
case "good first issue", "good-first-issue":
return ClassificationGoodFirstIssue
case "help wanted", "help-wanted":
return ClassificationHelpWanted
case "bug":
return ClassificationBug
case "enhancement", "feature":
return ClassificationEnhancement
case "documentation", "docs":
return ClassificationDocumentation
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize label names before matching to avoid false “other” classifications.

switch name is case/whitespace sensitive, so labels like Good First Issue or help wanted will miss and fall through to other, undercounting opportunities downstream in internal/opportunity/engine.go (Line 41-46 there).

Suggested fix
+import (
+    "strings"
+    "github.com/google/go-github/v55/github"
+)
-
-import "github.com/google/go-github/v55/github"

@@
-        name := *lbl.Name
+        name := strings.TrimSpace(strings.ToLower(*lbl.Name))
         switch name {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/classifier.go` around lines 25 - 37, Normalize the label
string before the switch in classify logic: replace name := *lbl.Name with a
normalized value (e.g., strings.TrimSpace(strings.ToLower(*lbl.Name))) so
matches like "Good First Issue" or "help wanted " hit the expected cases; add
the strings import if missing and keep the existing switch cases
(ClassificationGoodFirstIssue, ClassificationHelpWanted, ClassificationBug,
ClassificationEnhancement, ClassificationDocumentation) using the normalized
name.

Comment on lines +17 to +31
func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) {
var all []*github.Issue
opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}}
for {
issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt)
if err != nil {
return nil, err
}
all = append(all, issues...)
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return all, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List relevant files
ls -la internal/opportunity || true
ls -la internal/opportunity/github_service.go internal/opportunity/classifier.go internal/opportunity/score.go 2>/dev/null || true

# Show the exact ListIssues implementation
echo "==== internal/opportunity/github_service.go (context) ===="
sed -n '1,200p' internal/opportunity/github_service.go

# Show classifier logic that consumes issues
echo "==== internal/opportunity/classifier.go (context) ===="
sed -n '1,220p' internal/opportunity/classifier.go

# Show scoring logic that consumes issues
echo "==== internal/opportunity/score.go (context) ===="
sed -n '1,240p' internal/opportunity/score.go

# Find all call sites of ListIssues
echo "==== Call sites of ListIssues ===="
rg -n "ListIssues\\(" -S internal/opportunity || true

# Find if PRs are filtered elsewhere (PullRequestLinks or IsPullRequest fields)
echo "==== Search for PullRequestLinks / IsPullRequest usage ===="
rg -n "PullRequestLinks|IsPullRequest|pull request|PullRequest" internal/opportunity || true

# Also search for any helper that normalizes issue vs PR
echo "==== Search for IssueListByRepoOptions handling ===="
rg -n "IssueListByRepoOptions|ListByRepo\\(" internal/opportunity || true

Repository: agnivo988/Repo-lyzer

Length of output: 6692


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' internal/opportunity/engine.go
rg -n "computeScore\\(|Classify\\(|PullRequestLinks|pull_request" internal/opportunity -S

Repository: agnivo988/Repo-lyzer

Length of output: 2009


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "==== internal/opportunity/score.go (full) ===="
cat -n internal/opportunity/score.go

Repository: agnivo988/Repo-lyzer

Length of output: 1894


Filter pull requests out of ListIssues (and fix score.go missing time import).

  • Issues.ListByRepo includes PRs, and Engine.Run classifies/counts everything returned by ListIssues based on labels, inflating the opportunity score for labeled PRs—skip entries where issue.PullRequestLinks != nil.
  • internal/opportunity/score.go calls time.Since but doesn’t import time, so it won’t compile.
Suggested fix
 func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) {
     var all []*github.Issue
     opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}}
     for {
         issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt)
         if err != nil {
             return nil, err
         }
-        all = append(all, issues...)
+        for _, issue := range issues {
+            if issue == nil || issue.PullRequestLinks != nil {
+                continue
+            }
+            all = append(all, issue)
+        }
         if resp.NextPage == 0 {
             break
         }
         opt.Page = resp.NextPage
     }
     return all, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) {
var all []*github.Issue
opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}}
for {
issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt)
if err != nil {
return nil, err
}
all = append(all, issues...)
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return all, nil
func (s *GitHubService) ListIssues(ctx context.Context, owner, repo string, state string) ([]*github.Issue, error) {
var all []*github.Issue
opt := &github.IssueListByRepoOptions{State: state, ListOptions: github.ListOptions{PerPage: 100}}
for {
issues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, opt)
if err != nil {
return nil, err
}
for _, issue := range issues {
if issue == nil || issue.PullRequestLinks != nil {
continue
}
all = append(all, issue)
}
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return all, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/github_service.go` around lines 17 - 31, ListIssues is
returning both issues and PRs (because github.Issue objects for PRs have
PullRequestLinks set), which inflates scoring; modify
internal/opportunity/github_service.go inside GitHubService.ListIssues to skip
any issue where issue.PullRequestLinks != nil before appending to all, keeping
pagination logic intact. Also add the missing time import in
internal/opportunity/score.go (ensure the file imports "time") so calls to
time.Since compile. Reference: GitHubService.ListIssues and the score.go usage
of time.Since.

Comment on lines +53 to +61
for {
stats, resp, err := s.client.Repositories.ListContributorsStats(ctx, owner, repo)
if err != nil {
return nil, err
}
if resp.StatusCode == 202 {
// GitHub is computing statistics, wait briefly.
time.Sleep(2 * time.Second)
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the target file and print the relevant region with line numbers
ls -la internal/opportunity/github_service.go
sed -n '1,140p' internal/opportunity/github_service.go | cat -n

# Also search for the ListContributorsStats usage and any existing retry/Accepted handling
rg -n "ListContributorsStats|AcceptedError|StatusCode == 202|time\\.Sleep" internal/opportunity/github_service.go

Repository: agnivo988/Repo-lyzer

Length of output: 2730


🌐 Web query:

go-github v55 ListContributorsStats 202 AcceptedError

💡 Result:

In the go-github library, the ListContributorsStats method is used to retrieve contributor statistics for a repository [1][2]. When this method is called, GitHub may return a 202 Accepted status code if the statistics have not yet been cached or computed for the repository [1][3][4]. In go-github v55 (and other versions), this 202 status is surfaced to the caller as an *AcceptedError [1][2]. This design ensures that users are explicitly informed that the requested data is not yet available and that a background job has been triggered on the GitHub side to compute the statistics [2][4]. To handle this in your code, you should check for the presence of an *AcceptedError using errors.As. A common pattern is to wait briefly before retrying the request [1][2]: stats, _, err:= client.Repositories.ListContributorsStats(ctx, org, repo) if errors.As(err, new(*github.AcceptedError)) { // The request was accepted; statistics are being computed. // Wait for a short duration (e.g., a few seconds) and retry. log.Println("Statistics are being computed on the GitHub side. Please retry later.") } It is worth noting that as of June 2026, some developers have reported instances where the /stats/contributors endpoint may persistently return a 202 status code without progressing to a 200 OK, which may indicate a broader service issue for specific repositories rather than a typical caching delay [5].

Citations:


Handle *github.AcceptedError in ListContributors; current resp.StatusCode == 202 retry never runs.

In internal/opportunity/github_service.go, ListContributors returns immediately on err != nil, but go-github v55 surfaces a 202 Accepted from Repositories.ListContributorsStats as *github.AcceptedError, so the resp.StatusCode == 202 branch is unreachable. Catch AcceptedError and retry while honoring ctx.Done().

Suggested fix
 import (
     "context"
+    "errors"
     "time"
     "github.com/google/go-github/v55/github"
 )
@@
 func (s *GitHubService) ListContributors(ctx context.Context, owner, repo string) ([]*github.ContributorStats, error) {
     // GitHub provides contributor stats endpoint; may be delayed, handle 202 response.
     for {
-        stats, resp, err := s.client.Repositories.ListContributorsStats(ctx, owner, repo)
+        stats, _, err := s.client.Repositories.ListContributorsStats(ctx, owner, repo)
         if err != nil {
+            var accepted *github.AcceptedError
+            if errors.As(err, &accepted) {
+                select {
+                case <-ctx.Done():
+                    return nil, ctx.Err()
+                case <-time.After(2 * time.Second):
+                    continue
+                }
+            }
             return nil, err
         }
-        if resp.StatusCode == 202 {
-            // GitHub is computing statistics, wait briefly.
-            time.Sleep(2 * time.Second)
-            continue
-        }
         return stats, nil
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/github_service.go` around lines 53 - 61, In
ListContributors (the loop calling s.client.Repositories.ListContributorsStats)
currently returns immediately on err != nil, which prevents retrying for
GitHub's 202 case; change the error handling to detect github.AcceptedError
(type *github.AcceptedError) and treat it like a non-fatal retry: if err is an
AcceptedError (or resp.StatusCode == 202) then wait (respecting ctx.Done()) and
continue the loop, otherwise return the error; ensure the wait honors ctx
cancellation (select on ctx.Done() vs time.After) so the method exits promptly
when context is cancelled.

@@ -0,0 +1,52 @@
package opportunity

import "github.com/google/go-github/v55/github"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unresolved time symbol in score.go
set -euo pipefail
fd -a 'score.go$' | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,120p" "{}"'

Repository: agnivo988/Repo-lyzer

Length of output: 3011


Fix missing time import in internal/opportunity/score.go

computeScore calls time.Since (line 35), but the file’s import block only includes github.com/google/go-github/v55/github, so the package won’t compile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/score.go` at line 3, The file is missing the time
package import used by computeScore (it calls time.Since); update the import
block to include "time" (either add a separate import or include it in the
existing import group with "github.com/google/go-github/v55/github") so
computeScore can call time.Since and the package will compile.

Comment on lines +27 to +52
// computeScore aggregates issue scores and contributor count into a final contribution score.
func computeScore(issues []*github.Issue, contributors [] *github.ContributorStats) int {
total := 0
for _, iss := range issues {
weight := calculateWeight(iss)
// simple age penalty: newer issues get a small bonus (max 2 points)
ageDays := 0
if iss.CreatedAt != nil {
ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24)
}
bonus := 0
if ageDays < 30 {
bonus = 2
} else if ageDays < 90 {
bonus = 1
}
total += weight + bonus
}
// Add a point per contributor (capped at 50 for sanity)
contribPoints := len(contributors)
if contribPoints > 50 {
contribPoints = 50
}
total += contribPoints
return total
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Scoring utility appears unwired from the runtime path.

computeScore is implemented here, but internal/opportunity/engine.go (Line 48 there) still computes goodFirst*2 + helpWanted + len(contributors) directly and never calls this function. As-is, new weighting/age logic won’t affect CLI output.

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 35-35: undefined: time

(typecheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/score.go` around lines 27 - 52, The score logic in
engine.go is bypassing computeScore; replace the inline calculation (goodFirst*2
+ helpWanted + len(contributors)) with a call to computeScore, passing the
issues slice and the contributors slice to computeScore(issues, contributors),
and remove the duplicated weighting/age logic there; ensure the types match
(issues as []*github.Issue and contributors as []*github.ContributorStats) or
adapt computeScore signature accordingly and import/update any packages needed
so the CLI uses the centralized computeScore implementation.

Comment on lines +30 to +36
for _, iss := range issues {
weight := calculateWeight(iss)
// simple age penalty: newer issues get a small bonus (max 2 points)
ageDays := 0
if iss.CreatedAt != nil {
ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against nil issues before dereferencing CreatedAt.

issues is a slice of pointers; if any element is nil, iss.CreatedAt panics at runtime. Add if iss == nil { continue } before accessing fields.

Suggested fix
     for _, iss := range issues {
+        if iss == nil {
+            continue
+        }
         weight := calculateWeight(iss)
         // simple age penalty: newer issues get a small bonus (max 2 points)
         ageDays := 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, iss := range issues {
weight := calculateWeight(iss)
// simple age penalty: newer issues get a small bonus (max 2 points)
ageDays := 0
if iss.CreatedAt != nil {
ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24)
}
for _, iss := range issues {
if iss == nil {
continue
}
weight := calculateWeight(iss)
// simple age penalty: newer issues get a small bonus (max 2 points)
ageDays := 0
if iss.CreatedAt != nil {
ageDays = int(time.Since(*iss.CreatedAt).Hours() / 24)
}
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 35-35: undefined: time

(typecheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opportunity/score.go` around lines 30 - 36, Guard against nil
entries in the issues slice before dereferencing fields: in the loop over issues
(for _, iss := range issues) add a nil-check (if iss == nil { continue }) before
calling calculateWeight(iss) and before accessing iss.CreatedAt so you don't
panic on nil pointers; update references around calculateWeight, iss, and the
CreatedAt ageDays calculation to early-continue when iss is nil.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant