Skip to content

Commit d89073a

Browse files
committed
feat(review): capture bounded raw context in the linked_issue_scope_mismatch fired signal (#8129)
1 parent c02a277 commit d89073a

3 files changed

Lines changed: 51 additions & 4 deletions

File tree

src/queue/processors.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ import { buildSlopAssessment, type SlopBand } from "../signals/slop";
492492
import { copycatWouldActOnPersistedScore } from "../signals/copycat";
493493
import { buildStructuralImprovementAssessment } from "../signals/improvement";
494494
import { runLoopOverLinkedIssueSatisfaction } from "../services/linked-issue-satisfaction-run";
495+
import { MAX_BODY_CHARS, MAX_DIFF_CHARS, MAX_ISSUE_TEXT_CHARS } from "../services/linked-issue-satisfaction";
495496
import { decidePublicSurface } from "../signals/settings-preview";
496497
import {
497498
buildFocusManifestGuidance,
@@ -7553,7 +7554,18 @@ export async function runLinkedIssueSatisfactionForAdvisory(
75537554
targetKey: `${args.repoFullName}#${args.pr.number}`,
75547555
outcome: result.result.status,
75557556
occurredAt: nowIso(),
7556-
metadata: { confidence: result.result.confidence },
7557+
// #8129: also capture the bounded raw inputs the assessment was based on — a future backtest
7558+
// classify() re-runs new prompt/logic against these and compares to the recorded label; confidence
7559+
// alone can only validate threshold changes. Bounds mirror LinkedIssueSatisfactionInput's own
7560+
// (reused constants + the exact trim/slice shapes buildLinkedIssueSatisfactionPrompt applies), so
7561+
// what's stored is byte-what the assessment actually saw.
7562+
metadata: {
7563+
confidence: result.result.confidence,
7564+
issueText: issueText.trim().slice(0, MAX_ISSUE_TEXT_CHARS),
7565+
prTitle: args.pr.title,
7566+
prBody: (args.pr.body ?? "").trim().slice(0, MAX_BODY_CHARS),
7567+
diff: diff.slice(0, MAX_DIFF_CHARS),
7568+
},
75577569
})
75587570
.catch(() => undefined);
75597571
}

src/services/linked-issue-satisfaction.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ export type LinkedIssueSatisfactionStatus = (typeof LINKED_ISSUE_SATISFACTION_ST
2929
export const LINKED_ISSUE_SATISFACTION_CONFIDENCE_FLOOR = 0.5;
3030

3131
const MAX_RATIONALE_LENGTH = 400;
32-
const MAX_ISSUE_TEXT_CHARS = 6000;
33-
const MAX_DIFF_CHARS = 60000;
34-
const MAX_BODY_CHARS = 2000;
32+
// Exported (#8129) so the calibration fired-event capture reuses the assessment's OWN bounds for the raw
33+
// context it stores, instead of drifting behind a second set of hand-maintained limits.
34+
export const MAX_ISSUE_TEXT_CHARS = 6000;
35+
export const MAX_DIFF_CHARS = 60000;
36+
export const MAX_BODY_CHARS = 2000;
3537

3638
export type LinkedIssueSatisfactionInput = {
3739
/** The already-fetched linked-issue title + body text (grounding already resolved this — see

test/unit/linked-issue-satisfaction-run.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { afterEach, describe, expect, it, vi } from "vitest";
22
import { runLoopOverLinkedIssueSatisfaction, type LinkedIssueSatisfactionRunInput } from "../../src/services/linked-issue-satisfaction-run";
3+
import { MAX_BODY_CHARS, MAX_DIFF_CHARS } from "../../src/services/linked-issue-satisfaction";
34
import { BEST_REVIEW_MODELS, RELIABLE_FALLBACK_MODELS } from "../../src/services/ai-review";
45
import { buildAiReviewDiff, processJob, runLinkedIssueSatisfactionForAdvisory } from "../../src/queue/processors";
56
import { evaluateGateCheck } from "../../src/rules/advisory";
@@ -481,6 +482,38 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)"
481482
expect(history.overrides).toEqual([]); // firing alone is never an override
482483
});
483484

485+
it("captures the bounded raw context (issueText/prTitle/prBody/diff) on the fired signal, truncating an over-limit body (#8129)", async () => {
486+
stubIssueFetch();
487+
const run = vi.fn(async () => ({ response: satisfactionJson({ status: "unaddressed", confidence: 0.9 }) }));
488+
const env = enabledEnv(run);
489+
const longBody = "x".repeat(MAX_BODY_CHARS + 500);
490+
const longBodyPr = { ...pr, body: longBody };
491+
await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: blockMode, advisory: advisory(), repoFullName: "acme/widgets", pr: longBodyPr, author: "alice", files, confirmedContributor: true, installationId: 1 });
492+
493+
const history = await createSignalStore(env).queryRuleHistory("linked_issue_scope_mismatch", 0);
494+
expect(history.fired).toHaveLength(1);
495+
const metadata = history.fired[0]?.metadata as Record<string, string>;
496+
// The stored issue text is the same title+body composite the assessment itself consumed.
497+
expect(metadata.issueText).toContain("Enrich SN74 Gittensor — add SSE stream");
498+
expect(metadata.issueText).toContain("We need a live SSE stream surface for SN74 Gittensor.");
499+
expect(metadata.prTitle).toBe("Add SSE stream endpoint");
500+
// The over-limit body is truncated to the assessment's OWN bound, never stored raw.
501+
expect(metadata.prBody).toBe(longBody.slice(0, MAX_BODY_CHARS));
502+
expect(metadata.prBody).toHaveLength(MAX_BODY_CHARS);
503+
expect(metadata.diff).toBe(buildAiReviewDiff(files).slice(0, MAX_DIFF_CHARS));
504+
});
505+
506+
it("stores an empty prBody (not the string 'null'/'undefined') for a body-less PR (#8129)", async () => {
507+
stubIssueFetch();
508+
const run = vi.fn(async () => ({ response: satisfactionJson({ status: "unaddressed", confidence: 0.9 }) }));
509+
const env = enabledEnv(run);
510+
const bodylessPr = { ...pr, body: null as unknown as string };
511+
await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: blockMode, advisory: advisory(), repoFullName: "acme/widgets", pr: bodylessPr, author: "alice", files, confirmedContributor: true, installationId: 1 });
512+
513+
const history = await createSignalStore(env).queryRuleHistory("linked_issue_scope_mismatch", 0);
514+
expect((history.fired[0]?.metadata as Record<string, string>).prBody).toBe("");
515+
});
516+
484517
it("ADVISORY mode records NO fired signal for the same 'unaddressed' verdict (#8101 — no finding, no signal)", async () => {
485518
stubIssueFetch();
486519
const run = vi.fn(async () => ({ response: satisfactionJson({ status: "unaddressed", confidence: 0.9 }) }));

0 commit comments

Comments
 (0)