diff --git a/packages/gittensory-miner/lib/replay-task-generation.d.ts b/packages/gittensory-miner/lib/replay-task-generation.d.ts index 02cb3941b4..f42d381fef 100644 --- a/packages/gittensory-miner/lib/replay-task-generation.d.ts +++ b/packages/gittensory-miner/lib/replay-task-generation.d.ts @@ -80,6 +80,15 @@ export type ReplayScoringKey = { groundTruth: unknown; }; +// generateReplayScoringKey never lints/scrubs frozen context (it doesn't touch context text at all), so +// unlike ReplayTaskRejected it can only ever reject on selection -- narrower than reusing ReplayTaskRejected, +// which would advertise an "unscrubbable_forward_reference" branch this function can never actually produce. +export type ReplayScoringKeyRejected = { + eligible: false; + rejected: "selection"; + reasons: string[]; +}; + export function detectForwardReferences( text: unknown, context: ForwardRefContext | null | undefined, @@ -114,4 +123,4 @@ export function generateReplayTask( export function generateReplayScoringKey( candidate: FreezePointCandidate | null | undefined, options: ReplayTaskOptions | null | undefined, -): ReplayScoringKey | ReplayTaskRejected; +): ReplayScoringKey | ReplayScoringKeyRejected; diff --git a/test/unit/miner-replay-task-generation.test.ts b/test/unit/miner-replay-task-generation.test.ts index 4d2a8c72d6..115b4ace76 100644 --- a/test/unit/miner-replay-task-generation.test.ts +++ b/test/unit/miner-replay-task-generation.test.ts @@ -222,5 +222,27 @@ describe("gittensory-miner leakage-safe replay task generation (#3011)", () => { reasons: ["insufficient_prior_history", "insufficient_revealed_history"], }); }); + + // REGRESSION: pins the intentional eligibility asymmetry documented on generateReplayScoringKey itself -- + // it never lints/scrubs frozen context, so a candidate generateReplayTask rejects for an unscrubbable + // forward reference still yields a scoring key here. A caller must not assume the two are a matched pair + // without checking generateReplayTask's own result (see the same candidate's rejection above). + it("still returns a scoring key for a candidate whose frozen context generateReplayTask rejects as unscrubbable", () => { + const candidate = { ...eligible, frozenContextTexts: ["the tally hit 250 last month"] }; + + const replayTask = generateReplayTask(candidate, CONTEXT, options); + expect(replayTask).toEqual({ + eligible: false, + rejected: "unscrubbable_forward_reference", + residual: [{ kind: "bare-issue-number", value: 250 }], + }); + + const scoringKey = generateReplayScoringKey(candidate, options); + expect(scoringKey).toEqual({ + eligible: true, + commitCount: 10, + groundTruth: { merged: true, approach: "refactor" }, + }); + }); }); });