mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) (#1299)
* feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) * refactor(studio): extract readHfId helper, fix empty-string normalization, add comments (R7 review) - Extract readHfId(el) to domEditingLayers.ts — centralizes `?.trim() || undefined` normalisation; guards against empty-string data-hf-id reaching findTagByTarget - Wire readHfId into domEditingLayers.ts and useDomEditCommits.ts (the one site that still used `?? undefined` instead of `|| undefined`) - Re-export readHfId through domEditing.ts public API - Add readHfId unit tests: present, absent, empty-string, whitespace-only - Add comment on PatchTarget: runtime validation lives in findTagByTarget, type is docs-only - Suppress pre-existing unused re-exports in timelineDOM.ts (backward-compat re-exports brought into fallow scope by the T5b hfId changes) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(core): clear data-hf-id on split clone to prevent dual-match (R7 review) cloneNode(true) copies all attributes including data-hf-id. Without clearing it, both halves of a split share the same hf-id; the server's findByHfId picks the first match and silently patches the wrong clip. Remove the attribute from the clone so write-back re-mints a fresh id on the next preview load. Adds a test: splitElementInHtml — hfId clone isolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(studio): add hfId to DomEditLayerItem + getDomLayerPatchTarget return type (R7 review) - Add hfId to DomEditLayerItem interface (domEditingTypes.ts) so layer item construction in collectDomEditLayerItems compiles - Widen getDomLayerPatchTarget return type to include hfId + populate it from data-hf-id attribute (domEditingElement.ts) - Widen findDomEditSelectionTarget to check hfId-first when no id/selector - Widen Pick types in domEditOverlayGeometry.ts and useGsapScriptCommits.ts - Add hfId to buildMissingCompositionElements element construction - Add hfId-targeted test coverage in domEditing.test.ts, domEditOverlayGeometry.test.ts, timelineIframeHelpers.test.ts - Update hfIds.test.ts KNOWN LIMITATION labels — write-back landed in R7 T1-2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
82c754f27a
commit
0923bc0787
@@ -100,9 +100,9 @@ describe("ensureHfIds", () => {
|
||||
});
|
||||
|
||||
// Lock the edit-lifecycle behavior. These pin BOTH the guarantee that holds
|
||||
// once ids are persisted to source (pinning) AND the two limitations that hold
|
||||
// while they are not (design §3 write-back is not yet wired — see
|
||||
// notes/r1-stable-hf-ids-design.md "Implementation status & verified lifecycle gap").
|
||||
// once ids are persisted to source (pinning) AND the behavior for truly unpinned
|
||||
// HTML (no data-hf-id in the input — unreachable in production after write-back
|
||||
// landed in R7 Task 1-2, but still the correct contract for that path).
|
||||
describe("ensureHfIds — edit lifecycle (R1 stability)", () => {
|
||||
it("pinned id survives a content edit (the §3 write-back guarantee)", () => {
|
||||
// Element already carries data-hf-id in source (as it would after write-back).
|
||||
@@ -110,25 +110,24 @@ describe("ensureHfIds — edit lifecycle (R1 stability)", () => {
|
||||
expect(idOf(ensureHfIds(edited), "p.body")).toBe("hf-abcd");
|
||||
});
|
||||
|
||||
it("KNOWN LIMITATION: an unpinned id changes when the element's text is edited", () => {
|
||||
// No data-hf-id in source → every parse re-mints from content. Editing the
|
||||
// text changes the hash, so the id drifts. This is the "pure-hash" mode the
|
||||
// design rejected; flip this assertion to .toBe once write-back lands.
|
||||
it("unpinned id drifts when element text is edited (pure-hash, unreachable after write-back)", () => {
|
||||
// No data-hf-id in source → every parse re-mints from content. This path is
|
||||
// unreachable in production after R7 write-back: the first serve pins the id.
|
||||
const before = idOf(ensureHfIds(doc(`<p class="body">Hello</p>`)), "p.body");
|
||||
const after = idOf(ensureHfIds(doc(`<p class="body">Hello world</p>`)), "p.body");
|
||||
expect(before).not.toBe(after);
|
||||
});
|
||||
|
||||
it("KNOWN LIMITATION: an unpinned id changes when an attribute is edited", () => {
|
||||
it("unpinned id drifts when attribute is edited (pure-hash, unreachable after write-back)", () => {
|
||||
const before = idOf(ensureHfIds(doc(`<p class="body">x</p>`)), "p");
|
||||
const after = idOf(ensureHfIds(doc(`<p class="lead">x</p>`)), "p");
|
||||
expect(before).not.toBe(after);
|
||||
});
|
||||
|
||||
it("KNOWN LIMITATION: identical-content siblings have no content-stable id for the 2nd occurrence", () => {
|
||||
it("identical-content siblings: second occurrence gets a position-derived dedup id", () => {
|
||||
// Insertion stability holds for DISTINCT content (covered elsewhere), but a
|
||||
// second identical sibling collides and gets a position-derived dedup id —
|
||||
// there is no content-stable handle for it. The first keeps the base id.
|
||||
// second identical sibling collides and gets a position-derived dedup id.
|
||||
// First element keeps the base (content-derived) id; documented in project_hfid_dedup_tiebreak.
|
||||
const single = idOf(ensureHfIds(doc(`<p class="x">same</p>`)), "p.x");
|
||||
const pair = ids(ensureHfIds(doc(`<p class="x">same</p><p class="x">same</p>`)));
|
||||
expect(pair[0]).toBe(single); // first identical element: stable, content-derived
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
removeElementFromHtml,
|
||||
patchElementInHtml,
|
||||
probeElementInSource,
|
||||
splitElementInHtml,
|
||||
} from "./sourceMutation.js";
|
||||
|
||||
describe("removeElementFromHtml", () => {
|
||||
@@ -455,3 +456,14 @@ describe("T7 — data-hf-id targeting (spec for R1)", () => {
|
||||
expect(html).toContain('data-hf-id="hf-a1b2"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitElementInHtml — hfId clone isolation", () => {
|
||||
it("does not copy data-hf-id to the cloned second half", () => {
|
||||
const source = `<html><body><div data-composition-id="root"><div id="clip1" class="clip" data-start="0" data-duration="10" data-hf-id="hf-abc123"></div></div></body></html>`;
|
||||
const { html, matched } = splitElementInHtml(source, { id: "clip1" }, 5, "clip2");
|
||||
|
||||
expect(matched).toBe(true);
|
||||
const occurrences = (html.match(/data-hf-id="hf-abc123"/g) ?? []).length;
|
||||
expect(occurrences).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,6 +275,7 @@ export function splitElementInHtml(
|
||||
|
||||
const clone = el.cloneNode(true) as HTMLElement;
|
||||
clone.setAttribute("id", newId);
|
||||
clone.removeAttribute("data-hf-id");
|
||||
clone.setAttribute("data-start", String(Math.round(splitTime * 1000) / 1000));
|
||||
clone.setAttribute("data-duration", String(Math.round(secondDuration * 1000) / 1000));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user