Skip to content

Commit 30f7c4f

Browse files
vanceingallsclaude
andcommitted
fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline
- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9 parser-backed ops instead of silently no-opping — callers must never believe an animation edit succeeded when nothing was mutated - validateOp returns false for Phase 3b ops so can() feature-detects - root package.json build filter now includes @hyperframes/sdk (package is dist-only; top-level build previously produced no SDK artifacts). publish.yml intentionally NOT updated — sdk stays unpublished until Phase 3 completes. Adversarial-review findings F3 + F4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 38778d5 commit 30f7c4f

3 files changed

Lines changed: 61 additions & 5 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"type": "module",
1212
"scripts": {
1313
"dev": "bun run studio",
14-
"build": "bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run}' build && bun run --filter @hyperframes/cli build",
14+
"build": "bun run --filter @hyperframes/core build && bun run --filter '@hyperframes/{core,engine,producer,player,studio,shader-transitions,aws-lambda,gcp-cloud-run,sdk}' build && bun run --filter @hyperframes/cli build",
1515
"build:producer": "bun run --filter @hyperframes/producer build",
1616
"studio": "bun run --filter @hyperframes/studio dev",
1717
"build:hyperframes-runtime": "bun run --filter @hyperframes/core build:hyperframes-runtime",

packages/sdk/src/engine/mutate.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,30 @@ describe("validateOp", () => {
361361
expect(validateOp(fresh(), { type: "setCompositionMetadata", width: 100 })).toBe(true);
362362
});
363363
});
364+
365+
// ─── Phase 3b ops — fail loudly, feature-detectable ───────────────────────────
366+
367+
describe("Phase 3b ops", () => {
368+
it("applyOp throws UnsupportedOpError instead of silently no-opping", () => {
369+
expect(() =>
370+
applyOp(fresh(), {
371+
type: "addGsapTween",
372+
target: "hf-title",
373+
id: "tw-1",
374+
tween: { method: "from", fromProperties: { opacity: 0 } },
375+
}),
376+
).toThrowError(/Phase 3b/);
377+
});
378+
379+
it("validateOp returns false so can() feature-detects", () => {
380+
expect(validateOp(fresh(), { type: "removeGsapTween", animationId: "tw-1" })).toBe(false);
381+
expect(
382+
validateOp(fresh(), {
383+
type: "addGsapTween",
384+
target: "hf-title",
385+
id: "tw-1",
386+
tween: { method: "from", fromProperties: { opacity: 0 } },
387+
}),
388+
).toBe(false);
389+
});
390+
});

packages/sdk/src/engine/mutate.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,32 @@ export interface MutationResult {
4040

4141
const EMPTY: MutationResult = { forward: [], inverse: [] };
4242

43+
/** Ops that require the Phase 3b parser-backed engine (meriyah/css-tree). */
44+
const PHASE3B_OPS = new Set([
45+
"setClassStyle",
46+
"addGsapTween",
47+
"setGsapTween",
48+
"setGsapKeyframe",
49+
"addGsapKeyframe",
50+
"removeGsapKeyframe",
51+
"removeGsapTween",
52+
"addLabel",
53+
"removeLabel",
54+
]);
55+
56+
// Re-exported from the package entry in the next stacked PR (#1325).
57+
// fallow-ignore-next-line unused-export
58+
export class UnsupportedOpError extends Error {
59+
readonly code = "E_UNSUPPORTED_OP";
60+
constructor(opType: string) {
61+
super(
62+
`Op '${opType}' requires the Phase 3b parser-backed engine and is not available yet. ` +
63+
`Use can(op) to feature-detect before dispatching.`,
64+
);
65+
this.name = "UnsupportedOpError";
66+
}
67+
}
68+
4369
// ─── Target normalization ────────────────────────────────────────────────────
4470

4571
function targets(target: HfId | HfId[]): HfId[] {
@@ -75,7 +101,9 @@ export function applyOp(parsed: ParsedDocument, op: EditOp): MutationResult {
75101
return handleSetCompositionMetadata(parsed, op);
76102
case "setVariableValue":
77103
return handleSetVariableValue(parsed, op.id, op.value);
78-
// Phase 3b parser-backed ops — pass through without mutation for now
104+
// Phase 3b parser-backed ops — fail loudly rather than silently no-op:
105+
// a caller must never believe an animation edit succeeded when nothing
106+
// was mutated and no patch was emitted.
79107
case "setClassStyle":
80108
case "addGsapTween":
81109
case "setGsapTween":
@@ -85,7 +113,7 @@ export function applyOp(parsed: ParsedDocument, op: EditOp): MutationResult {
85113
case "removeGsapTween":
86114
case "addLabel":
87115
case "removeLabel":
88-
return EMPTY;
116+
throw new UnsupportedOpError(op.type);
89117
}
90118
}
91119

@@ -349,8 +377,9 @@ export function validateOp(parsed: ParsedDocument, op: EditOp): boolean {
349377
return findRoot(parsed.document) !== null;
350378
case "setCompositionMetadata":
351379
return true;
352-
// Phase 3b — defer validation; allow through
380+
// Phase 3b — not implemented yet; can() must report false so callers
381+
// can feature-detect instead of hitting UnsupportedOpError.
353382
default:
354-
return true;
383+
return !PHASE3B_OPS.has(op.type);
355384
}
356385
}

0 commit comments

Comments
 (0)