diff --git a/packages/core/src/parsers/htmlParser.roundtrip.test.ts b/packages/core/src/parsers/htmlParser.roundtrip.test.ts
new file mode 100644
index 000000000..a1df46c29
--- /dev/null
+++ b/packages/core/src/parsers/htmlParser.roundtrip.test.ts
@@ -0,0 +1,146 @@
+/**
+ * @vitest-environment jsdom
+ *
+ * T1 — parse→serialize round-trip (DOM/timing model only).
+ * Scope: GSAP script fidelity is T6 territory; these tests cover element structure and timing.
+ */
+import { describe, it, expect } from "vitest";
+import { existsSync, readdirSync, readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { parseHtml } from "./htmlParser.js";
+import { generateHyperframesHtml } from "../generators/hyperframes.js";
+import type { ParsedHtml } from "./htmlParser.js";
+
+function maxEndTime(elements: ParsedHtml["elements"]): number {
+ if (elements.length === 0) return 0;
+ return Math.max(...elements.map((e) => e.startTime + e.duration));
+}
+
+function serialize(parsed: ParsedHtml): string {
+ // Fixed compositionId prevents Date.now() churn from masking structural instability.
+ // The compositionId generation instability itself is tracked as R1 (stable hf- ids).
+ return generateHyperframesHtml(parsed.elements, maxEndTime(parsed.elements), {
+ compositionId: "test-comp",
+ resolution: parsed.resolution,
+ styles: parsed.styles ?? undefined,
+ keyframes: parsed.keyframes,
+ stageZoomKeyframes: parsed.stageZoomKeyframes,
+ });
+}
+
+describe("T1 — parse→serialize round-trip (DOM/timing)", () => {
+ it("preserves element count and ids through one round-trip", () => {
+ const html = `
+
+ `;
+ const parsed = parseHtml(html);
+ const reparsed = parseHtml(serialize(parsed));
+
+ expect(reparsed.elements).toHaveLength(parsed.elements.length);
+ expect(reparsed.elements.map((e) => e.id)).toEqual(parsed.elements.map((e) => e.id));
+ });
+
+ it("preserves startTime and duration through one round-trip", () => {
+ const html = `
+
+ `;
+ const parsed = parseHtml(html);
+ const reparsed = parseHtml(serialize(parsed));
+
+ for (const orig of parsed.elements) {
+ const round = reparsed.elements.find((e) => e.id === orig.id);
+ expect(round).toBeDefined();
+ expect(round?.startTime).toBe(orig.startTime);
+ expect(round?.duration).toBe(orig.duration);
+ }
+ });
+
+ it("preserves element types through one round-trip", () => {
+ const html = `
+
+
+
+

+
+
+ `;
+ const parsed = parseHtml(html);
+ const reparsed = parseHtml(serialize(parsed));
+
+ for (const orig of parsed.elements) {
+ const round = reparsed.elements.find((e) => e.id === orig.id);
+ expect(round).toBeDefined();
+ expect(round?.type).toBe(orig.type);
+ }
+ });
+
+ it("is stable — serialize(parse(serialize(parse(html)))) equals serialize(parse(html))", () => {
+ const html = `
+
+

+
+
+ `;
+ const parsed = parseHtml(html);
+ const once = serialize(parsed);
+ const twice = serialize(parseHtml(once));
+ expect(twice).toBe(once);
+ });
+
+ it("handles an empty stage without throwing", () => {
+ const html = ``;
+ const parsed = parseHtml(html);
+ expect(() => serialize(parsed)).not.toThrow();
+ const reparsed = parseHtml(serialize(parsed));
+ expect(reparsed.elements).toHaveLength(0);
+ });
+});
+
+describe("T1 — registry block round-trips (DOM/timing)", () => {
+ const BLOCKS_DIR = join(dirname(fileURLToPath(import.meta.url)), "../../../../registry/blocks");
+
+ const blockNames = existsSync(BLOCKS_DIR)
+ ? readdirSync(BLOCKS_DIR, { withFileTypes: true })
+ .filter((d) => d.isDirectory())
+ .map((d) => d.name)
+ : [];
+
+ for (const name of blockNames) {
+ it(`${name}: round-trip preserves element count`, () => {
+ const blockFile = join(BLOCKS_DIR, name, `${name}.html`);
+ expect(existsSync(blockFile)).toBe(true);
+ const html = readFileSync(blockFile, "utf8");
+ const parsed = parseHtml(html);
+ const reparsed = parseHtml(serialize(parsed));
+ expect(reparsed.elements).toHaveLength(parsed.elements.length);
+ });
+ }
+
+ // GSAP smoke: verify the includeScripts path survives parse→generate on a block known
+ // to contain