internal/rules: move rules implementation from github to its own package

Cleans up a bunch of TODOs. Separate out LLM code from the
github package, which is about storage of issues.

Change-Id: I4b430e25d83e70d5d6a93b5182df8d6bbcb48014
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/626338
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/internal/gaby/rules.go b/internal/gaby/rules.go
index ab4611d..9bb9cbb 100644
--- a/internal/gaby/rules.go
+++ b/internal/gaby/rules.go
@@ -15,6 +15,7 @@
 	"golang.org/x/oscar/internal/github"
 	"golang.org/x/oscar/internal/htmlutil"
 	"golang.org/x/oscar/internal/llm"
+	"golang.org/x/oscar/internal/rules"
 )
 
 // rulesPage holds the fields needed to display the results
@@ -26,8 +27,9 @@
 }
 
 type rulesResult struct {
-	github.IssueRulesResult               // the raw result
-	HTML                    safehtml.HTML // issue response as HTML
+	*github.Issue                   // the issue we're reporting on
+	rules.IssueResult               // the raw result
+	HTML              safehtml.HTML // issue response as HTML
 }
 
 // rulesForm holds the raw inputs to the rules form.
@@ -58,8 +60,17 @@
 			Error:     fmt.Errorf("invalid form value %q: %w", form.Query, err).Error(),
 		}
 	}
+	// Find issue in database.
+	i, err := github.LookupIssue(g.db, g.githubProject, int64(issue))
+	if err != nil {
+		return rulesPage{
+			rulesForm: form,
+			Error:     fmt.Errorf("error looking up issue %q: %w", form.Query, err).Error(),
+		}
+	}
+
 	// TODO: this llm.TextGenerator cast is kind of ugly. Redo somehow.
-	rules, err := github.IssueRules(r.Context(), g.embed.(llm.TextGenerator), g.db, g.githubProject, int64(issue))
+	rules, err := rules.Issue(r.Context(), g.embed.(llm.TextGenerator), i)
 	if err != nil {
 		return rulesPage{
 			rulesForm: form,
@@ -69,8 +80,9 @@
 	return rulesPage{
 		rulesForm: form,
 		Result: &rulesResult{
-			IssueRulesResult: *rules,
-			HTML:             htmlutil.MarkdownToSafeHTML(rules.Response),
+			Issue:       i,
+			IssueResult: *rules,
+			HTML:        htmlutil.MarkdownToSafeHTML(rules.Response),
 		},
 	}
 }
diff --git a/internal/github/rules.go b/internal/rules/rules.go
similarity index 87%
rename from internal/github/rules.go
rename to internal/rules/rules.go
index bd267b6..651577f 100644
--- a/internal/github/rules.go
+++ b/internal/rules/rules.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package github
+package rules
 
 import (
 	"bytes"
@@ -14,33 +14,25 @@
 	"strings"
 	"text/template"
 
+	"golang.org/x/oscar/internal/github"
 	"golang.org/x/oscar/internal/llm"
-	"golang.org/x/oscar/internal/storage"
 )
 
 // TODO: this is a one-shot request/response version of this feature.
 // Implement the version that comments on issues as they come in.
 
-// IssueRulesResult is the result of [IssueRules].
+// IssueResult is the result of [Issue].
 // It contains the text (in markdown format) of a response to
 // that issue mentioning any applicable rules that were violated.
 // If Response=="", then nothing to report.
-type IssueRulesResult struct {
-	*Issue   // the issue itself
+type IssueResult struct {
 	Response string
 }
 
-// IssueRules returns text describing the set of rules that the issue does not currently satisfy.
-// It does not make any requests to GitHub; the issue must already be stored in db.
-func IssueRules(ctx context.Context, llm llm.TextGenerator, db storage.DB, project string, issue int64) (*IssueRulesResult, error) {
-	var result IssueRulesResult
+// Issue returns text describing the set of rules that the issue does not currently satisfy.
+func Issue(ctx context.Context, llm llm.TextGenerator, i *github.Issue) (*IssueResult, error) {
+	var result IssueResult
 
-	// Find issue in database.
-	i, err := LookupIssue(db, project, issue)
-	if err != nil {
-		return nil, err
-	}
-	result.Issue = i
 	if i.PullRequest != nil {
 		result.Response += "## Issue response text\n**None required (pull request)**"
 		return &result, nil
@@ -48,7 +40,7 @@
 
 	// Extract issue text into a string.
 	var issueText bytes.Buffer
-	err = template.Must(template.New("prompt").Parse(body)).Execute(&issueText, bodyArgs{
+	err := template.Must(template.New("prompt").Parse(body)).Execute(&issueText, bodyArgs{
 		Title: i.Title,
 		Body:  i.Body,
 	})
diff --git a/internal/github/rules_test.go b/internal/rules/rules_test.go
similarity index 65%
rename from internal/github/rules_test.go
rename to internal/rules/rules_test.go
index 2dc718c..5daa621 100644
--- a/internal/github/rules_test.go
+++ b/internal/rules/rules_test.go
@@ -2,50 +2,34 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package github
+package rules
 
 import (
 	"context"
 	"strings"
 	"testing"
 
-	"golang.org/x/oscar/internal/storage"
-	"golang.org/x/oscar/internal/storage/timed"
-	"rsc.io/ordered"
+	"golang.org/x/oscar/internal/github"
 )
 
-func TestIssueRules(t *testing.T) {
+func TestIssue(t *testing.T) {
 	ctx := context.Background()
-	db := storage.MemDB()
 	llm := &ruleTestGenerator{}
-	project := "project1"
-	eventKind := "github.Event"
-	api := "/issues"
-	issueID = 999
 
-	// Put an issue into the database.
-	// TODO: this is kind of hacky.
-	i := new(Issue)
-	i.Number = issueID
-	i.User = User{Login: "user"}
+	// Construct a test issue.
+	i := new(github.Issue)
+	i.Number = 999
+	i.User = github.User{Login: "user"}
 	i.Title = "title"
 	i.Body = "body"
-	key := ordered.Encode(project, issueID, api, issueID)
-	val := ordered.Encode(ordered.Raw(storage.JSON(i)))
-	batch := db.Batch()
-	timed.Set(db, batch, eventKind, key, val)
-	batch.Apply()
 
 	// Ask about rule violations.
-	r, err := IssueRules(ctx, llm, db, project, issueID)
+	r, err := Issue(ctx, llm, i)
 	if err != nil {
 		t.Fatalf("IssueRules failed with %v", err)
 	}
 
 	// Check result.
-	if r.Issue.Number != issueID {
-		t.Errorf("issue ID did not round trip correctly, got %d want %d", r.Issue.Number, issueID)
-	}
 	if !strings.Contains(r.Response, "\n- The issue title must start") {
 		t.Errorf("expected the issue title rule failed, but it didn't. Total output: %s", r.Response)
 	}
diff --git a/internal/github/static/ruleset.json b/internal/rules/static/ruleset.json
similarity index 100%
rename from internal/github/static/ruleset.json
rename to internal/rules/static/ruleset.json