motionloom 2.5.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 +20 -0
- package/README.md +3 -2
- package/SKILL.md +1 -1
- package/agent-card.json +1 -1
- package/agent-surfaces.json +1 -1
- package/bin/motionloom.mjs +5 -4
- package/capability-registry.json +26 -26
- package/dev-lab/public/devlab.js +193 -0
- package/dev-lab/public/index.html +30 -0
- package/docs/STATUS.md +2 -2
- package/docs/releases/2.5.1.md +21 -0
- package/docs/releases/npm-publish-from-workstation.md +6 -6
- package/package.json +10 -4
- package/requirements.txt +1 -0
- package/rig-adapter-registry.json +3 -3
- 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/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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable MotionLoom changes are documented here. The project follows semantic
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.5.1] - 2026-08-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Preserve the caller's working directory through the npm CLI router, so default onboarding targets the consumer project instead of the installed package directory.
|
|
12
|
+
- Ship Dev Lab assets, the runtime harness, Python requirements and optional browser runtime dependencies in the npm tarball; verify the installed package from an isolated consumer project before release.
|
|
13
|
+
- Render Dev Lab metadata and checks with safe DOM APIs, enforce a same-origin script CSP, and reject invalid, terminal or expired browser-review candidates before enabling review.
|
|
14
|
+
- Restrict runtime evidence cleanup to a dedicated child of an explicit policy root and preserve caller data outside that boundary.
|
|
15
|
+
- Read the CLI version from `package.json` so displayed status stays aligned with release metadata.
|
|
16
|
+
- Publish from GitHub Actions through npm Trusted Publishing/OIDC with a pinned compatible npm client, without a long-lived `NPM_TOKEN`; provenance is generated automatically by npm.
|
|
17
|
+
|
|
18
|
+
### Verified
|
|
19
|
+
|
|
20
|
+
- Pass the full Python/Node regression suite, quality and signed-attestation gate, isolated installed-package smoke test, Dev Lab browser-security smoke test, real Rive/GSAP/Framer Motion runtime capture, 6,900-case deep stress run and twelve Swift package tests.
|
|
21
|
+
- Pass the pull-request Quality, Security Analysis, Documentation and Package Hygiene, Dev Lab and Apple workflows before preparing this patch release.
|
|
22
|
+
|
|
23
|
+
### Boundary
|
|
24
|
+
|
|
25
|
+
- This patch does not add framework capability, Apple signing, TestFlight distribution or production approval. Browser review and remote release actions remain explicit human-governed steps.
|
|
26
|
+
|
|
7
27
|
## [2.5.0] - 2026-08-17
|
|
8
28
|
|
|
9
29
|
### Added
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ MotionLoom is an independent open-source Agent Skill for building UI motion, Lot
|
|
|
15
15
|
|
|
16
16
|
> **MotionLoom is not an auto-approval layer.** A valid signature, a passing heuristic, or a successful render proves only the contract it checks. Visual quality, intent, accessibility and PR authorization remain reviewable human decisions.
|
|
17
17
|
|
|
18
|
-
> **Release posture:** MotionLoom 2.5.
|
|
18
|
+
> **Release posture:** MotionLoom 2.5.1 hardens installed-package onboarding, Dev Lab browser review, runtime-output cleanup and consumer-package acceptance. The native macOS/iOS review app remains an unsigned source alpha under `apps/apple/`; the npm package remains the cross-platform Node/Python Skill and documentation surface. Verify npm/GitHub publication metadata separately; passing evidence never implies user approval.
|
|
19
19
|
|
|
20
20
|
## Why MotionLoom
|
|
21
21
|
|
|
@@ -77,7 +77,7 @@ npx --no-install motionloom doctor # check the installed package
|
|
|
77
77
|
npx --no-install motionloom repair --yes # restore only missing managed pieces
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
MotionLoom supports **Node.js 18+** and **Python 3.11+** on Ubuntu, macOS and Windows. `npx` is the recommended first-run surface; after setup, use the project-local binary through `npx --no-install motionloom ...`. A global install remains optional, not required.
|
|
80
|
+
MotionLoom's onboarding and contract CLI supports **Node.js 18+** and **Python 3.11+** on Ubuntu, macOS and Windows. Browser-runtime capture uses the separately declared optional Playwright/Vite dependencies and follows the capability contract's Node.js 22 requirement. Signed-attestation commands additionally require the Python `cryptography` package declared in the shipped `requirements.txt`; `motionloom doctor` reports missing runtime prerequisites. `npx` is the recommended first-run surface; after setup, use the project-local binary through `npx --no-install motionloom ...`. A global install remains optional, not required.
|
|
81
81
|
|
|
82
82
|
### When you begin animation work
|
|
83
83
|
|
|
@@ -324,6 +324,7 @@ Read [the Apple workspace guide](apps/apple/README.md), [the contract boundary](
|
|
|
324
324
|
|
|
325
325
|
```bash
|
|
326
326
|
npm install
|
|
327
|
+
python3 -m pip install --requirement requirements.txt
|
|
327
328
|
python3 scripts/skill-doctor.py --json
|
|
328
329
|
python3 tests/scripts/run_tests.py
|
|
329
330
|
python3 scripts/eval-intelligence.py
|
package/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
validate, review, or deliver animation inside an existing project.
|
|
8
8
|
license: MIT
|
|
9
9
|
metadata:
|
|
10
|
-
version: "2.5.
|
|
10
|
+
version: "2.5.1"
|
|
11
11
|
target_frameworks: "lottie,dotlottie,rive,gsap,framer-motion,spine,threejs"
|
|
12
12
|
verified_runtimes: "lottie-json,dotlottie-package,svg-cutout-rig,rive,gsap,framer-motion"
|
|
13
13
|
---
|
package/agent-card.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"card_version": "1.0",
|
|
3
3
|
"name": "motionloom",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.1",
|
|
5
5
|
"description": "Project-aware animation production with durable Project Memory, provider-neutral Artifact Intake, hash-bound control-to-runtime candidates, tiered asset provenance, framework-neutral motion IR, rig compatibility, runtime verification and telemetry, Visual Truth, Remediation Learning, DSSE-compatible signed attestation, internal browser review, trust-boundary hardening and deterministic Agent handoff.",
|
|
6
6
|
"capabilities": [
|
|
7
7
|
"project.setup",
|
package/agent-surfaces.json
CHANGED
package/bin/motionloom.mjs
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { dirname, resolve } from "node:path";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
11
12
|
|
|
12
13
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const PACKAGE = JSON.parse(readFileSync(resolve(ROOT, "package.json"), "utf8"));
|
|
15
|
+
const VERSION = PACKAGE.version || "unknown";
|
|
13
16
|
const PYTHON = process.env.MOTIONLOOM_PYTHON || (process.platform === "win32" ? "python" : "python3");
|
|
14
17
|
|
|
15
18
|
const PYTHON_COMMANDS = {
|
|
@@ -32,8 +35,6 @@ const PYTHON_COMMANDS = {
|
|
|
32
35
|
pr: "scripts/pr.py",
|
|
33
36
|
"validate-lottie": "scripts/validate-lottie.py",
|
|
34
37
|
manifest: "scripts/manifest.py",
|
|
35
|
-
test: "tests/scripts/run_tests.py",
|
|
36
|
-
"deep-audit": "tests/scripts/deep-stress.py",
|
|
37
38
|
discovery: "scripts/discovery.py",
|
|
38
39
|
"visual-truth": "scripts/visual-truth.py",
|
|
39
40
|
"remediation-learning": "scripts/remediation-learning.py",
|
|
@@ -59,7 +60,7 @@ const COMMAND_ALIASES = {
|
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
function printHelp() {
|
|
62
|
-
console.log(`MotionLoom
|
|
63
|
+
console.log(`MotionLoom ${VERSION} — project-aware animation production and evidence contracts
|
|
63
64
|
|
|
64
65
|
Usage:
|
|
65
66
|
motionloom <command> [args...]
|
|
@@ -147,7 +148,7 @@ const delegatedArgs = alias
|
|
|
147
148
|
? ["init", ...args]
|
|
148
149
|
: args;
|
|
149
150
|
const result = spawnSync(executable, [resolve(ROOT, script), ...delegatedArgs], {
|
|
150
|
-
cwd:
|
|
151
|
+
cwd: process.cwd(),
|
|
151
152
|
stdio: "inherit",
|
|
152
153
|
env: process.env,
|
|
153
154
|
});
|
package/capability-registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "0.1",
|
|
3
|
-
"registry_id": "registry-motionloom-2.5.
|
|
4
|
-
"generated_at": "2026-08-
|
|
3
|
+
"registry_id": "registry-motionloom-2.5.1",
|
|
4
|
+
"generated_at": "2026-08-17T23:50:53Z",
|
|
5
5
|
"selection_policy": {
|
|
6
6
|
"require_verified": true,
|
|
7
7
|
"allow_scaffold_only": false,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"id": "runtime.lottie-json",
|
|
13
13
|
"kind": "renderer",
|
|
14
14
|
"status": "verified",
|
|
15
|
-
"adapter_version": "2.5.
|
|
15
|
+
"adapter_version": "2.5.1",
|
|
16
16
|
"inputs": [
|
|
17
17
|
"project-context",
|
|
18
18
|
"motion-spec",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
],
|
|
31
31
|
"node": ">=22"
|
|
32
32
|
},
|
|
33
|
-
"last_verified_at": "2026-08-
|
|
33
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
34
34
|
"evidence": [
|
|
35
35
|
{
|
|
36
36
|
"path": "scripts/runtime-adapters.mjs",
|
|
37
|
-
"sha256": "
|
|
37
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
38
38
|
"kind": "ci"
|
|
39
39
|
}
|
|
40
40
|
],
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"id": "runtime.dotlottie-package",
|
|
50
50
|
"kind": "packager",
|
|
51
51
|
"status": "verified",
|
|
52
|
-
"adapter_version": "2.5.
|
|
52
|
+
"adapter_version": "2.5.1",
|
|
53
53
|
"inputs": [
|
|
54
54
|
"project-context",
|
|
55
55
|
"motion-spec",
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
],
|
|
68
68
|
"node": ">=22"
|
|
69
69
|
},
|
|
70
|
-
"last_verified_at": "2026-08-
|
|
70
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
71
71
|
"evidence": [
|
|
72
72
|
{
|
|
73
73
|
"path": "scripts/runtime-adapters.mjs",
|
|
74
|
-
"sha256": "
|
|
74
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
75
75
|
"kind": "ci"
|
|
76
76
|
}
|
|
77
77
|
],
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"id": "runtime.svg-cutout-rig",
|
|
87
87
|
"kind": "renderer",
|
|
88
88
|
"status": "verified",
|
|
89
|
-
"adapter_version": "2.5.
|
|
89
|
+
"adapter_version": "2.5.1",
|
|
90
90
|
"inputs": [
|
|
91
91
|
"project-context",
|
|
92
92
|
"motion-spec",
|
|
@@ -104,11 +104,11 @@
|
|
|
104
104
|
],
|
|
105
105
|
"node": ">=22"
|
|
106
106
|
},
|
|
107
|
-
"last_verified_at": "2026-08-
|
|
107
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
108
108
|
"evidence": [
|
|
109
109
|
{
|
|
110
110
|
"path": "scripts/runtime-adapters.mjs",
|
|
111
|
-
"sha256": "
|
|
111
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
112
112
|
"kind": "ci"
|
|
113
113
|
}
|
|
114
114
|
],
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"id": "runtime.rive",
|
|
124
124
|
"kind": "runtime-adapter",
|
|
125
125
|
"status": "verified",
|
|
126
|
-
"adapter_version": "2.5.
|
|
126
|
+
"adapter_version": "2.5.1",
|
|
127
127
|
"inputs": [
|
|
128
128
|
"project-context",
|
|
129
129
|
"motion-spec",
|
|
@@ -141,11 +141,11 @@
|
|
|
141
141
|
],
|
|
142
142
|
"node": ">=22"
|
|
143
143
|
},
|
|
144
|
-
"last_verified_at": "2026-08-
|
|
144
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
145
145
|
"evidence": [
|
|
146
146
|
{
|
|
147
147
|
"path": "scripts/runtime-adapters.mjs",
|
|
148
|
-
"sha256": "
|
|
148
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
149
149
|
"kind": "ci"
|
|
150
150
|
}
|
|
151
151
|
],
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"id": "runtime.gsap",
|
|
161
161
|
"kind": "runtime-adapter",
|
|
162
162
|
"status": "verified",
|
|
163
|
-
"adapter_version": "2.5.
|
|
163
|
+
"adapter_version": "2.5.1",
|
|
164
164
|
"inputs": [
|
|
165
165
|
"project-context",
|
|
166
166
|
"motion-spec",
|
|
@@ -178,11 +178,11 @@
|
|
|
178
178
|
],
|
|
179
179
|
"node": ">=22"
|
|
180
180
|
},
|
|
181
|
-
"last_verified_at": "2026-08-
|
|
181
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
182
182
|
"evidence": [
|
|
183
183
|
{
|
|
184
184
|
"path": "scripts/runtime-adapters.mjs",
|
|
185
|
-
"sha256": "
|
|
185
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
186
186
|
"kind": "ci"
|
|
187
187
|
}
|
|
188
188
|
],
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
"id": "runtime.framer-motion",
|
|
198
198
|
"kind": "runtime-adapter",
|
|
199
199
|
"status": "verified",
|
|
200
|
-
"adapter_version": "2.5.
|
|
200
|
+
"adapter_version": "2.5.1",
|
|
201
201
|
"inputs": [
|
|
202
202
|
"project-context",
|
|
203
203
|
"motion-spec",
|
|
@@ -215,11 +215,11 @@
|
|
|
215
215
|
],
|
|
216
216
|
"node": ">=22"
|
|
217
217
|
},
|
|
218
|
-
"last_verified_at": "2026-08-
|
|
218
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
219
219
|
"evidence": [
|
|
220
220
|
{
|
|
221
221
|
"path": "scripts/runtime-adapters.mjs",
|
|
222
|
-
"sha256": "
|
|
222
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
223
223
|
"kind": "ci"
|
|
224
224
|
}
|
|
225
225
|
],
|
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
"id": "runtime.spine",
|
|
235
235
|
"kind": "renderer",
|
|
236
236
|
"status": "scaffold_only",
|
|
237
|
-
"adapter_version": "2.5.
|
|
237
|
+
"adapter_version": "2.5.1",
|
|
238
238
|
"inputs": [
|
|
239
239
|
"project-context",
|
|
240
240
|
"motion-spec",
|
|
@@ -252,11 +252,11 @@
|
|
|
252
252
|
],
|
|
253
253
|
"node": ">=22"
|
|
254
254
|
},
|
|
255
|
-
"last_verified_at": "2026-08-
|
|
255
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
256
256
|
"evidence": [
|
|
257
257
|
{
|
|
258
258
|
"path": "scripts/runtime-adapters.mjs",
|
|
259
|
-
"sha256": "
|
|
259
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
260
260
|
"kind": "ci"
|
|
261
261
|
}
|
|
262
262
|
],
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
"id": "runtime.threejs",
|
|
272
272
|
"kind": "renderer",
|
|
273
273
|
"status": "scaffold_only",
|
|
274
|
-
"adapter_version": "2.5.
|
|
274
|
+
"adapter_version": "2.5.1",
|
|
275
275
|
"inputs": [
|
|
276
276
|
"project-context",
|
|
277
277
|
"motion-spec",
|
|
@@ -289,11 +289,11 @@
|
|
|
289
289
|
],
|
|
290
290
|
"node": ">=22"
|
|
291
291
|
},
|
|
292
|
-
"last_verified_at": "2026-08-
|
|
292
|
+
"last_verified_at": "2026-08-17T23:50:53Z",
|
|
293
293
|
"evidence": [
|
|
294
294
|
{
|
|
295
295
|
"path": "scripts/runtime-adapters.mjs",
|
|
296
|
-
"sha256": "
|
|
296
|
+
"sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
|
|
297
297
|
"kind": "ci"
|
|
298
298
|
}
|
|
299
299
|
],
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const params = new URLSearchParams(location.search);
|
|
2
|
+
const scene = params.get("scene");
|
|
3
|
+
const taskId = params.get("task_id") || "unbound";
|
|
4
|
+
const candidateId = params.get("candidate_id");
|
|
5
|
+
const base = `/scenes/${encodeURIComponent(scene || "")}`;
|
|
6
|
+
const frames = [0, 50, 100];
|
|
7
|
+
const allowedStatuses = new Set(["prepared", "opened"]);
|
|
8
|
+
const $ = (id) => document.getElementById(id);
|
|
9
|
+
let manifest;
|
|
10
|
+
let spec;
|
|
11
|
+
let candidate;
|
|
12
|
+
let review;
|
|
13
|
+
|
|
14
|
+
function nearest(progress) {
|
|
15
|
+
return frames.reduce(
|
|
16
|
+
(current, frame) => Math.abs(frame - progress) < Math.abs(current - progress) ? frame : current,
|
|
17
|
+
frames[0],
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function showProgress(progress) {
|
|
22
|
+
const bounded = Math.max(0, Math.min(1, progress));
|
|
23
|
+
const percent = bounded * 100;
|
|
24
|
+
const frame = nearest(percent);
|
|
25
|
+
$("scrubber").value = percent;
|
|
26
|
+
$("timecode").textContent = `00:00:${(Number(spec?.duration_s || 0) * bounded).toFixed(3).padStart(6, "0")}`;
|
|
27
|
+
$("frame").src = `${base}/snapshot/frame-${String(frame).padStart(2, "0")}.png`;
|
|
28
|
+
$("frame").hidden = false;
|
|
29
|
+
$("empty").hidden = true;
|
|
30
|
+
window.__lab.lastProgress = bounded;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function currentChecks() {
|
|
34
|
+
return [...document.querySelectorAll("[data-check]")].map((element) => ({
|
|
35
|
+
id: element.dataset.check,
|
|
36
|
+
pass: element.checked,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function currentReview(decision) {
|
|
41
|
+
return {
|
|
42
|
+
review_version: "1.0",
|
|
43
|
+
task_id: taskId,
|
|
44
|
+
candidate_id: candidate.candidate_id,
|
|
45
|
+
scene,
|
|
46
|
+
decision,
|
|
47
|
+
reviewer: "user",
|
|
48
|
+
reviewed_at: new Date().toISOString(),
|
|
49
|
+
checks: currentChecks(),
|
|
50
|
+
notes: $("notes").value,
|
|
51
|
+
frames_inspected: frames,
|
|
52
|
+
spec: {
|
|
53
|
+
framework: spec.framework,
|
|
54
|
+
category: spec.category,
|
|
55
|
+
context_binding: spec.context_binding,
|
|
56
|
+
},
|
|
57
|
+
candidate: {
|
|
58
|
+
source_sha256: candidate.source_sha256,
|
|
59
|
+
context_sha256: candidate.context_sha256,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveLocal(payload) {
|
|
65
|
+
localStorage.setItem(`devlab:${taskId}:${candidate.candidate_id}`, JSON.stringify(payload));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function expose(payload) {
|
|
69
|
+
window.__lab.lastReview = payload;
|
|
70
|
+
window.__lab.getReview = () => window.__lab.lastReview;
|
|
71
|
+
window.__lab.exportReview = () => JSON.stringify(window.__lab.lastReview, null, 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchJson(url) {
|
|
75
|
+
const response = await fetch(url);
|
|
76
|
+
if (!response.ok) throw new Error(`Could not load ${url} (${response.status})`);
|
|
77
|
+
return response.json();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateCandidate() {
|
|
81
|
+
if (candidate.candidate_id !== candidateId) {
|
|
82
|
+
throw new Error("Candidate identity mismatch; refusing to review a different scene");
|
|
83
|
+
}
|
|
84
|
+
if (candidate.task_id !== taskId) {
|
|
85
|
+
throw new Error("Task identity mismatch; refusing unbound review");
|
|
86
|
+
}
|
|
87
|
+
if (!allowedStatuses.has(candidate.status)) {
|
|
88
|
+
throw new Error(`Candidate status is not reviewable: ${candidate.status || "missing"}`);
|
|
89
|
+
}
|
|
90
|
+
const expiresAt = Date.parse(candidate.expires_at);
|
|
91
|
+
if (!Number.isFinite(expiresAt)) {
|
|
92
|
+
throw new Error("Candidate expiry is missing or invalid");
|
|
93
|
+
}
|
|
94
|
+
if (Date.now() > expiresAt) {
|
|
95
|
+
throw new Error("Candidate has expired; prepare a new browser-review URL");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderMetadata() {
|
|
100
|
+
const values = [
|
|
101
|
+
["TASK", taskId],
|
|
102
|
+
["CANDIDATE", candidateId],
|
|
103
|
+
["FRAMEWORK", spec.framework],
|
|
104
|
+
["DURATION", `${spec.duration_s}s`],
|
|
105
|
+
["FPS", spec.fps],
|
|
106
|
+
["EASING", spec.easing],
|
|
107
|
+
["PRIMARY", spec.theme?.primary || "unbound"],
|
|
108
|
+
];
|
|
109
|
+
const nodes = values.map(([key, value]) => {
|
|
110
|
+
const pill = document.createElement("div");
|
|
111
|
+
pill.className = "pill";
|
|
112
|
+
const label = document.createElement("b");
|
|
113
|
+
label.textContent = key;
|
|
114
|
+
pill.append(label, document.createTextNode(` ${value ?? "—"}`));
|
|
115
|
+
return pill;
|
|
116
|
+
});
|
|
117
|
+
$("meta").replaceChildren(...nodes);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderChecks() {
|
|
121
|
+
const nodes = (manifest.checks || []).map((check) => {
|
|
122
|
+
const label = document.createElement("label");
|
|
123
|
+
label.className = "check";
|
|
124
|
+
const input = document.createElement("input");
|
|
125
|
+
input.type = "checkbox";
|
|
126
|
+
input.dataset.check = String(check.id || "");
|
|
127
|
+
input.checked = Boolean(review.checks.find((item) => item.id === check.id)?.pass);
|
|
128
|
+
const copy = document.createElement("span");
|
|
129
|
+
const title = document.createElement("b");
|
|
130
|
+
const detail = document.createElement("small");
|
|
131
|
+
title.textContent = check.label || check.id || "Unnamed check";
|
|
132
|
+
detail.textContent = check.detail || "";
|
|
133
|
+
copy.append(title, detail);
|
|
134
|
+
label.append(input, copy);
|
|
135
|
+
return label;
|
|
136
|
+
});
|
|
137
|
+
$("checks").replaceChildren(...nodes);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function confirmReview() {
|
|
141
|
+
const checks = currentChecks();
|
|
142
|
+
const approved = checks.length > 0 && checks.every((check) => check.pass);
|
|
143
|
+
const payload = currentReview(approved ? "approved" : "changes_requested");
|
|
144
|
+
saveLocal(payload);
|
|
145
|
+
expose(payload);
|
|
146
|
+
$("status").className = approved ? "ok" : "warn";
|
|
147
|
+
$("status").textContent = approved
|
|
148
|
+
? "BROWSER REVIEW APPROVED · browser Agent must persist review.json before PR"
|
|
149
|
+
: "CHANGES REQUESTED · browser Agent must persist notes and return to generation";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
window.__lab = { seek: showProgress, ready: false, taskId, candidateId, reviewRequired: true };
|
|
153
|
+
|
|
154
|
+
async function load() {
|
|
155
|
+
if (!scene || !candidateId) {
|
|
156
|
+
throw new Error("Missing scene or candidate_id; open the emitted browser-review URL");
|
|
157
|
+
}
|
|
158
|
+
[manifest, spec, candidate] = await Promise.all([
|
|
159
|
+
fetchJson(`${base}/manifest.json`),
|
|
160
|
+
fetchJson(`${base}/motion-spec.json`),
|
|
161
|
+
fetchJson(`${base}/browser-review.json`),
|
|
162
|
+
]);
|
|
163
|
+
validateCandidate();
|
|
164
|
+
|
|
165
|
+
$("title").textContent = `${manifest.name || scene} · candidate ${candidateId}`;
|
|
166
|
+
$("description").textContent = `${manifest.description || "Evidence review for this animation scene."} Review candidate ${candidateId} is bound to task ${taskId}.`;
|
|
167
|
+
$("stage-label").textContent = `${scene.toUpperCase()} · ${spec.category || "animation"} · CANDIDATE`;
|
|
168
|
+
$("stage-mode").textContent = `${spec.framework || "unknown"} · ${candidate.status} · runtime evidence`;
|
|
169
|
+
renderMetadata();
|
|
170
|
+
|
|
171
|
+
const saved = JSON.parse(localStorage.getItem(`devlab:${taskId}:${candidateId}`) || "null");
|
|
172
|
+
review = saved || {
|
|
173
|
+
checks: (manifest.checks || []).map((check) => ({ id: check.id, pass: false })),
|
|
174
|
+
notes: "",
|
|
175
|
+
decision: "pending",
|
|
176
|
+
};
|
|
177
|
+
renderChecks();
|
|
178
|
+
$("notes").value = review.notes || "";
|
|
179
|
+
$("scrubber").addEventListener("input", (event) => showProgress(Number(event.target.value) / 100));
|
|
180
|
+
$("confirm").addEventListener("click", confirmReview);
|
|
181
|
+
$("reset").addEventListener("click", () => {
|
|
182
|
+
localStorage.removeItem(`devlab:${taskId}:${candidateId}`);
|
|
183
|
+
location.reload();
|
|
184
|
+
});
|
|
185
|
+
window.__lab.ready = true;
|
|
186
|
+
showProgress(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
load().catch((error) => {
|
|
190
|
+
$("status").className = "warn";
|
|
191
|
+
$("status").textContent = `DEV LAB ERROR: ${error.message}`;
|
|
192
|
+
$("empty").textContent = "Unable to load the exact candidate evidence.";
|
|
193
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
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" />
|
|
6
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'" />
|
|
7
|
+
<title>Animation Dev Lab</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root { color-scheme: dark; --bg:#111218; --panel:#1b1d25; --line:#303440; --text:#e7e9ee; --muted:#8f96a6; --amber:#f59e0b; --good:#46d18c; --bad:#ff6b6b; }
|
|
10
|
+
* { box-sizing:border-box; } body { margin:0; background:radial-gradient(circle at 40% -10%,#2a2630 0,#111218 40%); color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,sans-serif; }
|
|
11
|
+
header { height:58px; display:flex; align-items:center; justify-content:space-between; padding:0 22px; border-bottom:1px solid var(--line); background:#15171e; }
|
|
12
|
+
header strong { letter-spacing:.12em; text-transform:uppercase; font-size:12px; } header span { color:var(--amber); font-family:ui-monospace,monospace; }
|
|
13
|
+
main { display:grid; grid-template-columns:minmax(0,1fr) 330px; min-height:calc(100vh - 58px); } .workbench { padding:24px; min-width:0; }
|
|
14
|
+
.stage { min-height:430px; display:flex; align-items:center; justify-content:center; border:1px solid #3b3f4c; background:repeating-conic-gradient(#181a22 0 25%,#15171e 0 50%) 50%/32px 32px; position:relative; overflow:hidden; }
|
|
15
|
+
.stage img { max-width:92%; max-height:390px; object-fit:contain; image-rendering:auto; filter:drop-shadow(0 18px 32px #0009); } .stage .empty { color:var(--muted); text-align:center; }
|
|
16
|
+
.stage-label { position:absolute; left:14px; top:12px; color:var(--muted); font:11px ui-monospace,monospace; letter-spacing:.08em; } .stage-mode { position:absolute; right:14px; top:12px; color:var(--amber); font:11px ui-monospace,monospace; }
|
|
17
|
+
.timeline { padding:18px 0 0; } input[type=range] { width:100%; accent-color:var(--amber); } .ticks { display:flex; justify-content:space-between; color:var(--muted); font:11px ui-monospace,monospace; }
|
|
18
|
+
.meta-row { display:flex; gap:10px; flex-wrap:wrap; margin-top:18px; } .pill { border:1px solid var(--line); background:#191b22; padding:8px 10px; color:var(--muted); } .pill b { color:var(--text); font-weight:600; }
|
|
19
|
+
aside { border-left:1px solid var(--line); background:#171920; padding:20px; } h1 { font-size:24px; margin:0 0 6px; } h2 { font-size:13px; letter-spacing:.09em; text-transform:uppercase; color:var(--muted); margin:24px 0 10px; }
|
|
20
|
+
p { color:var(--muted); margin:0; } .check { display:flex; gap:10px; padding:9px 0; border-bottom:1px solid #272a34; } .check input { accent-color:var(--good); } .check small { color:var(--muted); display:block; } textarea { width:100%; min-height:90px; resize:vertical; color:var(--text); background:#111218; border:1px solid var(--line); padding:10px; font:13px ui-monospace,monospace; }
|
|
21
|
+
button { border:0; padding:11px 14px; color:#17120b; background:var(--amber); font-weight:750; cursor:pointer; } button:hover { filter:brightness(1.08); } button:active { transform:scale(.98); } button.secondary { color:var(--text); background:#2a2d38; margin-left:8px; } .actions { margin-top:14px; } #status { margin-top:10px; font:12px ui-monospace,monospace; } .ok { color:var(--good); } .warn { color:var(--bad); }
|
|
22
|
+
@media (max-width:800px) { main { grid-template-columns:1fr; } aside { border-left:0; border-top:1px solid var(--line); } .stage { min-height:330px; } }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<header><strong>Animation Dev Lab</strong><span id="timecode">00:00:00.000</span></header>
|
|
27
|
+
<main><section class="workbench"><div class="stage" id="stage" data-snapshot><div class="stage-label" id="stage-label">LOADING SCENE</div><div class="stage-mode" id="stage-mode"></div><div class="empty" id="empty">Loading evidence…</div><img id="frame" alt="Rendered animation snapshot" hidden /></div><div class="timeline"><input id="scrubber" type="range" min="0" max="100" value="0" aria-label="Animation progress" /><div class="ticks"><span>00%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span></div></div><div class="meta-row" id="meta"></div></section><aside><h1 id="title">Scene</h1><p id="description">Loading manifest and signed motion spec.</p><h2>Quality checklist</h2><div id="checks"></div><h2>Fix notes</h2><textarea id="notes" placeholder="Describe a fix request or confirmation for the PR…"></textarea><div class="actions"><button id="confirm">Confirm review</button><button class="secondary" id="reset">Reset</button></div><div id="status"></div></aside></main>
|
|
28
|
+
<script src="/devlab.js" defer></script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
package/docs/STATUS.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Current release posture
|
|
6
6
|
|
|
7
|
-
MotionLoom's repository source and package manifest
|
|
7
|
+
MotionLoom's repository source and package manifest carry **2.5.1 release metadata**. The publication state of the npm registry and GitHub Release/tag must be verified against the latest release workflow rather than inferred from this document. The repository has a cross-platform CLI, one-command project onboarding, durable Project Memory, Agent interoperability surfaces, artifact-first handoff, runtime evidence, Visual Truth, Remediation Learning, signed attestation, AI-first asset provenance, a truthful code-authored runtime lane and a browser-based Dev Lab review contract. The protected manual release workflow verifies package/changelog/release-note alignment before publication.
|
|
8
8
|
|
|
9
9
|
The repository is **engineering-ready for continued integration work**, not a universal production certification for every animation framework or every host project. CI status must be read from the latest GitHub Actions run, not inferred from historical audit prose.
|
|
10
10
|
|
|
@@ -30,4 +30,4 @@ Use the following order when sources disagree:
|
|
|
30
30
|
|
|
31
31
|
## Known next work
|
|
32
32
|
|
|
33
|
-
The bounded analyzer has now been exercised against a labeled external corpus; see the [dated evidence note](audits/external-project-corpus-2026-08-13.md). The highest-value remaining evidence work is paired product evaluation across more real projects and
|
|
33
|
+
The bounded analyzer has now been exercised against a labeled external corpus; see the [dated evidence note](audits/external-project-corpus-2026-08-13.md). The 2.5.0 version–tag–GitHub Release–npm chain has been verified against public metadata. The highest-value remaining evidence work is paired product evaluation across more real projects and repeating the same provenance verification for each new release. Missing external projects are reported as `insufficient_evidence`, never as a pass. None of these items should weaken the user-review gate or convert heuristic warnings into approval.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MotionLoom 2.5.1
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
MotionLoom 2.5.1 is a hardening patch for the installed npm package, browser review boundary and runtime-output policy. It preserves the 2.5.0 capability surface while making the advertised onboarding, Dev Lab and runtime evidence paths work safely from an isolated consumer project.
|
|
6
|
+
|
|
7
|
+
## Fixed
|
|
8
|
+
|
|
9
|
+
- The npm CLI preserves the caller's working directory, so default setup and onboarding target the consumer project instead of the installed package directory. The displayed CLI version is read from package metadata.
|
|
10
|
+
- The npm tarball includes the Dev Lab assets, runtime harness, Python requirements and optional browser-runtime dependencies required by its documented commands. Release validation installs the packed tarball into an isolated consumer project.
|
|
11
|
+
- Dev Lab renders manifest values through safe DOM APIs, enforces a same-origin script content-security policy and rejects invalid, expired or terminal browser-review candidates before review can be enabled.
|
|
12
|
+
- Runtime evidence cleanup is constrained to a dedicated child directory below an explicit policy root and cannot remove caller data outside that boundary.
|
|
13
|
+
- The protected GitHub Actions release uses npm Trusted Publishing/OIDC with a compatible pinned npm client instead of a long-lived publish token. npm generates public-package provenance automatically for the trusted workflow.
|
|
14
|
+
|
|
15
|
+
## Verified behavior
|
|
16
|
+
|
|
17
|
+
The release candidate passes the full regression harness, quality and signed-attestation gate, isolated package-consumer smoke test, Dev Lab browser-security smoke test, real Rive/GSAP/Framer Motion runtime capture at 0/50/100%, a 6,900-case deep stress run, twelve Swift package tests, Skill Doctor, documentation audit, discovery, release verification and package inspection. The pull request carrying the implementation passed all twelve GitHub checks before merge.
|
|
18
|
+
|
|
19
|
+
## Trust boundary
|
|
20
|
+
|
|
21
|
+
This patch does not add a framework capability, sign or distribute the Apple alpha, grant production approval or authorize a pull request or release from machine-generated evidence. Browser review remains identity-bound and human-governed. npm publication and GitHub Release creation remain explicit maintainer actions after the prepared commit is green on `main`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Publish MotionLoom to npm from your workstation
|
|
2
2
|
|
|
3
|
-
This guide publishes the prepared `motionloom@2.5.
|
|
3
|
+
This guide publishes the prepared `motionloom@2.5.1` package from your own computer. The npm password, authenticator code and access token must stay on your computer; never paste them into chat, GitHub issues or repository files.
|
|
4
4
|
|
|
5
5
|
## 1. Install prerequisites
|
|
6
6
|
|
|
@@ -48,7 +48,7 @@ npm pack --dry-run --json --ignore-scripts
|
|
|
48
48
|
npm publish --dry-run --access public
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
The dry-run should report `motionloom@2.5.
|
|
51
|
+
The dry-run should report `motionloom@2.5.1`, public access, and the current package file list. Do not hard-code a historical file count: inspect the JSON output and compare it with the checked-in package allowlist. The prepack hook removes generated Python bytecode before packaging. Do not publish if the dry-run shows private keys, `.env` files, `artifacts/`, `dev-lab/` or `__pycache__/` entries.
|
|
52
52
|
|
|
53
53
|
## 5. Publish the package
|
|
54
54
|
|
|
@@ -60,15 +60,15 @@ npm publish --access public
|
|
|
60
60
|
|
|
61
61
|
Do not add `--provenance` unless the local npm client and its CI/provider integration explicitly support automatic npm provenance. Some workstation setups report `Automatic provenance generation not supported for provider: null`; in that case the package itself is still valid, and the supported fallback for this release is the command above without `--provenance`. Never retry by changing the version or by bypassing the package verification steps.
|
|
62
62
|
|
|
63
|
-
The version `2.5.
|
|
63
|
+
The version `2.5.1` becomes immutable on npm after a successful publish. If npm reports that the version already exists, stop and verify the registry instead of trying to overwrite it.
|
|
64
64
|
|
|
65
65
|
## 6. Verify the registry publication
|
|
66
66
|
|
|
67
|
-
Run the following commands and confirm that they return `2.5.
|
|
67
|
+
Run the following commands and confirm that they return `2.5.1` and a tarball URL:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
npm view motionloom version --registry=https://registry.npmjs.org/
|
|
71
|
-
npm view motionloom@2.5.
|
|
71
|
+
npm view motionloom@2.5.1 name version license dist.tarball dist.shasum --json \
|
|
72
72
|
--registry=https://registry.npmjs.org/
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -78,7 +78,7 @@ Then test installation in a clean temporary directory:
|
|
|
78
78
|
TMP_DIR="$(mktemp -d)"
|
|
79
79
|
cd "$TMP_DIR"
|
|
80
80
|
npm init --yes
|
|
81
|
-
npm install motionloom@2.5.
|
|
81
|
+
npm install motionloom@2.5.1
|
|
82
82
|
motionloom --help
|
|
83
83
|
motionloom doctor
|
|
84
84
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "motionloom",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@11.20.0",
|
|
6
6
|
"description": "MotionLoom: project-aware animation Skill with Agent interoperability, graph, provenance, Motion IR, runtime telemetry, Visual Truth, Remediation Learning, DSSE-compatible signed attestation, external evidence verification and Dev Lab review.",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"schemas/asset-provenance.schema.json",
|
|
34
34
|
"examples/agent-consumer/ai-generated-pilot-provenance.json",
|
|
35
35
|
"project-context.example.json",
|
|
36
|
+
"requirements.txt",
|
|
37
|
+
"dev-lab/public/index.html",
|
|
38
|
+
"dev-lab/public/devlab.js",
|
|
39
|
+
"tests/runtime-harness",
|
|
36
40
|
"tests/evals",
|
|
37
41
|
"tests/scripts/run_tests.py",
|
|
38
42
|
"tests/scripts/test_attestation.py",
|
|
@@ -124,7 +128,7 @@
|
|
|
124
128
|
},
|
|
125
129
|
"scripts": {
|
|
126
130
|
"prepack": "node scripts/prepack-clean.mjs",
|
|
127
|
-
"test": "
|
|
131
|
+
"test": "python3 tests/scripts/run_tests.py",
|
|
128
132
|
"validate": "npm run quality:attestation",
|
|
129
133
|
"doctor": "node bin/motionloom.mjs doctor --json",
|
|
130
134
|
"init": "node bin/motionloom.mjs init",
|
|
@@ -147,7 +151,8 @@
|
|
|
147
151
|
"intelligence:p1": "node bin/motionloom.mjs intelligence",
|
|
148
152
|
"quality:p1": "node bin/motionloom.mjs quality-gate --require-intelligence --require-p1 --require-benchmark",
|
|
149
153
|
"eval:intelligence": "node bin/motionloom.mjs eval-intelligence",
|
|
150
|
-
"audit:deep": "
|
|
154
|
+
"audit:deep": "python3 tests/scripts/deep-stress.py --iterations 5000 --seed 20260813 --report docs/audits/data/deep-stress-latest.json",
|
|
155
|
+
"package:smoke": "node scripts/package-consumer-smoke.mjs",
|
|
151
156
|
"memory:init": "node bin/motionloom.mjs memory init --project-root .",
|
|
152
157
|
"memory:bootstrap": "node bin/motionloom.mjs analyze . --init-memory",
|
|
153
158
|
"memory:recover": "node bin/motionloom.mjs memory recover --project-root .",
|
|
@@ -191,9 +196,10 @@
|
|
|
191
196
|
"react": "^19.2.8",
|
|
192
197
|
"react-dom": "^19.2.8"
|
|
193
198
|
},
|
|
194
|
-
"
|
|
199
|
+
"optionalDependencies": {
|
|
195
200
|
"playwright": "^1.62.1",
|
|
196
201
|
"vite": "^8.2.1"
|
|
197
202
|
},
|
|
203
|
+
"devDependencies": {},
|
|
198
204
|
"license": "MIT"
|
|
199
205
|
}
|
package/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography>=42.0,<47.0
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"target": "rive",
|
|
17
17
|
"framework": "rive",
|
|
18
18
|
"status": "project_integrated",
|
|
19
|
-
"evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "
|
|
19
|
+
"evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d"}],
|
|
20
20
|
"limitations": ["Runtime harness evidence must be captured for the concrete scene and source bytes."]
|
|
21
21
|
},
|
|
22
22
|
{
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"target": "gsap",
|
|
25
25
|
"framework": "gsap",
|
|
26
26
|
"status": "project_integrated",
|
|
27
|
-
"evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "
|
|
27
|
+
"evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d"}],
|
|
28
28
|
"limitations": ["Runtime harness evidence must be captured for the concrete scene and source bytes."]
|
|
29
29
|
},
|
|
30
30
|
{
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"target": "framer-motion",
|
|
33
33
|
"framework": "framer-motion",
|
|
34
34
|
"status": "project_integrated",
|
|
35
|
-
"evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "
|
|
35
|
+
"evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d"}],
|
|
36
36
|
"limitations": ["Runtime harness evidence must be captured for the concrete scene and source bytes."]
|
|
37
37
|
}
|
|
38
38
|
]
|
|
@@ -47,9 +47,15 @@ def main() -> int:
|
|
|
47
47
|
print("capture-runtime-telemetry: --max-age-days must be non-negative", file=sys.stderr)
|
|
48
48
|
return 2
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
package_root = Path(__file__).resolve().parents[1]
|
|
51
|
+
root = Path(os.environ.get("MOTIONLOOM_PROJECT_ROOT") or Path.cwd()).expanduser().resolve()
|
|
51
52
|
task_dir = Path(args.task_dir).expanduser()
|
|
52
53
|
task_dir = task_dir.resolve() if task_dir.is_absolute() else (root / task_dir).resolve()
|
|
54
|
+
try:
|
|
55
|
+
task_dir.relative_to(root)
|
|
56
|
+
except ValueError:
|
|
57
|
+
print("capture-runtime-telemetry: task directory must remain inside the project root", file=sys.stderr)
|
|
58
|
+
return 2
|
|
53
59
|
scene_dir = (root / "src" / "output" / args.scene).resolve()
|
|
54
60
|
manifest_path = scene_dir / "manifest.json"
|
|
55
61
|
task_path = task_dir / "task.json"
|
|
@@ -74,10 +80,6 @@ def main() -> int:
|
|
|
74
80
|
return 2
|
|
75
81
|
|
|
76
82
|
output_dir = task_dir / "runtime-adapters"
|
|
77
|
-
if output_dir.exists():
|
|
78
|
-
shutil.rmtree(output_dir)
|
|
79
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
-
|
|
81
83
|
env = os.environ.copy()
|
|
82
84
|
env.update(
|
|
83
85
|
{
|
|
@@ -87,17 +89,23 @@ def main() -> int:
|
|
|
87
89
|
"RUNTIME_SOURCE_PATH": str(source_path),
|
|
88
90
|
"RUNTIME_MANIFEST_PATH": str(manifest_path),
|
|
89
91
|
"RUNTIME_MOTION_IR_PATH": str(task_dir / "motion-ir.json"),
|
|
92
|
+
"MOTIONLOOM_RUNTIME_OUTPUT_ROOT": str(root),
|
|
90
93
|
}
|
|
91
94
|
)
|
|
92
|
-
|
|
93
|
-
runtime = subprocess.run(
|
|
95
|
+
node = shutil.which("node") or ("node.exe" if os.name == "nt" else "node")
|
|
96
|
+
runtime = subprocess.run(
|
|
97
|
+
[node, str(package_root / "scripts" / "runtime-adapters.mjs")],
|
|
98
|
+
cwd=root,
|
|
99
|
+
env=env,
|
|
100
|
+
check=False,
|
|
101
|
+
)
|
|
94
102
|
if runtime.returncode != 0:
|
|
95
103
|
return runtime.returncode
|
|
96
104
|
|
|
97
105
|
verifier = subprocess.run(
|
|
98
106
|
[
|
|
99
107
|
sys.executable,
|
|
100
|
-
str(
|
|
108
|
+
str(package_root / "scripts" / "evidence-verifier.py"),
|
|
101
109
|
"--scene-dir",
|
|
102
110
|
str(scene_dir),
|
|
103
111
|
"--task-dir",
|
package/scripts/devlab.py
CHANGED
|
@@ -87,17 +87,9 @@ def prepare_scene(root: Path, lab: Path, scene: str, task_dir: Path | None) -> t
|
|
|
87
87
|
return destination, task_destination, task_id
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def pnpm_executable() -> str:
|
|
91
|
-
candidate = "pnpm.cmd" if os.name == "nt" else "pnpm"
|
|
92
|
-
return shutil.which(candidate) or candidate
|
|
93
|
-
|
|
94
|
-
|
|
95
90
|
def serve(lab: Path, port: int) -> int:
|
|
96
91
|
if not (lab / "public").is_dir():
|
|
97
92
|
fail(f"Dev Lab public directory not found: {lab / 'public'}")
|
|
98
|
-
if not (lab / "node_modules").is_dir():
|
|
99
|
-
print("== installing Dev Lab dependencies (first run) ==")
|
|
100
|
-
subprocess.run([pnpm_executable(), "install", "--silent"], cwd=lab, check=True)
|
|
101
93
|
print(f"== Dev Lab ready: http://localhost:{port}/ ==")
|
|
102
94
|
return subprocess.run([sys.executable, "-m", "http.server", str(port), "--directory", str(lab / "public")], check=False).returncode
|
|
103
95
|
|
|
@@ -110,8 +102,9 @@ def main() -> int:
|
|
|
110
102
|
parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "3300")))
|
|
111
103
|
parser.add_argument("--prepare-only", action="store_true")
|
|
112
104
|
args = parser.parse_args()
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
package_root = Path(__file__).resolve().parents[1]
|
|
106
|
+
root = Path(os.environ.get("MOTIONLOOM_PROJECT_ROOT") or Path.cwd()).expanduser().resolve()
|
|
107
|
+
lab = Path(args.lab_dir or os.environ.get("MOTIONLOOM_DEV_LAB") or package_root / "dev-lab").expanduser().resolve()
|
|
115
108
|
task_dir = Path(args.task_dir).expanduser().resolve() if args.task_dir else None
|
|
116
109
|
destination, task_destination, task_id = prepare_scene(root, lab, args.scene, task_dir)
|
|
117
110
|
print(f"== Dev Lab scene prepared: {destination} ==")
|
package/scripts/docs-audit.py
CHANGED
|
@@ -93,9 +93,21 @@ if "--require-asset-provenance" not in (workflow_dir / "quality.yml").read_text(
|
|
|
93
93
|
errors.append(f"{workflow.relative_to(ROOT)}: secrets referenced in pull_request workflow")
|
|
94
94
|
|
|
95
95
|
release = (workflow_dir / "release.yml").read_text(encoding="utf-8")
|
|
96
|
-
for required in [
|
|
96
|
+
for required in [
|
|
97
|
+
"workflow_dispatch:",
|
|
98
|
+
"environment: npm-release",
|
|
99
|
+
"id-token: write",
|
|
100
|
+
"release_version:",
|
|
101
|
+
"scripts/release-verify.py",
|
|
102
|
+
'node-version: "24"',
|
|
103
|
+
"npm@11.19.0",
|
|
104
|
+
'npm publish --access public --tag "$NPM_TAG"',
|
|
105
|
+
]:
|
|
97
106
|
if required not in release:
|
|
98
107
|
errors.append(f"release.yml: missing release safety control {required}")
|
|
108
|
+
for forbidden in ["NODE_AUTH_TOKEN", "secrets.NPM_TOKEN", "--provenance"]:
|
|
109
|
+
if forbidden in release:
|
|
110
|
+
errors.append(f"release.yml: token-based or redundant trusted-publishing control remains: {forbidden}")
|
|
99
111
|
|
|
100
112
|
devlab = (workflow_dir / "devlab.yml").read_text(encoding="utf-8")
|
|
101
113
|
for required in [
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const expectedVersion = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version;
|
|
11
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "motionloom-consumer-"));
|
|
12
|
+
const consumer = path.join(temporary, "consumer");
|
|
13
|
+
fs.mkdirSync(consumer);
|
|
14
|
+
fs.writeFileSync(
|
|
15
|
+
path.join(consumer, "package.json"),
|
|
16
|
+
`${JSON.stringify({ name: "motionloom-package-smoke", private: true }, null, 2)}\n`,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
function run(command, args, options = {}) {
|
|
20
|
+
const result = spawnSync(command, args, {
|
|
21
|
+
cwd: options.cwd || root,
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
env: process.env,
|
|
24
|
+
...options,
|
|
25
|
+
});
|
|
26
|
+
if (result.status !== 0) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`${command} ${args.join(" ")} failed (${result.status})\n${result.stdout || ""}\n${result.stderr || ""}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return result.stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const packOutput = JSON.parse(run("npm", ["pack", "--json", "--pack-destination", temporary]));
|
|
36
|
+
const tarball = path.join(temporary, packOutput[0].filename);
|
|
37
|
+
run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tarball], { cwd: consumer });
|
|
38
|
+
|
|
39
|
+
const bin = process.platform === "win32"
|
|
40
|
+
? path.join(consumer, "node_modules", ".bin", "motionloom.cmd")
|
|
41
|
+
: path.join(consumer, "node_modules", ".bin", "motionloom");
|
|
42
|
+
const installedRoot = path.dirname(fs.realpathSync(path.join(consumer, "node_modules", "motionloom", "package.json")));
|
|
43
|
+
const help = run(bin, ["--help"], { cwd: consumer });
|
|
44
|
+
const init = JSON.parse(run(bin, ["init", "--dry-run", "--json"], { cwd: consumer }));
|
|
45
|
+
const doctor = JSON.parse(run(bin, ["doctor", "--json"], { cwd: consumer }));
|
|
46
|
+
|
|
47
|
+
if (!help.startsWith(`MotionLoom ${expectedVersion}`)) throw new Error(`unexpected help version: ${help.split("\n")[0]}`);
|
|
48
|
+
if (fs.realpathSync(init.project_root || ".") !== fs.realpathSync(consumer)) {
|
|
49
|
+
throw new Error(`installed CLI used the wrong project_root: ${init.project_root}`);
|
|
50
|
+
}
|
|
51
|
+
if (doctor.status !== "pass") throw new Error(`installed doctor failed: ${JSON.stringify(doctor.errors)}`);
|
|
52
|
+
for (const relative of [
|
|
53
|
+
"requirements.txt",
|
|
54
|
+
"dev-lab/public/index.html",
|
|
55
|
+
"dev-lab/public/devlab.js",
|
|
56
|
+
"tests/runtime-harness/index.html",
|
|
57
|
+
]) {
|
|
58
|
+
if (!fs.existsSync(path.join(installedRoot, relative))) throw new Error(`tarball omitted ${relative}`);
|
|
59
|
+
}
|
|
60
|
+
const scene = "browser-review-smoke";
|
|
61
|
+
const consumerScene = path.join(consumer, "src", "output", scene);
|
|
62
|
+
fs.mkdirSync(path.dirname(consumerScene), { recursive: true });
|
|
63
|
+
fs.cpSync(path.join(installedRoot, "src", "output", scene), consumerScene, { recursive: true });
|
|
64
|
+
const candidatePath = path.join(consumerScene, "browser-review.json");
|
|
65
|
+
const candidate = JSON.parse(fs.readFileSync(candidatePath, "utf8"));
|
|
66
|
+
candidate.expires_at = "2099-01-01T00:00:00Z";
|
|
67
|
+
fs.writeFileSync(candidatePath, `${JSON.stringify(candidate, null, 2)}\n`);
|
|
68
|
+
run(bin, ["devlab", scene, "--prepare-only"], { cwd: consumer });
|
|
69
|
+
if (!fs.existsSync(path.join(installedRoot, "dev-lab", "public", "scenes", scene, "browser-review.json"))) {
|
|
70
|
+
throw new Error("installed Dev Lab did not prepare the consumer scene");
|
|
71
|
+
}
|
|
72
|
+
run(process.execPath, ["--input-type=module", "-e", "await import('playwright'); await import('vite');"], { cwd: consumer });
|
|
73
|
+
console.log(JSON.stringify({ status: "pass", project_root: init.project_root, installed_root: installedRoot }, null, 2));
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
76
|
+
}
|
|
@@ -6,9 +6,11 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
7
|
import { chromium } from "playwright";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
10
|
-
const ROOT = path.resolve(
|
|
11
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
12
|
const outputRoot = path.resolve(process.env.RUNTIME_EVIDENCE_DIR || path.join(ROOT, "artifacts/runtime-adapters"));
|
|
13
|
+
const outputPolicyRoot = path.resolve(process.env.MOTIONLOOM_RUNTIME_OUTPUT_ROOT || ROOT);
|
|
12
14
|
const port = Number(process.env.RUNTIME_HARNESS_PORT || 4179);
|
|
13
15
|
const supportedFrameworks = new Set(["rive", "gsap", "framer-motion"]);
|
|
14
16
|
const frameworks = (process.env.RUNTIME_FRAMEWORKS || "rive,gsap,framer-motion")
|
|
@@ -17,8 +19,31 @@ const unsupported = frameworks.filter((name) => !supportedFrameworks.has(name));
|
|
|
17
19
|
if (unsupported.length) {
|
|
18
20
|
throw new Error(`unsupported runtime framework(s): ${unsupported.join(", ")}`);
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
function canonicalPath(target) {
|
|
23
|
+
let existing = target;
|
|
24
|
+
const missing = [];
|
|
25
|
+
while (!fs.existsSync(existing)) {
|
|
26
|
+
const parent = path.dirname(existing);
|
|
27
|
+
if (parent === existing) break;
|
|
28
|
+
missing.unshift(path.basename(existing));
|
|
29
|
+
existing = parent;
|
|
30
|
+
}
|
|
31
|
+
const base = fs.existsSync(existing) ? fs.realpathSync(existing) : path.resolve(existing);
|
|
32
|
+
return path.resolve(base, ...missing);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isStrictChild(target, parent) {
|
|
36
|
+
const relative = path.relative(parent, target);
|
|
37
|
+
return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const canonicalOutputRoot = canonicalPath(outputRoot);
|
|
41
|
+
const canonicalPolicyRoot = canonicalPath(outputPolicyRoot);
|
|
42
|
+
if (!isStrictChild(canonicalOutputRoot, canonicalPolicyRoot)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`RUNTIME_EVIDENCE_DIR must be a dedicated child of ${outputPolicyRoot}; ` +
|
|
45
|
+
"set MOTIONLOOM_RUNTIME_OUTPUT_ROOT explicitly to authorize another parent",
|
|
46
|
+
);
|
|
22
47
|
}
|
|
23
48
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
24
49
|
const runId = `${Date.now()}-${process.pid}`;
|
package/scripts/skill-doctor.py
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import importlib.util
|
|
7
8
|
import json
|
|
8
9
|
import re
|
|
9
10
|
import sys
|
|
@@ -11,7 +12,15 @@ from pathlib import Path
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
-
REQUIRED_FILES = [
|
|
15
|
+
REQUIRED_FILES = [
|
|
16
|
+
"SKILL.md",
|
|
17
|
+
"agent-card.json",
|
|
18
|
+
"package.json",
|
|
19
|
+
"requirements.txt",
|
|
20
|
+
"dev-lab/public/index.html",
|
|
21
|
+
"dev-lab/public/devlab.js",
|
|
22
|
+
"tests/runtime-harness/index.html",
|
|
23
|
+
]
|
|
15
24
|
REQUIRED_DIRS = ["scripts", "templates", "references", "schemas"]
|
|
16
25
|
REQUIRED_SCRIPT_FILES = [
|
|
17
26
|
"scripts/report-contract.py",
|
|
@@ -135,9 +144,26 @@ def run() -> int:
|
|
|
135
144
|
for script in ("test", "validate", "doctor", "setup", "setup:dry", "status", "repair", "report", "report:check", "review", "memory:bootstrap", "memory:recover", "memory:validate", "devlab", "pack:dotlottie"):
|
|
136
145
|
if script not in package.get("scripts", {}):
|
|
137
146
|
warnings.append({"code": "missing_package_script", "message": f"package.json has no {script} script."})
|
|
147
|
+
runtime_dependencies = {
|
|
148
|
+
**package.get("dependencies", {}),
|
|
149
|
+
**package.get("optionalDependencies", {}),
|
|
150
|
+
}
|
|
151
|
+
for dependency in ("playwright", "vite"):
|
|
152
|
+
present = dependency in runtime_dependencies
|
|
153
|
+
checks.append({"id": f"runtime-dependency:{dependency}", "status": "pass" if present else "fail"})
|
|
154
|
+
if not present:
|
|
155
|
+
errors.append({"code": "missing_runtime_dependency", "message": f"package.json runtime dependencies omit {dependency}."})
|
|
138
156
|
except (FileNotFoundError, json.JSONDecodeError) as exc:
|
|
139
157
|
errors.append({"code": "invalid_package_json", "message": str(exc)})
|
|
140
158
|
|
|
159
|
+
cryptography_available = importlib.util.find_spec("cryptography") is not None
|
|
160
|
+
checks.append({"id": "python-dependency:cryptography", "status": "pass" if cryptography_available else "fail"})
|
|
161
|
+
if not cryptography_available:
|
|
162
|
+
errors.append({
|
|
163
|
+
"code": "missing_python_dependency",
|
|
164
|
+
"message": "Python package cryptography is required; install dependencies from requirements.txt.",
|
|
165
|
+
})
|
|
166
|
+
|
|
141
167
|
result = {
|
|
142
168
|
"doctor_version": "1.0",
|
|
143
169
|
"skill_root": str(ROOT),
|
|
@@ -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:
|