From ddf693272e48b2e12cde03a66679761cd168042f Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:19:59 -0700 Subject: [PATCH] feat(review): add issue RAG query builder --- src/review/issue-rag-wire.ts | 37 +++++++++++++++++++++ src/review/rag.ts | 2 +- test/unit/issue-rag-wire.test.ts | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/review/issue-rag-wire.ts create mode 100644 test/unit/issue-rag-wire.test.ts diff --git a/src/review/issue-rag-wire.ts b/src/review/issue-rag-wire.ts new file mode 100644 index 0000000000..394ac01528 --- /dev/null +++ b/src/review/issue-rag-wire.ts @@ -0,0 +1,37 @@ +// Issue-centric RAG query composition (#2320). The miner analyze phase has no PR diff yet, so it feeds retrieval +// from the issue's title/body/labels while reusing the existing RAG engine unchanged. + +import { MIN_QUERY_CHARS } from "./rag"; + +const MAX_ISSUE_BODY_CHARS = 4000; +const MAX_ISSUE_LABELS = 20; + +export type IssueRagQueryInput = { + title: string; + body?: string | undefined; + labels?: string[] | undefined; +}; + +function cleanLabels(labels: string[] | undefined): string[] { + if (!labels) return []; + return labels + .map((label) => label.trim()) + .filter(Boolean) + .slice(0, MAX_ISSUE_LABELS); +} + +export function buildIssueRagQuery(input: IssueRagQueryInput): { queryText: string } { + const sections: string[] = []; + const title = input.title.trim(); + if (title) sections.push(title); + + const body = (input.body ?? "").trim().slice(0, MAX_ISSUE_BODY_CHARS); + if (body) sections.push(body); + + const labels = cleanLabels(input.labels); + if (labels.length > 0) sections.push(`Labels: ${labels.join(", ")}`); + + const queryText = sections.join("\n\n").trim(); + if (queryText.length < MIN_QUERY_CHARS) return { queryText: "" }; + return { queryText }; +} diff --git a/src/review/rag.ts b/src/review/rag.ts index f7ce37f524..eba90a1d19 100644 --- a/src/review/rag.ts +++ b/src/review/rag.ts @@ -355,7 +355,7 @@ export async function deleteChunksForPaths(infra: RagInfra, project: string, rep // ── Retrieval (fail-safe: "" when anything is missing/broken) ──────────────────────────────────── /** Skip retrieval for a trivially-short query (e.g. a one-word scope string): not worth an embed + * a vector query, and the matches would be noise. (#cloud-opt) */ -const MIN_QUERY_CHARS = 40; +export const MIN_QUERY_CHARS = 40; /** Hard cap on neighbours per query — bounds vector-index cost even if a caller passes a large topK. (#cloud-opt) */ const RAG_MAX_TOPK = 20; const EMPTY_RAG_RETRIEVAL_METRICS: RagRetrievalMetrics = { diff --git a/test/unit/issue-rag-wire.test.ts b/test/unit/issue-rag-wire.test.ts new file mode 100644 index 0000000000..a9df5b9bcd --- /dev/null +++ b/test/unit/issue-rag-wire.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { buildIssueRagQuery } from "../../src/review/issue-rag-wire"; + +describe("buildIssueRagQuery (#2320)", () => { + it("uses a sufficiently descriptive title when the issue has an empty body", () => { + expect( + buildIssueRagQuery({ + title: "Add observability context for self-hosted review planning failures", + body: "", + }), + ).toEqual({ + queryText: "Add observability context for self-hosted review planning failures", + }); + }); + + it("prepends the issue title, includes the body, and appends labels as a hint line", () => { + const { queryText } = buildIssueRagQuery({ + title: "Improve SQLite backup readiness checks", + body: "Operators need restore guidance tied to the existing self-host backup flow.", + labels: ["gittensor:feature", "selfhost", " "], + }); + + expect(queryText).toContain("Improve SQLite backup readiness checks"); + expect(queryText).toContain("Operators need restore guidance"); + expect(queryText).toContain("Labels: gittensor:feature, selfhost"); + expect(queryText.indexOf("Improve SQLite")).toBeLessThan(queryText.indexOf("Operators need")); + expect(queryText.indexOf("Operators need")).toBeLessThan(queryText.indexOf("Labels:")); + }); + + it("bounds long issue bodies without dropping the label hint", () => { + const { queryText } = buildIssueRagQuery({ + title: "Investigate flaky queue dispatch telemetry", + body: `${"a".repeat(4100)}SHOULD_NOT_APPEAR`, + labels: ["queue"], + }); + + expect(queryText).toContain("Investigate flaky queue dispatch telemetry"); + expect(queryText).toContain("Labels: queue"); + expect(queryText).not.toContain("SHOULD_NOT_APPEAR"); + }); + + it("returns an empty query for a one-line issue below the retrieval floor", () => { + expect(buildIssueRagQuery({ title: "Tiny" })).toEqual({ queryText: "" }); + }); + + it("uses a descriptive body when the title is blank and omits blank labels", () => { + const { queryText } = buildIssueRagQuery({ + title: " ", + body: "Document how the miner should build issue context before a pull request exists.", + labels: [" ", ""], + }); + + expect(queryText).toBe( + "Document how the miner should build issue context before a pull request exists.", + ); + }); +});