mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
feat(studio-server): default the GSAP writer to acorn
HYPERFRAMES_GSAP_WRITER=recast stays as the fallback for one watch window before recast is deleted. acornDefaultBlockers -> behavioralParityOps: the 18 ops without a byte-diff oracle are now pinned in a test, not a gate. files.test.ts "stale semantic no-op" pinned to the recast writer: its interleave seam is recast's lazy import, which acorn (statically imported, fully synchronous) never performs. The 409 revalidation it covers is writer-independent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
26ea8a0740
commit
407cacf0ae
@@ -1307,30 +1307,40 @@ tl.fromTo("#box", { opacity: 0, x: -50 }, { opacity: 1, x: 0, duration: 1.5, eas
|
||||
});
|
||||
|
||||
it("rejects a stale semantic no-op after a concurrent file write", async () => {
|
||||
const projectDir = createProjectDir();
|
||||
writeHtml(projectDir, "comp.html", FROMTO_COMP);
|
||||
const app = new Hono();
|
||||
registerFileRoutes(app, createAdapter(projectDir));
|
||||
const successor = FROMTO_COMP.replace('data-duration="3"', 'data-duration="9"');
|
||||
let releaseImport = () => {};
|
||||
recastImportGate.wait = new Promise<void>((resolve) => {
|
||||
releaseImport = resolve;
|
||||
});
|
||||
const parserEntered = new Promise<void>((resolve) => {
|
||||
recastImportGate.onEnter = resolve;
|
||||
});
|
||||
// Pins the recast writer: this test's interleave seam is recast's LAZY module
|
||||
// import, which the acorn default no longer performs (acorn is statically
|
||||
// imported). The 409 revalidation under test is writer-independent.
|
||||
const previousWriter = process.env.HYPERFRAMES_GSAP_WRITER;
|
||||
process.env.HYPERFRAMES_GSAP_WRITER = "recast";
|
||||
try {
|
||||
const projectDir = createProjectDir();
|
||||
writeHtml(projectDir, "comp.html", FROMTO_COMP);
|
||||
const app = new Hono();
|
||||
registerFileRoutes(app, createAdapter(projectDir));
|
||||
const successor = FROMTO_COMP.replace('data-duration="3"', 'data-duration="9"');
|
||||
let releaseImport = () => {};
|
||||
recastImportGate.wait = new Promise<void>((resolve) => {
|
||||
releaseImport = resolve;
|
||||
});
|
||||
const parserEntered = new Promise<void>((resolve) => {
|
||||
recastImportGate.onEnter = resolve;
|
||||
});
|
||||
|
||||
const pending = postGsapMutationBatch(app, "comp.html", {
|
||||
mutations: [{ type: "shift-positions", targetSelector: "#missing", delta: 1 }],
|
||||
});
|
||||
await parserEntered;
|
||||
writeHtml(projectDir, "comp.html", successor);
|
||||
releaseImport();
|
||||
const response = await pending;
|
||||
const pending = postGsapMutationBatch(app, "comp.html", {
|
||||
mutations: [{ type: "shift-positions", targetSelector: "#missing", delta: 1 }],
|
||||
});
|
||||
await parserEntered;
|
||||
writeHtml(projectDir, "comp.html", successor);
|
||||
releaseImport();
|
||||
const response = await pending;
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(await response.json()).toMatchObject({ conflict: true });
|
||||
expect(readFileSync(join(projectDir, "comp.html"), "utf-8")).toBe(successor);
|
||||
expect(response.status).toBe(409);
|
||||
expect(await response.json()).toMatchObject({ conflict: true });
|
||||
expect(readFileSync(join(projectDir, "comp.html"), "utf-8")).toBe(successor);
|
||||
} finally {
|
||||
if (previousWriter === undefined) delete process.env.HYPERFRAMES_GSAP_WRITER;
|
||||
else process.env.HYPERFRAMES_GSAP_WRITER = previousWriter;
|
||||
}
|
||||
});
|
||||
|
||||
it("applies an ordered GSAP mutation batch with one before/after write result", async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GSAP_MUTATION_CAPABILITIES,
|
||||
acornDefaultBlockers,
|
||||
behavioralParityOps,
|
||||
renderGsapMutationCapabilityReport,
|
||||
resolveGsapWriter,
|
||||
} from "./gsapMutationCapabilities.js";
|
||||
@@ -61,14 +61,37 @@ describe("GSAP writer capability matrix", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("renders every classified operation and keeps default blocked until parity is differential", () => {
|
||||
it("renders every classified operation and names the behavioral-only ops", () => {
|
||||
const report = renderGsapMutationCapabilityReport();
|
||||
for (const operation of operations) expect(report).toContain(`| ${operation} |`);
|
||||
expect(acornDefaultBlockers().length).toBeGreaterThan(0);
|
||||
expect(report).toContain("Behavioral-only parity");
|
||||
expect(behavioralParityOps().sort()).toEqual(
|
||||
[
|
||||
"add-with-keyframes",
|
||||
"consolidate-position-writes",
|
||||
"convert-to-keyframes",
|
||||
"delete-all-for-selector",
|
||||
"materialize-keyframes",
|
||||
"move-keyframe",
|
||||
"remove-all-keyframes",
|
||||
"remove-arc-path",
|
||||
"replace-with-keyframes",
|
||||
"resize-keyframed-tween",
|
||||
"scale-positions",
|
||||
"set-arc-path",
|
||||
"shift-positions",
|
||||
"shift-positions-batch",
|
||||
"split-animations",
|
||||
"split-into-property-groups",
|
||||
"unroll-timeline",
|
||||
"update-arc-segment",
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to Recast and requires an explicit Acorn canary selection", () => {
|
||||
expect(resolveGsapWriter({})).toBe("recast");
|
||||
it("defaults to Acorn; recast is an explicit fallback", () => {
|
||||
expect(resolveGsapWriter({})).toBe("acorn");
|
||||
expect(resolveGsapWriter({ HYPERFRAMES_GSAP_WRITER: "recast" })).toBe("recast");
|
||||
expect(resolveGsapWriter({ HYPERFRAMES_GSAP_WRITER: "acorn" })).toBe("acorn");
|
||||
expect(() => resolveGsapWriter({ HYPERFRAMES_GSAP_WRITER: "unknown" })).toThrow(
|
||||
"expected recast or acorn",
|
||||
|
||||
@@ -68,16 +68,17 @@ const GSAP_WRITER_MIGRATION = Object.freeze({
|
||||
owner: "studio-foundations",
|
||||
deadline: "2026-09-30",
|
||||
graduationCriteria:
|
||||
"Every operation has differential parity, the Acorn path imports no Recast runtime, and canary divergence is zero for the agreed soak window.",
|
||||
"Acorn is the default writer. Recast is retained one watch window as HYPERFRAMES_GSAP_WRITER=recast, then deleted (spec 2026-09-17 §5).",
|
||||
});
|
||||
|
||||
export function resolveGsapWriter(env: { HYPERFRAMES_GSAP_WRITER?: string }): "recast" | "acorn" {
|
||||
const configured = env.HYPERFRAMES_GSAP_WRITER ?? "recast";
|
||||
const configured = env.HYPERFRAMES_GSAP_WRITER ?? "acorn";
|
||||
if (configured === "recast" || configured === "acorn") return configured;
|
||||
throw new Error(`Invalid ${GSAP_WRITER_MIGRATION.flag}=${configured}; expected recast or acorn`);
|
||||
}
|
||||
|
||||
export function acornDefaultBlockers(): GsapMutationType[] {
|
||||
/** Ops whose acorn/recast parity rests on behavioral tests, not a byte-diff oracle. */
|
||||
export function behavioralParityOps(): GsapMutationType[] {
|
||||
return (
|
||||
Object.entries(GSAP_MUTATION_CAPABILITIES) as Array<[GsapMutationType, GsapMutationCapability]>
|
||||
)
|
||||
@@ -97,13 +98,13 @@ export function renderGsapMutationCapabilityReport(): string {
|
||||
"",
|
||||
`Owner: ${GSAP_WRITER_MIGRATION.owner}`,
|
||||
`Deadline: ${GSAP_WRITER_MIGRATION.deadline}`,
|
||||
`Flag: \`${GSAP_WRITER_MIGRATION.flag}=recast|acorn\``,
|
||||
`Flag: \`${GSAP_WRITER_MIGRATION.flag}=recast|acorn\` (default: acorn)`,
|
||||
"",
|
||||
"| Operation | Family | Acorn | Recast | Parity evidence |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
...rows,
|
||||
"",
|
||||
`Default blockers: ${acornDefaultBlockers().join(", ") || "none"}`,
|
||||
`Behavioral-only parity (no byte-diff oracle): ${behavioralParityOps().join(", ") || "none"}`,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user