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>
This commit is contained in:
Vance Ingalls
2026-06-30 00:16:20 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent f24a1a9ce7
commit a4eacaec37
3 changed files with 85 additions and 3 deletions
@@ -260,7 +260,9 @@ export function useDomEditSession({
onTrySdkPersist: sdkSession
? (selection, operations, originalContent, targetPath, options) => {
// Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
runResolverShadow(sdkSession, selection.hfId, operations);
// Pass originalContent so the runtime-node filter can suppress hf-ids
// absent from source (script-created nodes the SDK can't model).
runResolverShadow(sdkSession, selection.hfId, operations, originalContent);
return sdkCutoverPersist(
selection,
operations,
@@ -226,6 +226,53 @@ describe("C. Resolver-parity detection", () => {
});
});
it("C8 runtime-node filter: hfId absent from source → suppressed (not a resolver bug)", () => {
// The studio resolved a live-DOM element to an hf-id that the SDK session
// doesn't contain AND that never appears in the on-disk source — it's a
// node a composition <script> created at runtime (e.g. caption spans). Not
// a resolver divergence; suppress.
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
typeof sdkResolverShadowCheck
>[0];
const source = `<div data-hf-id="hf-static">no runtime id here</div>`;
const mismatches = sdkResolverShadowCheck(
session,
"hf-runtimeonly",
[{ type: "inline-style", property: "color", value: "red" }],
source,
);
expect(mismatches).toHaveLength(0);
});
it("C8 runtime-node filter: hfId PRESENT in source but missing from session → still flagged (real bug)", () => {
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
typeof sdkResolverShadowCheck
>[0];
const source = `<div data-hf-id="hf-realbug">in source, not in SDK session</div>`;
const mismatches = sdkResolverShadowCheck(
session,
"hf-realbug",
[{ type: "inline-style", property: "color", value: "red" }],
source,
);
expect(mismatches).toHaveLength(1);
expect(mismatches[0]?.kind).toBe("element_not_found");
});
it("C8 sourceHfIdCount: emitted element_not_found carries source occurrence count", async () => {
mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
const session = { getElement: () => null, getElements: () => [] } as unknown as Composition;
// id present twice in source (duplicate-id ambiguity) but absent from session
const source = `<div data-hf-id="hf-dup">a</div><div data-hf-id="hf-dup">b</div>`;
runResolverShadow(
session,
"hf-dup",
[{ type: "inline-style", property: "color", value: "red" }],
source,
);
expect(lastShadow()?.sourceHfIdCount).toBe(2);
});
it("C10: unmappable op type produces no mismatch (excluded, not flagged)", async () => {
const session = await openComposition(BASE_HTML);
// "unknown-op" is not in MAPPED_OP_TYPES, so it must be silently excluded.
+35 -2
View File
@@ -82,6 +82,14 @@ type AttrMap = Record<string, string | null>;
* Mirror resolveScoped here: exact scoped-path match, then canonical bare
* match, then first bare match — the resolvability dispatch actually has.
*/
// Count static `data-hf-id="<id>"` occurrences (both quote styles) in source.
// Substring split, not regex — no escaping, and the id never contains a quote.
function countHfIdInSource(source: string, id: string): number {
return (
source.split(`data-hf-id="${id}"`).length - 1 + (source.split(`data-hf-id='${id}'`).length - 1)
);
}
function resolveSnapshot(session: Composition, id: string): FlatEl | null {
const els = session.getElements();
const exact = els.find((el) => el.scopedId === id);
@@ -164,8 +172,17 @@ export function sdkResolverShadowCheck(
session: Composition,
hfId: string,
ops: PatchOperation[],
sourceContent?: string,
): SdkResolverMismatch[] {
if (!resolveSnapshot(session, hfId)) {
// Runtime-node filter: an hf-id absent from the on-disk source the SDK
// parsed was never in the static DOM — it belongs to an element a
// composition <script> creates at runtime (e.g. caption word/group spans),
// which the SDK session cannot model by design. That is NOT a resolver bug,
// so suppress it. An hf-id PRESENT in source but missing from the session IS
// a genuine resolver divergence (the v0.6.110 class) — keep emitting that.
// ponytail: substring match; biases toward keeping signal on a loose hit.
if (sourceContent !== undefined && !sourceContent.includes(hfId)) return [];
return [{ kind: "element_not_found", hfId }];
}
@@ -241,17 +258,30 @@ export function runResolverShadow(
session: Composition,
hfId: string | null | undefined,
ops: PatchOperation[],
sourceContent?: string,
): void {
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
if (!hfId) return;
try {
const mismatches = sdkResolverShadowCheck(session, hfId, ops);
const mismatches = sdkResolverShadowCheck(session, hfId, ops, sourceContent);
// Emit only on divergence — parity is silent, matching recordResolverParity
// and recordAnimationResolverParity. Otherwise this fires a PostHog event on
// every style/text/attr edit (the editor's chattiest path) at default-ON.
if (mismatches.length === 0) return;
const isElementNotFound = mismatches.some((m) => m.kind === "element_not_found");
trackStudioEvent("sdk_resolver_shadow", {
hfId,
// sessionElementCount > 0 + element_not_found = runtime-only element;
// sessionElementCount === 0 = session is empty/broken (actionable).
sessionElementCount: session.getElements().length,
// Count of data-hf-id="<id>" occurrences in source for an emitted
// element_not_found (the runtime-node filter already dropped absent-from-
// source ids, so an emitted one is in source ≥1×). >1 = duplicate ids →
// resolver picked the wrong instance; =1 = single static node the SDK
// parse dropped (foreign-content exclusion / sub-comp inlining gap).
...(isElementNotFound && sourceContent !== undefined
? { sourceHfIdCount: countHfIdInSource(sourceContent, hfId) }
: {}),
mismatchCount: mismatches.length,
mismatches: JSON.stringify(redactMismatches(mismatches)),
});
@@ -282,6 +312,7 @@ export function recordResolverParity(
trackStudioEvent("sdk_resolver_shadow", {
hfId,
opLabel,
sessionElementCount: session.getElements().length,
mismatchCount: 1,
mismatches: JSON.stringify([
{ kind: "element_not_found", hfId } satisfies SdkResolverMismatch,
@@ -310,11 +341,13 @@ export function recordAnimationResolverParity(
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
if (!session || !animationId) return;
try {
const resolves = session.getElements().some((el) => el.animationIds.includes(animationId));
const elements = session.getElements();
const resolves = elements.some((el) => el.animationIds.includes(animationId));
if (resolves) return; // SDK locates the animation — parity
trackStudioEvent("sdk_resolver_shadow", {
animationId,
opLabel,
sessionElementCount: elements.length,
mismatchCount: 1,
mismatches: JSON.stringify([
{ kind: "animation_not_found", animationId } satisfies SdkResolverMismatch,