From 99669925476ba62e2e6db500ac672c0c021819f7 Mon Sep 17 00:00:00 2001 From: reyanthony062001-ops Date: Wed, 8 Jul 2026 20:33:32 -0400 Subject: [PATCH] feat(engine): extract buildIssueRagQuery to gittensory-engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildIssueRagQuery is pure, string-only query composition (#2320) that the miner analyze phase needs before any PR diff exists, but it lived in src/review/issue-rag-wire.ts where the miner cannot import it. This moves it to packages/gittensory-engine/src/issue-rag-query.ts and turns issue-rag-wire.ts into a thin re-export shim, following the same layout as the #2278 duplicate-winner and #2282 scoring extractions. MIN_QUERY_CHARS moves with it: the constant is the floor both the query builder and rag.ts's retrieval guard check, so it is defined once in the engine module and re-exported from src/review/rag.ts — keeping a single source of truth instead of two drifting copies. The Vectorize/D1-bound retrieval backend (retrieveContext) intentionally stays in src, exactly as the issue scopes it. Exported from the package entrypoint, covered by an engine-side barrel test mirroring the duplicate-winner extraction's, and the existing test/unit/issue-rag-wire.test.ts passes unmodified through the shim. Closes #4254 --- packages/gittensory-engine/src/index.ts | 3 ++ .../gittensory-engine/src/issue-rag-query.ts | 43 +++++++++++++++++ .../test/issue-rag-query.test.ts | 34 +++++++++++++ src/review/issue-rag-wire.ts | 48 ++++--------------- src/review/rag.ts | 6 ++- 5 files changed, 94 insertions(+), 40 deletions(-) create mode 100644 packages/gittensory-engine/src/issue-rag-query.ts create mode 100644 packages/gittensory-engine/test/issue-rag-query.test.ts diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 3e46336637..824e208d79 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -251,6 +251,9 @@ export { resolveDuplicateClusterWinnerNumber, type DuplicateClaimMember, } from "./duplicate-winner.js"; +// Issue-centric RAG query composition (#2320, extracted in #4254): the pure query builder + the shared +// minimum-query floor; the Vectorize/D1 retrieval backend intentionally stays in the backend. +export { MIN_QUERY_CHARS, buildIssueRagQuery, type IssueRagQueryInput } from "./issue-rag-query.js"; export { buildPredictedGateVerdict, predictedGateNote, diff --git a/packages/gittensory-engine/src/issue-rag-query.ts b/packages/gittensory-engine/src/issue-rag-query.ts new file mode 100644 index 0000000000..611d091336 --- /dev/null +++ b/packages/gittensory-engine/src/issue-rag-query.ts @@ -0,0 +1,43 @@ +// Issue-centric RAG query composition (#2320), extracted from `src/review/issue-rag-wire.ts` (#4254) so the +// gittensory-miner analyze phase can build the identical retrieval query without importing the review stack. +// Pure, string-only: the miner has no PR diff yet, so retrieval is fed from the issue's title/body/labels +// while the RAG engine itself stays unchanged (retrieveContext remains Vectorize/D1-bound in `src/review/rag.ts` +// and is intentionally NOT part of this module). + +/** 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. Single source of truth — `src/review/rag.ts` + * re-exports this so the retrieval guard and the query builder can never drift apart. (#cloud-opt) */ +export const MIN_QUERY_CHARS = 40; + +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/packages/gittensory-engine/test/issue-rag-query.test.ts b/packages/gittensory-engine/test/issue-rag-query.test.ts new file mode 100644 index 0000000000..cc34664a86 --- /dev/null +++ b/packages/gittensory-engine/test/issue-rag-query.test.ts @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { MIN_QUERY_CHARS, buildIssueRagQuery } from "../dist/index.js"; + +test("barrel: the public entrypoint re-exports the issue-rag-query API", () => { + assert.equal(typeof buildIssueRagQuery, "function"); + assert.equal(MIN_QUERY_CHARS, 40); +}); + +test("composes title, bounded body, and label hint in order", () => { + 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", " "], + }); + assert.ok(queryText.indexOf("Improve SQLite backup readiness checks") === 0); + assert.ok(queryText.indexOf("Operators need") < queryText.indexOf("Labels:")); + assert.ok(queryText.includes("Labels: gittensor:feature, selfhost")); +}); + +test("returns an empty query below the shared retrieval floor", () => { + assert.deepEqual(buildIssueRagQuery({ title: "Tiny" }), { queryText: "" }); +}); + +test("bounds long bodies without dropping the label hint", () => { + const { queryText } = buildIssueRagQuery({ + title: "Investigate flaky queue dispatch telemetry", + body: `${"a".repeat(4100)}SHOULD_NOT_APPEAR`, + labels: ["queue"], + }); + assert.ok(!queryText.includes("SHOULD_NOT_APPEAR")); + assert.ok(queryText.includes("Labels: queue")); +}); diff --git a/src/review/issue-rag-wire.ts b/src/review/issue-rag-wire.ts index ca044ff7ca..0efc20c62e 100644 --- a/src/review/issue-rag-wire.ts +++ b/src/review/issue-rag-wire.ts @@ -1,38 +1,10 @@ -// 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); -} - -// Intentionally pre-built and currently unreached: no miner-side issue-analysis caller exists yet (#2320). -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 }; -} +/** + * Issue-centric RAG query composition (#2320), extracted to `@jsonbored/gittensory-engine` (#4254) so the + * miner analyze phase can compose the identical retrieval query from an issue's title/body/labels without + * importing the review stack. The retrieval backend itself (`retrieveContext` in `./rag`) is Vectorize/D1-bound + * and intentionally stays in `src` — this shim only re-exports the pure query builder. + * + * packages/gittensory-engine/src/issue-rag-query.ts (imported via relative source path, not the published + * module, matching the #2278/#2282 extraction shims) is the source of truth. + */ +export { buildIssueRagQuery, type IssueRagQueryInput } from "../../packages/gittensory-engine/src/issue-rag-query"; diff --git a/src/review/rag.ts b/src/review/rag.ts index 0832f62091..38ddbf7593 100644 --- a/src/review/rag.ts +++ b/src/review/rag.ts @@ -384,8 +384,10 @@ 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) */ -export const MIN_QUERY_CHARS = 40; + * a vector query, and the matches would be noise. Defined in the engine next to the issue-query + * builder that guards on it (#4254) and re-exported here so the two can never drift. (#cloud-opt) */ +export { MIN_QUERY_CHARS } from "../../packages/gittensory-engine/src/issue-rag-query"; +import { MIN_QUERY_CHARS } from "../../packages/gittensory-engine/src/issue-rag-query"; /** 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 = {