feat(studio): enable SDK cutover by default for every op family

Master flag defaults true; unset family list now means all five families.
VITE_STUDIO_SDK_CUTOVER_ENABLED=false / VITE_STUDIO_SDK_CUTOVER_FAMILIES=...
remain as runtime kill switches via __HF_STUDIO_ENV__. Gate: 0 divergences on
0.8.41-0.8.46 shadow tile, 3455/0 success/failed on dev builds (30d).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-09-17 21:33:40 -07:00
co-authored by Claude Fable 5.1
parent 82f49262ec
commit 26ea8a0740
5 changed files with 49 additions and 14 deletions
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveStudioBooleanEnvFlag } from "./manualEditingAvailability";
import { STUDIO_SDK_OPERATION_FAMILIES } from "../../utils/sdkCutoverPolicy";
async function loadAvailabilityWithEnv(env: Record<string, string | undefined>) {
vi.resetModules();
@@ -88,4 +89,22 @@ describe("manual editing availability", () => {
});
expect(off.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(false);
});
it("defaults SDK cutover on for every family", async () => {
const all = await loadAvailabilityWithEnv({});
expect(all.STUDIO_SDK_CUTOVER_ENABLED).toBe(true);
expect(all.STUDIO_SDK_CUTOVER_FAMILIES).toEqual(new Set(STUDIO_SDK_OPERATION_FAMILIES));
});
it("honors the SDK cutover kill switches", async () => {
const off = await loadAvailabilityWithEnv({ VITE_STUDIO_SDK_CUTOVER_ENABLED: "false" });
expect(off.STUDIO_SDK_CUTOVER_ENABLED).toBe(false);
expect(off.STUDIO_SDK_CUTOVER_FAMILIES.size).toBe(0);
const subset = await loadAvailabilityWithEnv({
VITE_STUDIO_SDK_CUTOVER_FAMILIES: "dom,timing",
});
expect(subset.STUDIO_SDK_CUTOVER_ENABLED).toBe(true);
expect(subset.STUDIO_SDK_CUTOVER_FAMILIES).toEqual(new Set(["dom", "timing"]));
});
});
@@ -38,16 +38,20 @@ const runtimeEnv =
: {};
const env = { ...(import.meta.env ?? {}), ...runtimeEnv } as StudioFeatureFlagEnv;
// Stage 7 Step 3c: SDK cutover — routes inline-style ops through SDK dispatch
// instead of the server patch-element API. Default false; enable via
// VITE_STUDIO_SDK_CUTOVER_ENABLED=true. Requires SDK session to be open.
// Stage 7 SDK cutover — routes Studio edits through SDK dispatch instead of the
// server patch/mutation routes. Default TRUE as of the flip release (spec:
// 2026-09-17-studio-sdk-cutover-flip-design.md). Kill switch:
// VITE_STUDIO_SDK_CUTOVER_ENABLED=false (works at runtime via __HF_STUDIO_ENV__).
export const STUDIO_SDK_CUTOVER_ENABLED = resolveStudioBooleanEnvFlag(
env,
["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
false,
true,
);
/** Explicit per-operation-family canary selection; the master flag alone enables nothing. */
/**
* Per-family selection. Unset = every family (the master alone enables all).
* Set `VITE_STUDIO_SDK_CUTOVER_FAMILIES=dom,timing` to RESTRICT to a subset.
*/
export const STUDIO_SDK_CUTOVER_FAMILIES = resolveEnabledSdkFamilies(
env,
STUDIO_SDK_CUTOVER_ENABLED,
@@ -1,11 +1,12 @@
import { describe, expect, it, vi } from "vitest";
// Dark-launch contract: with STUDIO_SDK_CUTOVER_ENABLED=false, EVERY cutover
// Kill-switch contract: with VITE_STUDIO_SDK_CUTOVER_ENABLED=false, EVERY cutover
// persist chokepoint must explicitly decline so the caller takes the legacy server
// path — even when a valid SDK session exists (one always does, for
// shadow/selection). This is the contract the prod flag-flip rests on; a future
// refactor of the gate guards that silently re-enables cutover on flag-off
// turns these red. (sdkCutover.test.ts mocks the flag TRUE; this is its sibling.)
// shadow/selection). Default is ON (manualEditingAvailability.test.ts pins that);
// this file pins that the env override still turns it fully off. A future
// refactor of the gate guards that ignores the flag turns these red.
// (sdkCutover.test.ts mocks the flag TRUE; this is its sibling.)
vi.mock("../components/editor/manualEditingAvailability", () => ({
STUDIO_SDK_CUTOVER_ENABLED: false,
STUDIO_SDK_RESOLVER_SHADOW_ENABLED: false,
@@ -7,8 +7,17 @@ import {
} from "./sdkCutoverPolicy";
describe("Studio SDK operation-family policy", () => {
it("enables no family from the master switch alone", () => {
expect(resolveEnabledSdkFamilies({}, true).size).toBe(0);
it("enables every family when the master switch is on and no family list is set", () => {
const all = new Set(STUDIO_SDK_OPERATION_FAMILIES);
expect(resolveEnabledSdkFamilies({}, true)).toEqual(all);
expect(resolveEnabledSdkFamilies({ VITE_STUDIO_SDK_CUTOVER_FAMILIES: "" }, true)).toEqual(all);
expect(resolveEnabledSdkFamilies({ VITE_STUDIO_SDK_CUTOVER_FAMILIES: " " }, true)).toEqual(
all,
);
});
it("enables no family when the master switch is off, regardless of the family list", () => {
expect(resolveEnabledSdkFamilies({}, false).size).toBe(0);
expect(resolveEnabledSdkFamilies({ VITE_STUDIO_SDK_CUTOVER_FAMILIES: "dom" }, false).size).toBe(
0,
);
@@ -54,7 +54,8 @@ export function resolveEnabledSdkFamilies(
): ReadonlySet<StudioSdkOperationFamily> {
if (!masterEnabled) return new Set();
const raw = env["VITE_STUDIO_SDK_CUTOVER_FAMILIES"];
if (typeof raw !== "string" || raw.trim() === "") return new Set();
// Unset/blank = every family. The list exists to RESTRICT (kill switch per family).
if (typeof raw !== "string" || raw.trim() === "") return new Set(STUDIO_SDK_OPERATION_FAMILIES);
const requested = raw
.split(",")
.map((family) => family.trim())
@@ -85,8 +86,9 @@ export function renderStudioSdkCutoverReport(): string {
return [
"# Studio SDK cutover report",
"",
"Master flag: `VITE_STUDIO_SDK_CUTOVER_ENABLED=true`",
"Family flag: `VITE_STUDIO_SDK_CUTOVER_FAMILIES=dom,timing,...`",
"Default: SDK persist ON for every family. Kill switches:",
" `VITE_STUDIO_SDK_CUTOVER_ENABLED=false` (all families → server path)",
" `VITE_STUDIO_SDK_CUTOVER_FAMILIES=dom,timing` (only listed families → SDK; rest → server)",
"",
"A family graduates only after zero unexplained resolver/serialization divergences over its agreed corpus and soak window.",
"",