fix(studio): single-flight reorder source read, tag loose-match sourceHfIdCount, brace style

This commit is contained in:
Vance Ingalls
2026-07-01 14:51:49 -07:00
parent 5149394b00
commit 69b927a300
4 changed files with 54 additions and 10 deletions
@@ -294,7 +294,12 @@ export function useDomEditSession({
// the SDK resolves each reordered element (the reorderElements op's targets).
onReorderShadow: sdkSession
? (targets: string[]) => {
const reorderSrc = activeCompPath ? () => readProjectFile(activeCompPath) : undefined;
// Single-flight: every target in one reorder batch shares the same file, so
// memoize the read instead of firing one fetch per unresolved target.
let reorderSrcPromise: Promise<string> | undefined;
const reorderSrc = activeCompPath
? () => (reorderSrcPromise ??= readProjectFile(activeCompPath))
: undefined;
for (const target of targets)
void recordResolverParity(sdkSession, target, "reorderElements", reorderSrc);
}
+2 -1
View File
@@ -221,12 +221,13 @@ export function sdkGsapTweenPersist(
"addGsapTween",
gsapSrc ? () => gsapSrc(targetPath) : undefined,
);
} else
} else {
recordAnimationResolverParity(
sdkSession,
op.animationId,
op.kind === "set" ? "setGsapTween" : "removeGsapTween",
);
}
// Leading dark-launch gate so flag-off does no SDK touch (getElement) at all —
// matches the other three chokepoints' discipline.
if (!STUDIO_SDK_CUTOVER_ENABLED) return Promise.resolve(false);
@@ -273,6 +273,22 @@ describe("C. Resolver-parity detection", () => {
expect(lastShadow()?.sourceHfIdCount).toBe(2);
});
it("C8 sourceLooseMatchOnly: hfId matches source only as plain text, not a data-hf-id attribute", async () => {
mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
const session = { getElement: () => null, getElements: () => [] } as unknown as Composition;
// "hf-widget" appears only inside a class name, never as data-hf-id="hf-widget".
const source = `<div class="hf-widget-container">no attribute match here</div>`;
runResolverShadow(
session,
"hf-widget",
[{ type: "inline-style", property: "color", value: "red" }],
source,
);
const ev = lastShadow();
expect(ev?.sourceHfIdCount).toBe(0);
expect(ev?.sourceLooseMatchOnly).toBe(true);
});
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.
@@ -423,6 +439,19 @@ describe("F. recordResolverParity", () => {
expect(ev?.mismatchCount).toBe(1);
expect(ev?.sourceHfIdCount).toBeUndefined();
});
it("tags sourceLooseMatchOnly when hfId matches source only as plain text, not a data-hf-id attribute", async () => {
mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
const session = await openComposition(BASE_HTML);
// "hf-widget" appears only inside a class name, never as data-hf-id="hf-widget".
await recordResolverParity(session, "hf-widget", "setTiming", () =>
Promise.resolve('<div class="hf-widget-container"></div>'),
);
const ev = lastShadow();
expect(ev?.mismatchCount).toBe(1);
expect(ev?.sourceHfIdCount).toBe(0);
expect(ev?.sourceLooseMatchOnly).toBe(true);
});
});
// ─── G. recordAnimationResolverParity (GSAP animationId ops) ──────────────────
+17 -8
View File
@@ -272,6 +272,10 @@ export function runResolverShadow(
// 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");
const strictCount =
isElementNotFound && sourceContent !== undefined
? countHfIdInSource(sourceContent, hfId)
: undefined;
trackStudioEvent("sdk_resolver_shadow", {
hfId,
// sessionElementCount > 0 + element_not_found = runtime-only element;
@@ -282,13 +286,11 @@ export function runResolverShadow(
// instance; =1 = single static node the SDK parse dropped (foreign-content
// exclusion / sub-comp inlining gap); =0 = the runtime-node filter above
// uses a loose substring match (biased toward keeping signal) while this
// count uses a strict attribute match — an emitted event with count 0
// means hfId appeared in source as plain text (e.g. a class name, comment,
// or script string) but never as a data-hf-id attribute. Treat 0 as "not
// a genuine attribute occurrence," not as a contradiction.
...(isElementNotFound && sourceContent !== undefined
? { sourceHfIdCount: countHfIdInSource(sourceContent, hfId) }
: {}),
// count uses a strict attribute match — see sourceLooseMatchOnly below.
...(strictCount !== undefined ? { sourceHfIdCount: strictCount } : {}),
// Loose suppression check matched (kept this event) but the strict
// attribute count came back 0 — see the sourceHfIdCount comment above.
...(strictCount === 0 ? { sourceLooseMatchOnly: true } : {}),
mismatchCount: mismatches.length,
mismatches: JSON.stringify(redactMismatches(mismatches)),
});
@@ -334,6 +336,7 @@ export async function recordResolverParity(
}
// Runtime-generated node the static parse can't model — suppress (mirrors the dom-edit path).
if (source !== undefined && !source.includes(hfId)) return;
const strictCount = source !== undefined ? countHfIdInSource(source, hfId) : undefined;
trackStudioEvent("sdk_resolver_shadow", {
hfId,
opLabel,
@@ -342,7 +345,13 @@ export async function recordResolverParity(
// on an emitted (non-suppressed) event — the suppression check above is a
// loose substring match (biased toward keeping signal); see the longer
// comment on this field in runResolverShadow for the full explanation.
...(source !== undefined ? { sourceHfIdCount: countHfIdInSource(source, hfId) } : {}),
...(strictCount !== undefined ? { sourceHfIdCount: strictCount } : {}),
// Loose suppression check matched (kept this event) but the strict
// attribute count came back 0 — hfId appeared as plain text (class name,
// comment, script string) but never as a data-hf-id="..." attribute.
// Lets telemetry consumers filter this cohort without parsing the
// sourceHfIdCount comment above.
...(strictCount === 0 ? { sourceLooseMatchOnly: true } : {}),
mismatchCount: 1,
mismatches: JSON.stringify([
{ kind: "element_not_found", hfId } satisfies SdkResolverMismatch,