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.
- package/CHANGELOG.md +41 -0
- package/README.md +22 -2
- package/SKILL.md +1 -1
- package/agent-card.json +4 -1
- package/agent-surfaces.json +1 -1
- package/bin/motionloom.mjs +20 -6
- package/capability-registry.json +308 -0
- package/dev-lab/public/devlab.js +193 -0
- package/dev-lab/public/index.html +30 -0
- package/docs/STATUS.md +2 -2
- package/docs/apple-distribution.md +24 -0
- package/docs/architecture/apple/cloudkit.md +18 -0
- package/docs/architecture/apple/contracts.md +9 -0
- package/docs/architecture/apple/implementation-status-2026-08-16.md +55 -0
- package/docs/architecture/apple/studio.md +35 -0
- package/docs/releases/2.5.0.md +31 -0
- package/docs/releases/2.5.1.md +21 -0
- package/docs/releases/npm-publish-from-workstation.md +6 -6
- package/docs/research/ai-repository-benchmark-2026-08-16.md +75 -0
- package/package.json +12 -4
- package/requirements.txt +1 -0
- package/rig-adapter-registry.json +3 -3
- package/scripts/attestation.py +8 -0
- package/scripts/capture-runtime-telemetry.py +16 -8
- package/scripts/devlab.py +3 -10
- package/scripts/docs-audit.py +13 -1
- package/scripts/evidence-verifier.py +18 -9
- package/scripts/intelligence.py +85 -18
- package/scripts/package-consumer-smoke.mjs +76 -0
- 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 +85 -0
|
@@ -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,
|