Skip to content

Commit a4eacae

Browse files
vanceingallsclaude
andauthored
fix(studio): filter runtime-generated nodes from resolver-shadow telemetry (#1795)
The sdk_resolver_shadow tripwire flagged element_not_found for nodes a composition <script> creates at runtime (caption word/group spans, etc.). These have no static data-hf-id, so the SDK session (a static parse) cannot model them by design; the divergence is noise, not a resolver bug. - Runtime-node filter: suppress element_not_found when the resolved hf-id is absent from the on-disk source. An id PRESENT in source but missing from the session stays flagged (the genuine v0.6.110-class resolver divergence). - Add sessionElementCount to all element_not_found / animation_not_found emits (0 = empty/broken session, >0 = element-specific). - Add sourceHfIdCount to emitted element_not_found: =1 = static node the parse dropped (foreign-content exclusion / sub-comp gap), >1 = duplicate-id resolver ambiguity. Scoped to the DOM-edit path. Telemetry-only; no disk writes, no edit change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f24a1a9 commit a4eacae

3 files changed

Lines changed: 85 additions & 3 deletions

File tree

packages/studio/src/hooks/useDomEditSession.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,9 @@ export function useDomEditSession({
260260
onTrySdkPersist: sdkSession
261261
? (selection, operations, originalContent, targetPath, options) => {
262262
// Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
263-
runResolverShadow(sdkSession, selection.hfId, operations);
263+
// Pass originalContent so the runtime-node filter can suppress hf-ids
264+
// absent from source (script-created nodes the SDK can't model).
265+
runResolverShadow(sdkSession, selection.hfId, operations, originalContent);
264266
return sdkCutoverPersist(
265267
selection,
266268
operations,

packages/studio/src/utils/sdkResolverShadow.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,53 @@ describe("C. Resolver-parity detection", () => {
226226
});
227227
});
228228

229+
it("C8 runtime-node filter: hfId absent from source → suppressed (not a resolver bug)", () => {
230+
// The studio resolved a live-DOM element to an hf-id that the SDK session
231+
// doesn't contain AND that never appears in the on-disk source — it's a
232+
// node a composition <script> created at runtime (e.g. caption spans). Not
233+
// a resolver divergence; suppress.
234+
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
235+
typeof sdkResolverShadowCheck
236+
>[0];
237+
const source = `<div data-hf-id="hf-static">no runtime id here</div>`;
238+
const mismatches = sdkResolverShadowCheck(
239+
session,
240+
"hf-runtimeonly",
241+
[{ type: "inline-style", property: "color", value: "red" }],
242+
source,
243+
);
244+
expect(mismatches).toHaveLength(0);
245+
});
246+
247+
it("C8 runtime-node filter: hfId PRESENT in source but missing from session → still flagged (real bug)", () => {
248+
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
249+
typeof sdkResolverShadowCheck
250+
>[0];
251+
const source = `<div data-hf-id="hf-realbug">in source, not in SDK session</div>`;
252+
const mismatches = sdkResolverShadowCheck(
253+
session,
254+
"hf-realbug",
255+
[{ type: "inline-style", property: "color", value: "red" }],
256+
source,
257+
);
258+
expect(mismatches).toHaveLength(1);
259+
expect(mismatches[0]?.kind).toBe("element_not_found");
260+
});
261+
262+
it("C8 sourceHfIdCount: emitted element_not_found carries source occurrence count", async () => {
263+
mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
264+
const session = { getElement: () => null, getElements: () => [] } as unknown as Composition;
265+
// id present twice in source (duplicate-id ambiguity) but absent from session
266+
const source = `<div data-hf-id="hf-dup">a</div><div data-hf-id="hf-dup">b</div>`;
267+
runResolverShadow(
268+
session,
269+
"hf-dup",
270+
[{ type: "inline-style", property: "color", value: "red" }],
271+
source,
272+
);
273+
expect(lastShadow()?.sourceHfIdCount).toBe(2);
274+
});
275+
229276
it("C10: unmappable op type produces no mismatch (excluded, not flagged)", async () => {
230277
const session = await openComposition(BASE_HTML);
231278
// "unknown-op" is not in MAPPED_OP_TYPES, so it must be silently excluded.

packages/studio/src/utils/sdkResolverShadow.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ type AttrMap = Record<string, string | null>;
8282
* Mirror resolveScoped here: exact scoped-path match, then canonical bare
8383
* match, then first bare match — the resolvability dispatch actually has.
8484
*/
85+
// Count static `data-hf-id="<id>"` occurrences (both quote styles) in source.
86+
// Substring split, not regex — no escaping, and the id never contains a quote.
87+
function countHfIdInSource(source: string, id: string): number {
88+
return (
89+
source.split(`data-hf-id="${id}"`).length - 1 + (source.split(`data-hf-id='${id}'`).length - 1)
90+
);
91+
}
92+
8593
function resolveSnapshot(session: Composition, id: string): FlatEl | null {
8694
const els = session.getElements();
8795
const exact = els.find((el) => el.scopedId === id);
@@ -164,8 +172,17 @@ export function sdkResolverShadowCheck(
164172
session: Composition,
165173
hfId: string,
166174
ops: PatchOperation[],
175+
sourceContent?: string,
167176
): SdkResolverMismatch[] {
168177
if (!resolveSnapshot(session, hfId)) {
178+
// Runtime-node filter: an hf-id absent from the on-disk source the SDK
179+
// parsed was never in the static DOM — it belongs to an element a
180+
// composition <script> creates at runtime (e.g. caption word/group spans),
181+
// which the SDK session cannot model by design. That is NOT a resolver bug,
182+
// so suppress it. An hf-id PRESENT in source but missing from the session IS
183+
// a genuine resolver divergence (the v0.6.110 class) — keep emitting that.
184+
// ponytail: substring match; biases toward keeping signal on a loose hit.
185+
if (sourceContent !== undefined && !sourceContent.includes(hfId)) return [];
169186
return [{ kind: "element_not_found", hfId }];
170187
}
171188

@@ -241,17 +258,30 @@ export function runResolverShadow(
241258
session: Composition,
242259
hfId: string | null | undefined,
243260
ops: PatchOperation[],
261+
sourceContent?: string,
244262
): void {
245263
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
246264
if (!hfId) return;
247265
try {
248-
const mismatches = sdkResolverShadowCheck(session, hfId, ops);
266+
const mismatches = sdkResolverShadowCheck(session, hfId, ops, sourceContent);
249267
// Emit only on divergence — parity is silent, matching recordResolverParity
250268
// and recordAnimationResolverParity. Otherwise this fires a PostHog event on
251269
// every style/text/attr edit (the editor's chattiest path) at default-ON.
252270
if (mismatches.length === 0) return;
271+
const isElementNotFound = mismatches.some((m) => m.kind === "element_not_found");
253272
trackStudioEvent("sdk_resolver_shadow", {
254273
hfId,
274+
// sessionElementCount > 0 + element_not_found = runtime-only element;
275+
// sessionElementCount === 0 = session is empty/broken (actionable).
276+
sessionElementCount: session.getElements().length,
277+
// Count of data-hf-id="<id>" occurrences in source for an emitted
278+
// element_not_found (the runtime-node filter already dropped absent-from-
279+
// source ids, so an emitted one is in source ≥1×). >1 = duplicate ids →
280+
// resolver picked the wrong instance; =1 = single static node the SDK
281+
// parse dropped (foreign-content exclusion / sub-comp inlining gap).
282+
...(isElementNotFound && sourceContent !== undefined
283+
? { sourceHfIdCount: countHfIdInSource(sourceContent, hfId) }
284+
: {}),
255285
mismatchCount: mismatches.length,
256286
mismatches: JSON.stringify(redactMismatches(mismatches)),
257287
});
@@ -282,6 +312,7 @@ export function recordResolverParity(
282312
trackStudioEvent("sdk_resolver_shadow", {
283313
hfId,
284314
opLabel,
315+
sessionElementCount: session.getElements().length,
285316
mismatchCount: 1,
286317
mismatches: JSON.stringify([
287318
{ kind: "element_not_found", hfId } satisfies SdkResolverMismatch,
@@ -310,11 +341,13 @@ export function recordAnimationResolverParity(
310341
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
311342
if (!session || !animationId) return;
312343
try {
313-
const resolves = session.getElements().some((el) => el.animationIds.includes(animationId));
344+
const elements = session.getElements();
345+
const resolves = elements.some((el) => el.animationIds.includes(animationId));
314346
if (resolves) return; // SDK locates the animation — parity
315347
trackStudioEvent("sdk_resolver_shadow", {
316348
animationId,
317349
opLabel,
350+
sessionElementCount: elements.length,
318351
mismatchCount: 1,
319352
mismatches: JSON.stringify([
320353
{ kind: "animation_not_found", animationId } satisfies SdkResolverMismatch,

0 commit comments

Comments
 (0)