From ba1876727a83b588338f97a24849c5b4317dcb9c Mon Sep 17 00:00:00 2001 From: web-dev0521 Date: Wed, 3 Jun 2026 03:19:32 -0600 Subject: [PATCH] feat(agent): add duplicate and stale-work scenario blockers Extends the scenario planning pipeline with two new advisory blockers so contributors are warned about conflicting or low-value work before opening a PR. Both blockers are reducer-severity (visible in public summaries via generic text, exact counts in private surfaces) and integrate directly into every ScoreScenarioPreview via blockedByFor(). src/scoring/preview.ts: - ScorePreviewInput: add duplicateRiskCount (count of duplicate-risk issues or PRs, e.g. from buildCollisionReport). - ScoreGateBlocker["code"]: add "stale_work" and "duplicate_risk". - blockedByFor(): emit stale_work when observedStalePrCount > 0 and duplicate_risk when duplicateRiskCount > 0. Both use "reducer" severity so they appear in public scenario output with generic phrasing and in private surfaces with the exact count and action text. test/unit/scenario-blockers.test.ts (new, 14 tests): - stale_work fixture: emits reducer blocker with count; absent when zero or missing; propagates into scenario previews; detail is sanitizable. - duplicate_risk fixture: emits reducer blocker with count; absent when zero or missing; count in detail matches input; detail is sanitizable. - Combined fixture: both blockers present independently when both signals set; existing gate blockers (open_pr_threshold) are not displaced. - No-blocker baseline: neither code emitted when inputs are absent. - Public sanitizer fixtures: stale_work and duplicate_risk detail text passes sanitizePublicComment without leaking forbidden language; full blockedBy array on a combined preview is fully sanitizable. --- src/scoring/preview.ts | 23 +++- test/unit/scenario-blockers.test.ts | 178 ++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 test/unit/scenario-blockers.test.ts diff --git a/src/scoring/preview.ts b/src/scoring/preview.ts index 5301b3426a..27a4c6d74c 100644 --- a/src/scoring/preview.ts +++ b/src/scoring/preview.ts @@ -29,6 +29,7 @@ export type ScorePreviewInput = { observedDraftPrCount?: number | undefined; observedBlockedPrCount?: number | undefined; observedMaintainerPrCount?: number | undefined; + duplicateRiskCount?: number | undefined; expectedOpenPrCountAfterMerge?: number | undefined; projectedCredibility?: number | undefined; scenarioNotes?: string[] | undefined; @@ -94,7 +95,9 @@ export type ScoreGateBlocker = { | "linked_issue_invalid" | "linked_issue_unvalidated" | "branch_ineligible" - | "branch_eligibility_missing"; + | "branch_eligibility_missing" + | "duplicate_risk" + | "stale_work"; severity: "blocker" | "reducer" | "context"; detail: string; }; @@ -574,6 +577,24 @@ function blockedByFor(input: ScorePreviewInput, repo: RepositoryRecord | null, c }, ] : []), + ...(nonNegative(input.observedStalePrCount) > 0 + ? [ + { + code: "stale_work" as const, + severity: "reducer" as const, + detail: `${nonNegative(input.observedStalePrCount)} stale open PR(s) detected; consider closing stale work before opening new contributions.`, + }, + ] + : []), + ...(nonNegative(input.duplicateRiskCount) > 0 + ? [ + { + code: "duplicate_risk" as const, + severity: "reducer" as const, + detail: `${nonNegative(input.duplicateRiskCount)} duplicate-risk issue(s) or PR(s) detected; verify there is no conflicting work before proceeding.`, + }, + ] + : []), ]; } diff --git a/test/unit/scenario-blockers.test.ts b/test/unit/scenario-blockers.test.ts new file mode 100644 index 0000000000..b149b1b27e --- /dev/null +++ b/test/unit/scenario-blockers.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, it } from "vitest"; +import { sanitizePublicComment } from "../../src/github/commands"; +import { buildScorePreview, type ScoreGateBlocker, type ScorePreviewInput } from "../../src/scoring/preview"; +import type { ScoringModelSnapshotRecord } from "../../src/types"; + +// Minimal scoring model snapshot sufficient for gate/blocker tests. +const snapshot: ScoringModelSnapshotRecord = { + id: "blocker-test-model", + sourceKind: "test", + sourceUrl: "fixture://constants.py", + fetchedAt: "2026-06-03T00:00:00.000Z", + activeModel: "current_density_model", + constants: { + OSS_EMISSION_SHARE: 0.9, + MERGED_PR_BASE_SCORE: 25, + MIN_TOKEN_SCORE_FOR_BASE_SCORE: 5, + MAX_CODE_DENSITY_MULTIPLIER: 1.15, + MAX_CONTRIBUTION_BONUS: 25, + CONTRIBUTION_SCORE_FOR_FULL_BONUS: 1500, + STANDARD_ISSUE_MULTIPLIER: 1.33, + MAINTAINER_ISSUE_MULTIPLIER: 1.66, + MIN_CREDIBILITY: 0.8, + REVIEW_PENALTY_RATE: 0.15, + EXCESSIVE_PR_PENALTY_BASE_THRESHOLD: 2, + OPEN_PR_THRESHOLD_TOKEN_SCORE: 300, + MAX_OPEN_PR_THRESHOLD: 30, + OPEN_PR_COLLATERAL_PERCENT: 0.2, + SRC_TOK_SATURATION_SCALE: 58, + }, + programmingLanguages: {}, + registrySnapshotId: "registry-fixture", + warnings: [], + payload: {}, +}; + +const registeredRepo = { + fullName: "octo/demo", + owner: "octo", + name: "demo", + isInstalled: true, + isRegistered: true, + isPrivate: false, + registryConfig: { repo: "octo/demo", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, maintainerCut: 0, raw: {} }, +}; + +function preview(input: Partial = {}) { + return buildScorePreview({ + repo: registeredRepo, + snapshot, + input: { + repoFullName: "octo/demo", + sourceTokenScore: 60, + totalTokenScore: 80, + sourceLines: 50, + openPrCount: 1, + credibility: 1, + ...input, + }, + }); +} + +function blockerCodes(result: ReturnType): ScoreGateBlocker["code"][] { + return result.blockedBy.map((b) => b.code); +} + +// ── Stale-work blocker ───────────────────────────────────────────────────── + +describe("stale_work scenario blocker", () => { + it("emits a stale_work reducer blocker when observedStalePrCount is positive", () => { + const result = preview({ observedStalePrCount: 2 }); + const stale = result.blockedBy.find((b) => b.code === "stale_work"); + expect(stale).toBeDefined(); + expect(stale?.severity).toBe("reducer"); + expect(stale?.detail).toMatch(/2 stale open PR/i); + }); + + it("does not emit stale_work when observedStalePrCount is zero or absent", () => { + expect(blockerCodes(preview({ observedStalePrCount: 0 }))).not.toContain("stale_work"); + expect(blockerCodes(preview({}))).not.toContain("stale_work"); + }); + + it("emits stale_work in every scenario preview that shares the current blocked-by evaluation", () => { + const result = preview({ observedStalePrCount: 1 }); + const staleInScenarios = result.scenarioPreviews.filter((s) => s.blockedBy.some((b) => b.code === "stale_work")); + expect(staleInScenarios.length).toBeGreaterThan(0); + }); + + it("stale_work detail text is free of forbidden public language", () => { + const result = preview({ observedStalePrCount: 3 }); + const stale = result.blockedBy.find((b) => b.code === "stale_work")!; + expect(sanitizePublicComment(stale.detail)).not.toMatch( + /wallet|hotkey|coldkey|mnemonic|reward|payout|raw trust|scoreability|private reviewability/i, + ); + }); +}); + +// ── Duplicate-risk blocker ───────────────────────────────────────────────── + +describe("duplicate_risk scenario blocker", () => { + it("emits a duplicate_risk reducer blocker when duplicateRiskCount is positive", () => { + const result = preview({ duplicateRiskCount: 1 }); + const dup = result.blockedBy.find((b) => b.code === "duplicate_risk"); + expect(dup).toBeDefined(); + expect(dup?.severity).toBe("reducer"); + expect(dup?.detail).toMatch(/1 duplicate-risk/i); + }); + + it("does not emit duplicate_risk when duplicateRiskCount is zero or absent", () => { + expect(blockerCodes(preview({ duplicateRiskCount: 0 }))).not.toContain("duplicate_risk"); + expect(blockerCodes(preview({}))).not.toContain("duplicate_risk"); + }); + + it("emits duplicate_risk with correct count in the detail", () => { + const result = preview({ duplicateRiskCount: 5 }); + const dup = result.blockedBy.find((b) => b.code === "duplicate_risk")!; + expect(dup.detail).toMatch(/5 duplicate-risk/i); + }); + + it("duplicate_risk detail text is free of forbidden public language", () => { + const result = preview({ duplicateRiskCount: 2 }); + const dup = result.blockedBy.find((b) => b.code === "duplicate_risk")!; + expect(sanitizePublicComment(dup.detail)).not.toMatch( + /wallet|hotkey|coldkey|mnemonic|reward|payout|raw trust|scoreability|private reviewability/i, + ); + }); +}); + +// ── Both blockers together ───────────────────────────────────────────────── + +describe("stale_work + duplicate_risk combined", () => { + it("surfaces both blockers independently when both signals are present", () => { + const result = preview({ observedStalePrCount: 2, duplicateRiskCount: 3 }); + const codes = blockerCodes(result); + expect(codes).toContain("stale_work"); + expect(codes).toContain("duplicate_risk"); + }); + + it("stale and duplicate blockers appear after existing gate blockers without displacing them", () => { + const result = preview({ openPrCount: 5, observedStalePrCount: 1, duplicateRiskCount: 1 }); + expect(blockerCodes(result)).toContain("open_pr_threshold"); + expect(blockerCodes(result)).toContain("stale_work"); + expect(blockerCodes(result)).toContain("duplicate_risk"); + }); +}); + +// ── No-blocker fixture ───────────────────────────────────────────────────── + +describe("no-blocker baseline", () => { + it("does not emit stale_work or duplicate_risk when neither signal is present", () => { + const result = preview(); + expect(blockerCodes(result)).not.toContain("stale_work"); + expect(blockerCodes(result)).not.toContain("duplicate_risk"); + }); +}); + +// ── Public sanitizer tests for evidence summaries ────────────────────────── + +describe("blocker detail sanitizer fixtures", () => { + it("sanitized stale_work detail avoids forbidden language", () => { + const detail = "3 stale open PR(s) detected; consider closing stale work before opening new contributions."; + const sanitized = sanitizePublicComment(detail); + expect(sanitized).not.toMatch(/wallet|hotkey|payout|reward|raw trust|scoreability/i); + expect(sanitized).toContain("stale"); + }); + + it("sanitized duplicate_risk detail avoids forbidden language", () => { + const detail = "2 duplicate-risk issue(s) or PR(s) detected; verify there is no conflicting work before proceeding."; + const sanitized = sanitizePublicComment(detail); + expect(sanitized).not.toMatch(/wallet|hotkey|payout|reward|raw trust|scoreability/i); + expect(sanitized).toContain("duplicate"); + }); + + it("blockedBy array on a scenario preview with both signals is fully sanitizable", () => { + const result = preview({ observedStalePrCount: 2, duplicateRiskCount: 1 }); + const allDetail = result.blockedBy.map((b) => sanitizePublicComment(b.detail)).join(" "); + expect(allDetail).not.toMatch(/wallet|hotkey|payout|reward|raw trust|scoreability|private reviewability/i); + }); +});