Skip to content

Commit f348166

Browse files
authored
Merge pull request #564 from Wibias/fix/issue-quality-retitle-softpass
fix(ci): soft-pass detailed issues after maintainer retitle
2 parents 0fa45cf + ec59881 commit f348166

2 files changed

Lines changed: 120 additions & 13 deletions

File tree

.github/scripts/issue-quality.cjs

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ function resolveSection(body, headings) {
150150
}
151151

152152
/**
153-
* True when the body has at least one non-empty h2–h4 section with enough
154-
* detail. Used for soft-pass only — unstructured length alone is not enough.
153+
* True when the body has multiple non-empty h2–h4 sections with enough detail.
154+
* Soft-pass only — unstructured length alone is not enough, and a single
155+
* arbitrary heading must not bypass the quality gate (Codex on #564).
155156
*/
156-
function hasSubstantialStructuredContent(body, minSectionLen = 40) {
157+
function hasSubstantialStructuredContent(body, minSectionLen = 40, minRichSections = 2) {
157158
if (typeof body !== "string") return false;
158159
const lines = body.split("\n");
159160
let capturing = false;
@@ -173,7 +174,7 @@ function hasSubstantialStructuredContent(body, minSectionLen = 40) {
173174
if (capturing) bucket.push(line);
174175
}
175176
if (capturing) flush();
176-
return richSections >= 1;
177+
return richSections >= minRichSections;
177178
}
178179

179180
// ---------------------------------------------------------------------------
@@ -541,7 +542,6 @@ function validateIssue(issue) {
541542
const reasons = [];
542543
const guidance = [];
543544
let softPass = false;
544-
const titleLower = title.toLowerCase();
545545

546546
if (!kind) {
547547
const failure = untemplatedIssueFailure(issue);
@@ -578,9 +578,11 @@ function validateIssue(issue) {
578578
goal !== null || blocker !== null || behaviour !== null || example !== null;
579579

580580
if (emptyCore.length > 0) {
581+
// Soft-pass rich non-template bodies once kind is already feature (title
582+
// prefix, enhancement label, or stored kind). Do not require the title to
583+
// keep a `[Feature]:` prefix — maintainer retitles must not re-arm closure.
581584
const canSoftPass =
582585
!mappedHeadingPresent &&
583-
titleLower.startsWith("[feature]:") &&
584586
hasSubstantialStructuredContent(body);
585587
if (canSoftPass) {
586588
softPass = true;
@@ -625,10 +627,14 @@ function validateIssue(issue) {
625627
const os = extractSection(body, "Operating system") ?? extractSection(body, "OS");
626628

627629
if (isEmpty(summary) && isEmpty(repro)) {
630+
// Soft-pass substantial non-English / freeform structured reports once
631+
// kind is already bug (label, stored kind, or prior `[Bug]:` detection).
632+
// Requiring the title to keep a `[Bug]:` prefix caused #545: a maintainer
633+
// retitle of an already detailed report was treated as empty Summary/
634+
// Reproduction and auto-closed.
628635
const canSoftPass =
629636
summary === null &&
630637
repro === null &&
631-
titleLower.startsWith("[bug]:") &&
632638
hasSubstantialStructuredContent(body);
633639
if (canSoftPass) {
634640
softPass = true;
@@ -687,27 +693,39 @@ function validateIssue(issue) {
687693
if (version !== null && isRawPlaceholder(version) === false && isEmpty(version)) emptyCore.push("OpenCodex version");
688694
if (endpoint !== null && isEmpty(endpoint)) emptyCore.push("endpoint or capability");
689695
if (emptyCore.length > 0) {
690-
reasons.push(`Required sections are missing or empty: ${emptyCore.join(", ")}.`);
691-
guidance.push("Describe both the current and expected behaviour.");
696+
// Same soft-pass as bug/feature: label- or maintainer-scoped provider
697+
// reports often use non-English structured headings after a retitle.
698+
const mappedHeadingPresent =
699+
current !== null || expected !== null || repro !== null || response !== null || docs !== null ||
700+
provider !== null || version !== null || endpoint !== null;
701+
const canSoftPass =
702+
!mappedHeadingPresent &&
703+
hasSubstantialStructuredContent(body);
704+
if (canSoftPass) {
705+
softPass = true;
706+
} else {
707+
reasons.push(`Required sections are missing or empty: ${emptyCore.join(", ")}.`);
708+
guidance.push("Describe both the current and expected behaviour.");
709+
}
692710
}
693711

694-
if (!isEmpty(current) && !isEmpty(expected) && canonicalise(current) === canonicalise(expected)) {
712+
if (!softPass && !isEmpty(current) && !isEmpty(expected) && canonicalise(current) === canonicalise(expected)) {
695713
reasons.push("Current and expected behaviour are effectively identical.");
696714
guidance.push("Explain the difference between what happens now and what should happen.");
697715
}
698716

699717
const allSections = [current, expected, repro, response].filter((s) => !isEmpty(s));
700-
if (allSections.length >= 2 && allRepeatTitle(allSections, title)) {
718+
if (!softPass && allSections.length >= 2 && allRepeatTitle(allSections, title)) {
701719
reasons.push("All sections merely repeat the issue title.");
702720
guidance.push("Add specific detail in each section.");
703721
}
704722

705-
if (isEmpty(repro) && isEmpty(response)) {
723+
if (!softPass && isEmpty(repro) && isEmpty(response)) {
706724
reasons.push("Both the request/reproduction and the actual response/error are absent.");
707725
guidance.push("Include at least a minimal redacted request or the actual error output.");
708726
}
709727

710-
if (isEmpty(docs)) {
728+
if (!softPass && isEmpty(docs)) {
711729
reasons.push("Upstream documentation is empty without stating that no public specification exists.");
712730
guidance.push("Add a URL to the provider specification, or state that no public spec exists.");
713731
}

.github/scripts/issue-quality.test.cjs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,95 @@ describe("translated feature headings and soft-pass", () => {
998998
assert.equal(result.valid, false);
999999
});
10001000

1001+
it("soft-passes retitled feature reports that drop the [Feature]: prefix", () => {
1002+
const body = [
1003+
"### Concrete user workflow that fails",
1004+
"User pastes an image in Codex App while a text-only routed model is selected and the App blocks upload.",
1005+
"### Why this matters",
1006+
"Vision sidecar is advertised but never reached from the App client path.",
1007+
"### Verification",
1008+
"Same proxy config works end-to-end in Claude Code with the sidecar describing the image.",
1009+
].join("\n");
1010+
const result = validateIssue({
1011+
title: "Vision sidecar unusable from Codex App",
1012+
body,
1013+
labels: ["enhancement"],
1014+
storedKind: "feature",
1015+
});
1016+
assert.equal(result.kind, "feature");
1017+
assert.equal(result.softPass, true);
1018+
});
1019+
1020+
it("soft-passes retitled bug reports with substantial non-English structure (#545)", () => {
1021+
// Maintainer retitle removed `[Bug]:`; Korean structured body has no English
1022+
// Summary/Reproduction headings but is clearly actionable.
1023+
const body = [
1024+
"## 환경",
1025+
"- opencodex 2.7.41 (launchd, port 10100)",
1026+
"- Claude Desktop 3P + Anthropic OAuth (Pro/Max)",
1027+
"- Auto Mode classifier model = `claude-sonnet-5`",
1028+
"",
1029+
"## 증상",
1030+
"Auto Mode classifier requests truncate at outputTokens=64 with max_output_tokens,",
1031+
"then retry the same payload up to 5 times. Dashboard previously showed 502.",
1032+
"",
1033+
"## 재현",
1034+
"1. `ocx login anthropic` and enable Claude Desktop 3P gateway key mode",
1035+
"2. Enable Auto Mode and trigger a tool permission classifier turn",
1036+
"3. Observe five identical 64-token incomplete terminals for one approval",
1037+
"",
1038+
"## 증거",
1039+
"Inbound+outbound correlated captures show max_tokens:64 and stop_sequences preserved.",
1040+
].join("\n");
1041+
const result = validateIssue({
1042+
title: "Claude Desktop 3P Auto Mode classifier retries after 64-token Anthropic OAuth outputs",
1043+
body,
1044+
labels: ["bug", "provider-compatibility"],
1045+
storedKind: "bug",
1046+
});
1047+
assert.equal(result.kind, "bug");
1048+
assert.equal(result.softPass, true, `Expected soft-pass but got: ${result.reasons.join("; ")}`);
1049+
assert.equal(result.valid, false);
1050+
});
1051+
1052+
it("does not soft-pass a single arbitrary rich heading (Codex #564)", () => {
1053+
const result = validateIssue({
1054+
title: "Something broke after upgrade",
1055+
body: [
1056+
"## Notes",
1057+
"x".repeat(80),
1058+
].join("\n"),
1059+
labels: ["bug"],
1060+
storedKind: "bug",
1061+
});
1062+
assert.equal(result.kind, "bug");
1063+
assert.equal(result.softPass, false);
1064+
assert.equal(result.valid, false);
1065+
assert.match(result.reasons.join(" "), /Summary and Reproduction are empty/);
1066+
});
1067+
1068+
it("does not soft-pass provider reports that only fill mapped metadata headings", () => {
1069+
const result = validateIssue({
1070+
title: "Provider X fails on Responses",
1071+
body: [
1072+
"### Provider or upstream service",
1073+
"custom-openai-compatible gateway hosted on our internal mesh",
1074+
"### OpenCodex version",
1075+
"2.7.41",
1076+
"### Endpoint or capability",
1077+
"`POST /v1/responses` with streaming tool calls",
1078+
"## Extra notes",
1079+
"We see intermittent 502s after rotating the upstream API key for this gateway.",
1080+
].join("\n"),
1081+
labels: ["provider-compatibility"],
1082+
storedKind: "provider-compatibility",
1083+
});
1084+
assert.equal(result.kind, "provider-compatibility");
1085+
assert.equal(result.softPass, false);
1086+
assert.equal(result.valid, false);
1087+
assert.match(result.reasons.join(" "), /current behaviour|expected behaviour/i);
1088+
});
1089+
10011090
it("still rejects empty [Feature]: bodies", () => {
10021091
const result = validateIssue({
10031092
title: "[Feature]: do something cool",

0 commit comments

Comments
 (0)