refactor(core,studio): extract draft-marker constants + R7 code-review fixes (Task 4) (#1292)

* refactor(core,studio): extract draft-marker constants to core (R7, Task 4)

Create draftMarkers.ts in core with 5 shared CSS custom property names and the
gesture DOM attribute. PreviewAdapter imports from draftMarkers.ts instead of
hardcoding strings. Adds @hyperframes/core/studio-api/draft-markers export
subpath. Studio's manualEditsTypes.ts re-exports the shared constants from core
so all existing call sites are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): address R7 code-review findings (C1–C14, P6–P7)

- previewAdapter: auto-revert previous gesture in applyDraft (C3); clearDraftProps
  on commitPreview not just revertDraft (C4); isVisible NaN→visible for JSDOM (P7);
  remove redundant GestureState.hfId field (C12); remove Array.from (C14);
  extract clearDraftProps/revertGesture helpers (C5/C6)
- hfIdPersist: replace string-equality change detection with attribute count to
  avoid false-positive writes on single-quoted HTML (C1); re-read disk before
  write for TOCTOU guard (C7); remove normalizeHfIds wrapper (C11)
- preview.ts: remove dead null-check on normalizedDisk after diskMain guard (C9);
  catch path re-reads disk fresh instead of using stale pre-request snapshot (C8)
- hfIds.test.ts: replace tautological second stability test with cross-document
  content-keyed id stability test (P6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): follow-up R7 review fixes — CSS.escape fallback, invariant docs, new edge-case tests

- hfIdPersist: remove ensureHfIds re-export (P2); add JSDoc invariant note;
  improve TOCTOU comment; pass err to console.warn
- preview.ts: split import — ensureHfIds from parsers/hfIds.js (not re-export)
- previewAdapter: CSS.escape + inline fallback for non-browser environments;
  add JSDoc for atTime caller-seek contract; add 0.01 opacity-threshold comment
- previewAdapter.test: rename atTime test to clarify adapter-does-not-seek;
  add nested-hf-root-without-id test; add resize→move prop-leak test;
  add revertDraft-after-commit no-op test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(core): bundle-vs-disk id-stability test; comment double ensureHfIds (P3)

- preview.test: add "bundle returning untagged HTML gets same ids as disk" test —
  guards against id divergence when bundler reads a pre-write cache snapshot;
  content-keyed FNV1a minting ensures served ids == disk ids for same source HTML
- preview.ts: comment the second ensureHfIds call explaining it's intentional for
  adapter-injected elements and idempotent on the no-bundle path (P3 from miguel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(core): wire-contract comment on mintHfId + fallow suppressions (R7)

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:
Vance Ingalls
2026-06-09 01:13:54 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 1a23938bef
commit 3d7d7c0291
11 changed files with 253 additions and 95 deletions
+8 -6
View File
@@ -88,12 +88,14 @@ describe("ensureHfIds", () => {
expect(ids(ensureHfIds(edited))).toContain(originalId);
});
it("pinned id survives attribute edit after first persist", () => {
const raw = `<!doctype html><html><body><div class="old">text</div></body></html>`;
const persisted = ensureHfIds(raw); // simulates write-back on first serve
const [originalId] = ids(persisted);
const edited = persisted.replace('class="old"', 'class="new"');
expect(ids(ensureHfIds(edited))).toContain(originalId);
// Hash-based stability (no prior pin): the same element content yields the
// same id regardless of what sibling elements appear in the document.
it("content-keyed minting is stable: same element content → same id in different documents", () => {
const alone = `<!doctype html><html><body><div class="card">hello</div></body></html>`;
const [idAlone] = ids(ensureHfIds(alone));
// The <div class="card">hello</div> appears alongside a new sibling here.
const withSibling = `<!doctype html><html><body><span>prefix</span><div class="card">hello</div></body></html>`;
expect(ids(ensureHfIds(withSibling))).toContain(idAlone);
});
});
+7
View File
@@ -67,6 +67,13 @@ function contentKey(el: Element): string {
* (`if (el.getAttribute("data-hf-id")) continue`), so normal operation
* never re-exposes the ordering after first persist.
*/
// WIRE CONTRACT: id minting is content-keyed (FNV1a of innerHTML + tag). R7's
// preview route relies on mintHfId producing identical ids across mint contexts
// (disk-persist pass vs. in-memory bundle pass) — see preview.test.ts
// "bundle returning untagged HTML gets same ids as disk". Any change that adds
// positional, session, or random input to the hash breaks that invariant and
// makes hf- ids diverge between disk and served HTML, silently corrupting
// drag-to-edit targeting.
export function mintHfId(el: Element, assigned: Set<string>): string {
const key = contentKey(el);
let id = toHfId(fnv1a(key));