motionloom 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +52 -1
- package/README.md +20 -8
- package/SECURITY.md +6 -7
- package/SKILL.md +5 -3
- package/agent-card.json +4 -4
- package/agent-surfaces.json +1 -1
- package/bin/motionloom.mjs +5 -4
- package/capability-registry.json +26 -26
- package/dev-lab/public/action-library.js +899 -0
- package/dev-lab/public/devlab.js +722 -0
- package/dev-lab/public/index.html +163 -0
- package/dev-lab/public/runtime-bridge.js +109 -0
- package/docs/CHECKLIST.md +15 -2
- package/docs/DEV-LAB-RUNTIME.md +160 -0
- package/docs/DEV-LAB-STATE-TRANSITIONS.md +121 -0
- package/docs/STATUS.md +3 -3
- package/docs/releases/2.5.1.md +21 -0
- package/docs/releases/2.6.0.md +38 -0
- package/docs/releases/npm-publish-from-workstation.md +6 -6
- package/package.json +12 -4
- package/requirements.txt +1 -0
- package/rig-adapter-registry.json +3 -3
- package/schemas/browser-review-candidate.schema.json +37 -0
- package/schemas/devlab-runtime.schema.json +140 -0
- package/schemas/devlab-state-machine.schema.json +103 -0
- package/scripts/capture-runtime-telemetry.py +16 -8
- package/scripts/devlab.py +3 -10
- package/scripts/docs-audit.py +13 -1
- package/scripts/package-consumer-smoke.mjs +76 -0
- package/scripts/review-hook.py +255 -19
- package/scripts/runtime-adapters.mjs +28 -3
- package/scripts/skill-doctor.py +27 -1
- package/tests/runtime-harness/index.html +24 -0
- package/tests/runtime-harness/main.jsx +111 -0
- package/tests/scripts/run_tests.py +41 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { gsap } from "gsap";
|
|
4
|
+
import { Rive } from "@rive-app/canvas";
|
|
5
|
+
import { FramerRuntimePilot } from "../../src/output/runtime-pilot-framer/scene.jsx";
|
|
6
|
+
|
|
7
|
+
const params = new URLSearchParams(window.location.search);
|
|
8
|
+
const framework = params.get("framework") || "gsap";
|
|
9
|
+
const statusNode = document.getElementById("status");
|
|
10
|
+
const rootNode = document.getElementById("root");
|
|
11
|
+
|
|
12
|
+
function status(text) {
|
|
13
|
+
if (statusNode) statusNode.textContent = text;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function expose(adapter) {
|
|
17
|
+
window.__animationAdapter = adapter;
|
|
18
|
+
status(`${adapter.framework}: ${adapter.status}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mountGsap() {
|
|
22
|
+
const stage = document.createElement("div");
|
|
23
|
+
stage.className = "stage";
|
|
24
|
+
const box = document.createElement("div");
|
|
25
|
+
box.className = "gsap-box";
|
|
26
|
+
stage.appendChild(box);
|
|
27
|
+
rootNode.replaceChildren(stage);
|
|
28
|
+
const timeline = gsap.timeline({ paused: true });
|
|
29
|
+
timeline.fromTo(
|
|
30
|
+
box,
|
|
31
|
+
{ x: 0, y: 0, rotation: 0, opacity: 0.25, scale: 0.82 },
|
|
32
|
+
{ x: 240, y: -36, rotation: 28, opacity: 1, scale: 1, duration: 1, ease: "power2.out" },
|
|
33
|
+
);
|
|
34
|
+
const adapter = {
|
|
35
|
+
framework: "gsap",
|
|
36
|
+
runtime: `gsap@${gsap.version}`,
|
|
37
|
+
status: "ready",
|
|
38
|
+
ready: true,
|
|
39
|
+
setProgress(value) { timeline.progress(Math.max(0, Math.min(1, Number(value)))); },
|
|
40
|
+
getState() { return { progress: timeline.progress(), transform: getComputedStyle(box).transform, opacity: getComputedStyle(box).opacity }; },
|
|
41
|
+
};
|
|
42
|
+
expose(adapter);
|
|
43
|
+
adapter.setProgress(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function mountRive() {
|
|
47
|
+
const canvas = document.createElement("canvas");
|
|
48
|
+
canvas.width = 420;
|
|
49
|
+
canvas.height = 420;
|
|
50
|
+
rootNode.replaceChildren(canvas);
|
|
51
|
+
let riveInstance;
|
|
52
|
+
const bytes = await fetch("/assets/library/rive/state-machine-test.riv").then((response) => {
|
|
53
|
+
if (!response.ok) throw new Error(`Rive fixture HTTP ${response.status}`);
|
|
54
|
+
return response.arrayBuffer();
|
|
55
|
+
});
|
|
56
|
+
await new Promise((resolve, reject) => {
|
|
57
|
+
riveInstance = new Rive({
|
|
58
|
+
buffer: bytes,
|
|
59
|
+
canvas,
|
|
60
|
+
stateMachines: "StateMachine",
|
|
61
|
+
autoplay: true,
|
|
62
|
+
autoBind: false,
|
|
63
|
+
onLoad: resolve,
|
|
64
|
+
onLoadError: reject,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
riveInstance.resizeDrawingSurfaceToCanvas();
|
|
68
|
+
riveInstance.pause();
|
|
69
|
+
const inputs = riveInstance.stateMachineInputs("StateMachine") || [];
|
|
70
|
+
const byName = Object.fromEntries(inputs.map((input) => [input.name, input]));
|
|
71
|
+
if (!byName.MyNum || !byName.MyBool || !byName.MyTrig) {
|
|
72
|
+
throw new Error(`Rive StateMachine inputs missing: ${inputs.map((input) => input.name).join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
const adapter = {
|
|
75
|
+
framework: "rive",
|
|
76
|
+
runtime: "@rive-app/canvas@2.39.2",
|
|
77
|
+
status: "ready",
|
|
78
|
+
ready: true,
|
|
79
|
+
setProgress(value) {
|
|
80
|
+
const progress = Math.max(0, Math.min(1, Number(value)));
|
|
81
|
+
byName.MyNum.value = progress * 12;
|
|
82
|
+
byName.MyBool.value = progress >= 0.5;
|
|
83
|
+
if (progress > 0) byName.MyTrig.fire();
|
|
84
|
+
if (typeof riveInstance.advance === "function") riveInstance.advance(1 / 60);
|
|
85
|
+
},
|
|
86
|
+
getState() {
|
|
87
|
+
return {
|
|
88
|
+
inputs: inputs.map((input) => ({ name: input.name, type: input.type, value: input.value })),
|
|
89
|
+
stateMachines: riveInstance.playingStateMachineNames,
|
|
90
|
+
loaded: riveInstance.loaded,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
dispose() { riveInstance.cleanup(); },
|
|
94
|
+
};
|
|
95
|
+
expose(adapter);
|
|
96
|
+
adapter.setProgress(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function main() {
|
|
100
|
+
try {
|
|
101
|
+
if (framework === "gsap") mountGsap();
|
|
102
|
+
else if (framework === "framer-motion") createRoot(rootNode).render(<FramerRuntimePilot expose={expose} />);
|
|
103
|
+
else if (framework === "rive") await mountRive();
|
|
104
|
+
else throw new Error(`Unknown framework: ${framework}`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
expose({ framework, status: "error", ready: false, error: message, getState: () => ({ error: message }) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
main();
|
|
@@ -402,6 +402,47 @@ def test_task_bundle_resolver_contract():
|
|
|
402
402
|
)
|
|
403
403
|
check("runtime adapter rejects unsupported framework path", unsafe_runtime.returncode != 0 and "unsupported" in (unsafe_runtime.stderr + unsafe_runtime.stdout))
|
|
404
404
|
|
|
405
|
+
with tempfile.TemporaryDirectory() as td:
|
|
406
|
+
outside = Path(td) / "protected-output"
|
|
407
|
+
outside.mkdir()
|
|
408
|
+
sentinel = outside / "keep.txt"
|
|
409
|
+
sentinel.write_text("do not delete", encoding="utf-8")
|
|
410
|
+
unsafe_output = subprocess.run(
|
|
411
|
+
["node", str(ROOT / "scripts/runtime-adapters.mjs")],
|
|
412
|
+
env={**os.environ, "RUNTIME_FRAMEWORKS": "rive", "RUNTIME_EVIDENCE_DIR": str(outside)},
|
|
413
|
+
capture_output=True,
|
|
414
|
+
text=True,
|
|
415
|
+
)
|
|
416
|
+
check(
|
|
417
|
+
"runtime adapter rejects destructive output outside policy root",
|
|
418
|
+
unsafe_output.returncode != 0
|
|
419
|
+
and "MOTIONLOOM_RUNTIME_OUTPUT_ROOT" in (unsafe_output.stderr + unsafe_output.stdout)
|
|
420
|
+
and sentinel.read_text(encoding="utf-8") == "do not delete",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
with tempfile.TemporaryDirectory() as td:
|
|
424
|
+
outside_task = Path(td) / "protected-task"
|
|
425
|
+
outside_task.mkdir()
|
|
426
|
+
sentinel = outside_task / "keep.txt"
|
|
427
|
+
sentinel.write_text("do not delete", encoding="utf-8")
|
|
428
|
+
unsafe_capture = subprocess.run(
|
|
429
|
+
[
|
|
430
|
+
sys.executable,
|
|
431
|
+
str(ROOT / "scripts/capture-runtime-telemetry.py"),
|
|
432
|
+
"browser-review-smoke",
|
|
433
|
+
str(outside_task),
|
|
434
|
+
],
|
|
435
|
+
cwd=ROOT,
|
|
436
|
+
capture_output=True,
|
|
437
|
+
text=True,
|
|
438
|
+
)
|
|
439
|
+
check(
|
|
440
|
+
"runtime telemetry rejects task output outside project before cleanup",
|
|
441
|
+
unsafe_capture.returncode != 0
|
|
442
|
+
and "inside the project root" in (unsafe_capture.stderr + unsafe_capture.stdout)
|
|
443
|
+
and sentinel.read_text(encoding="utf-8") == "do not delete",
|
|
444
|
+
)
|
|
445
|
+
|
|
405
446
|
|
|
406
447
|
def test_runtime_telemetry_verifier_contract():
|
|
407
448
|
with tempfile.TemporaryDirectory() as td:
|