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
This commit is contained in:
pulse-triage[bot]
2026-10-24 22:38:54 +01:00
parent 96fb44a238
commit 45f779bad3
3 changed files with 120 additions and 2 deletions
+24 -2
View File
@@ -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",
@@ -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_", "<!-- optional -->"]) {
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",
"<pre>\n### Agent version\n6.5.0\n</pre>",
"<!--\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 = ["<!--", "### Additional actionable topics", "A second bug.",
+5
View File
@@ -243,6 +243,11 @@ cannot supply a field or feedback type, and the next form field's contents are
not secondary topics. These labels surface review work; they do not establish
that a defect is reproduced or a request will be built.
Edited reports may reorder fields. Any current form field at the same or a
shallower heading level ends the topic field; changing that context alone must
not reopen decomposition. Deeper topic subheadings remain part of the field,
even when named after a form field such as **Installation type**.
Headings and inline fields inside backtick or tilde code blocks and indented
logs are evidence, not form declarations or field boundaries. A real field can
still contain a fenced version or topic; do not discard that evidence. Version