Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions packages/gittensory-engine/src/issue-rag-query.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
34 changes: 34 additions & 0 deletions packages/gittensory-engine/test/issue-rag-query.test.ts
Original file line number Diff line number Diff line change
@@ -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"));
});
48 changes: 10 additions & 38 deletions src/review/issue-rag-wire.ts
Original file line number Diff line number Diff line change
@@ -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";
6 changes: 4 additions & 2 deletions src/review/rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down