mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
feat(skills): add composition-structure block + soft-warn feedback lint
Extend the CLI feedback reproduction packet (#2498) with a fifth mandated field, `COMPOSITION_STRUCTURE:`, and enforce presence of `REPRO COMMAND:` / `COMPOSITION_STRUCTURE:` at feedback-submit time. - Skill + reference now specify `COMPOSITION_STRUCTURE:` — a privacy-preserving structural anatomy (element census + attribute presence + timeline shape + delta + defect location) — required for any rating <=7 that describes a visual defect. - `buildCompositionCensus()` + `renderCompositionCensusBlock()` auto-fill the block from composition HTML so agents don't ask the human user to hand-count `<video>` / `<img>` / sub-comp mounts. Counts + presence flags only — no file paths, no src URLs, no user text. - `hyperframes feedback` soft-warns (never blocks) when a non-10 `--comment` is missing `REPRO COMMAND:`, and when a rating-<=7 visual-defect comment is missing `COMPOSITION_STRUCTURE:`. The warning points at the auto-census helper so agents remediate themselves. - `coreSkillContent.test.ts` locks the new literal in both the skill and the reference file, following #2498's pattern. Extends #2498. Follow-up: no change to `doctorSummary` generation, no change to the feedback-submission API endpoint, no refactor of #2498's doc-content Jest test. Signed-off-by: Via
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildCompositionCensus, renderCompositionCensusBlock } from "./compositionCensus.js";
|
||||
|
||||
const MINIMAL_HTML = `<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<div data-composition-id="main" data-start="0" data-duration="5"></div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const RICH_HTML = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.card { filter: blur(4px); mix-blend-mode: multiply; }
|
||||
.fixed-bar { position: fixed; overflow: hidden; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div data-composition-id="main" data-start="0" data-duration="10">
|
||||
<video data-has-audio="true"></video>
|
||||
<video></video>
|
||||
<audio></audio>
|
||||
<img />
|
||||
<img />
|
||||
<img />
|
||||
<svg></svg>
|
||||
<canvas></canvas>
|
||||
<div data-composition-src="scenes/intro.html" data-start="0"></div>
|
||||
<div data-composition-src="scenes/outro.html" data-start="5"></div>
|
||||
<div style="clip-path: circle(50%); transform: translateX(10px); z-index: 3"></div>
|
||||
<div style="background-image: url('bg.png')"></div>
|
||||
<div style="mask-image: url('mask.svg')"></div>
|
||||
</div>
|
||||
<script>
|
||||
gsap.timeline().to(".card", { x: 100 });
|
||||
</script>
|
||||
</body>
|
||||
</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 <style> rules", () => {
|
||||
const c = buildCompositionCensus(RICH_HTML);
|
||||
// Inline-style probes
|
||||
expect(c.structuralAttributes.clipPath).toBe(true);
|
||||
expect(c.structuralAttributes.transform).toBe(true);
|
||||
expect(c.structuralAttributes.zIndex).toBe(true);
|
||||
expect(c.structuralAttributes.backgroundImage).toBe(true);
|
||||
expect(c.structuralAttributes.maskImage).toBe(true);
|
||||
// <style> tag probes
|
||||
expect(c.structuralAttributes.filter).toBe(true);
|
||||
expect(c.structuralAttributes.mixBlendMode).toBe(true);
|
||||
expect(c.structuralAttributes.positionFixed).toBe(true);
|
||||
expect(c.structuralAttributes.overflowHidden).toBe(true);
|
||||
// data-* attribute probes
|
||||
expect(c.structuralAttributes.dataHasAudio).toBe(true);
|
||||
expect(c.structuralAttributes.dataDuration).toBe(true);
|
||||
expect(c.structuralAttributes.dataStart).toBe(true);
|
||||
expect(c.structuralAttributes.dataCompositionSrc).toBe(true);
|
||||
});
|
||||
|
||||
it("reports absent attributes as false on minimal HTML", () => {
|
||||
const c = buildCompositionCensus(MINIMAL_HTML);
|
||||
expect(c.structuralAttributes.clipPath).toBe(false);
|
||||
expect(c.structuralAttributes.filter).toBe(false);
|
||||
expect(c.structuralAttributes.mixBlendMode).toBe(false);
|
||||
expect(c.structuralAttributes.dataHasAudio).toBe(false);
|
||||
expect(c.structuralAttributes.backgroundImage).toBe(false);
|
||||
expect(c.structuralAttributes.maskImage).toBe(false);
|
||||
});
|
||||
|
||||
it("detects gsap from both script src and inline gsap.* calls", () => {
|
||||
expect(buildCompositionCensus(RICH_HTML).timelineShape.usesGsap).toBe(true);
|
||||
const inlineOnly = `<html><body><div data-composition-id="m"></div><script>gsap.to('.x', {})</script></body></html>`;
|
||||
expect(buildCompositionCensus(inlineOnly).timelineShape.usesGsap).toBe(true);
|
||||
const noGsap = `<html><body><div data-composition-id="m"></div><script>console.log('hi')</script></body></html>`;
|
||||
expect(buildCompositionCensus(noGsap).timelineShape.usesGsap).toBe(false);
|
||||
});
|
||||
|
||||
it("marks timelines as nested when sub-comp mounts exist", () => {
|
||||
const c = buildCompositionCensus(RICH_HTML);
|
||||
expect(c.timelineShape.nested).toBe(true);
|
||||
expect(c.timelineShape.subCompositionCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderCompositionCensusBlock", () => {
|
||||
it("emits a REPRO-packet-compatible block starting with the mandated header", () => {
|
||||
const block = renderCompositionCensusBlock(buildCompositionCensus(RICH_HTML));
|
||||
expect(block.startsWith("COMPOSITION_STRUCTURE:")).toBe(true);
|
||||
expect(block).toContain("elements: video=2 audio=1 img=3 svg=1 canvas=1 subComps=2");
|
||||
expect(block).toContain("attributes:");
|
||||
expect(block).toContain("timeline: nested (2 sub-comps); driver=gsap+data-timeline");
|
||||
// Placeholder slots the parser can't infer.
|
||||
expect(block).toContain("delta:");
|
||||
expect(block).toContain("defect:");
|
||||
});
|
||||
|
||||
it("emits '(none present)' on the attributes line when no structural attrs are found", () => {
|
||||
const empty = `<html><body><div data-composition-id="m"></div></body></html>`;
|
||||
const block = renderCompositionCensusBlock(buildCompositionCensus(empty));
|
||||
expect(block).toContain("attributes: (none present)");
|
||||
expect(block).toContain("timeline: flat; driver=none");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user