From 6cb5d3046b8c872da5f0eeaa7cabf763fabd7dce Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:23:37 +0100 Subject: [PATCH] Preserve secondary issue feedback Contract-Neutral: issue-triage intake only; no product, deployment, or release contract change Change-source: pulse-maintainer --- .github/ISSUE_TEMPLATE/bug_report.yml | 9 ++ .github/ISSUE_TEMPLATE/feature_request.md | 30 ------- .github/ISSUE_TEMPLATE/feature_request.yml | 65 ++++++++++++++ .github/ISSUE_TEMPLATE/v6_rc_feedback.yml | 9 ++ .github/scripts/issue-version-triage.cjs | 50 ++++++++++- .github/scripts/issue-version-triage.test.cjs | 88 +++++++++++++++++++ .github/workflows/README.md | 4 +- CONTRIBUTING.md | 5 +- docs/AI_TRANSPARENCY.md | 7 +- docs/ISSUE_TRIAGE.md | 55 ++++++++++++ 10 files changed, 287 insertions(+), 35 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 docs/ISSUE_TRIAGE.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d5634308b..66ac87d22 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -41,6 +41,15 @@ body: validations: required: true + - type: textarea + id: additional_topics + attributes: + label: Additional actionable topics + description: Enter "None" for a single bug. If this report also raises another bug, feature, or workflow, list each topic here; triage will preserve it with a separate linked disposition. + placeholder: None + validations: + required: true + - type: input id: pulse_version attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 19f6514a6..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Feature request -about: Describe an operator problem or workflow Pulse could improve -title: "[Feature]: " -labels: enhancement -assignees: "" ---- - -## Problem - -What are you trying to accomplish, and what makes it difficult today? - -## Current workflow - -Describe how you handle this now. Include the relevant platform, Pulse page, -deployment type, and constraints. - -## Desired outcome - -What would a successful Pulse workflow let you do? Focus on the outcome rather -than prescribing an implementation when possible. - -## Alternatives considered - -What workarounds, other tools, or different approaches have you tried? - -## Additional context - -Add screenshots, examples, or related issues. Do not include credentials, -tokens, private keys, or unsanitized configuration files. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..ad6067e15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,65 @@ +name: Feature request +description: Describe one operator problem or workflow Pulse could improve +title: "[Feature]: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Lead with one primary operator outcome. If context exposes another actionable topic, list it in the dedicated field below; triage will preserve it rather than silently folding it into the primary request. + + Do not include credentials, tokens, private keys, or unsanitized configuration files. + + - type: textarea + id: problem + attributes: + label: Problem + description: What are you trying to accomplish, and what makes it difficult today? + validations: + required: true + + - type: textarea + id: current_workflow + attributes: + label: Current workflow + description: Describe how you handle this now. Include the relevant platform, Pulse page, deployment type, and constraints. + validations: + required: true + + - type: textarea + id: desired_outcome + attributes: + label: Desired outcome + description: What would a successful Pulse workflow let you do? Focus on the outcome rather than prescribing an implementation when possible. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: What workarounds, other tools, or different approaches have you tried? + + - type: textarea + id: additional_topics + attributes: + label: Additional actionable topics + description: Enter "None" for a single request. If the context also raises another bug, feature, or workflow, list each topic here; triage will preserve it with a separate linked disposition. + placeholder: None + validations: + required: true + + - type: textarea + id: additional_context + attributes: + label: Additional context + description: Add screenshots, examples, or related issues. Do not include credentials, tokens, private keys, or unsanitized configuration files. + + - type: checkboxes + id: confirmations + attributes: + label: Confirmations + options: + - label: I searched existing issues for duplicates. + required: true diff --git a/.github/ISSUE_TEMPLATE/v6_rc_feedback.yml b/.github/ISSUE_TEMPLATE/v6_rc_feedback.yml index cd118b6dc..312ce0433 100644 --- a/.github/ISSUE_TEMPLATE/v6_rc_feedback.yml +++ b/.github/ISSUE_TEMPLATE/v6_rc_feedback.yml @@ -123,6 +123,15 @@ body: validations: required: true + - type: textarea + id: additional_topics + attributes: + label: Additional actionable topics + description: Enter "None" for a single topic. If this report also raises another bug, feature, or workflow, list each topic here; triage will preserve it with a separate linked disposition. + placeholder: None + validations: + required: true + - type: textarea id: evidence attributes: diff --git a/.github/scripts/issue-version-triage.cjs b/.github/scripts/issue-version-triage.cjs index 95056207a..33b54e1f8 100644 --- a/.github/scripts/issue-version-triage.cjs +++ b/.github/scripts/issue-version-triage.cjs @@ -1,6 +1,7 @@ const VERSION_LABEL_PREFIX = "affects-"; const NEEDS_VERSION_LABEL = "needs-version-info"; const RETEST_LABEL = "needs-retest-on-latest"; +const NEEDS_DECOMPOSITION_LABEL = "needs-decomposition"; const RETEST_COMMENT_MARKER = ""; const CLOSE_COMMENT_MARKER = ""; const TRIAGE_FOOTER = @@ -28,6 +29,28 @@ function extractSectionValue(body, heading) { return value || null; } +function classifyAdditionalActionableTopics(body) { + const value = extractSectionValue(body, "Additional actionable topics"); + if (value === null) return null; + + const normalized = value + .replace(//g, "") + .trim() + .toLowerCase() + .replace(/[.!]+$/g, ""); + if (!normalized) return false; + + return !new Set([ + "_no response_", + "n/a", + "na", + "no", + "none", + "none known", + "not applicable", + ]).has(normalized); +} + function normalizeVersion(value) { if (!value) return null; const match = String(value).match(/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/i); @@ -170,6 +193,14 @@ function buildTriageState(issue, core, latestVersion) { nextLabels.add(v6FeedbackClass); } + const hasAdditionalActionableTopics = classifyAdditionalActionableTopics(issue.body); + if (hasAdditionalActionableTopics === true) { + core.info("Issue declares additional actionable topics; decomposition is required."); + nextLabels.add(NEEDS_DECOMPOSITION_LABEL); + } else if (hasAdditionalActionableTopics === false) { + nextLabels.delete(NEEDS_DECOMPOSITION_LABEL); + } + const reportedVersion = extractPulseVersion(issue.title, issue.body); core.info(`Reported Pulse version: ${reportedVersion || "not found"}`); core.info(`Latest stable release: ${latestVersion || "unknown"}`); @@ -179,6 +210,7 @@ function buildTriageState(issue, core, latestVersion) { nextLabels, reportedVersion, v6FeedbackClass, + hasAdditionalActionableTopics, isBugLike: nextLabels.has(BUG_LABEL), comparison: reportedVersion && latestVersion ? compareCore(reportedVersion, latestVersion) : null, @@ -248,13 +280,27 @@ async function syncLabels({ github, context, core }) { nextLabels, reportedVersion, v6FeedbackClass, + hasAdditionalActionableTopics, isBugLike, comparison, } = buildTriageState(issue, core, latestVersion); + if (hasAdditionalActionableTopics === true) { + await ensureLabel( + github, + context, + NEEDS_DECOMPOSITION_LABEL, + "fbca04", + "Issue declares additional actionable topics that need linked dispositions" + ); + } + if (!isBugLike) { core.info("Issue is not bug-like after classification. Skipping version triage."); - if (v6FeedbackClass && !labelNames.has(v6FeedbackClass)) { + const labelsChanged = + labelNames.size !== nextLabels.size || + [...labelNames].some((label) => !nextLabels.has(label)); + if (labelsChanged) { await github.rest.issues.setLabels({ owner: context.repo.owner, repo: context.repo.repo, @@ -433,6 +479,7 @@ module.exports = { CLOSE_COMMENT_MARKER, DOCS_LABEL, ENHANCEMENT_LABEL, + NEEDS_DECOMPOSITION_LABEL, NEEDS_VERSION_LABEL, RETEST_COMMENT_MARKER, RETEST_COMMENT_GRACE_MS, @@ -444,6 +491,7 @@ module.exports = { buildTimeoutCloseCommentBody, buildTriageState, canPostRetestComment, + classifyAdditionalActionableTopics, classifyV6FeedbackType, compareCore, extractPulseVersion, diff --git a/.github/scripts/issue-version-triage.test.cjs b/.github/scripts/issue-version-triage.test.cjs index 642f04edc..b230f7f28 100644 --- a/.github/scripts/issue-version-triage.test.cjs +++ b/.github/scripts/issue-version-triage.test.cjs @@ -1,5 +1,7 @@ const test = require("node:test"); const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); const triage = require("./issue-version-triage.cjs"); @@ -124,6 +126,92 @@ test("syncLabels only adds documentation classification for non-bug v6 feedback" assert.deepEqual(calls.setLabels[0].labels, ["documentation"]); }); +test("syncLabels marks declared secondary topics for decomposition", async () => { + const { github, calls } = createGithub({ latestVersion: "6.4.1" }); + const issue = { + number: 1796, + title: "Availability workflow feedback", + body: [ + "## Problem", + "Machine availability is hard to scan.", + "", + "## Additional actionable topics", + "The triage bot should preserve secondary requests.", + ].join("\n"), + labels: [{ name: "enhancement" }], + }; + + await triage.syncLabels({ + github, + context: createContext({ issue }), + core: createCore(), + }); + + assert.deepEqual(calls.createLabel.map((call) => call.name), [ + "needs-decomposition", + ]); + assert.deepEqual(calls.setLabels[0].labels, [ + "enhancement", + "needs-decomposition", + ]); +}); + +test("syncLabels clears decomposition after every declared topic has a disposition", async () => { + const { github, calls } = createGithub({ latestVersion: "6.4.1" }); + const issue = { + number: 1796, + title: "Availability workflow feedback", + body: "## Additional actionable topics\nNone.\n", + labels: [{ name: "enhancement" }, { name: "needs-decomposition" }], + }; + + await triage.syncLabels({ + github, + context: createContext({ action: "edited", issue }), + core: createCore(), + }); + + assert.equal(calls.createLabel.length, 0); + assert.deepEqual(calls.setLabels[0].labels, ["enhancement"]); +}); + +test("additional topic classification is explicit and fail-quiet for legacy forms", () => { + const { classifyAdditionalActionableTopics } = triage.internals; + + assert.equal(classifyAdditionalActionableTopics("## Problem\nOne thing\n"), null); + assert.equal( + classifyAdditionalActionableTopics("## Additional actionable topics\n_No response_\n"), + false + ); + assert.equal( + classifyAdditionalActionableTopics("## Additional actionable topics\nNone known.\n"), + false + ); + assert.equal( + classifyAdditionalActionableTopics( + "## Additional actionable topics\n- Add a storage filter\n- Reduce log noise\n" + ), + true + ); +}); + +test("every actionable issue form exposes the decomposition signal", () => { + const templateDir = path.resolve(__dirname, "../ISSUE_TEMPLATE"); + for (const name of [ + "bug_report.yml", + "feature_request.yml", + "v6_rc_feedback.yml", + ]) { + const form = fs.readFileSync(path.join(templateDir, name), "utf8"); + assert.match(form, /id: additional_topics/); + assert.match(form, /label: Additional actionable topics/); + assert.match( + form, + /id: additional_topics[\s\S]*?validations:\s*\n\s+required: true/ + ); + } +}); + test("postRetestComment comments once for older non-maintainer bug reports", async () => { const { github, calls } = createGithub({ latestVersion: "6.0.1" }); const issue = { diff --git a/.github/workflows/README.md b/.github/workflows/README.md index ff2493251..c1974f1c0 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -3,14 +3,16 @@ ## Issue Triage Automation **Files**: + - `issue-version-label-sync.yml` - `issue-version-retest-comment.yml` Issue intake is split deliberately: -- `issue-version-label-sync.yml` is the silent metadata path. It runs on `opened`, `edited`, and `reopened` issue events so version labels, `needs-version-info`, and `needs-retest-on-latest` stay correct when maintainers tidy issue metadata. +- `issue-version-label-sync.yml` is the silent metadata path. It runs on `opened`, `edited`, and `reopened` issue events so version labels, `needs-version-info`, `needs-retest-on-latest`, and the explicit `needs-decomposition` topic-integrity signal stay correct when maintainers tidy issue metadata. - `issue-version-retest-comment.yml` is the scheduled public guidance path. It gives maintainers a grace window, then posts reporter-facing retest guidance only when an older-version bug report from a non-maintainer has no existing maintainer response. This prevents generic stable-version advice from contradicting a specific maintainer fix or prerelease boundary. - Both workflows load the shared helper at `.github/scripts/issue-version-triage.cjs` so parsing and classification logic lives in one place instead of drifting across duplicated inline scripts. +- `needs-decomposition` is driven only by the structured **Additional actionable topics** form field. The [triage contract](../../docs/ISSUE_TRIAGE.md) requires human or agent judgment to create linked dispositions; the workflow does not infer or auto-create issues from free text. ## Update Demo Server diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e45031aa..6df41b817 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,10 @@ and installer tooling. - State the Pulse version and image or package you are actually running. - Include screenshots, logs, API output, or diagnostics when they clarify the problem. -- Separate bug reports from feature requests; avoid mixing both into one issue. +- Lead with one primary bug or operator outcome. If the context also exposes + another actionable topic, put it in the issue form's dedicated field. Triage + will preserve it with a linked disposition; you do not need to refile text + you already supplied. See [Issue Triage and Topic Integrity](docs/ISSUE_TRIAGE.md). --- diff --git a/docs/AI_TRANSPARENCY.md b/docs/AI_TRANSPARENCY.md index 019a7269f..832e96a71 100644 --- a/docs/AI_TRANSPARENCY.md +++ b/docs/AI_TRANSPARENCY.md @@ -29,8 +29,11 @@ released builds still go through Pulse's release qualification process. and the evidence used to qualify a release. - **Issue triage and support.** Automated issue and discussion replies post under the dedicated `pulse-triage` bot identity and link back to this page. - Automated issue state changes use that identity as well. Automated support - replies are sent as Pulse Triage and link here as well. + Automated issue state changes use that identity as well. Mixed reports follow + the [topic-integrity triage contract](ISSUE_TRIAGE.md): automation can surface + declared secondary topics, but a maintainer or triage agent must give every + actionable topic a linked disposition. Automated support replies are sent as + Pulse Triage and link here as well. - **Change provenance.** Commits made by the continuously running maintainer carry a dedicated bot author and committer identity. Issue-driven changes link back to the originating report where applicable. diff --git a/docs/ISSUE_TRIAGE.md b/docs/ISSUE_TRIAGE.md new file mode 100644 index 000000000..092361224 --- /dev/null +++ b/docs/ISSUE_TRIAGE.md @@ -0,0 +1,55 @@ +# Issue Triage and Topic Integrity + +Pulse issue triage must preserve every actionable topic a reporter contributes. +Resolving the primary defect does not dispose of secondary bugs, feature requests, +documentation gaps, or operator workflows described in the same report. + +## Intake contract + +Issue forms ask for one primary outcome and provide an **Additional actionable +topics** field. Entering anything other than `None` applies the +`needs-decomposition` label automatically. This is a queue-integrity signal, +not a statement that every topic will be built. + +Reporters may still write free-form issues, edit form output, or discover a +second topic during discussion. Triage owns decomposition in those cases; it +must not require the reporter to refile information they already supplied. + +## Required disposition + +Before removing `needs-decomposition` or declaring a mixed report triaged: + +1. Enumerate each independently actionable topic in the issue body and comments. +2. Keep the original issue focused on its primary reproducible problem. +3. Give every other topic one linked disposition: + - a new or existing issue for a distinct defect or independently actionable + product request; + - a demand-ledger entry for evidence that is useful but not yet build-ready; + - a Discussion or support path when there is no reproducible defect or + product decision to track; + - an explicit decline, with the reason, when the topic conflicts with Pulse's + product or safety boundaries. +4. Use GitHub sub-issue relationships when separate issues share the same source + report and the authenticated mutation path supports them. A plain backlink + remains in each child body so the evidence survives clients that do not + render sub-issues and remains the fallback for bounded bot identities. +5. Post a concise topic-to-disposition summary on the source issue. Never say a + topic is “recorded” without naming where it is recoverable. + +Decomposition does not multiply demand. Every child points to the same reporter +and source thread, and the demand ledger counts that as one signal per capability. +Do not copy credentials, private diagnostics, or personal data into a child +issue; summarize only the minimum sanitized evidence needed to preserve the +operator problem. + +## Automation boundary + +The label synchronizer detects the structured form field deterministically and +fails quietly for legacy forms. It does not use keyword heuristics to invent +topics or create issues automatically. A maintainer or triage agent reviews the +source context, chooses the correct destination, creates links, and is +accountable for the disposition. + +This boundary is deliberate: preserving an explicit reporter declaration is +safe to automate, while deciding whether two observations are one root cause is +a product and technical judgment.