File size: 2,887 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
 * Issue classification: quarantine filter + engagement override + skip rules.
 */

const HARDCODED_BOTS = new Set(["github-actions", "dependabot", "renovate", "claude", "copilot"]);

export function isBot(user) {
  if (!user) return false;
  if (user.__typename === "Bot") return true;
  const login = user.login || "";
  if (login.endsWith("[bot]")) return true;
  if (HARDCODED_BOTS.has(login)) return true;
  return false;
}

function countThumbs(reactionGroups) {
  if (!Array.isArray(reactionGroups)) return 0;
  const g = reactionGroups.find((rg) => rg.content === "THUMBS_UP");
  return g?.users?.totalCount ?? 0;
}

function uniqueCommenters(comments, authorLogin) {
  if (!Array.isArray(comments)) return [];
  const seen = new Set();
  for (const c of comments) {
    const u = c.author;
    if (!u || !u.login) continue;
    if (u.login === authorLogin) continue;
    if (isBot(u)) continue;
    seen.add(u.login);
  }
  return [...seen];
}

function openLinkedPrs(timelineItems) {
  if (!Array.isArray(timelineItems)) return [];
  const result = [];
  for (const item of timelineItems) {
    if (item.__typename !== "CrossReferencedEvent") continue;
    const src = item.source;
    if (!src || src.__typename !== "PullRequest") continue;
    if (String(src.state).toLowerCase() !== "open") continue;
    result.push({ number: src.number, state: "open" });
  }
  return result;
}

export function classifyIssue(issue, thresholds, now = new Date()) {
  const assignees = issue.assignees ?? [];
  if (assignees.length > 0) {
    return {
      bucket: "skip_assigned",
      reason: "issue has assignees",
      assignees: assignees.map((a) => a.login),
    };
  }

  const linkedPrs = openLinkedPrs(issue.timelineItems);
  if (linkedPrs.length > 0) {
    return {
      bucket: "skip_has_pr",
      reason: "issue has open linked PR",
      linkedPrs,
    };
  }

  const createdAt = new Date(issue.createdAt).getTime();
  let ageDays = Math.floor((now.getTime() - createdAt) / 86400_000);
  if (ageDays < 0) ageDays = 0;

  const thumbs = countThumbs(issue.reactionGroups);
  const commenterLogins = uniqueCommenters(issue.comments, issue.author?.login);
  const commenters = commenterLogins.length;

  const meta = { ageDays, thumbs, commenters };

  if (ageDays >= thresholds.quarantineDays) {
    return { bucket: "absorb", reason: `age>=${thresholds.quarantineDays}`, ...meta };
  }

  const overrides = [];
  if (thumbs >= thresholds.overrideThumbs) overrides.push("thumbs");
  if (commenters >= thresholds.overrideCommenters) overrides.push("commenters");
  if (overrides.length > 0) {
    return { bucket: "absorb", reason: `override:${overrides.join("+")}`, ...meta };
  }

  return {
    bucket: "dormant",
    reason: `age<${thresholds.quarantineDays} && thumbs<${thresholds.overrideThumbs} && commenters<${thresholds.overrideCommenters}`,
    ...meta,
  };
}