diff --git a/packages/cli/src/commands/coreSkillContent.test.ts b/packages/cli/src/commands/coreSkillContent.test.ts index c537dc9d1f..e7578ed58e 100644 --- a/packages/cli/src/commands/coreSkillContent.test.ts +++ b/packages/cli/src/commands/coreSkillContent.test.ts @@ -35,6 +35,20 @@ describe("hyperframes-core contract docs", () => { expect(renderReference).toContain("OUTCOME:"); expect(renderReference).toContain("WORKAROUND:"); }); + + it("mandates a composition-structure block for visual-defect feedback", () => { + const skill = read("skills", "hyperframes-cli", "SKILL.md"); + const renderReference = read("skills", "hyperframes-cli", "references", "preview-render.md"); + + // Skill teaches the mandate at a high level. + expect(skill).toContain("COMPOSITION_STRUCTURE:"); + // Reference carries the fillable block + agent-helper pointer. + expect(renderReference).toContain("COMPOSITION_STRUCTURE:"); + expect(renderReference).toContain("elements: video="); + expect(renderReference).toContain("attributes:"); + expect(renderReference).toContain("timeline:"); + expect(renderReference).toContain("buildCompositionCensus"); + }); }); describe("media-use TTS documentation", () => { diff --git a/packages/cli/src/commands/feedback.ts b/packages/cli/src/commands/feedback.ts index d4eb23fbac..e2a1d81e09 100644 --- a/packages/cli/src/commands/feedback.ts +++ b/packages/cli/src/commands/feedback.ts @@ -12,6 +12,7 @@ import { buildIssueUrl, HYPERFRAMES_REPO_URL } from "../utils/feedbackIssue.js"; import { VERSION } from "../version.js"; import { c } from "../ui/colors.js"; import { parseFeedbackRating } from "../utils/feedbackRating.js"; +import { lintFeedbackComment, type FeedbackLintInput } from "../utils/feedbackLint.js"; export const examples: Example[] = [ ["Submit render feedback", 'hyperframes feedback --rating 8 --comment "fast but font missing"'], @@ -78,6 +79,18 @@ async function publishRepro(dir: string): Promise { } } +/** + * Print soft-warn feedback-lint messages to stdout. Extracted so the + * command's `run` stays a flat control-flow driver — the warning loop is + * incidental to the command logic and its complexity would otherwise push + * `run` over the Fallow CRAP threshold. + */ +function printFeedbackLintWarnings(input: FeedbackLintInput): void { + for (const warning of lintFeedbackComment(input)) { + console.log(c.warn(`⚠ ${warning.message}`)); + } +} + async function openAndPrintIssue(url: string): Promise { if (process.stdout.isTTY) { try { @@ -156,6 +169,11 @@ export default defineCommand({ const comment = normalizeComment(args.comment); const doctorSummary = await getDoctorSummary(); + // Soft-warn (never blocks) when the comment for a non-clean report is + // missing the mandated reproduction-packet markers. Prints before the + // submission ack so the reporter sees the nudge while their run is fresh. + printFeedbackLintWarnings({ rating, comment }); + // The standalone command runs separately from `render`, so it has no real // elapsed time to report. Omit it rather than recording a fake duration. trackRenderFeedback({ rating, comment, doctorSummary }); diff --git a/packages/cli/src/utils/compositionCensus.test.ts b/packages/cli/src/utils/compositionCensus.test.ts new file mode 100644 index 0000000000..0996ad9388 --- /dev/null +++ b/packages/cli/src/utils/compositionCensus.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, it } from "vitest"; + +import { buildCompositionCensus, renderCompositionCensusBlock } from "./compositionCensus.js"; + +const MINIMAL_HTML = ` + + +
+ +`; + +const RICH_HTML = ` + + + + + + +
+ + + + + + + + +
+
+
+
+
+
+ + +`; + +describe("buildCompositionCensus", () => { + it("counts zero media on a minimal composition", () => { + const c = buildCompositionCensus(MINIMAL_HTML); + expect(c.elementCensus).toEqual({ + video: 0, + audio: 0, + img: 0, + svg: 0, + canvas: 0, + subCompositionMounts: 0, + }); + expect(c.timelineShape.nested).toBe(false); + expect(c.timelineShape.subCompositionCount).toBe(0); + expect(c.timelineShape.usesGsap).toBe(false); + expect(c.timelineShape.usesDataTimeline).toBe(true); + }); + + it("counts each element category on a rich composition", () => { + const c = buildCompositionCensus(RICH_HTML); + expect(c.elementCensus).toEqual({ + video: 2, + audio: 1, + img: 3, + svg: 1, + canvas: 1, + subCompositionMounts: 2, + }); + }); + + it("detects structural attributes from both inline style and