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

@philluiz2323

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

    bugSomething isn't working

    Projects

    Status
    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions