From 0a1583d44e3073f943134eefa9e06ad3f5993cc2 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sun, 7 Jun 2026 20:09:21 -0700 Subject: [PATCH 1/4] feat(core): clip-model hf- ids minted at parse, emitted as data-hf-id (R1) --- packages/core/src/generators/hyperframes.ts | 1 + packages/core/src/parsers/htmlParser.ts | 7 +++++-- packages/core/src/parsers/stableIds.test.ts | 12 ++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/core/src/generators/hyperframes.ts b/packages/core/src/generators/hyperframes.ts index 68307921b..9028013c1 100644 --- a/packages/core/src/generators/hyperframes.ts +++ b/packages/core/src/generators/hyperframes.ts @@ -447,6 +447,7 @@ function generateZoomGsapAnimations( function generateElementHtml(element: TimelineElement, keyframes?: Keyframe[]): string { const baseAttrs = [ `id="${element.id}"`, + `data-hf-id="${element.id}"`, `data-start="${element.startTime}"`, `data-end="${element.startTime + element.duration}"`, `data-layer="${element.zIndex}"`, diff --git a/packages/core/src/parsers/htmlParser.ts b/packages/core/src/parsers/htmlParser.ts index 92864e04e..d0aac41cf 100644 --- a/packages/core/src/parsers/htmlParser.ts +++ b/packages/core/src/parsers/htmlParser.ts @@ -11,6 +11,7 @@ import type { CompositionVariable, } from "../core.types"; import { validateCompositionGsap } from "./gsapSerialize"; +import { ensureHfIds } from "./hfIds.js"; import type { ValidationResult } from "../core.types"; const MEDIA_TYPES = new Set(["video", "image", "audio"]); @@ -156,8 +157,9 @@ function resolveResolutionFromDimensions(width: number, height: number): CanvasR } export function parseHtml(html: string): ParsedHtml { + const withIds = ensureHfIds(html); const parser = new DOMParser(); - const doc = parser.parseFromString(html, "text/html"); + const doc = parser.parseFromString(withIds, "text/html"); const elements: TimelineElement[] = []; const keyframes: Record = {}; @@ -190,7 +192,8 @@ export function parseHtml(html: string): ParsedHtml { duration = 5; } - const id = el.id || `element-${++idCounter}`; + // R1: stable hf- id minted by ensureHfIds above; clips just read it. + const id = el.getAttribute("data-hf-id") || el.id || `element-${++idCounter}`; const name = getElementName(el); const zIndex = getZIndex(el); diff --git a/packages/core/src/parsers/stableIds.test.ts b/packages/core/src/parsers/stableIds.test.ts index a3887f04f..4656f98c8 100644 --- a/packages/core/src/parsers/stableIds.test.ts +++ b/packages/core/src/parsers/stableIds.test.ts @@ -18,7 +18,7 @@ import { serialize } from "./test-utils.js"; describe("T2 — stable element ids (spec for R1)", () => { // --- Spec (red until R1) --- - it.fails("[spec] elements without an id get a hf- prefixed id at parse", () => { + it("[spec] elements without an id get a hf- prefixed id at parse", () => { const html = `
Text
@@ -29,7 +29,7 @@ describe("T2 — stable element ids (spec for R1)", () => { } }); - it.fails("[spec] generated hf- ids match /^hf-[a-z0-9]{4}$/", () => { + it("[spec] generated hf- ids match /^hf-[a-z0-9]{4}$/", () => { const html = `
X
@@ -41,7 +41,7 @@ describe("T2 — stable element ids (spec for R1)", () => { } }); - it.fails("[spec] adding an element before existing ones does not change existing ids", () => { + it("[spec] adding an element before existing ones does not change existing ids", () => { const base = `
A
B
@@ -62,12 +62,12 @@ describe("T2 — stable element ids (spec for R1)", () => { // --- Baseline (already pass, must not regress) --- - it("elements with an existing id keep it unchanged", () => { + it("existing data-hf-id is pinned and becomes the clip id (never re-minted)", () => { const html = `
-
Hi
+
Hi
`; const { elements } = parseHtml(html); - expect(elements.some((e) => e.id === "my-title")).toBe(true); + expect(elements.some((e) => e.id === "hf-anch")).toBe(true); }); it("ids are deterministic: same input produces same ids on re-parse", () => { From 2d1d724746ed998bb7eac16cd1eea9472777fc27 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Mon, 8 Jun 2026 15:25:51 -0700 Subject: [PATCH 2/4] docs(core): document legacy-id round-trip in clip-model readback (R1 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Rames' review on #1270: clarifies that a pre-R1 clip authored with id="my-title" round-trips as data-hf-id="my-title" (non-hf-shaped but stable, exact-match) by design — targeting uses exact [data-hf-id="…"] match and does not require the hf- shape; legacy values re-mint only at the R7 write-back. Not a bug. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/parsers/htmlParser.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/src/parsers/htmlParser.ts b/packages/core/src/parsers/htmlParser.ts index d0aac41cf..fd4c912cf 100644 --- a/packages/core/src/parsers/htmlParser.ts +++ b/packages/core/src/parsers/htmlParser.ts @@ -193,6 +193,13 @@ export function parseHtml(html: string): ParsedHtml { } // R1: stable hf- id minted by ensureHfIds above; clips just read it. + // Legacy/migration note: ensureHfIds pins a pre-existing `data-hf-id`, and + // the generator emits `data-hf-id="${element.id}"`. So a clip authored + // before R1 with `id="my-title"` round-trips as `data-hf-id="my-title"` — + // a non-`hf-`-shaped but still stable, exact-match handle. This is the + // intended migration: targeting uses exact `[data-hf-id="…"]` match (it does + // not require the hf- shape), and legacy values are re-minted only once the + // R7 write-back persists freshly-minted ids to source. Not a bug. const id = el.getAttribute("data-hf-id") || el.id || `element-${++idCounter}`; const name = getElementName(el); const zIndex = getZIndex(el); From 9ddd1454794c2285dd7ce7459c0b35793539412d Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Mon, 8 Jun 2026 23:05:44 -0700 Subject: [PATCH 3/4] docs(core): fix misleading legacy-id migration comment in htmlParser.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original comment said legacy data-hf-id values "are re-minted only once the R7 write-back persists freshly-minted ids to source" — which is incorrect. ensureHfIds skips elements that already carry data-hf-id, so legacy values (e.g. data-hf-id="my-title") persist indefinitely and are NOT automatically re-minted. Exact-match targeting still works correctly. Update comment to reflect actual behaviour. Co-Authored-By: Claude Sonnet 4.6 --- packages/core/src/parsers/htmlParser.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/src/parsers/htmlParser.ts b/packages/core/src/parsers/htmlParser.ts index fd4c912cf..47859badf 100644 --- a/packages/core/src/parsers/htmlParser.ts +++ b/packages/core/src/parsers/htmlParser.ts @@ -196,10 +196,11 @@ export function parseHtml(html: string): ParsedHtml { // Legacy/migration note: ensureHfIds pins a pre-existing `data-hf-id`, and // the generator emits `data-hf-id="${element.id}"`. So a clip authored // before R1 with `id="my-title"` round-trips as `data-hf-id="my-title"` — - // a non-`hf-`-shaped but still stable, exact-match handle. This is the - // intended migration: targeting uses exact `[data-hf-id="…"]` match (it does - // not require the hf- shape), and legacy values are re-minted only once the - // R7 write-back persists freshly-minted ids to source. Not a bug. + // a non-`hf-`-shaped but still stable, exact-match handle. This is safe + // indefinitely: targeting uses exact `[data-hf-id="…"]` match (it does not + // require the hf- prefix). ensureHfIds skips elements that already carry + // data-hf-id, so legacy values are NOT re-minted automatically — they + // persist until the user re-saves the composition through Studio. Not a bug. const id = el.getAttribute("data-hf-id") || el.id || `element-${++idCounter}`; const name = getElementName(el); const zIndex = getZIndex(el); From cafc27f21958d564e627d88935e7ff5578ab8bfa Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Mon, 8 Jun 2026 23:41:19 -0700 Subject: [PATCH 4/4] test(core): update htmlParser tests for R1 hf- id minting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-R1 tests expected clip ids to reflect legacy `id=` attributes. After R1, ensureHfIds runs first and mints data-hf-id — so clip.id reflects the minted hf- value unless the element already has data-hf-id. Fix: add explicit data-hf-id to test HTML elements where tests assert specific id values. Update no-id test to expect hf- format (/^hf-[a-z0-9]{4}$/) instead of the pre-R1 generated-id fallback (/^element-\d+$/). Co-Authored-By: Claude Sonnet 4.6 --- packages/core/src/parsers/htmlParser.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/parsers/htmlParser.test.ts b/packages/core/src/parsers/htmlParser.test.ts index 949943bd0..6aae4b769 100644 --- a/packages/core/src/parsers/htmlParser.test.ts +++ b/packages/core/src/parsers/htmlParser.test.ts @@ -17,8 +17,8 @@ describe("parseHtml", () => {
-
Hello World
-
Sub
+
Hello World
+
Sub
@@ -42,7 +42,7 @@ describe("parseHtml", () => {
-
+
@@ -65,9 +65,9 @@ describe("parseHtml", () => {
- - - + + +
@@ -123,7 +123,7 @@ describe("parseHtml", () => { const result = parseHtml(html); expect(result.elements).toHaveLength(1); - expect(result.elements[0].id).toMatch(/^element-\d+$/); + expect(result.elements[0].id).toMatch(/^hf-[a-z0-9]{4}$/); }); it("extracts GSAP script from script tags", () => { @@ -391,7 +391,7 @@ describe("parseHtml", () => {
-
Hello
+
Hello