motionloom 2.4.0 → 2.5.1

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.
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Animation Skill Runtime Harness</title>
7
+ <style>
8
+ html, body { margin: 0; min-height: 100%; background: #111827; color: #f8fafc; font-family: system-ui, sans-serif; }
9
+ body { display: grid; place-items: center; }
10
+ #root { width: 512px; height: 512px; display: grid; place-items: center; }
11
+ .stage { position: relative; width: 420px; height: 420px; overflow: hidden; background: #f8fafc; border-radius: 12px; }
12
+ .stage::after { content: ""; position: absolute; inset: 0; pointer-events: none; border: 1px solid rgba(15,23,42,.2); border-radius: inherit; }
13
+ .gsap-box, .motion-box { width: 96px; height: 96px; position: absolute; left: 40px; top: 160px; border-radius: 18px; background: #2563eb; transform-origin: center; }
14
+ .gsap-box::after, .motion-box::after { content: ""; position: absolute; width: 16px; height: 16px; border-radius: 50%; right: 14px; top: 14px; background: #f59e0b; }
15
+ canvas { width: 420px; height: 420px; display: block; }
16
+ #status { position: fixed; left: 12px; bottom: 12px; font: 12px ui-monospace, monospace; color: #fbbf24; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div id="root"></div>
21
+ <div id="status">loading</div>
22
+ <script type="module" src="/tests/runtime-harness/main.jsx"></script>
23
+ </body>
24
+ </html>
@@ -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:
@@ -554,6 +595,35 @@ def test_intelligence_core_contracts():
554
595
  "--registry", str(registry), "--capability", "runtime.rive",
555
596
  ], capture_output=True, text=True)
556
597
  check("intelligence selects verified runtime", selected.returncode == 0 and '"status": "verified"' in selected.stdout)
598
+
599
+ capability_card = subprocess.run([
600
+ sys.executable, str(intelligence), "capabilities", "card",
601
+ "--registry", str(registry), "--format", "json",
602
+ ], capture_output=True, text=True)
603
+ capability_card_data = json.loads(capability_card.stdout) if capability_card.returncode == 0 else {}
604
+ rive_card = next((entry for entry in capability_card_data.get("capabilities", []) if entry.get("id") == "runtime.rive"), {})
605
+ check(
606
+ "intelligence exports read-only capability card",
607
+ capability_card.returncode == 0
608
+ and capability_card_data.get("kind") == "motionloom-capability-card"
609
+ and capability_card_data.get("selection", {}).get("required") is True
610
+ and capability_card_data.get("review", {}).get("production_approval") == "not_derived"
611
+ and rive_card.get("declared_status") == "verified"
612
+ and rive_card.get("last_verified_at"),
613
+ capability_card.stderr.strip(),
614
+ )
615
+
616
+ capability_alias = subprocess.run([
617
+ "node", str(ROOT / "bin/motionloom.mjs"), "capability", "card",
618
+ "--registry", str(registry), "--format", "json",
619
+ ], capture_output=True, text=True)
620
+ capability_alias_data = json.loads(capability_alias.stdout) if capability_alias.returncode == 0 else {}
621
+ check(
622
+ "motionloom capability alias exports card",
623
+ capability_alias.returncode == 0 and capability_alias_data.get("kind") == "motionloom-capability-card",
624
+ capability_alias.stderr.strip(),
625
+ )
626
+
557
627
  scaffold = subprocess.run([
558
628
  sys.executable, str(intelligence), "capabilities", "select",
559
629
  "--registry", str(registry), "--capability", "runtime.spine",
@@ -582,6 +652,11 @@ def test_intelligence_core_contracts():
582
652
  "--registry", str(tampered_path), "--capability", "runtime.rive",
583
653
  ], capture_output=True, text=True)
584
654
  check("intelligence blocks tampered capability evidence", tampered_select.returncode != 0)
655
+ tampered_card = subprocess.run([
656
+ sys.executable, str(intelligence), "capabilities", "card",
657
+ "--registry", str(tampered_path), "--format", "json",
658
+ ], capture_output=True, text=True)
659
+ check("intelligence blocks capability card with tampered evidence", tampered_card.returncode != 0)
585
660
 
586
661
  replay = subprocess.run([
587
662
  sys.executable, str(intelligence), "replay", "capture",
@@ -952,6 +1027,16 @@ def test_quality_workflow_rebuilds_replay_after_generated_artifacts():
952
1027
  setup_tests.returncode == 0 and "setup onboarding tests: PASS" in setup_tests.stdout,
953
1028
  setup_tests.stdout.strip() or setup_tests.stderr.strip(),
954
1029
  )
1030
+ apple_contract_tests = subprocess.run(
1031
+ [sys.executable, str(ROOT / "tests/scripts/test_apple_contracts.py")],
1032
+ capture_output=True,
1033
+ text=True,
1034
+ )
1035
+ check(
1036
+ "Apple review contracts preserve hash binding and human-governed boundaries",
1037
+ apple_contract_tests.returncode == 0 and "apple contract tests: PASS" in apple_contract_tests.stdout,
1038
+ apple_contract_tests.stdout.strip() or apple_contract_tests.stderr.strip(),
1039
+ )
955
1040
  visual_tests = subprocess.run(
956
1041
  [sys.executable, str(ROOT / "tests/scripts/test_visual_truth.py")],
957
1042
  capture_output=True,