Skip to content

[Bug]: The duplicate_risk scoreability blocker can never fire — duplicateRiskCount is consumed (and tested) but no production code ever produces it #385

Description

@philluiz2323

Summary

The score-preview gate blockedByFor emits a duplicate_risk reducer when input.duplicateRiskCount > 0:

// src/scoring/preview.ts:589-597
...(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.`,
      },
    ]
  : []),

But no production code ever sets duplicateRiskCount. A grep across src/ returns only the type declaration (preview.ts:32) and this consumer (preview.ts:589/594) — every producer omits it:

  • buildLocalScoreInput (src/signals/local-branch.ts) threads every other observed-PR signal into the score input, but not duplicate-risk:
    observedApprovedPrCount: args.observedPullRequestScenarios.approvedOrMergeable,
    observedStalePrCount: args.observedPullRequestScenarios.stale,        // -> drives the stale_work reducer
    observedClosedPrCount: args.observedPullRequestScenarios.closed,
    observedDraftPrCount: args.observedPullRequestScenarios.draft,
    observedBlockedPrCount: args.observedPullRequestScenarios.blocked,
    observedMaintainerPrCount: args.observedPullRequestScenarios.maintainerLane,
    // duplicateRiskCount: never set
  • buildRepoRewardRisk (src/signals/reward-risk.ts) builds its commonPreviewInput for buildScorePreview without duplicateRiskCount.
  • The public API scorePreviewSchema (src/api/routes.ts) and the MCP score-preview input don't declare a duplicateRiskCount field, so an external caller can't supply it either.

So the duplicate_risk reducer is dead code on every path. Only test/unit/scenario-blockers.test.ts exercises it by passing duplicateRiskCount directly to buildScorePreview.

Why this is wrong

The sibling reducer right above it, stale_work (preview.ts:580), is fed — buildLocalScoreInput sets observedStalePrCount from the observed-PR scenarios. The two were added together (the merged "duplicate and stale-work scenario blockers" feature, issue #289 / PR #346), but only stale-work got wired to a producer. The asymmetry is the bug: stale work surfaces a private reducer; duplicate work never does.

The duplicate-risk data the reducer needs is already computed on both producing paths:

  • Local-branch path: buildPreflightResult's collisions (a CollisionReport of overlap/WIP clusters with per-cluster risk) is computed and even rendered into the contributor-facing packet's "Overlap/WIP Check" (local-branch.ts:1055). It is simply never converted into duplicateRiskCount for the score input.
  • Reward-risk path: buildRepoRewardRisk already computes collisions = buildCollisionReport(...) and uses collisions.summary.highRiskCount / clusterCount for riskPenalty (reward-risk.ts:216), but never passes that count into its buildScorePreview input.

So the system detects the conflicting work and warns about it in the public Overlap/WIP packet, yet the private scoreability surface silently reports no duplicate-risk reducer.

Failure mode (concrete example)

A contributor runs the MCP/agent preflight_branch / explain_branch_blockers flow on a branch whose linked issue (or changed paths) collides with another open PR/issue, so buildCollisionReport returns a high-risk cluster and the public packet warns "Possible overlap or WIP (high)…".

  • Current: scorePreview.blockedBy and warnings contain no duplicate_risk entry, and the private "explain blockers" action (agent-orchestrator.ts, which surfaces scenarioScorePreview.blockedBy) lists stale/credibility/open-PR reducers but is silent on the duplicate collision.
  • Correct: a duplicate_risk reducer with the cluster count appears in blockedBy / warnings and in the contributor-facing blocker explanation, matching the public Overlap/WIP warning.

Downstream impact

The private decision/blocker surface under-reports conflicting work: a miner is privately told the branch has no extra duplicate risk while another open PR already targets the same issue — exactly the collision the system computes and is meant to flag — increasing wasted duplicate submissions and maintainer review churn. The feature was clearly intended (full type + gate + test coverage) but is unreachable in production.

Steps to reproduce

  1. Build a local-branch analysis (or a reward-risk preview) for a branch/repo where buildCollisionReport returns a high-risk overlap cluster.
  2. Inspect the resulting ScorePreviewResult.blockedBy / warnings.
  3. Observe there is no duplicate_risk reducer, even though the public Overlap/WIP packet flags the collision and the reward-risk path already has collisions.summary.highRiskCount > 0.

Expected behavior

When duplicate/overlap risk is detected (a high-risk collision cluster), the score preview surfaces the duplicate_risk reducer in blockedBy / warnings, consistent with the public Overlap/WIP warning and with how the sibling stale_work reducer is wired.

Actual behavior

duplicateRiskCount is never set by any producer, so the duplicate_risk reducer never fires; only a direct unit test exercises the consumer.

Suggested fix

  • Thread a duplicate-risk count into the score input on the producing paths:
    • In buildLocalScoreInput (local-branch.ts), derive duplicateRiskCount from the already-available preflight.collisions (e.g. the count of high-risk overlap clusters relevant to the branch) and add it to the returned ScorePreviewInput. This requires threading preflight.collisions into buildLocalScoreInput (a new param) since it currently doesn't receive it.
    • In buildRepoRewardRisk (reward-risk.ts), add duplicateRiskCount: collisions.summary.highRiskCount (or clusterCount) to commonPreviewInput.
  • Optionally accept duplicateRiskCount on the public scorePreviewSchema (routes.ts) and the MCP score-preview input so external callers can supply it too, matching the other observed-PR inputs.
  • Add fail-on-revert coverage: a local-branch analysis / reward-risk preview with a high-risk collision cluster yields a duplicate_risk reducer in blockedBy, not just the direct-input unit test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    slopAI slop and/or attempts to game additional points via manipulation or alt profiles.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions