Skip to content

feat(issues): respect declined (closed) contributor-issue drafts to stop re-proposal churn #517

Description

@philluiz2323

Repo: JSONbored/gittensory
Area: src/services/contributor-issue-draft.ts (follow-up to #462)
Type: feature / correctness enhancement · Value: medium-high (maintainer trust, signal-to-noise)

Background

#462 added generateContributorIssueDrafts, which turns repo policy-readiness
warnings, upstream registry drift, and focus-manifest wanted paths into
structured, fingerprinted GitHub issue drafts and (optionally) creates them. The
generator dedupes against issues that already exist via
findDuplicateContributorDraft (marker fingerprint, then normalized title).

By design, that duplicate check only considers open issues — it is the
documented, tested behavior:

// src/services/contributor-issue-draft.ts:156
for (const issue of openIssues) {
  if (issue.state !== "open") continue;          // closed issues are skipped
  if (issue.body?.includes(marker)) return { number: issue.number, ..., reason: "marker" };
}
// test/unit/contributor-issue-draft.test.ts:318
it("ignores closed issues and empty title keys when checking duplicates", () => {
  expect(findDuplicateContributorDraft([{ ...openIssue(1, title), state: "closed" }], { fingerprint, title })).toBeNull();
});

And the context only loads open issues (loadContributorIssueDraftContext
listOpenIssues, which filters state = "open").

Current Behavior

Because the generator has no memory of declined drafts, a maintainer who
closes a generated draft (declines it as wontfix / out-of-scope) sees the same
draft re-proposed — and, in create mode, re-created — on the next
generation run, as long as the originating warning persists. The fingerprint is
stable for policy:* and upstream:registry_drift topics
(candidateKey returns the topic, so
contributorIssueDraftFingerprint(repo, topic, topic) is deterministic across
runs), so the same declined draft reappears indefinitely.

This produces recurring noise on the maintainer's tracker and erodes trust in
the automation — the system keeps re-filing work a human already rejected.

Desired Behavior

The generator should respect a maintainer's decision to close a generated draft:

  • A closed issue carrying a contributor-draft marker for a draft's
    fingerprint should suppress re-proposal of that draft (status
    skipped_declined), rather than being ignored.
  • Optionally, support a cooldown (e.g. re-propose only if the warning is
    still present N days after closure, or never for an explicit wontfix label)
    so genuine regressions can still resurface while routine churn is silenced.
  • The dry-run preview and create path must both honor the suppression so the
    preview never advertises a draft that would be skipped.

This intentionally does not change findDuplicateContributorDraft's
open-only semantics (that function answers "is there an open duplicate?"); it
adds a separate, explicit "was this draft declined?" check fed by closed
marked issues.

Implementation Requirements

  • Add a loader for closed issues carrying the draft marker (e.g.
    listClosedContributorDraftIssues(env, repoFullName) or extend the context to
    fetch a bounded set of recently-closed issues), scoped and bounded like the
    existing listers (limit, recent-first), to avoid unbounded scans.
  • Add a findDeclinedContributorDraft(closedIssues, draft, { cooldownDays?, wontfixLabels? })
    helper that matches by stable marker fingerprint and applies the
    cooldown/wontfix policy.
  • In generateContributorIssueDrafts, after the unsafe and open-duplicate
    checks, short-circuit to a new skipped_declined status (add it to
    ContributorIssueDraftStatus, the result counters, and the audit metadata).
  • Keep behavior backward compatible: with no closed marked issues, output is
    unchanged.

Public/Private Output Boundaries

  • Reuse isContributorIssueDraftPublicSafe for any new draft body content.
  • Do not surface private maintainer closure reasons in public GitHub output;
    closure handling is an internal suppression signal only.
  • Generated GitHub issues must stay advisory and must not imply guaranteed
    participation outcomes.

Acceptance Criteria

  • A draft whose stable marker appears on a closed issue is reported as
    skipped_declined (with duplicateOf/declinedBy provenance) and is not
    re-created in create mode.
  • Existing open-duplicate and unsafe behavior is unchanged; the state: "closed"
    case in findDuplicateContributorDraft keeps returning null.
  • Cooldown/wontfix policy is covered: a stale closure past the cooldown with a
    still-active warning may re-propose; a wontfix-labeled closure never does.
  • Result counters and audit metadata account for the new status.

Testing Requirements

  • Unit tests for findDeclinedContributorDraft (marker match, cooldown boundary,
    wontfix label, no-match passthrough).
  • Generation tests: closed marked issue → skipped_declined, no GitHub POST;
    closed past cooldown with active warning → re-proposed.
  • Run the repository's documented validation command before requesting review.
  • Add tests for every new branch, fallback path, and counter, and keep public
    GitHub output advisory.

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