From 45f779bad34843f248c2dc6f01398f3deee09cce Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:38:54 +0100 Subject: [PATCH] Keep neighbouring issue fields out of topic review Bound edited topic declarations by all current form fields without discarding deeper topic subheadings. Prevent unrelated version, environment and recovery edits from inventing or reopening decomposition work; preserve genuine revisions and Community labels. Contract-Neutral: Silent issue-topic classification and registered regression controls only; no Pulse runtime, rendered product surface, dependency, access or public-contact change. Change-source: pulse-maintainer --- .github/scripts/issue-version-triage.cjs | 26 +++++- .github/scripts/issue-version-triage.test.cjs | 91 +++++++++++++++++++ docs/ISSUE_TRIAGE.md | 5 + 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/.github/scripts/issue-version-triage.cjs b/.github/scripts/issue-version-triage.cjs index c0af496d91..472e2cf8ce 100644 --- a/.github/scripts/issue-version-triage.cjs +++ b/.github/scripts/issue-version-triage.cjs @@ -52,7 +52,8 @@ function declarationLines(body) { for (const token of lexer(String(body || ""), { gfm: true })) { if (token.type === "heading") { const text = inlineProjection(token.tokens).trim(); - lines.push({ text, metadata: text, delimiter: false, declaration: true, heading: text }); + lines.push({ text, metadata: text, delimiter: false, declaration: true, + heading: text, headingDepth: token.depth }); } else if (token.type === "paragraph") { append(inlineProjection(token.tokens), true, inlineProjection(token.tokens, true)); } else if (token.type === "code") { @@ -100,7 +101,8 @@ function extractSectionValue(body, heading, followingHeadings = []) { ? new RegExp(`^(?:${followingHeadings.map(escapeRegExp).join("|")})$`, "i") : null; let end = start + 1; while (end < lines.length && !(lines[end].heading !== null && - (!boundary || boundary.test(lines[end].heading)))) { + (!boundary || (lines[end].headingDepth <= lines[start].headingDepth && + boundary.test(lines[end].heading))))) { end += 1; } // An empty declared field differs from a legacy report with no field. @@ -108,8 +110,28 @@ function extractSectionValue(body, heading, followingHeadings = []) { } function additionalActionableTopicsValue(body) { + // Edited reports can move this field beside any other form field. Context + // from those fields must not invent a topic or reopen a completed review. + // Deeper subheadings still belong to the topic, even with a familiar name. return extractSectionValue(body, "Additional actionable topics", [ + "Describe the bug", + "Steps or original sequence", + "Expected behavior", + "Feedback type", "Pulse version", + "Last known working Pulse version", + "Agent version", + "Image tag or digest", + "Running Pulse container image tag or digest", + "Install path", + "Installation type", + "OS / environment", + "Installer or helper source", + "Upgrade attempt and recovery", + "License tier", + "What happened?", + "Steps or original sequence / usage context", + "What did you expect instead?", "Additional context", "Logs, screenshots, or diagnostics", "Confirmations", diff --git a/.github/scripts/issue-version-triage.test.cjs b/.github/scripts/issue-version-triage.test.cjs index 1c2e5f032b..98e7033ab9 100644 --- a/.github/scripts/issue-version-triage.test.cjs +++ b/.github/scripts/issue-version-triage.test.cjs @@ -540,6 +540,97 @@ test("topic classification stops at form fields but retains topic subheadings", } }); +function topicBoundaryFields() { + const fields = new Set(["Additional context"]); + for (const name of ["bug_report.yml", "v6_rc_feedback.yml"]) { + const form = fs.readFileSync(path.resolve(__dirname, "../ISSUE_TEMPLATE", name), "utf8"); + for (const [, label] of form.matchAll(/^ label: (.+)$/gm)) { + if (label !== "Additional actionable topics") fields.add(label); + } + } + return [...fields]; +} + +test("every current form field bounds a reordered topic declaration", () => { + const { classifyAdditionalActionableTopics } = triage.internals; + for (const field of topicBoundaryFields()) { + for (const newline of ["\n", "\r\n"]) { + for (const level of [2, 3]) { + for (const empty of ["", "None", "_No response_", ""]) { + const body = ["### Additional actionable topics", empty, "", + `${"#".repeat(level)} ${field.toUpperCase()}`, "Existing context, not another topic."].join(newline); + assert.equal(classifyAdditionalActionableTopics(body), false, body); + } + } + } + } +}); + +test("a form-named topic subheading remains actionable inside the declaration", () => { + const { classifyAdditionalActionableTopics } = triage.internals; + for (const field of topicBoundaryFields()) { + for (const level of [1, 2, 3, 4, 5]) { + const body = [`${"#".repeat(level)} Additional actionable topics`, "None", "", + `${"#".repeat(level + 1)} ${field}`, "A separate problem on this surface.", "", + `${"#".repeat(level)} Pulse version`, "6.5.0"].join("\n"); + assert.equal(classifyAdditionalActionableTopics(body), true, body); + } + } +}); + +test("copied form-named headings cannot hide a later topic", () => { + const { classifyAdditionalActionableTopics } = triage.internals; + for (const copied of [ + "```text\n### Agent version\n6.5.0\n```", + "> ### Agent version\n> 6.5.0", + "
\n### Agent version\n6.5.0\n", + "", + ]) { + const body = ["### Additional actionable topics", "None", "", copied, "", + "A separate problem still needs a disposition.", "", "### OS / environment", "API only"].join("\n"); + assert.equal(classifyAdditionalActionableTopics(body), true, body); + } +}); + +test("opening or editing neighbouring context cannot invent a decomposition task", async () => { + for (const field of topicBoundaryFields()) { + for (const action of ["opened", "edited"]) { + const { github, calls } = createGithub(); + const body = (value) => `### Additional actionable topics\nNone\n\n### ${field}\n${value}\n`; + const labels = ["enhancement", "needs-human", "release-blocker", "needs-retest-on-latest", "affects-6.5.0"]; + const issue = { number: 2923, title: "Existing report", body: body("Corrected existing context"), + labels: labels.map(name => ({ name })) }; + await syncLabels({ github, core: createCore(), + context: createContext({ action, issue, previousBody: body("Earlier existing context") }) }); + assert.deepEqual(calls.addLabels, [], `${action}: ${field}`); + assert.deepEqual(calls.removeLabel, [], field); + assert.deepEqual(calls.createLabel, [], field); + assert.deepEqual(calls.createComment, [], field); + assert.deepEqual(calls.getLatestRelease, [], field); + assert.deepEqual(calls.paginate, [], field); + } + } +}); + +test("real revised topics still reopen review without disposing of existing tasks", async () => { + for (const existingTask of [false, true]) { + const { github, calls } = createGithub(); + const previousBody = "### Additional actionable topics\nNone\n### Agent version\n6.5.0"; + const body = "### Additional actionable topics\nNone\n#### Installation type\n" + + "The saved installer selection is also lost.\n### Agent version\n6.5.0"; + const issue = { number: 2924, title: "Existing report", body, + labels: ["enhancement", "needs-human", "needs-retest-on-latest", + ...(existingTask ? ["needs-decomposition"] : [])].map(name => ({ name })) }; + await syncLabels({ github, core: createCore(), + context: createContext({ action: "edited", issue, previousBody }) }); + assert.deepEqual(calls.addLabels.map(call => call.labels), existingTask ? [] : [["needs-decomposition"]]); + assert.deepEqual(calls.removeLabel, []); + assert.deepEqual(calls.createComment, []); + assert.deepEqual(calls.getLatestRelease, []); + assert.deepEqual(calls.paginate, []); + } +}); + test("hidden template fields cannot declare topics or override feedback type", () => { const { classifyAdditionalActionableTopics, classifyV6FeedbackType } = triage.internals; const hidden = ["