Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
839b36f
test(issue-quality): pin #1672 generic sync failure
Wibias Aug 15, 2026
5745009
test(issue-triage): pin deterministic duplicate closure
Wibias Aug 15, 2026
752bdc3
test(issue-triage): run split regression suites
Wibias Aug 15, 2026
a89a464
fix(issue-quality): reject summary-only reproductions
Wibias Aug 15, 2026
0cc528c
feat(issue-triage): add deterministic duplicate close gate
Wibias Aug 15, 2026
323d866
fix(issue-triage): close proven duplicates safely
Wibias Aug 15, 2026
83d7cdf
test(issue-triage): require specific duplicate evidence
Wibias Aug 15, 2026
e6617bb
fix(issue-triage): require specific auto-close evidence
Wibias Aug 15, 2026
6927eb1
fix(issue-quality): compare independent reproduction evidence
Wibias Aug 15, 2026
d665c89
fix(issue-quality): normalise ordered repro steps
Wibias Aug 15, 2026
5a39a7d
test(issue-quality): cover ordered summary echo
Wibias Aug 15, 2026
951464f
fix(issue-triage): rank all strong duplicate matches
Wibias Aug 15, 2026
1963cc6
test(issue-triage): cover fails and global match ranking
Wibias Aug 15, 2026
2f8e92d
test(issue-quality): cover review edge cases
Wibias Aug 15, 2026
001abca
test(issue-triage): cover short named errors
Wibias Aug 15, 2026
58a8e43
fix(issue-quality): preserve code evidence while normalizing lists
Wibias Aug 15, 2026
79924d0
fix(issue-triage): recognize short named errors
Wibias Aug 15, 2026
c6eeef3
chore(issue-triage): document write permission
Wibias Aug 15, 2026
51115c9
test(issue-quality): assert ordered-list guard directly
Wibias Aug 15, 2026
251fa81
fix(issue-triage): harden duplicate signature matching
Wibias Aug 15, 2026
c2171e3
test(issue-triage): cover outside-diff review findings
Wibias Aug 15, 2026
d401744
fix(issue-triage): defer duplicate closure claim until revalidation
Wibias Aug 15, 2026
d82ac99
fix(issue-triage): bound closed duplicate history
Wibias Aug 15, 2026
75e2737
test(issue-triage): require recent closed candidate window
Wibias Aug 15, 2026
a477924
fix(issue-triage): stop semantic versions and case folding from forgi…
lidge-jun Aug 16, 2026
5011fd7
fix(issue-triage): exclude v-prefixed and prerelease versions, keep IPv4
lidge-jun Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .github/scripts/issue-quality-1672.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use strict";

const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const {
validateIssue,
stripOrderedListPrefixes,
independentReproductionText,
reproductionOnlyEchoesSummary,
} = require("./issue-quality.cjs");

function bugBody({ summary, reproduction }) {
return [
"### Client or integration",
"Codex CLI",
"",
"### Area",
"CLI",
"",
"### Summary",
summary,
"",
"### Reproduction",
reproduction,
"",
"### Version",
"v2.15.0",
"",
"### Operating system",
"Windows 11",
"",
"### Provider and model",
"_No response_",
"",
"### Logs or error output",
"```shell",
"",
"```",
].join("\n");
}

describe("issue #1672 regression", () => {
it("rejects a reproduction that only echoes the generic final sync failure from Summary", () => {
const genericFailure =
"Codex sync did not complete. Fix the reported Codex config issue and retry.";
const body = bugBody({
summary: `ocx sync\n${genericFailure}`,
reproduction: genericFailure,
});

const result = validateIssue({
title: genericFailure,
body,
labels: ["bug"],
});

assert.equal(result.kind, "bug");
assert.equal(result.valid, false);
assert.ok(
result.reasons.some((reason) => /reproduction.*repeat|echo/i.test(reason)),
`Expected summary-echo rejection, got: ${result.reasons.join("; ")}`,
);
});

it("rejects ordered-list formatting when it only repeats Summary evidence", () => {
const genericFailure =
"Codex sync did not complete. Fix the reported Codex config issue and retry.";
const body = bugBody({
summary: ["Run `ocx sync`.", genericFailure].join("\n"),
reproduction: ["1. Run `ocx sync`.", `2. ${genericFailure}`].join("\n"),
});

const result = validateIssue({
title: genericFailure,
body,
labels: ["bug"],
});

assert.equal(result.kind, "bug");
assert.equal(result.valid, false);
assert.ok(
result.reasons.some((reason) => /reproduction.*repeat|echo/i.test(reason)),
`Expected ordered summary-echo rejection, got: ${result.reasons.join("; ")}`,
);
});

it("normalizes identical multi-line ordered lists on both sides", () => {
const genericFailure =
"Codex sync did not complete. Fix the reported Codex config issue and retry.";
const repeated = ["1. Run `ocx sync`.", `2. ${genericFailure}`].join("\n");
const body = bugBody({
summary: repeated,
reproduction: repeated,
});

assert.equal(reproductionOnlyEchoesSummary(repeated, repeated), true);

const result = validateIssue({
title: genericFailure,
body,
labels: ["bug"],
});

assert.equal(result.kind, "bug");
assert.equal(result.valid, false);
});

it("preserves numeric failure evidence inside fenced and indented code blocks", () => {
const fenced = ["```text", "404. Not Found", "```"].join("\n");
const indented = " 404. Not Found";

assert.equal(stripOrderedListPrefixes(fenced), fenced);
assert.equal(stripOrderedListPrefixes(indented), indented);
assert.match(independentReproductionText("Not Found", fenced), /404\. Not Found/);
});

it("keeps the same failure text valid when Reproduction adds an actionable command", () => {
const genericFailure =
"Codex sync did not complete. Fix the reported Codex config issue and retry.";
const body = bugBody({
summary: genericFailure,
reproduction: [
"1. Run `ocx sync`.",
`2. Observe: ${genericFailure}`,
].join("\n"),
});

const result = validateIssue({
title: "ocx sync fails after configuration injection",
body,
labels: ["bug"],
});

assert.equal(
result.valid,
true,
`Expected actionable reproduction to remain valid, got: ${result.reasons.join("; ")}`,
);
});
});
103 changes: 102 additions & 1 deletion .github/scripts/issue-quality.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,114 @@ function detectIssueKind(issue) {
return core.detectIssueKind(normalizeEquivalentBugEvidence(issue));
}

/**
* Ordered-list numbers are presentation, not evidence, but numeric output in a
* fenced or indented code block may be the failure itself (for example
* `404. Not Found`). Strip list prefixes only from prose lines.
*/
function stripOrderedListPrefixes(text) {
let fence = null;

return String(text || "")
.split(/\r?\n/)
.map((line) => {
if (fence) {
const closing = line.match(/^\s{0,3}(`{3,}|~{3,})\s*$/);
if (
closing &&
closing[1][0] === fence.char &&
closing[1].length >= fence.length
) {
fence = null;
}
return line;
}

const opening = line.match(/^\s{0,3}(`{3,}|~{3,})/);
if (opening) {
fence = { char: opening[1][0], length: opening[1].length };
return line;
}

if (/^(?: {4,}|\t)/.test(line)) return line;
return line.replace(/^\s{0,3}\d+[.)]\s+/, "");
})
.join("\n");
}

function independentReproductionText(summary, reproduction) {
const summaryCan = core.canonicalise(stripOrderedListPrefixes(summary));
return stripOrderedListPrefixes(reproduction)
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter((line) => {
const lineCan = core.canonicalise(line);
return lineCan && !summaryCan.includes(lineCan);
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.join("\n");
}

/**
* Reject the narrow #1672 class: Reproduction is only text already present in
* Summary and contributes no independent actionable evidence. Compute the
* actionable check only over reproduction-only lines so phrases like
* "Codex config" inside the shared generic error cannot be misread by the
* legacy command heuristic as a `codex config` invocation.
*/
function reproductionOnlyEchoesSummary(summary, reproduction) {
// Ordered step numbers are presentation, not evidence. Normalize both sides
// consistently while preserving numeric output inside code blocks.
const summaryCan = core.canonicalise(stripOrderedListPrefixes(summary));
const reproductionCan = core.canonicalise(stripOrderedListPrefixes(reproduction));
if (!summaryCan || !reproductionCan) return false;

if (summaryCan === reproductionCan || summaryCan.includes(reproductionCan)) {
return true;
}
if (!reproductionCan.includes(summaryCan)) return false;

const independent = independentReproductionText(summary, reproduction);
if (!independent) return true;

const explicitOcxAction =
/\b(?:run|execute|invoke|retry)\s+[`'"*_~]*ocx\s+(?:sync|restore|update|doctor|start|stop|restart)\b/i.test(
independent,
);
return !(explicitOcxAction || core.hasActionableReproductionDetail(independent));
}

function validateIssue(issue) {
return core.validateIssue(normalizeEquivalentBugEvidence(issue));
const normalizedIssue = normalizeEquivalentBugEvidence(issue);
const result = core.validateIssue(normalizedIssue);

if (result.kind !== "bug" || result.softPass || !result.valid) return result;

const body = String(normalizedIssue?.body || "");
const summary = core.extractSection(body, "Summary");
const reproduction = core.extractSection(body, "Reproduction");
if (!reproductionOnlyEchoesSummary(summary, reproduction)) return result;

return {
...result,
valid: false,
reasons: [
...result.reasons,
"Reproduction only echoes the Summary and does not add actionable steps or failure evidence.",
],
guidance: [
...result.guidance,
"List the exact command or steps that trigger the problem and include the underlying error or observed output, not only the final summary message.",
],
};
}

module.exports = {
...core,
detectIssueKind,
validateIssue,
normalizeEquivalentBugEvidence,
stripOrderedListPrefixes,
independentReproductionText,
reproductionOnlyEchoesSummary,
};
Loading
Loading