motionloom 2.0.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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/SKILL.md +122 -0
  4. package/agent-card.json +161 -0
  5. package/assets/library/ATTRIBUTION.md +6 -0
  6. package/assets/library/README.md +20 -0
  7. package/assets/library/avatar-base.svg +19 -0
  8. package/assets/library/error-alert.json +1 -0
  9. package/assets/library/rive/ATTRIBUTION.md +12 -0
  10. package/assets/library/rive/state-machine-test.riv +0 -0
  11. package/assets/library/success-check.json +1 -0
  12. package/bin/motionloom.mjs +81 -0
  13. package/docs/BROWSER-REVIEW-E2E.md +78 -0
  14. package/docs/CATEGORIES.md +16 -0
  15. package/docs/CHECKLIST.md +31 -0
  16. package/docs/DEEP-AUDIT-WORKING-NOTES.md +22 -0
  17. package/docs/FRAMEWORK-SELECTION.md +26 -0
  18. package/docs/PROJECT-MANIFEST.md +37 -0
  19. package/docs/ROADMAP-INTELLIGENCE.md +224 -0
  20. package/docs/audits/1.10.0-attestation-research-notes.md +19 -0
  21. package/docs/audits/1.8.0-trust-boundary-hardening.md +56 -0
  22. package/docs/audits/1.9.0-evidence-interoperability-threat-model.md +37 -0
  23. package/docs/audits/2.0.0-attestation-acceptance.md +30 -0
  24. package/docs/releases/1.5.0.md +25 -0
  25. package/docs/releases/1.6.0.md +27 -0
  26. package/docs/releases/1.7.0.md +23 -0
  27. package/docs/releases/1.8.0.md +23 -0
  28. package/docs/releases/1.9.0.md +21 -0
  29. package/docs/releases/2.0.0.md +21 -0
  30. package/docs/releases/npm-publish-from-workstation.md +88 -0
  31. package/docs/research/AGENT-PROTOCOL-FINDINGS.md +43 -0
  32. package/examples/report-demo/REPORT.md +50 -0
  33. package/examples/report-demo/artifact-manifest.json +25 -0
  34. package/examples/report-demo/decision-log.jsonl +0 -0
  35. package/examples/report-demo/execution-report.json +70 -0
  36. package/examples/report-demo/handoff.json +22 -0
  37. package/examples/report-demo/issue-register.json +5 -0
  38. package/examples/report-demo/task.json +13 -0
  39. package/package.json +95 -0
  40. package/project-context.example.json +26 -0
  41. package/references/browser-review-contract.md +32 -0
  42. package/references/dotlottie-source-notes.md +21 -0
  43. package/references/intelligence-core.md +98 -0
  44. package/references/reporting-contract.md +38 -0
  45. package/references/runtime-capability.md +12 -0
  46. package/references/signed-attestation.md +31 -0
  47. package/schemas/artifact-manifest.schema.json +22 -0
  48. package/schemas/browser-review-candidate.schema.json +24 -0
  49. package/schemas/capability-registry.schema.json +61 -0
  50. package/schemas/continuity-report.schema.json +52 -0
  51. package/schemas/evidence-verifier-report.schema.json +35 -0
  52. package/schemas/execution-report.schema.json +35 -0
  53. package/schemas/fix-plan.schema.json +44 -0
  54. package/schemas/handoff.schema.json +19 -0
  55. package/schemas/motion-ir.schema.json +74 -0
  56. package/schemas/project-graph.schema.json +70 -0
  57. package/schemas/provenance.schema.json +76 -0
  58. package/schemas/runtime-evidence.schema.json +48 -0
  59. package/schemas/runtime-telemetry.schema.json +50 -0
  60. package/schemas/scene-manifest.schema.json +45 -0
  61. package/schemas/semantic-benchmark.schema.json +26 -0
  62. package/schemas/semantic-lint-report.schema.json +48 -0
  63. package/schemas/signed-attestation.schema.json +95 -0
  64. package/schemas/task.schema.json +39 -0
  65. package/schemas/trust-policy.schema.json +53 -0
  66. package/scripts/analyze.sh +13 -0
  67. package/scripts/attestation-keygen.py +63 -0
  68. package/scripts/attestation-verifier.py +178 -0
  69. package/scripts/attestation.py +288 -0
  70. package/scripts/capture-runtime-telemetry.sh +37 -0
  71. package/scripts/devlab.sh +77 -0
  72. package/scripts/eval-intelligence.py +377 -0
  73. package/scripts/evidence-verifier.py +222 -0
  74. package/scripts/fetch-library.sh +57 -0
  75. package/scripts/intelligence.py +1543 -0
  76. package/scripts/manifest.py +61 -0
  77. package/scripts/pr.sh +103 -0
  78. package/scripts/quality-gate.py +378 -0
  79. package/scripts/render-node.mjs +53 -0
  80. package/scripts/render.sh +37 -0
  81. package/scripts/report-contract.py +181 -0
  82. package/scripts/report.py +588 -0
  83. package/scripts/review-hook.py +199 -0
  84. package/scripts/runtime-adapters.mjs +187 -0
  85. package/scripts/skill-doctor.py +150 -0
  86. package/scripts/to-dotlottie.mjs +99 -0
  87. package/scripts/to-dotlottie.sh +25 -0
  88. package/scripts/validate-lottie.py +102 -0
  89. package/src/core/analyzer.py +226 -0
  90. package/src/core/snapshot.py +124 -0
  91. package/src/core/spec.py +240 -0
  92. package/src/output/browser-review-smoke/animation.json +57 -0
  93. package/src/output/browser-review-smoke/browser-review.json +21 -0
  94. package/src/output/browser-review-smoke/manifest.json +22 -0
  95. package/src/output/browser-review-smoke/motion-spec.json +28 -0
  96. package/src/output/browser-review-smoke/snapshot/.render-meta.json +10 -0
  97. package/src/output/browser-review-smoke/snapshot/frame-00.png +0 -0
  98. package/src/output/browser-review-smoke/snapshot/frame-100.png +0 -0
  99. package/src/output/browser-review-smoke/snapshot/frame-50.png +0 -0
  100. package/src/rig/README.md +35 -0
  101. package/src/rig/cutout_rig.py +211 -0
  102. package/templates/framer-motion/ui-micro.tsx +50 -0
  103. package/templates/gsap/scroll-scene.js +54 -0
  104. package/templates/lottie/README.md +21 -0
  105. package/templates/lottie/react-component.tsx +82 -0
  106. package/templates/lottie/scaffold/animation.json +57 -0
  107. package/templates/lottie/scaffold/character-rig.svg +19 -0
  108. package/templates/lottie/vanilla.js +68 -0
  109. package/templates/rive/README.md +36 -0
  110. package/tests/evals/intelligence-cases.json +131 -0
  111. package/tests/scripts/run_tests.py +843 -0
  112. package/tests/scripts/test_attestation.py +172 -0
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ """Verify MotionLoom runtime evidence without granting approval.
3
+
4
+ The verifier is intentionally independent from the builder-side quality gate.
5
+ It checks local identity, hashes, telemetry shape and optional freshness policy.
6
+ Exit 0 means the evidence is internally consistent; it never means approved.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import hashlib
13
+ import json
14
+ import re
15
+ import sys
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+
20
+ SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
21
+ VERIFIER = "motionloom-evidence-verifier/1.0"
22
+
23
+
24
+ def sha256_file(path: Path) -> str:
25
+ return hashlib.sha256(path.read_bytes()).hexdigest()
26
+
27
+
28
+ def sha256_json(value: object) -> str:
29
+ encoded = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
30
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
31
+
32
+
33
+ def parse_json(path: Path) -> object:
34
+ return json.loads(path.read_text(encoding="utf-8"))
35
+
36
+
37
+ def reject_symlink_components(path: Path) -> None:
38
+ current = Path(path.anchor) if path.anchor else Path(".")
39
+ for part in path.parts:
40
+ if part in (path.anchor, ""):
41
+ continue
42
+ current = current / part
43
+ if current.is_symlink():
44
+ raise ValueError(f"symlink path component is not allowed: {path}")
45
+
46
+
47
+ def safe_relative_file(root: Path, relative: object, label: str) -> Path:
48
+ if not isinstance(relative, str) or not relative.strip():
49
+ raise ValueError(f"{label} must be a non-empty relative path")
50
+ candidate = Path(relative)
51
+ if candidate.is_absolute() or "\x00" in relative:
52
+ raise ValueError(f"{label} must be relative")
53
+ if any(part in ("", ".", "..") for part in candidate.parts):
54
+ raise ValueError(f"{label} contains unsafe path components")
55
+ joined = root / candidate
56
+ reject_symlink_components(joined)
57
+ resolved_root = root.resolve()
58
+ resolved = joined.resolve(strict=False)
59
+ if resolved != resolved_root and resolved_root not in resolved.parents:
60
+ raise ValueError(f"{label} escapes its evidence root")
61
+ if not resolved.is_file():
62
+ raise ValueError(f"{label} does not point to an existing file")
63
+ return resolved
64
+
65
+
66
+ def verify(scene_dir: Path, task_dir: Path, runtime_evidence_name: str, max_age_days: float | None) -> dict:
67
+ issues: list[str] = []
68
+ scene_dir = scene_dir.absolute()
69
+ task_dir = task_dir.absolute()
70
+ bindings: dict[str, object] = {
71
+ "scene": scene_dir.name,
72
+ "task_id": "",
73
+ "source_sha256": "",
74
+ "manifest_sha256": "",
75
+ "motion_ir_sha256": "",
76
+ "telemetry_files": [],
77
+ }
78
+
79
+ try:
80
+ if not SAFE_NAME.fullmatch(scene_dir.name):
81
+ raise ValueError("scene directory name is unsafe")
82
+ if not SAFE_NAME.fullmatch(task_dir.name):
83
+ raise ValueError("task directory name is unsafe")
84
+ reject_symlink_components(scene_dir)
85
+ reject_symlink_components(task_dir)
86
+ manifest_path = safe_relative_file(scene_dir, "manifest.json", "manifest.json")
87
+ source_manifest = parse_json(manifest_path)
88
+ if not isinstance(source_manifest, dict):
89
+ raise ValueError("manifest.json must contain an object")
90
+ task_path = safe_relative_file(task_dir, "task.json", "task.json")
91
+ task = parse_json(task_path)
92
+ if not isinstance(task, dict) or not str(task.get("task_id") or "").strip():
93
+ raise ValueError("task.json.task_id is required")
94
+ task_id = str(task["task_id"])
95
+ bindings["task_id"] = task_id
96
+ if source_manifest.get("scene") and source_manifest.get("scene") != scene_dir.name:
97
+ issues.append("manifest scene does not match scene directory")
98
+ source_path = safe_relative_file(scene_dir, source_manifest.get("file"), "manifest.file")
99
+ source_sha = sha256_file(source_path)
100
+ manifest_sha = sha256_file(manifest_path)
101
+ motion_ir_path = safe_relative_file(task_dir, "motion-ir.json", "motion-ir.json")
102
+ motion_ir_sha = sha256_file(motion_ir_path)
103
+ bindings.update({
104
+ "source_sha256": source_sha,
105
+ "manifest_sha256": manifest_sha,
106
+ "motion_ir_sha256": motion_ir_sha,
107
+ })
108
+
109
+ evidence_path = safe_relative_file(task_dir, runtime_evidence_name, "runtime evidence")
110
+ evidence = parse_json(evidence_path)
111
+ if not isinstance(evidence, dict):
112
+ raise ValueError("runtime evidence must contain an object")
113
+ if evidence.get("mode") != "runtime":
114
+ issues.append("runtime evidence mode must be runtime")
115
+ if evidence.get("status") != "pass":
116
+ issues.append("runtime evidence status must be pass")
117
+ for field, expected in (("scene", scene_dir.name), ("task_id", task_id), ("source_sha256", source_sha), ("manifest_sha256", manifest_sha), ("motion_ir_sha256", motion_ir_sha)):
118
+ if evidence.get(field) != expected:
119
+ issues.append(f"runtime evidence {field} does not match canonical binding")
120
+
121
+ generated_at = evidence.get("generated_at")
122
+ if max_age_days is not None:
123
+ try:
124
+ observed_at = datetime.fromisoformat(str(generated_at).replace("Z", "+00:00"))
125
+ if observed_at.tzinfo is None:
126
+ raise ValueError("generated_at must include timezone")
127
+ age_days = (datetime.now(timezone.utc) - observed_at.astimezone(timezone.utc)).total_seconds() / 86400
128
+ if age_days < -1 / 86400:
129
+ issues.append("runtime evidence generated_at is in the future")
130
+ elif age_days > max_age_days:
131
+ issues.append(f"runtime evidence is stale: {age_days:.3f} days > {max_age_days:.3f}")
132
+ except (TypeError, ValueError) as exc:
133
+ issues.append(f"runtime evidence generated_at is invalid: {exc}")
134
+
135
+ frameworks = evidence.get("frameworks")
136
+ if not isinstance(frameworks, list) or not frameworks:
137
+ issues.append("runtime evidence frameworks must be a non-empty array")
138
+ frameworks = []
139
+ evidence_root = evidence_path.parent
140
+ for item in frameworks:
141
+ if not isinstance(item, dict):
142
+ issues.append("runtime evidence framework entry must be an object")
143
+ continue
144
+ framework = str(item.get("framework") or "unknown")
145
+ telemetry = item.get("telemetry")
146
+ if not isinstance(telemetry, dict):
147
+ issues.append(f"{framework}: telemetry binding is missing")
148
+ continue
149
+ telemetry_name = telemetry.get("file")
150
+ try:
151
+ telemetry_path = safe_relative_file(evidence_root, telemetry_name, f"{framework}.telemetry.file")
152
+ telemetry_sha = sha256_file(telemetry_path)
153
+ if telemetry.get("sha256") != telemetry_sha:
154
+ issues.append(f"{framework}: telemetry sha256 mismatch")
155
+ telemetry_doc = parse_json(telemetry_path)
156
+ if not isinstance(telemetry_doc, dict):
157
+ raise ValueError("telemetry must contain an object")
158
+ for field, expected in (("mode", "runtime-telemetry"), ("scene", scene_dir.name), ("task_id", task_id), ("framework", framework), ("source_sha256", source_sha), ("manifest_sha256", manifest_sha), ("motion_ir_sha256", motion_ir_sha), ("status", "pass")):
159
+ if telemetry_doc.get(field) != expected:
160
+ issues.append(f"{framework}: telemetry {field} does not match canonical binding")
161
+ samples = telemetry_doc.get("samples")
162
+ if not isinstance(samples, list) or len(samples) < 3:
163
+ issues.append(f"{framework}: telemetry requires at least three samples")
164
+ samples = []
165
+ intervals: list[float] = []
166
+ for index, sample in enumerate(samples):
167
+ if not isinstance(sample, dict):
168
+ issues.append(f"{framework}: sample {index} is not an object")
169
+ continue
170
+ state = sample.get("state")
171
+ if not isinstance(state, dict) or sample.get("state_sha256") != sha256_json(state):
172
+ issues.append(f"{framework}: sample {index} state hash mismatch")
173
+ values = sample.get("raf_intervals_ms")
174
+ if not isinstance(values, list) or not values or any(not isinstance(value, (int, float)) or value <= 0 or value > 1000 for value in values):
175
+ issues.append(f"{framework}: sample {index} has invalid RAF intervals")
176
+ else:
177
+ intervals.extend(float(value) for value in values)
178
+ metrics = telemetry_doc.get("metrics")
179
+ if not isinstance(metrics, dict) or metrics.get("sample_count") != len(samples) or metrics.get("raf_interval_count") != len(intervals):
180
+ issues.append(f"{framework}: telemetry metrics count mismatch")
181
+ if intervals and isinstance(metrics, dict):
182
+ ordered = sorted(intervals)
183
+ p95_index = min(len(ordered) - 1, max(0, int(__import__("math").ceil(len(ordered) * 0.95) - 1)))
184
+ if metrics.get("max_raf_interval_ms") != max(intervals) or metrics.get("p95_raf_interval_ms") != ordered[p95_index]:
185
+ issues.append(f"{framework}: telemetry aggregate metrics mismatch")
186
+ bindings["telemetry_files"].append(str(telemetry_name))
187
+ except (ValueError, OSError, json.JSONDecodeError) as exc:
188
+ issues.append(f"{framework}: {exc}")
189
+ except (ValueError, OSError, json.JSONDecodeError) as exc:
190
+ issues.append(str(exc))
191
+
192
+ return {
193
+ "schema_version": "1.0",
194
+ "verifier": VERIFIER,
195
+ "verified": not issues,
196
+ "approval": False,
197
+ "issues": issues,
198
+ "bindings": bindings,
199
+ "policy": {"max_age_days": max_age_days} if max_age_days is not None else {},
200
+ }
201
+
202
+
203
+ def main() -> int:
204
+ parser = argparse.ArgumentParser(description="Verify MotionLoom runtime evidence without granting approval")
205
+ parser.add_argument("--scene-dir", required=True)
206
+ parser.add_argument("--task-dir", required=True)
207
+ parser.add_argument("--runtime-evidence", default="runtime-adapters/runtime-evidence.json")
208
+ parser.add_argument("--max-age-days", type=float)
209
+ parser.add_argument("--output")
210
+ args = parser.parse_args()
211
+ report = verify(Path(args.scene_dir), Path(args.task_dir), args.runtime_evidence, args.max_age_days)
212
+ rendered = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
213
+ if args.output:
214
+ output = Path(args.output)
215
+ output.parent.mkdir(parents=True, exist_ok=True)
216
+ output.write_text(rendered, encoding="utf-8")
217
+ print(rendered, end="")
218
+ return 0 if report["verified"] else 1
219
+
220
+
221
+ if __name__ == "__main__":
222
+ sys.exit(main())
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # fetch-library.sh — Pull vetted open-source animation assets into
3
+ # assets/library/ from official public sources. Every asset is recorded
4
+ # with its license and source URL in assets/library/ATTRIBUTION.md so the
5
+ # PR review can verify provenance.
6
+ #
7
+ # Usage: bash scripts/fetch-library.sh
8
+ set -uo pipefail
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ LIB="$SCRIPT_DIR/../assets/library"
11
+ mkdir -p "$LIB"
12
+
13
+ ATTR="$LIB/ATTRIBUTION.md"
14
+ echo "# Asset Library Attribution" > "$ATTR"
15
+ echo "" >> "$ATTR"
16
+ echo "| Asset | Source | License | Downloaded |" >> "$ATTR"
17
+ echo "|---|---|---|---|" >> "$ATTR"
18
+
19
+ fetch() {
20
+ local name="$1" url="$2" source="$3" license="$4"
21
+ local out="$LIB/$name"
22
+ if curl -sfL -o "$out" "$url"; then
23
+ local ok
24
+ ok=$(python3 - "$out" <<'EOF'
25
+ import json, sys
26
+ try:
27
+ d = json.load(open(sys.argv[1]))
28
+ print("v=%s fr=%s op=%s layers=%d" % (d.get("v"), d.get("fr"), d.get("op"), len(d.get("layers", []))))
29
+ except Exception as e:
30
+ print("INVALID: %s" % e)
31
+ sys.exit(1)
32
+ EOF
33
+ ) || { rm -f "$out"; echo " [skip] $name invalid"; return; }
34
+ echo "| \`$name\` | $source | $license | $ok |" >> "$ATTR"
35
+ echo " [ok] $name ($ok)"
36
+ else
37
+ echo " [fail] $name"
38
+ fi
39
+ }
40
+
41
+ echo "== Fetching vetted open Lottie assets =="
42
+ fetch "loading-dots.json" \
43
+ "https://assets1.lottiefiles.com/packages/lf20_owfp8w4p.json" \
44
+ "https://lottiefiles.com/free-animation/loading" \
45
+ "LottieFiles Free (check per-asset license page)"
46
+ fetch "success-check.json" \
47
+ "https://assets2.lottiefiles.com/packages/lf20_qp1q7mct.json" \
48
+ "https://lottiefiles.com/free-animation/success-check" \
49
+ "LottieFiles Free (check per-asset license page)"
50
+ fetch "error-alert.json" \
51
+ "https://assets5.lottiefiles.com/packages/lf20_u4yrau.json" \
52
+ "https://lottiefiles.com/free-animation/error-alert" \
53
+ "LottieFiles Free (check per-asset license page)"
54
+
55
+ echo ""
56
+ echo "Update URLs above with current vetted assets; always verify the license page"
57
+ echo "before shipping. Attribution table -> $ATTR"