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
15 changes: 11 additions & 4 deletions packages/gittensory-miner/lib/replay-task-generation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ export type ReplayTask = {
commitT: string | null;
contextTexts: string[];
};
revealed: {
commitCount: number;
groundTruth: unknown;
};
};

export type ReplayScoringKey = {
eligible: true;
commitCount: number;
groundTruth: unknown;
};

export function detectForwardReferences(
Expand Down Expand Up @@ -108,3 +110,8 @@ export function generateReplayTask(
context: ForwardRefContext | null | undefined,
options: ReplayTaskOptions | null | undefined,
): ReplayTask | ReplayTaskRejected;

export function generateReplayScoringKey(
candidate: FreezePointCandidate | null | undefined,
options: ReplayTaskOptions | null | undefined,
): ReplayScoringKey | ReplayTaskRejected;
41 changes: 31 additions & 10 deletions packages/gittensory-miner/lib/replay-task-generation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
// history on both sides to be worth scoring, and (b) nothing in the frozen context lets a replay run infer
// the future by pattern-matching text rather than reasoning. This module selects calibration-worthy freeze
// points, scrubs forward references out of the frozen context, tags each point's recency pool, and returns
// the frozen snapshot and the revealed post-T ground truth as *separate* bundles so the replay pipeline never
// holds both at once. Every function here is pure and deterministic — no clock, no randomness, no IO — so a
// given (candidate, context) always yields an identical task.
// the frozen replay task without the revealed post-T ground truth. Scoring data is exposed through a separate
// function so replay execution never has to hold both sides at once. Every function here is pure and
// deterministic — no clock, no randomness, no IO — so a given (candidate, context) always yields an
// identical task.

// What a scrubbed-away forward reference is replaced with. A fixed, self-delimiting token so the scrubbed
// text stays readable and the substitution is itself deterministic.
Expand Down Expand Up @@ -168,9 +169,9 @@ export function classifyRecencyPool(candidate, options) {
return lastActivityAt >= modelCutoffIso ? "recent" : "older";
}

// One-shot generator. Applies selection, then scrubs and lints the frozen context, then returns the frozen
// snapshot and the revealed post-T ground truth as SEPARATE bundles — never merged — so a caller persists and
// scopes them independently. An ineligible or un-scrubbable candidate is rejected without producing a task.
// One-shot replay generator. Applies selection, then scrubs and lints the frozen context, then returns only
// the frozen replay task. Revealed post-T ground truth is intentionally available only through
// generateReplayScoringKey, so a replay worker/serializer/logger/model call never receives both sides.
export function generateReplayTask(candidate, context, options) {
const selection = selectFreezePoint(candidate, options?.thresholds);
if (!selection.eligible) {
Expand All @@ -194,9 +195,29 @@ export function generateReplayTask(candidate, context, options) {
commitT: typeof candidate?.commitT === "string" ? candidate.commitT : null,
contextTexts: scrubbedTexts,
},
revealed: {
commitCount: selection.revealedCommitCount,
groundTruth: candidate?.revealedGroundTruth ?? null,
},
};
}

// Scoring-only accessor. Call this from the isolated scorer path after replay execution has finished; do not
// pass its result to replay workers. It deliberately shares only selection eligibility with generateReplayTask
// and never carries frozen context.
//
// IMPORTANT: `eligible: true` here means only that selectFreezePoint accepted the candidate -- it does NOT
// mean generateReplayTask would also produce a usable frozen task for it. generateReplayTask can still reject
// a selection-eligible candidate afterward (`rejected: "unscrubbable_forward_reference"`, from
// lintFrozenContext), because scrub/lint eligibility is about the FROZEN CONTEXT TEXT, which this function
// never touches -- it only reveals commitCount/groundTruth, so lint/scrub has nothing to check here. A caller
// must not assume a scoring key implies a replay task was ever generated for the same candidate; check
// generateReplayTask's own result independently before treating the two as a matched pair.
export function generateReplayScoringKey(candidate, options) {
const selection = selectFreezePoint(candidate, options?.thresholds);
if (!selection.eligible) {
return { eligible: false, rejected: "selection", reasons: selection.reasons };
}

return {
eligible: true,
commitCount: selection.revealedCommitCount,
groundTruth: candidate?.revealedGroundTruth ?? null,
};
}
41 changes: 38 additions & 3 deletions test/unit/miner-replay-task-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
RECENCY_POOLS,
classifyRecencyPool,
detectForwardReferences,
generateReplayScoringKey,
generateReplayTask,
lintFrozenContext,
scrubForwardReferences,
Expand Down Expand Up @@ -137,7 +138,7 @@ describe("gittensory-miner leakage-safe replay task generation (#3011)", () => {
modelCutoffIso: "2026-01-01T00:00:00Z",
};

it("produces a scrubbed frozen bundle and a SEPARATE revealed bundle for an eligible clean point", () => {
it("produces only a scrubbed frozen bundle for an eligible clean point", () => {
const task = generateReplayTask(
{ ...eligible, frozenContextTexts: ["intro references #300 and old #12"] },
CONTEXT,
Expand All @@ -150,8 +151,8 @@ describe("gittensory-miner leakage-safe replay task generation (#3011)", () => {
commitT: "abc1234def",
contextTexts: [`intro references ${FORWARD_REF_PLACEHOLDER} and old #12`],
});
// Ground truth lives only on the revealed side — never merged into the frozen bundle.
expect(task.revealed).toEqual({ commitCount: 10, groundTruth: { merged: true, approach: "refactor" } });
expect(task).not.toHaveProperty("revealed");
expect(JSON.stringify(task)).not.toContain("refactor");
expect(task.frozen).not.toHaveProperty("groundTruth");
});

Expand Down Expand Up @@ -187,5 +188,39 @@ describe("gittensory-miner leakage-safe replay task generation (#3011)", () => {
generateReplayTask(input, CONTEXT, options),
);
});

it("exposes revealed ground truth only through the scoring-only accessor", () => {
const replayTask = generateReplayTask(
{ ...eligible, frozenContextTexts: ["intro references #300"] },
CONTEXT,
options,
);
const scoringKey = generateReplayScoringKey(
{ ...eligible, frozenContextTexts: ["intro references #300"] },
options,
);

if (!replayTask.eligible) {
throw new Error(`expected eligible replay task, got ${JSON.stringify(replayTask)}`);
}
if (!scoringKey.eligible) {
throw new Error(`expected eligible scoring key, got ${JSON.stringify(scoringKey)}`);
}
expect(replayTask).not.toHaveProperty("revealed");
expect(scoringKey).toEqual({
commitCount: 10,
eligible: true,
groundTruth: { merged: true, approach: "refactor" },
});
expect(scoringKey).not.toHaveProperty("frozen");
});

it("rejects a scoring key for a candidate that fails selection", () => {
expect(generateReplayScoringKey({ priorCommitCount: 2, revealedCommitCount: 1 }, options)).toEqual({
eligible: false,
rejected: "selection",
reasons: ["insufficient_prior_history", "insufficient_revealed_history"],
});
});
});
});
Loading