Skip to content
Closed
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
31 changes: 27 additions & 4 deletions packages/gittensory-miner/lib/attempt-runner.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildOpenPrSpec } from "@loopover/engine";
import { fingerprintFromChangedFiles } from "@loopover/engine";
import { runIterateLoop } from "@loopover/engine";
import { checkSubmissionFreshness } from "./submission-freshness-check.js";
import { evaluateGovernorChokepointGatePersisted } from "./governor-chokepoint-persisted.js";
Expand Down Expand Up @@ -31,10 +32,11 @@ import { prepareOpenPrSubmission } from "./harness-submission-trigger.js";
// evaluateGovernorChokepointGatePersisted -- callers no longer need to hand-thread honest empty/zero defaults
// on every invocation; `capUsage` is loaded from that same store but its post-attempt save stays the caller's
// job (see governor-chokepoint-persisted.js's own header for why: nothing computes "the next capUsage" from a
// verdict, only the attempt's real outcome does). Reputation/self-plagiarism state also has real persistence
// primitives (governor-state.js) but isn't auto-loaded here yet -- `input.governor.reputationHistory`/
// `selfPlagiarismCandidate`/`selfPlagiarismRecentSubmissions` are still caller-supplied optional fields on
// GovernorChokepointInput, same as before.
// verdict, only the attempt's real outcome does). `selfPlagiarismCandidate`/`selfPlagiarismRecentSubmissions` are
// now computed HERE (#5676), at the open_pr chokepoint call, because the prospective submission's real
// changed-files fingerprint only exists once the loop reaches handoff -- too late for the caller's single early
// governor snapshot; the miner's recent-submission history comes from governor-state.js's listRecentOwnSubmissions.
// `input.governor.reputationHistory` remains the caller's early per-repo read (#5675).

/** True once the loop reaches handoff AND every downstream gate (freshness, submission, governor) allows. */
export const ATTEMPT_OUTCOMES = Object.freeze(["abandon", "stale", "blocked", "governed", "submitted"]);
Expand Down Expand Up @@ -141,13 +143,34 @@ export async function runMinerAttempt(input, deps) {
return { outcome: "blocked", decision: submission.decision, loopResult };
}

// Real self-plagiarism inputs (#5676): the prospective submission's own changed-files fingerprint -- computed the
// same way attempt-cli.js's recordOwnSubmission does (fingerprintFromChangedFiles over the handoff packet's
// changed files) -- plus the miner's real recent-submission history, so the chokepoint's self-plagiarism throttle
// catches a near-duplicate re-submission. Needs a governorState to read history from and a non-empty fingerprint;
// without either the fields stay absent -- an honest skip of that stage, never a fabricated clean history.
const candidateFingerprint = fingerprintFromChangedFiles(handoffPacket.changedFiles?.map((file) => file.path) ?? []);
const selfPlagiarism =
deps.governorState && candidateFingerprint
? {
selfPlagiarismCandidate: {
repoFullName: input.loopInput.repoFullName,
fingerprint: candidateFingerprint,
submittedAt: new Date(deps.nowMs).toISOString(),
},
selfPlagiarismRecentSubmissions: deps.governorState.listRecentOwnSubmissions({
repoFullName: input.loopInput.repoFullName,
}),
}
: {};

const governed = evaluateGovernorChokepointGatePersisted(
{
actionClass: "open_pr",
repoFullName: input.loopInput.repoFullName,
nowMs: deps.nowMs,
wouldBeAction: submission.openPrInput,
...input.governor,
...selfPlagiarism,
},
{
...(deps.governorLedgerAppend ? { append: deps.governorLedgerAppend } : {}),
Expand Down
35 changes: 34 additions & 1 deletion test/unit/miner-attempt-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { runMinerAttempt } from "../../packages/gittensory-miner/lib/attempt-run
import { initEventLedger } from "../../packages/gittensory-miner/lib/event-ledger.js";
import { initGovernorLedger } from "../../packages/gittensory-miner/lib/governor-ledger.js";
import { openGovernorState } from "../../packages/gittensory-miner/lib/governor-state.js";
import { parseFocusManifest, type CodingAgentDriver, type CodingAgentDriverResult } from "../../packages/gittensory-engine/src/index";
import { fingerprintFromChangedFiles, parseFocusManifest, type CodingAgentDriver, type CodingAgentDriverResult } from "../../packages/gittensory-engine/src/index";

const roots: string[] = [];
const closers: Array<{ close(): void }> = [];
Expand Down Expand Up @@ -267,6 +267,39 @@ describe("runMinerAttempt (#2337) — the real create->review->gate->submit pipe
expect(result.decision.stage).toBe("kill_switch");
});

it("REGRESSION (#5676): a near-duplicate of a real recent own submission is throttled at the self-plagiarism stage", async () => {
const deps = baseDeps();
// The driver's changed files (okDriverResult -> ["src/upload.ts"]) fingerprint the prospective submission the
// same way attempt-runner computes it; a prior own submission with that SAME fingerprint is the near-duplicate
// the chokepoint must catch now that the candidate + recent-history reach it (#5676).
deps.governorState.recordOwnSubmission({
repoFullName: "acme/widgets",
fingerprint: fingerprintFromChangedFiles(["src/upload.ts"]),
// Earlier than the prospective submission (its submittedAt is new Date(deps.nowMs)), so THIS attempt is the
// later near-duplicate the self-plagiarism throttle denies -- matching self-plagiarism.ts's "the later of two
// near-identical submissions is the plagiarist" tie-break.
submittedAt: "1970-01-01T00:00:00.000Z",
pullRequestNumber: 1,
});
const result = await runMinerAttempt(baseAttemptInput(), deps);
expect(result.outcome).toBe("governed");
if (result.outcome !== "governed") throw new Error("expected governed");
expect(result.decision.allowed).toBe(false);
expect(result.decision.stage).toBe("self_plagiarism");
});

it("REGRESSION (#5676): a submission genuinely distinct from every recent own submission is NOT throttled", async () => {
const deps = baseDeps();
deps.governorState.recordOwnSubmission({
repoFullName: "acme/widgets",
fingerprint: fingerprintFromChangedFiles(["docs/README.md"]),
submittedAt: "2026-07-10T00:00:00Z",
pullRequestNumber: 1,
});
const result = await runMinerAttempt(baseAttemptInput(), deps);
expect(result.outcome).toBe("submitted");
});

it("falls back to the real default governor-ledger append when governorLedgerAppend is omitted", async () => {
const root = mkdtempSync(join(tmpdir(), "gittensory-miner-attempt-runner-default-governor-"));
roots.push(root);
Expand Down
Loading