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:
Vance Ingalls
2026-09-17 21:38:38 -07:00
co-authored by Claude Fable 5.1
parent 26ea8a0740
commit 407cacf0ae
3 changed files with 66 additions and 32 deletions
+32 -22
View File
@@ -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");
}