feat(studio): wire hfId through DOM-edit patch targets, activate hfId lookup path (R7, T5a)#1297
Conversation
… lookup path (R7, T5a)
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
miguel-heygen
left a comment
There was a problem hiding this comment.
The Studio logic is correct: buildDomEditPatchTarget is a clean extraction of the three previously-duplicated inline literals, and the updated guard (!id && !selector && !hfId) correctly admits hfId-only targets. The z-index reorder path reading data-hf-id directly off the entry element is the right call since there's no DomEditSelection available there.
P2 — producer fixture noise needs explanation before merge.
packages/producer/tests/style-10-prod/failures/actual.html and style-2-prod/failures/actual.html carry +589/-34 and +324/-30 of compiled output that includes __hfAuthoredRootId, __hfAuthoredRootAttr, __hfReplaceAuthoredRootIdSelectors, and data-hf-authored-id attributes — none of which are related to this PR. The chat test failure count jumped from 34 → 71 failed checkpoints. These look like they were carried in from an unrelated compiler PR somewhere in the stack. Two questions:
- What PR introduced the
data-hf-authored-id/ authored-root-id runtime code that shows up in these fixtures? - Are those pre-existing test failures, or did this stack introduce new regressions in the
chatproducer test?
If these are pre-existing failures from an upstream PR and the count jump is explainable, they're fine to leave as-is. But the fixture diff should be acknowledged so reviewers aren't left wondering whether +958 lines of compiled HTML is expected.
Everything else is good.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 0e9eec4. Stack 2/3 — wires hfId through DOM-edit patch targets (+958/-81, 6 files). The actual code change is small (~45 lines across domEditing.ts, domEditingLayers.ts, useDomEditCommits.ts); the +913 lines of diff are in two regression-test fixture files (style-2-prod/failures/actual.html, style-10-prod/failures/actual.html) — those are the load-bearing oddity here.
Net on the code: clean extraction of buildDomEditPatchTarget(selection) into a single helper, replacing two duplicate hand-rolled patch-target constructions. The no-target check at useDomEditCommits.ts:475 correctly accepts hfId alone as a valid target (!patchTarget.id && !patchTarget.selector && !patchTarget.hfId), so hfId becomes a first-class identifier — exactly the contract the R1 server side has been waiting for. End-to-end check on Home's review angles:
- hfId presence/absence (angle #1): ✓ helper passes
hfIdthrough optionally; legacy elements fall back to id/selector via the existing R1 server-sidefindTargetElementprecedence. - Server acceptance (angle #2) + Type parity (angle #3): ✓ inherits from #1296. The patch target shape matches
MutationTargetandPatchTarget. - Cutover ordering (angle #4): ✓ Once deployed, clients now send
hfIdin patch bodies. Server-sidefindByHfIdwas already in place via R1. If server hasn't deployed R1 (theoretical — R1 is merged), client falls back via id/selector since server readstarget.id/target.selectorindependently. Safe. - Backwards-compat (angle #6): ✓ The new attribute read at
useDomEditCommits.ts:556(entry.element.getAttribute("data-hf-id") ?? undefined) handles missing attributes. Same empty-string caveat as my #1296 concern #1 applies here too. - R7 prior-stack interaction (angle #8): ✓ Studio's selection-aware patch construction now matches the contract PreviewAdapter (R7 T1) uses for hit-testing —
data-hf-idis read at both ends.
The fixture-file diff is the thing I'd want resolved before merge. Otherwise, two minor concerns.
Concerns
1. The 913-line failures/actual.html diff in two fixture files is the odd thing in this PR
Files affected:
packages/producer/tests/style-2-prod/failures/actual.html(+324/-30)packages/producer/tests/style-10-prod/failures/actual.html(+589/-34)
Naming convention tests/<style>/failures/actual.html strongly implies "actual output captured when the regression test failed" — which would mean these are dev-time artifacts, not load-bearing golden files. The diff content (sampled the first 50 lines of the style-2 diff) shows a @font-face import being added with an inlined data:font/woff2 base64 payload, which is also not directly related to hfId wiring.
Three possible explanations:
- (a) These ARE the golden expected files despite the name — the regression compares against them, and they had to be regenerated to include freshly-minted
data-hf-idattributes from R7's write-back path. If so, the naming is misleading and the diff is load-bearing. - (b) They're dev-time captures committed accidentally — the test framework writes these on failure, the author ran tests locally, captured the output, and added the files to the PR without realizing. CI passes either way because the framework regenerates them.
- (c) Stale snapshots updated to match a separate (unrelated) producer change — the font-face import addition hints at something other than hf-id work.
CI status confirms all regression-shards pass (including style-2-prod in shard-5 and style-10-prod in shard-3), which means the test currently agrees with whatever shape these files are in. So they're consistent with the test, but the question is whether they SHOULD be in this PR at all.
Two paths to resolve:
- Revert the fixture diffs if they're (b) or (c) and confirm CI still passes (which it does — the tests don't depend on these files).
- Add a one-line PR-body note explaining the regen if they're (a): "fixture HTML updated to capture freshly-minted
data-hf-idattributes from R7 write-back; this PR also picks up<style-2/10>font-face change from #NNNN."
(a)-as-confirmation costs zero; (b)-as-cleanup tightens the PR by ~75%. Either is fine — just want certainty.
2. Third site reads data-hf-id with the same empty-string issue as #1296
useDomEditCommits.ts:556:
{
element: entry.element,
id: entry.id ?? null,
hfId: entry.element.getAttribute("data-hf-id") ?? undefined,
...
}Same pattern as domEditingLayers.ts:372 (in #1296). The ?? undefined only converts null to undefined; an empty-string data-hf-id="" passes through as "", which then becomes [data-hf-id=""] in the selector and matches any other empty-hf-id elements.
There are now three sites reading this attribute (resolveDomEditSelection, buildDomEditPatchTarget's caller, and this batch path). Three opportunities for the same bug. Worth a one-line helper:
// domEditingLayers.ts
export function readHfId(element: Element): string | undefined {
return element.getAttribute("data-hf-id")?.trim() || undefined;
}…then replace the three sites. Tightens the contract and gives a single place to handle whitespace, empty values, and any future normalization.
(Same root cause as my #1296 concern #1 — confirming it generalizes.)
Nits
useDomEditCommits.ts:189— the original code constructedpatchTargetwith an explicit type annotation ({ id?: string | null; selector?: string; selectorIndex?: number }) which carried documentation value. The replacementconst patchTarget = buildDomEditPatchTarget(selection)loses that annotation. The helper's return type covers it, but a one-line// patch target including hfId for R7 lookup precedencecomment would land in lieu of the lost type tag.buildDomEditPatchTargetreturns a plain{ id, hfId, selector, selectorIndex }object. Ifselection.hfId === undefined, the returned object still has the key with valueundefined(e.g.,{ hfId: undefined, ... }). When serialized to JSON,undefinedfields are dropped (good). But: if any caller does a shallow comparison or iteratesObject.keys(target), the key appears. Defensive: omit undefined fields with...(selection.hfId !== undefined && { hfId: selection.hfId }). Cosmetic.- Tests for
buildDomEditPatchTargetcover (a) hfId present and (b) id-without-hfId. Missing the empty-string case (per concern #2) and the "all three identifiers" case (id + hfId + selector — what does the server-side precedence pick?). The third case is more about documenting R1's server-side contract than verifyingbuildDomEditPatchTarget, but worth at least a comment in the test that exercises it. - Telemetry: the
trackStudioEventcalls inuseDomEditCommits.tsdon't tag whether a patch usedhfIdvsidvsselector. If the team wants to track adoption of the hf-id keyspace post-cutover (e.g. what % of patches hit the hf-id lookup branch), this is the moment to add alookupKey: "hfId" | "id" | "selector"tag.
Questions
- Fixture HTML diffs — please confirm whether the
style-2-prodandstyle-10-prodfailures/actual.htmlupdates are intentional regenerations (concern #1). - Cutover deployment plan — is the studio + server going to deploy atomically, or is there a window where studio sends
hfIdto a server version that doesn't yet have R1'sfindByHfId? (My read: R1 is already in main and deployed, so this is theoretical. Confirming.) buildDomEditPatchTargetshape vsPatchTargetinterface — the helper returns an inline object literal, notPatchTarget. Would typing it asPatchTargetlock the contract more cleanly?
What I didn't verify
- Whether the same fixture diff appears in other PRs in main that haven't merged yet (could indicate an upstream change being pulled in via this PR's rebase). Quick
git logon those two fixture files in main would resolve this. - Cross-package contract: studio's
PatchTargetand server'sMutationTargetare now both{ id?, hfId?, selector?, selectorIndex? }. Verified the shape matches; didn't verify the wire format (JSON body) is byte-identical. - The full chain from studio commit through to
findByHfIdlookup — the integration test is implicit in CI's regression-shards passing.
Clean, focused wiring change. The fixture-file diff is the only thing that requires a confirmation note rather than just a code review.
— Review by Rames D Jusso

Summary
buildDomEditPatchTarget(selection)helper indomEditingLayers.ts— a pure function that mapsDomEditSelectionfields (includinghfId) to a patch target objectpatchTargetliteral constructions inuseDomEditCommits.tswith calls to this helper, forwardinghfIdautomatically at the patch-element and remove-element commit siteshfId: entry.element.getAttribute("data-hf-id") ?? undefinedto the z-index reorder cast object (this path lacks aDomEditSelectionso reads the attribute directly)!id && !selector) to also allowhfIdas a valid patch target identityWhy
Second and final step of R7 Task 5a (Studio wire). PR #1296 planted
hfIdintoDomEditSelectionviaresolveDomEditSelection. This PR forwards it into the JSON body of every DOM-edit patch request, activating thehfId-first lookup branches in both patch engines:sourceMutation.ts:65(findByHfIdvia[data-hf-id="…"]querySelector) — server pathsourcePatcher.ts:278(execDataAttrPatternregex) — client pathPrior to these two PRs, every edit targeted by CSS selector only, meaning an element's source position could drift if the selector matched a different element after an edit. With both PRs merged, DOM edits for elements that have a pinned
data-hf-idnow target by id directly, giving the stable-id guarantee R1 was built for.Architecture note: timeline path not included (Task 5b)
The 3 sites in
useTimelineEditing.tsbuild targets from a serializedTimelineElementwith no DOM node —getAttributeisn't callable there. That path still falls through to the selector fallback and keeps working. Task 5b (carryhfIdonTimelineElement) is tracked in the plan doc as a separate open decision.Test plan
buildDomEditPatchTargetunit tests indomEditingLayers.test.ts:hfIdwhen selection has itid/selectorwhenhfIdis absent🤖 Generated with Claude Code