motionloom 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +78 -11
- package/ROADMAP.md +8 -5
- package/SKILL.md +24 -5
- package/agent-card.json +31 -5
- package/agent-surfaces.json +1 -1
- package/artifact-adapter-registry.json +53 -0
- package/bin/motionloom.mjs +29 -5
- package/docs/CHECKLIST.md +16 -0
- package/docs/STATUS.md +2 -2
- package/docs/audits/data/deep-stress-latest.json +65 -65
- package/docs/releases/2.4.0.md +31 -0
- package/docs/releases/npm-publish-from-workstation.md +19 -8
- package/docs/research/agent-skill-ecosystem-notes.md +47 -0
- package/docs/research/ai-animation-tools-2026-notes.md +81 -0
- package/docs/research/ai-animation-tools-2026-report.md +168 -0
- package/docs/research/ai-pilot-ingest-notes.md +29 -0
- package/examples/agent-consumer/ai-pilot-scout/CHATGPT-HANDOFF.md +70 -0
- package/examples/agent-consumer/ai-pilot-scout/CODEX-HANDOFF.md +80 -0
- package/examples/agent-consumer/ai-pilot-scout/README.md +39 -0
- package/examples/agent-consumer/ai-pilot-scout/chatgpt-geometry-correction-prompt.md +55 -0
- package/examples/agent-consumer/ai-pilot-scout/chatgpt-pose-generation-prompt.md +78 -0
- package/examples/agent-consumer/ai-pilot-scout/partial-handoff.json +73 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-controls.json +15 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-export.json +14 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-provenance.json +20 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-receipt.json +16 -0
- package/examples/agent-consumer/asset-consistency/assets/forest-back.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/forest-front.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/forest-mid.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-atlas.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-00.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-01.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-02.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-03.png +0 -0
- package/examples/agent-consumer/asset-consistency/forest-layered-map.json +53 -0
- package/examples/agent-consumer/asset-consistency/hero-atlas-contract.json +24 -0
- package/examples/agent-consumer/asset-consistency/hero-identity.json +51 -0
- package/examples/agent-consumer/asset-consistency/hero-walk-action-set.json +27 -0
- package/examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json +63 -0
- package/examples/agent-consumer/rig-compatibility/hero-walk-fixture-rig.json +13 -0
- package/examples/agent-consumer/rive-package-gate/README.md +20 -0
- package/examples/agent-consumer/runtime-candidate/hero-walk-candidate.json +18 -0
- package/examples/agent-consumer/runtime-pilot/action-set.json +19 -0
- package/examples/agent-consumer/runtime-pilot/asset-identity.json +44 -0
- package/examples/agent-consumer/runtime-pilot/candidate.json +21 -0
- package/examples/agent-consumer/runtime-pilot/controls.json +35 -0
- package/examples/agent-consumer/runtime-pilot/export.json +17 -0
- package/examples/agent-consumer/runtime-pilot/provenance.json +38 -0
- package/examples/agent-consumer/runtime-pilot/receipt.json +30 -0
- package/package.json +56 -4
- package/references/browser-review-contract.md +7 -1
- package/references/intelligence-core.md +22 -1
- package/rig-adapter-registry.json +39 -0
- package/schemas/action-set.schema.json +42 -0
- package/schemas/artifact-adapter-registry.schema.json +16 -0
- package/schemas/asset-identity.schema.json +117 -0
- package/schemas/asset-provenance.schema.json +1 -1
- package/schemas/atlas-contract.schema.json +48 -0
- package/schemas/control-track.schema.json +18 -0
- package/schemas/export-manifest.schema.json +19 -0
- package/schemas/frame-geometry.schema.json +79 -0
- package/schemas/generation-receipt.schema.json +20 -0
- package/schemas/layered-map.schema.json +75 -0
- package/schemas/rig-adapter-registry.schema.json +14 -0
- package/schemas/rig-compatibility.schema.json +53 -0
- package/schemas/rive-package-manifest.schema.json +43 -0
- package/schemas/runtime-candidate.schema.json +48 -0
- package/schemas/scene-manifest.schema.json +18 -0
- package/scripts/artifact-intake.py +408 -0
- package/scripts/asset-consistency.py +517 -0
- package/scripts/asset-provenance.py +8 -3
- package/scripts/build-ai-pilot.py +504 -0
- package/scripts/isolate-alpha-background.py +147 -0
- package/scripts/quality-gate.py +88 -2
- package/scripts/report.py +85 -0
- package/scripts/resolve-task-bundle.py +84 -0
- package/scripts/review-hook.py +26 -4
- package/scripts/rig-compatibility.py +251 -0
- package/scripts/rive-package-gate.py +245 -0
- package/scripts/runtime-candidate.py +301 -0
- package/scripts/setup.mjs +54 -26
- package/src/output/runtime-pilot-framer/asset-provenance.json +38 -0
- package/src/output/runtime-pilot-framer/browser-review.json +20 -0
- package/src/output/runtime-pilot-framer/framer-motion/runtime-telemetry.json +103 -0
- package/src/output/runtime-pilot-framer/manifest.json +31 -0
- package/src/output/runtime-pilot-framer/motion-spec.json +25 -0
- package/src/output/runtime-pilot-framer/runtime-evidence.json +96 -0
- package/src/output/runtime-pilot-framer/runtime-telemetry.json +100 -0
- package/src/output/runtime-pilot-framer/scene.jsx +52 -0
- package/src/output/runtime-pilot-framer/snapshot/.render-meta.json +9 -0
- package/src/output/runtime-pilot-framer/snapshot/frame-00.png +0 -0
- package/src/output/runtime-pilot-framer/snapshot/frame-100.png +0 -0
- package/src/output/runtime-pilot-framer/snapshot/frame-50.png +0 -0
- package/tests/scripts/run_tests.py +107 -0
- package/tests/scripts/test_ai_pilot_builder.py +128 -0
- package/tests/scripts/test_alpha_isolation.py +60 -0
- package/tests/scripts/test_artifact_intake.py +146 -0
- package/tests/scripts/test_asset_consistency.py +199 -0
- package/tests/scripts/test_rig_compatibility.py +105 -0
- package/tests/scripts/test_rive_package_gate.py +101 -0
- package/tests/scripts/test_runtime_candidate.py +95 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""MotionLoom control-to-runtime candidate bridge.
|
|
3
|
+
|
|
4
|
+
This contract links a provider-neutral Artifact Intake bundle to the existing
|
|
5
|
+
identity/action/frame/atlas/map contracts. It is deterministic, standard
|
|
6
|
+
library only, and intentionally reports runtime-test readiness rather than any
|
|
7
|
+
form of production approval or user review outcome.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import hashlib
|
|
14
|
+
import importlib.util
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import asdict, dataclass
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
25
|
+
SAFE = re.compile(r"^(?!/)(?!.*(?:^|/)\.\.(?:/|$)).+")
|
|
26
|
+
SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
|
27
|
+
KINDS = {"asset_identity": "identity", "action_set": "action-set", "frame_geometry": "frame-geometry", "atlas": "atlas", "layered_map": "layered-map"}
|
|
28
|
+
PROFILE_REQUIREMENTS = {
|
|
29
|
+
"image": {"asset_identity"},
|
|
30
|
+
"frame_sequence": {"asset_identity", "action_set", "frame_geometry"},
|
|
31
|
+
"sprite_atlas": {"asset_identity", "action_set", "frame_geometry", "atlas"},
|
|
32
|
+
"layered_map": {"asset_identity", "layered_map"},
|
|
33
|
+
"video": {"asset_identity", "action_set"},
|
|
34
|
+
"rigged_2d": {"asset_identity", "action_set"},
|
|
35
|
+
"rigged_3d": {"asset_identity", "action_set"},
|
|
36
|
+
"runtime_scene": {"asset_identity", "action_set"},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Issue:
|
|
42
|
+
severity: str
|
|
43
|
+
code: str
|
|
44
|
+
message: str
|
|
45
|
+
path: str = ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def issue(issues: list[Issue], code: str, message: str, path: str = "", severity: str = "error") -> None:
|
|
49
|
+
issues.append(Issue(severity, code, message, path))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def safe(value: Any) -> bool:
|
|
53
|
+
return isinstance(value, str) and "\\" not in value and bool(SAFE.fullmatch(value))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve(root: Path, value: Any) -> Path | None:
|
|
57
|
+
if not safe(value):
|
|
58
|
+
return None
|
|
59
|
+
raw = root / str(value)
|
|
60
|
+
try:
|
|
61
|
+
path = raw.resolve()
|
|
62
|
+
path.relative_to(root.resolve())
|
|
63
|
+
except ValueError:
|
|
64
|
+
return None
|
|
65
|
+
return path if not raw.is_symlink() and path.is_file() else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_json(path: Path, issues: list[Issue], label: str) -> dict[str, Any] | None:
|
|
69
|
+
try:
|
|
70
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
71
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
72
|
+
issue(issues, "invalid_json", f"{label}: {exc}", str(path))
|
|
73
|
+
return None
|
|
74
|
+
if not isinstance(value, dict):
|
|
75
|
+
issue(issues, "invalid_document", f"{label} root must be an object", str(path))
|
|
76
|
+
return None
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def digest(path: Path) -> str:
|
|
81
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_module(name: str, path: Path) -> Any:
|
|
85
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
86
|
+
if spec is None or spec.loader is None:
|
|
87
|
+
raise RuntimeError(f"cannot load {path}")
|
|
88
|
+
module = importlib.util.module_from_spec(spec)
|
|
89
|
+
sys.modules[name] = module
|
|
90
|
+
spec.loader.exec_module(module)
|
|
91
|
+
return module
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def required(document: dict[str, Any], fields: tuple[str, ...], prefix: str, issues: list[Issue]) -> None:
|
|
95
|
+
for field in fields:
|
|
96
|
+
if field not in document:
|
|
97
|
+
issue(issues, "missing_field", f"{prefix}.{field} is required", f"{prefix}.{field}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def valid_datetime(value: Any) -> bool:
|
|
101
|
+
try:
|
|
102
|
+
datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
103
|
+
return True
|
|
104
|
+
except ValueError:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def resolve_group(root: Path, document: dict[str, Any], key: str, fields: tuple[str, ...], issues: list[Issue]) -> dict[str, Path]:
|
|
109
|
+
group = document.get(key) if isinstance(document.get(key), dict) else {}
|
|
110
|
+
required(group, fields, key, issues)
|
|
111
|
+
resolved: dict[str, Path] = {}
|
|
112
|
+
for field in fields:
|
|
113
|
+
path = resolve(root, group.get(field))
|
|
114
|
+
if path is None:
|
|
115
|
+
issue(issues, "missing_or_unsafe_ref", f"{key}.{field} must be an existing safe relative file", f"{key}.{field}")
|
|
116
|
+
else:
|
|
117
|
+
resolved[field] = path
|
|
118
|
+
return resolved
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def validate_runtime_evidence(path: Path, issues: list[Issue]) -> bool:
|
|
122
|
+
evidence = load_json(path, issues, "runtime evidence")
|
|
123
|
+
if evidence is None:
|
|
124
|
+
return False
|
|
125
|
+
if evidence.get("mode") != "runtime" or evidence.get("status") != "pass":
|
|
126
|
+
issue(issues, "runtime_evidence_not_pass", "runtime evidence must record mode=runtime and status=pass", "runtime.runtime_evidence_ref")
|
|
127
|
+
return False
|
|
128
|
+
frameworks = evidence.get("frameworks")
|
|
129
|
+
if not isinstance(frameworks, list) or not frameworks or any(item.get("status") != "pass" or item.get("ready") is not True for item in frameworks if isinstance(item, dict)):
|
|
130
|
+
issue(issues, "runtime_framework_not_ready", "runtime evidence requires at least one ready passing framework", "runtime.runtime_evidence_ref")
|
|
131
|
+
return False
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def validate_candidate(args: argparse.Namespace) -> dict[str, Any]:
|
|
136
|
+
root = args.root.resolve()
|
|
137
|
+
issues: list[Issue] = []
|
|
138
|
+
candidate_path = args.input.resolve()
|
|
139
|
+
candidate = load_json(candidate_path, issues, "runtime candidate")
|
|
140
|
+
if candidate is None:
|
|
141
|
+
return result(None, issues, {}, None, False)
|
|
142
|
+
required(candidate, ("schema_version", "candidate_id", "asset_id", "created_at", "artifact_intake", "consistency", "runtime"), "candidate", issues)
|
|
143
|
+
if candidate.get("schema_version") != "0.1":
|
|
144
|
+
issue(issues, "schema_version", "candidate.schema_version must be 0.1", "candidate.schema_version")
|
|
145
|
+
if not isinstance(candidate.get("candidate_id"), str) or len(candidate["candidate_id"]) < 3:
|
|
146
|
+
issue(issues, "invalid_candidate_id", "candidate_id must be a non-empty stable ID", "candidate.candidate_id")
|
|
147
|
+
if not isinstance(candidate.get("asset_id"), str) or not candidate["asset_id"].strip():
|
|
148
|
+
issue(issues, "invalid_asset_id", "asset_id must be non-empty", "candidate.asset_id")
|
|
149
|
+
if not valid_datetime(candidate.get("created_at")):
|
|
150
|
+
issue(issues, "invalid_datetime", "created_at must be ISO-8601", "candidate.created_at")
|
|
151
|
+
intake_paths = resolve_group(root, candidate, "artifact_intake", ("registry", "receipt", "controls", "export_manifest"), issues)
|
|
152
|
+
consistency_paths = resolve_group(root, candidate, "consistency", ("asset_identity", "action_set"), issues)
|
|
153
|
+
consistency_group = candidate.get("consistency") if isinstance(candidate.get("consistency"), dict) else {}
|
|
154
|
+
for name in ("frame_geometry", "atlas", "layered_map"):
|
|
155
|
+
if name in consistency_group:
|
|
156
|
+
path = resolve(root, consistency_group.get(name))
|
|
157
|
+
if path is None:
|
|
158
|
+
issue(issues, "missing_or_unsafe_ref", f"consistency.{name} must be an existing safe relative file", f"consistency.{name}")
|
|
159
|
+
else:
|
|
160
|
+
consistency_paths[name] = path
|
|
161
|
+
runtime = candidate.get("runtime") if isinstance(candidate.get("runtime"), dict) else {}
|
|
162
|
+
if runtime.get("review_required") is not True:
|
|
163
|
+
issue(issues, "review_required", "runtime.review_required must be true; candidate evidence cannot replace human review", "runtime.review_required")
|
|
164
|
+
if runtime.get("target") not in {"sprite", "canvas", "lottie", "dotlottie", "rive", "gsap", "framer-motion", "spine", "three"}:
|
|
165
|
+
issue(issues, "invalid_runtime_target", "runtime.target is unsupported", "runtime.target")
|
|
166
|
+
|
|
167
|
+
intake_result: dict[str, Any] | None = None
|
|
168
|
+
controls: dict[str, Any] | None = None
|
|
169
|
+
export: dict[str, Any] | None = None
|
|
170
|
+
if len(intake_paths) == 4:
|
|
171
|
+
try:
|
|
172
|
+
intake = load_module("motionloom_artifact_intake_bridge", ROOT / "scripts" / "artifact-intake.py")
|
|
173
|
+
# Intake strictness evaluates provider adapter authority. Candidate strictness
|
|
174
|
+
# evaluates actual runtime evidence below; a static intake adapter must not be
|
|
175
|
+
# promoted, but it can feed a separately tested runtime candidate.
|
|
176
|
+
intake_result = intake.evaluate_bundle(argparse.Namespace(root=root, registry=intake_paths["registry"], receipt=intake_paths["receipt"], controls=intake_paths["controls"], export_manifest=intake_paths["export_manifest"], strict=False))
|
|
177
|
+
for entry in intake_result.get("errors", []):
|
|
178
|
+
issue(issues, f"intake_{entry.get('code', 'error')}", entry.get("message", "artifact intake error"), entry.get("path", "artifact_intake"))
|
|
179
|
+
controls = load_json(intake_paths["controls"], issues, "control track")
|
|
180
|
+
export = load_json(intake_paths["export_manifest"], issues, "export manifest")
|
|
181
|
+
receipt = load_json(intake_paths["receipt"], issues, "generation receipt")
|
|
182
|
+
for label, document in (("receipt", receipt), ("controls", controls), ("export", export)):
|
|
183
|
+
if document is not None and document.get("asset_id", document.get("asset", {}).get("id")) != candidate.get("asset_id"):
|
|
184
|
+
issue(issues, "asset_id_mismatch", f"candidate.asset_id must match {label} asset ID", f"{label}.asset_id")
|
|
185
|
+
except (OSError, ValueError, AttributeError, KeyError, TypeError, RuntimeError) as exc:
|
|
186
|
+
issue(issues, "artifact_intake_unavailable", str(exc), "artifact_intake")
|
|
187
|
+
|
|
188
|
+
consistency_results: dict[str, Any] = {}
|
|
189
|
+
try:
|
|
190
|
+
analyzer = load_module("motionloom_asset_consistency_bridge", ROOT / "scripts" / "asset-consistency.py")
|
|
191
|
+
for name, path in consistency_paths.items():
|
|
192
|
+
document = load_json(path, issues, f"consistency {name}")
|
|
193
|
+
if document is None:
|
|
194
|
+
continue
|
|
195
|
+
check = analyzer.run(KINDS[name], document, path.parent.resolve(), args.strict)
|
|
196
|
+
consistency_results[name] = check
|
|
197
|
+
for entry in check.get("errors", []):
|
|
198
|
+
issue(issues, f"consistency_{entry.get('code', 'error')}", entry.get("message", "consistency error"), f"consistency.{name}")
|
|
199
|
+
if name in {"asset_identity", "action_set", "frame_geometry"} and document.get("asset_id", document.get("asset_identity")) != candidate.get("asset_id"):
|
|
200
|
+
issue(issues, "consistency_asset_id_mismatch", f"candidate.asset_id must match consistency.{name}", f"consistency.{name}")
|
|
201
|
+
except (OSError, ValueError, AttributeError, KeyError, TypeError, RuntimeError) as exc:
|
|
202
|
+
issue(issues, "consistency_unavailable", str(exc), "consistency")
|
|
203
|
+
|
|
204
|
+
profile = ((controls or {}).get("output_profile") or {}).get("kind")
|
|
205
|
+
for name in PROFILE_REQUIREMENTS.get(str(profile), set()):
|
|
206
|
+
if name not in consistency_paths:
|
|
207
|
+
issue(issues, "missing_profile_contract", f"{profile} requires consistency.{name}", f"consistency.{name}")
|
|
208
|
+
if profile not in PROFILE_REQUIREMENTS:
|
|
209
|
+
issue(issues, "unknown_output_profile", "control output_profile.kind is unsupported for runtime bridging", "controls.output_profile.kind")
|
|
210
|
+
|
|
211
|
+
output_paths: dict[Path, dict[str, Any]] = {}
|
|
212
|
+
for entry in ((export or {}).get("outputs") or []):
|
|
213
|
+
if isinstance(entry, dict):
|
|
214
|
+
path = resolve(root, entry.get("path"))
|
|
215
|
+
if path is not None:
|
|
216
|
+
output_paths[path] = entry
|
|
217
|
+
action = consistency_results.get("action_set", {}).get("metrics", {}).get("actions", [])
|
|
218
|
+
action_doc = load_json(consistency_paths["action_set"], issues, "action set") if "action_set" in consistency_paths else None
|
|
219
|
+
action_frames: list[Path] = []
|
|
220
|
+
if isinstance(action_doc, dict):
|
|
221
|
+
for item in action_doc.get("actions", []):
|
|
222
|
+
if isinstance(item, dict):
|
|
223
|
+
action_frames.extend((consistency_paths["action_set"].parent / str(frame)).resolve() for frame in item.get("frames", []) if safe(frame))
|
|
224
|
+
profile_data = (controls or {}).get("output_profile") or {}
|
|
225
|
+
expected_count = profile_data.get("expected_frame_count")
|
|
226
|
+
if profile in {"frame_sequence", "sprite_atlas"} and isinstance(expected_count, int) and len(action_frames) != expected_count:
|
|
227
|
+
issue(issues, "frame_count_mismatch", "control expected_frame_count must match action-set frame count", "controls.output_profile.expected_frame_count")
|
|
228
|
+
if profile in {"frame_sequence", "sprite_atlas"}:
|
|
229
|
+
for frame in action_frames:
|
|
230
|
+
if frame not in output_paths:
|
|
231
|
+
issue(issues, "unexported_action_frame", f"action frame is not hash-bound in export manifest: {frame.name}", "export_manifest.outputs")
|
|
232
|
+
geometry_doc = load_json(consistency_paths["frame_geometry"], issues, "frame geometry") if "frame_geometry" in consistency_paths else None
|
|
233
|
+
if isinstance(geometry_doc, dict):
|
|
234
|
+
for frame in geometry_doc.get("frames", []):
|
|
235
|
+
if isinstance(frame, dict) and safe(frame.get("image")):
|
|
236
|
+
path = (consistency_paths["frame_geometry"].parent / str(frame["image"])).resolve()
|
|
237
|
+
export_entry = output_paths.get(path)
|
|
238
|
+
if export_entry is None:
|
|
239
|
+
issue(issues, "unexported_geometry_frame", f"geometry frame is not hash-bound in export manifest: {path.name}", "export_manifest.outputs")
|
|
240
|
+
elif not isinstance(frame.get("sha256"), str) or frame.get("sha256") != export_entry.get("sha256") or digest(path) != frame.get("sha256"):
|
|
241
|
+
issue(issues, "frame_hash_mismatch", f"geometry/export hash mismatch: {path.name}", "frame_geometry.frames")
|
|
242
|
+
|
|
243
|
+
runtime_verified = False
|
|
244
|
+
if "runtime_evidence_ref" in runtime:
|
|
245
|
+
evidence_path = resolve(root, runtime.get("runtime_evidence_ref"))
|
|
246
|
+
if evidence_path is None:
|
|
247
|
+
issue(issues, "missing_or_unsafe_runtime_evidence", "runtime.runtime_evidence_ref must be an existing safe relative file", "runtime.runtime_evidence_ref")
|
|
248
|
+
else:
|
|
249
|
+
runtime_verified = validate_runtime_evidence(evidence_path, issues)
|
|
250
|
+
if args.strict and not runtime_verified:
|
|
251
|
+
issue(issues, "runtime_evidence_required", "strict runtime candidate validation requires passing runtime evidence", "runtime.runtime_evidence_ref")
|
|
252
|
+
return result(candidate, issues, consistency_results, intake_result, runtime_verified)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def result(candidate: dict[str, Any] | None, issues: list[Issue], consistency: dict[str, Any], intake: dict[str, Any] | None, runtime_verified: bool) -> dict[str, Any]:
|
|
256
|
+
errors = [asdict(item) for item in issues if item.severity == "error"]
|
|
257
|
+
warnings = [asdict(item) for item in issues if item.severity == "warning"]
|
|
258
|
+
ready = not errors
|
|
259
|
+
return {
|
|
260
|
+
"contract": "runtime-candidate",
|
|
261
|
+
"status": "blocked" if errors else "review_required",
|
|
262
|
+
"runtime_test_ready": ready,
|
|
263
|
+
"runtime_verified": runtime_verified,
|
|
264
|
+
"review_required": True,
|
|
265
|
+
"production_eligible": False,
|
|
266
|
+
"production_approved": False,
|
|
267
|
+
"candidate_id": (candidate or {}).get("candidate_id"),
|
|
268
|
+
"asset_id": (candidate or {}).get("asset_id"),
|
|
269
|
+
"intake": {"status": (intake or {}).get("status"), "adapter": (intake or {}).get("adapter")},
|
|
270
|
+
"consistency": {name: {"status": value.get("status"), "ready": value.get("ready")} for name, value in consistency.items()},
|
|
271
|
+
"errors": errors,
|
|
272
|
+
"warnings": warnings,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main(argv: list[str] | None = None) -> int:
|
|
277
|
+
parser = argparse.ArgumentParser(description="Bind MotionLoom artifact intake and consistency evidence into a runtime-test candidate")
|
|
278
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
279
|
+
for command in ("validate", "report"):
|
|
280
|
+
item = sub.add_parser(command)
|
|
281
|
+
item.add_argument("--input", required=True, type=Path)
|
|
282
|
+
item.add_argument("--root", type=Path, default=Path("."))
|
|
283
|
+
item.add_argument("--strict", action="store_true")
|
|
284
|
+
item.add_argument("--output", type=Path)
|
|
285
|
+
item.add_argument("--json", action="store_true")
|
|
286
|
+
args = parser.parse_args(argv)
|
|
287
|
+
payload = validate_candidate(args)
|
|
288
|
+
if args.output:
|
|
289
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
args.output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
291
|
+
if args.command == "validate" or args.json:
|
|
292
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
293
|
+
else:
|
|
294
|
+
print(f"runtime-candidate: {payload['status']} ({len(payload['errors'])} errors, {len(payload['warnings'])} warnings)")
|
|
295
|
+
for entry in payload["errors"] + payload["warnings"]:
|
|
296
|
+
print(f"- {entry['severity']}: {entry['code']}: {entry['message']}")
|
|
297
|
+
return 0 if payload["runtime_test_ready"] else 1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
raise SystemExit(main())
|
package/scripts/setup.mjs
CHANGED
|
@@ -19,12 +19,16 @@ const MARKER_START = "<!-- MOTIONLOOM:START -->";
|
|
|
19
19
|
const MARKER_END = "<!-- MOTIONLOOM:END -->";
|
|
20
20
|
|
|
21
21
|
function usage() {
|
|
22
|
-
console.log(`MotionLoom ${VERSION} —
|
|
22
|
+
console.log(`MotionLoom ${VERSION} — quick start for project-aware animation
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
motionloom
|
|
26
|
-
motionloom status [options]
|
|
27
|
-
motionloom
|
|
24
|
+
Start here:
|
|
25
|
+
motionloom init [options] One safe setup for the current project
|
|
26
|
+
motionloom status [options] Short, read-only project readiness check
|
|
27
|
+
motionloom doctor Validate the installed package
|
|
28
|
+
|
|
29
|
+
Maintenance:
|
|
30
|
+
motionloom setup [options] Alias for init (kept for compatibility)
|
|
31
|
+
motionloom repair [options] Re-apply only missing managed pieces
|
|
28
32
|
|
|
29
33
|
Options:
|
|
30
34
|
--project-root <path> Host project (default: current directory)
|
|
@@ -43,13 +47,16 @@ Safe defaults:
|
|
|
43
47
|
- idempotent AGENTS.md merge, never overwrite existing project guidance
|
|
44
48
|
- Project Memory stays bound to the current project root
|
|
45
49
|
- no commit, push, PR, approval or production promotion
|
|
50
|
+
|
|
51
|
+
No asset or runtime gate runs during init. Choose an animation task first;
|
|
52
|
+
the installed Agent router will load the relevant MotionLoom workflow then.
|
|
46
53
|
`);
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
function parseArgs(argv) {
|
|
50
57
|
const args = [...argv];
|
|
51
58
|
let action = "setup";
|
|
52
|
-
if (["setup", "status", "repair"].includes(args[0])) action = args.shift();
|
|
59
|
+
if (["setup", "init", "status", "repair"].includes(args[0])) action = args.shift();
|
|
53
60
|
const options = {
|
|
54
61
|
action,
|
|
55
62
|
projectRoot: process.cwd(),
|
|
@@ -119,6 +126,23 @@ function projectPackage(root) {
|
|
|
119
126
|
return readJson(join(root, "package.json"));
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
function detectProjectKind(packageJson) {
|
|
130
|
+
const dependencies = {
|
|
131
|
+
...(packageJson?.dependencies || {}),
|
|
132
|
+
...(packageJson?.devDependencies || {}),
|
|
133
|
+
};
|
|
134
|
+
if (dependencies.next) return "Next.js";
|
|
135
|
+
if (dependencies["@angular/core"]) return "Angular";
|
|
136
|
+
if (dependencies.vue || dependencies.nuxt) return "Vue";
|
|
137
|
+
if (dependencies.svelte || dependencies["@sveltejs/kit"]) return "Svelte";
|
|
138
|
+
if (dependencies.phaser) return "Phaser";
|
|
139
|
+
if (dependencies["pixi.js"] || dependencies["@pixi/react"]) return "PixiJS";
|
|
140
|
+
if (dependencies["@babylonjs/core"]) return "Babylon.js";
|
|
141
|
+
if (dependencies.three || dependencies["@react-three/fiber"]) return "Three.js";
|
|
142
|
+
if (dependencies.react || dependencies["react-dom"]) return "React";
|
|
143
|
+
return "Node / unclassified";
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
function hasDependency(packageJson) {
|
|
123
147
|
if (!packageJson) return false;
|
|
124
148
|
return Boolean(
|
|
@@ -219,8 +243,9 @@ When a task involves animation, motion, assets, runtime rendering or Dev Lab rev
|
|
|
219
243
|
- Read the installed MotionLoom package's canonical \`SKILL.md\` and \`agent-card.json\` first; do not copy them into the host project.
|
|
220
244
|
- Run \`npx --no-install motionloom status --json\` before planning animation work.
|
|
221
245
|
- Keep project context and \`.motionloom/project-memory.json\` bound to this project; never copy them from another project.
|
|
222
|
-
-
|
|
223
|
-
-
|
|
246
|
+
- Continue normal product work without running MotionLoom gates until the task actually includes animation or an animation asset.
|
|
247
|
+
- For an animation task, follow the applicable canonical workflow; use ingest/runtime checks only when importing assets or testing a target runtime.
|
|
248
|
+
- After rendering, open the MotionLoom Dev Lab candidate for user review before any PR handoff. Evidence never implies user approval.
|
|
224
249
|
- Keep Git side effects local-only until the user explicitly confirms commit, push or PR.
|
|
225
250
|
|
|
226
251
|
${MARKER_END}`;
|
|
@@ -310,6 +335,11 @@ function statusReport(options) {
|
|
|
310
335
|
context: { path: join(projectRoot, "project-context.json"), exists: context },
|
|
311
336
|
memory: { path: join(projectRoot, ".motionloom", "project-memory.json"), exists: memory, validation: memoryCheck },
|
|
312
337
|
discovery,
|
|
338
|
+
quick_start: {
|
|
339
|
+
project_kind: detectProjectKind(packageJson),
|
|
340
|
+
gates_active: false,
|
|
341
|
+
next_action: "Continue normal work. Start an animation task when you are ready; MotionLoom will then load only the relevant workflow.",
|
|
342
|
+
},
|
|
313
343
|
};
|
|
314
344
|
}
|
|
315
345
|
|
|
@@ -319,15 +349,14 @@ function printStatus(report, json) {
|
|
|
319
349
|
return;
|
|
320
350
|
}
|
|
321
351
|
const label = report.status === "ready" ? "READY" : report.status === "needs_review" ? "NEEDS REVIEW" : "NEEDS SETUP";
|
|
322
|
-
console.log(`MotionLoom
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
console.log(`
|
|
329
|
-
console.log(
|
|
330
|
-
if (report.status !== "ready") console.log("Next step: npx --yes motionloom setup");
|
|
352
|
+
console.log(`MotionLoom: ${label} · ${report.quick_start.project_kind}`);
|
|
353
|
+
if (report.status === "ready") {
|
|
354
|
+
console.log("Project memory and Agent routing are ready. No animation gate is active.");
|
|
355
|
+
console.log("Next: continue normal work. When you start animation, ask the Agent to use MotionLoom.");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
console.log(`Missing setup pieces: package ${report.package.installed ? "ready" : "missing"}, memory ${report.memory.exists ? "present" : "missing"}, router ${report.router.status}.`);
|
|
359
|
+
console.log("Next: npx --yes motionloom init");
|
|
331
360
|
}
|
|
332
361
|
|
|
333
362
|
function setup(options) {
|
|
@@ -335,8 +364,10 @@ function setup(options) {
|
|
|
335
364
|
const packageJson = projectPackage(projectRoot);
|
|
336
365
|
const result = {
|
|
337
366
|
command: options.action,
|
|
367
|
+
onboarding: "quick_start",
|
|
338
368
|
status: "blocked",
|
|
339
369
|
project_root: projectRoot,
|
|
370
|
+
project_kind: detectProjectKind(packageJson),
|
|
340
371
|
dry_run: options.dryRun,
|
|
341
372
|
changed: [],
|
|
342
373
|
planned: [],
|
|
@@ -432,19 +463,16 @@ function printSetup(result, json) {
|
|
|
432
463
|
return;
|
|
433
464
|
}
|
|
434
465
|
const label = result.status === "ready" ? "READY" : result.status === "planned" ? "DRY RUN" : result.status === "needs_review" ? "NEEDS REVIEW" : "BLOCKED";
|
|
435
|
-
console.log(`MotionLoom
|
|
436
|
-
console.log(`Project: ${result.project_root}`);
|
|
437
|
-
for (const item of result.changed) console.log(` PASS ${typeof item === "string" ? item : `${item.step}: ${item.action}`}`);
|
|
438
|
-
for (const item of result.planned) console.log(` PLAN ${typeof item === "string" ? item : item.command ? item.command.join(" ") : `${item.step}: ${item.action || "planned"}`}`);
|
|
439
|
-
for (const warning of result.warnings) console.log(` WARN ${warning}`);
|
|
466
|
+
console.log(`MotionLoom quick start: ${label} · ${result.project_kind || "project"}`);
|
|
440
467
|
for (const error of result.errors) console.log(` BLOCK ${error}`);
|
|
468
|
+
for (const warning of result.warnings) console.log(` NOTE ${warning}`);
|
|
441
469
|
if (result.status === "ready") {
|
|
442
|
-
console.log("
|
|
443
|
-
console.log("
|
|
470
|
+
console.log("Ready. Project memory is set up and your normal work stays uninterrupted.");
|
|
471
|
+
console.log("When an animation task begins, ask your Agent to use MotionLoom. Asset and runtime checks appear only when relevant.");
|
|
444
472
|
} else if (result.status === "planned") {
|
|
445
|
-
console.log("Dry run only: rerun without --dry-run to apply the safe
|
|
473
|
+
console.log("Dry run only: rerun without --dry-run to apply the same safe quick start.");
|
|
446
474
|
} else {
|
|
447
|
-
console.log("Next: run `npx --no-install motionloom status --json`
|
|
475
|
+
console.log("Next: run `npx --no-install motionloom status --json` for details, then resolve the reported setup item.");
|
|
448
476
|
}
|
|
449
477
|
}
|
|
450
478
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"provenance_id": "runtime-pilot-framer-scene-provenance",
|
|
4
|
+
"task_id": "runtime-pilot-001",
|
|
5
|
+
"scene": "runtime-pilot-framer",
|
|
6
|
+
"created_at": "2026-08-15T00:00:00Z",
|
|
7
|
+
"asset": {
|
|
8
|
+
"id": "runtime/framer-motion-pilot",
|
|
9
|
+
"path": "scene.jsx",
|
|
10
|
+
"type": "runtime_scene",
|
|
11
|
+
"framework": "framer-motion",
|
|
12
|
+
"version": "13.1.0"
|
|
13
|
+
},
|
|
14
|
+
"authority": "code_authored",
|
|
15
|
+
"readiness": "review_required",
|
|
16
|
+
"license": {
|
|
17
|
+
"spdx": "MIT",
|
|
18
|
+
"source": "MotionLoom repository source",
|
|
19
|
+
"attribution": "MotionLoom contributors"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "scene.jsx",
|
|
24
|
+
"role": "runtime-source",
|
|
25
|
+
"sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b",
|
|
26
|
+
"bytes": 1769
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"provenance_chain": [
|
|
30
|
+
{
|
|
31
|
+
"step": "code-authoring",
|
|
32
|
+
"actor": "MotionLoom runtime-pilot author",
|
|
33
|
+
"source": "scene.jsx",
|
|
34
|
+
"timestamp": "2026-08-15T00:00:00Z",
|
|
35
|
+
"sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"candidate_id": "8cccc4ac5a7e493ffe40",
|
|
4
|
+
"task_id": "runtime-pilot-001",
|
|
5
|
+
"scene": "runtime-pilot-framer",
|
|
6
|
+
"url": "https://animdevlab-hcxnxr9c.manus.space/lab/?scene=runtime-pilot-framer&task_id=runtime-pilot-001&candidate_id=8cccc4ac5a7e493ffe40&artifact_base=https%3A%2F%2Fanimdevlab-hcxnxr9c.manus.space%2Fscenes%2Fruntime-pilot-framer&task_base=https%3A%2F%2Fanimdevlab-hcxnxr9c.manus.space%2Ftasks%2Fruntime-pilot-001",
|
|
7
|
+
"status": "prepared",
|
|
8
|
+
"context_sha256": "9e9bb3bf2e490d23e9f5d52dfc4779b69adf0cc63b32df66adfde7050845fc74",
|
|
9
|
+
"source_sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b",
|
|
10
|
+
"runtime": "framer-motion",
|
|
11
|
+
"checkpoints": [
|
|
12
|
+
0,
|
|
13
|
+
50,
|
|
14
|
+
100
|
|
15
|
+
],
|
|
16
|
+
"review_artifact": "review.json",
|
|
17
|
+
"requires_user_approval": true,
|
|
18
|
+
"prepared_at": "2026-08-15T15:44:47Z",
|
|
19
|
+
"expires_at": "2026-08-16T15:44:47Z"
|
|
20
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"run_id": "1786808870894-145958",
|
|
4
|
+
"generated_at": "2026-08-15T15:47:52.478Z",
|
|
5
|
+
"mode": "runtime-telemetry",
|
|
6
|
+
"task_id": "runtime-pilot-001",
|
|
7
|
+
"scene": "runtime-pilot-framer",
|
|
8
|
+
"framework": "framer-motion",
|
|
9
|
+
"runtime": "framer-motion@13.1.0",
|
|
10
|
+
"source_sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b",
|
|
11
|
+
"manifest_sha256": "2b53bf3b6fc8dfe16a271ecd52ec51c2acb5b612c0b77d9fd4e485696989011d",
|
|
12
|
+
"motion_ir_sha256": "1d339ca2ec9c510e54e53c37ef32c2ddf1485dbaec97c5e9aa178a8f6c9c3287",
|
|
13
|
+
"samples": [
|
|
14
|
+
{
|
|
15
|
+
"sequence": 0,
|
|
16
|
+
"percent": 0,
|
|
17
|
+
"captured_at_ms": 767.8999999761581,
|
|
18
|
+
"raf_intervals_ms": [
|
|
19
|
+
16.699999999999932,
|
|
20
|
+
16.600000000000023,
|
|
21
|
+
16.799999999999955
|
|
22
|
+
],
|
|
23
|
+
"state_sha256": "0769bb6867251ff8d951b01e7d817ab4644d1f7d4a38b0e91ad54ec5b0f81f6b",
|
|
24
|
+
"state": {
|
|
25
|
+
"progress": 0,
|
|
26
|
+
"transform": "matrix(0.82, 0, 0, 0.82, 0, 0)",
|
|
27
|
+
"opacity": "0.28",
|
|
28
|
+
"contract": {
|
|
29
|
+
"framework": "framer-motion",
|
|
30
|
+
"checkpoints": [
|
|
31
|
+
0,
|
|
32
|
+
50,
|
|
33
|
+
100
|
|
34
|
+
],
|
|
35
|
+
"durationSeconds": 1.2,
|
|
36
|
+
"loop": true,
|
|
37
|
+
"sourceAuthority": "code_authored"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"sequence": 1,
|
|
43
|
+
"percent": 50,
|
|
44
|
+
"captured_at_ms": 851.1000000238419,
|
|
45
|
+
"raf_intervals_ms": [
|
|
46
|
+
16.700000000000045,
|
|
47
|
+
16.699999999999932,
|
|
48
|
+
16.600000000000023
|
|
49
|
+
],
|
|
50
|
+
"state_sha256": "015ad2af517be79a3cf102b48b5f61282acd1843c7b5efe20472c2752d79c890",
|
|
51
|
+
"state": {
|
|
52
|
+
"progress": 0.5,
|
|
53
|
+
"transform": "matrix(1.02851, -0.256437, 0.256437, 1.02851, 176, -48)",
|
|
54
|
+
"opacity": "1",
|
|
55
|
+
"contract": {
|
|
56
|
+
"framework": "framer-motion",
|
|
57
|
+
"checkpoints": [
|
|
58
|
+
0,
|
|
59
|
+
50,
|
|
60
|
+
100
|
|
61
|
+
],
|
|
62
|
+
"durationSeconds": 1.2,
|
|
63
|
+
"loop": true,
|
|
64
|
+
"sourceAuthority": "code_authored"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"sequence": 2,
|
|
70
|
+
"percent": 100,
|
|
71
|
+
"captured_at_ms": 934.6000000238419,
|
|
72
|
+
"raf_intervals_ms": [
|
|
73
|
+
16.700000000000045,
|
|
74
|
+
16.600000000000023,
|
|
75
|
+
16.699999999999932
|
|
76
|
+
],
|
|
77
|
+
"state_sha256": "275a832573d3c660f6d820256f95412e253f0a29c02e8f535dfdec2c47e56de2",
|
|
78
|
+
"state": {
|
|
79
|
+
"progress": 1,
|
|
80
|
+
"transform": "matrix(0.882948, 0.469472, -0.469472, 0.882948, 240, -36)",
|
|
81
|
+
"opacity": "1",
|
|
82
|
+
"contract": {
|
|
83
|
+
"framework": "framer-motion",
|
|
84
|
+
"checkpoints": [
|
|
85
|
+
0,
|
|
86
|
+
50,
|
|
87
|
+
100
|
|
88
|
+
],
|
|
89
|
+
"durationSeconds": 1.2,
|
|
90
|
+
"loop": true,
|
|
91
|
+
"sourceAuthority": "code_authored"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"metrics": {
|
|
97
|
+
"sample_count": 3,
|
|
98
|
+
"raf_interval_count": 9,
|
|
99
|
+
"max_raf_interval_ms": 16.799999999999955,
|
|
100
|
+
"p95_raf_interval_ms": 16.799999999999955
|
|
101
|
+
},
|
|
102
|
+
"status": "pass"
|
|
103
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scene": "runtime-pilot-framer",
|
|
3
|
+
"task_id": "runtime-pilot-001",
|
|
4
|
+
"name": "Framer Motion Runtime Pilot",
|
|
5
|
+
"description": "Code-authored Framer Motion scene rendered at deterministic 0/50/100 checkpoints before human review.",
|
|
6
|
+
"framework": "framer-motion",
|
|
7
|
+
"category": "runtime-pilot",
|
|
8
|
+
"file": "scene.jsx",
|
|
9
|
+
"runtime_evidence": "runtime-evidence.json",
|
|
10
|
+
"asset_provenance": "asset-provenance.json",
|
|
11
|
+
"source_binding": {
|
|
12
|
+
"kind": "project",
|
|
13
|
+
"source_path": "scene.jsx",
|
|
14
|
+
"authority": "code_authored",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"attribution": "MotionLoom contributors",
|
|
17
|
+
"sha256": "f4c0e56de842cae6b234566b556e4f07154f8109dde382a474218a9de8b45a8b"
|
|
18
|
+
},
|
|
19
|
+
"checks": [
|
|
20
|
+
{
|
|
21
|
+
"id": "source-bound",
|
|
22
|
+
"label": "Runtime source is hash-bound before adapter execution",
|
|
23
|
+
"pass": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "review-first",
|
|
27
|
+
"label": "Runtime evidence does not approve a production release or pull request",
|
|
28
|
+
"pass": true
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|