From 8a2135012273a8f223b8e31aa806823b14932074 Mon Sep 17 00:00:00 2001 From: "heygengenesis[bot]" <262951085+heygengenesis[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 00:49:49 +0000 Subject: [PATCH] fix(cli): require non-empty feedback repro commands Co-authored-by: GPT-5.6 Sol Co-authored-by: miguel.sierra --- packages/cli/src/utils/feedbackLint.test.ts | 24 ++++++++++++ packages/cli/src/utils/feedbackLint.ts | 41 +++++++++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/utils/feedbackLint.test.ts b/packages/cli/src/utils/feedbackLint.test.ts index 5420f928c0..eeb21473fc 100644 --- a/packages/cli/src/utils/feedbackLint.test.ts +++ b/packages/cli/src/utils/feedbackLint.test.ts @@ -30,6 +30,17 @@ describe("lintFeedbackComment", () => { expect(warnings[0]?.message).toContain("REPRO COMMAND:"); }); + it.each([ + ["marker only", "REPRO COMMAND:"], + ["whitespace only", "REPRO COMMAND: \n \t"], + ["same-line next packet section", "REPRO COMMAND: EXPECTED / ACTUAL: completes / hangs"], + ["next packet section", ["REPRO COMMAND:", "EXPECTED / ACTUAL: completes / hangs"].join("\n")], + ])("warns when the repro command body is %s", (_case, comment) => { + const warnings = lintFeedbackComment({ rating: 8, comment }); + + expect(warnings.map((warning) => warning.code)).toContain("missing-repro-command"); + }); + it("stays silent when the reporter already included a REPRO COMMAND: block", () => { const warnings = lintFeedbackComment({ rating: 4, @@ -42,6 +53,19 @@ describe("lintFeedbackComment", () => { expect(warnings).toEqual([]); }); + it("accepts a repro command body on the following line", () => { + const warnings = lintFeedbackComment({ + rating: 8, + comment: [ + "Repro command:", + " cd project && npx hyperframes render", + "EXPECTED / ACTUAL: completes / hangs", + ].join("\n"), + }); + + expect(warnings).toEqual([]); + }); + it("warns on rating<=7 visual-defect comments missing COMPOSITION_STRUCTURE:", () => { const warnings = lintFeedbackComment({ rating: 5, diff --git a/packages/cli/src/utils/feedbackLint.ts b/packages/cli/src/utils/feedbackLint.ts index 6b53ea6d2f..ce95536d65 100644 --- a/packages/cli/src/utils/feedbackLint.ts +++ b/packages/cli/src/utils/feedbackLint.ts @@ -34,6 +34,14 @@ export const COMPOSITION_STRUCTURE_RATING_CEILING = 7; const REPRO_MARKER = "REPRO COMMAND:"; const STRUCTURE_MARKER = "COMPOSITION_STRUCTURE:"; +const FEEDBACK_PACKET_MARKERS = [ + REPRO_MARKER, + "EXPECTED / ACTUAL:", + "EXACT ERROR:", + "OUTCOME:", + "WORKAROUND:", + STRUCTURE_MARKER, +] as const; export interface FeedbackLintInput { rating: number; @@ -56,7 +64,7 @@ export interface FeedbackLintWarning { * 2. Comment missing / empty — no check. `feedback --rating 6` with no * comment is a valid quick vote; the maintainer sees rating drift without * the reporter having to synthesize a fake repro. - * 3. Comment present + rating < 10 + no `REPRO COMMAND:` — warn. + * 3. Comment present + rating < 10 + no non-empty `REPRO COMMAND:` body — warn. * 4. Comment present + rating ≤ 7 + visual-defect keyword + no * `COMPOSITION_STRUCTURE:` — warn (in addition to any #3 warning). */ @@ -72,11 +80,11 @@ export function lintFeedbackComment(input: FeedbackLintInput): FeedbackLintWarni const upperTrimmed = trimmed.toUpperCase(); const warnings: FeedbackLintWarning[] = []; - if (!upperTrimmed.includes(REPRO_MARKER)) { + if (!hasReproCommandBody(trimmed)) { warnings.push({ code: "missing-repro-command", message: [ - `Comment on a ${rating}/${FEEDBACK_RATING_SCALE} report is missing a "${REPRO_MARKER}" block —`, + `Comment on a ${rating}/${FEEDBACK_RATING_SCALE} report is missing a non-empty "${REPRO_MARKER}" block —`, "maintainers can't rerun the failure from a symptom summary alone.", "See `references/preview-render.md` → feedback for the required packet shape.", ].join(" "), @@ -102,6 +110,33 @@ export function lintFeedbackComment(input: FeedbackLintInput): FeedbackLintWarni return warnings; } +/** + * A marker is useful only when it carries a command. Accept the command on the + * marker line or on the next non-empty line, but do not let the next packet + * section masquerade as the command body. + */ +function hasReproCommandBody(comment: string): boolean { + const lines = comment.split(/\r?\n/); + + return lines.some((line, index) => { + const markerIndex = line.toUpperCase().indexOf(REPRO_MARKER); + if (markerIndex === -1) return false; + + const inlineBody = line.slice(markerIndex + REPRO_MARKER.length).trim(); + if (inlineBody) return !startsWithFeedbackPacketMarker(inlineBody); + + const nextContentLine = lines.slice(index + 1).find((candidate) => candidate.trim()); + if (!nextContentLine) return false; + + return !startsWithFeedbackPacketMarker(nextContentLine.trimStart()); + }); +} + +function startsWithFeedbackPacketMarker(value: string): boolean { + const upperValue = value.toUpperCase(); + return FEEDBACK_PACKET_MARKERS.some((marker) => upperValue.startsWith(marker)); +} + // Compile once. Word-boundary at both sides prevents "black" matching // "blackboard", "blank" matching "blanket", "visual" matching "visualize". // Plural forms (e.g. "flickers") won't match — accepted tradeoff for a soft