motionloom 2.2.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +123 -17
  3. package/ROADMAP.md +8 -3
  4. package/SKILL.md +34 -3
  5. package/agent-card.json +48 -5
  6. package/agent-surfaces.json +8 -1
  7. package/artifact-adapter-registry.json +53 -0
  8. package/bin/motionloom.mjs +41 -4
  9. package/docs/AGENT-INTEGRATION.md +15 -2
  10. package/docs/CHECKLIST.md +21 -0
  11. package/docs/STATUS.md +2 -2
  12. package/docs/audits/data/deep-stress-latest.json +65 -65
  13. package/docs/releases/2.3.0.md +33 -0
  14. package/docs/releases/2.4.0.md +31 -0
  15. package/docs/releases/npm-publish-from-workstation.md +19 -8
  16. package/docs/research/agent-skill-ecosystem-notes.md +47 -0
  17. package/docs/research/ai-animation-tools-2026-notes.md +81 -0
  18. package/docs/research/ai-animation-tools-2026-report.md +168 -0
  19. package/docs/research/ai-pilot-ingest-notes.md +29 -0
  20. package/examples/agent-consumer/ai-generated-pilot/hero-male.json +10 -0
  21. package/examples/agent-consumer/ai-generated-pilot-provenance.json +55 -0
  22. package/examples/agent-consumer/ai-pilot-scout/CHATGPT-HANDOFF.md +70 -0
  23. package/examples/agent-consumer/ai-pilot-scout/CODEX-HANDOFF.md +80 -0
  24. package/examples/agent-consumer/ai-pilot-scout/README.md +39 -0
  25. package/examples/agent-consumer/ai-pilot-scout/chatgpt-geometry-correction-prompt.md +55 -0
  26. package/examples/agent-consumer/ai-pilot-scout/chatgpt-pose-generation-prompt.md +78 -0
  27. package/examples/agent-consumer/ai-pilot-scout/partial-handoff.json +73 -0
  28. package/examples/agent-consumer/artifact-intake/hero-motion-controls.json +15 -0
  29. package/examples/agent-consumer/artifact-intake/hero-motion-export.json +14 -0
  30. package/examples/agent-consumer/artifact-intake/hero-motion-provenance.json +20 -0
  31. package/examples/agent-consumer/artifact-intake/hero-motion-receipt.json +16 -0
  32. package/examples/agent-consumer/asset-consistency/assets/forest-back.png +0 -0
  33. package/examples/agent-consumer/asset-consistency/assets/forest-front.png +0 -0
  34. package/examples/agent-consumer/asset-consistency/assets/forest-mid.png +0 -0
  35. package/examples/agent-consumer/asset-consistency/assets/hero-atlas.png +0 -0
  36. package/examples/agent-consumer/asset-consistency/assets/hero-frame-00.png +0 -0
  37. package/examples/agent-consumer/asset-consistency/assets/hero-frame-01.png +0 -0
  38. package/examples/agent-consumer/asset-consistency/assets/hero-frame-02.png +0 -0
  39. package/examples/agent-consumer/asset-consistency/assets/hero-frame-03.png +0 -0
  40. package/examples/agent-consumer/asset-consistency/forest-layered-map.json +53 -0
  41. package/examples/agent-consumer/asset-consistency/hero-atlas-contract.json +24 -0
  42. package/examples/agent-consumer/asset-consistency/hero-identity.json +51 -0
  43. package/examples/agent-consumer/asset-consistency/hero-walk-action-set.json +27 -0
  44. package/examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json +63 -0
  45. package/examples/agent-consumer/rig-compatibility/hero-walk-fixture-rig.json +13 -0
  46. package/examples/agent-consumer/rive-package-gate/README.md +20 -0
  47. package/examples/agent-consumer/runtime-candidate/hero-walk-candidate.json +18 -0
  48. package/examples/agent-consumer/runtime-pilot/action-set.json +19 -0
  49. package/examples/agent-consumer/runtime-pilot/asset-identity.json +44 -0
  50. package/examples/agent-consumer/runtime-pilot/candidate.json +21 -0
  51. package/examples/agent-consumer/runtime-pilot/controls.json +35 -0
  52. package/examples/agent-consumer/runtime-pilot/export.json +17 -0
  53. package/examples/agent-consumer/runtime-pilot/provenance.json +38 -0
  54. package/examples/agent-consumer/runtime-pilot/receipt.json +30 -0
  55. package/package.json +65 -4
  56. package/references/agent-interoperability.md +11 -0
  57. package/references/browser-review-contract.md +7 -1
  58. package/references/intelligence-core.md +29 -2
  59. package/rig-adapter-registry.json +39 -0
  60. package/schemas/action-set.schema.json +42 -0
  61. package/schemas/agent-surfaces.schema.json +1 -1
  62. package/schemas/artifact-adapter-registry.schema.json +16 -0
  63. package/schemas/asset-identity.schema.json +117 -0
  64. package/schemas/asset-provenance.schema.json +183 -0
  65. package/schemas/atlas-contract.schema.json +48 -0
  66. package/schemas/control-track.schema.json +18 -0
  67. package/schemas/export-manifest.schema.json +19 -0
  68. package/schemas/frame-geometry.schema.json +79 -0
  69. package/schemas/generation-receipt.schema.json +20 -0
  70. package/schemas/layered-map.schema.json +75 -0
  71. package/schemas/rig-adapter-registry.schema.json +14 -0
  72. package/schemas/rig-compatibility.schema.json +53 -0
  73. package/schemas/rive-package-manifest.schema.json +43 -0
  74. package/schemas/runtime-candidate.schema.json +48 -0
  75. package/schemas/scene-manifest.schema.json +19 -0
  76. package/scripts/artifact-intake.py +408 -0
  77. package/scripts/asset-consistency.py +517 -0
  78. package/scripts/asset-provenance.py +395 -0
  79. package/scripts/build-ai-pilot.py +504 -0
  80. package/scripts/docs-audit.py +14 -2
  81. package/scripts/isolate-alpha-background.py +147 -0
  82. package/scripts/pr.py +2 -0
  83. package/scripts/quality-gate.py +127 -3
  84. package/scripts/report.py +135 -0
  85. package/scripts/resolve-task-bundle.py +84 -0
  86. package/scripts/review-hook.py +26 -4
  87. package/scripts/rig-compatibility.py +251 -0
  88. package/scripts/rive-package-gate.py +245 -0
  89. package/scripts/runtime-candidate.py +301 -0
  90. package/scripts/setup.mjs +500 -0
  91. package/scripts/skill-doctor.py +2 -1
  92. package/src/output/browser-review-smoke/asset-provenance.json +77 -0
  93. package/src/output/browser-review-smoke/manifest.json +1 -0
  94. package/src/output/browser-review-smoke/visual-truth.json +3 -3
  95. package/src/output/runtime-pilot-framer/asset-provenance.json +38 -0
  96. package/src/output/runtime-pilot-framer/browser-review.json +20 -0
  97. package/src/output/runtime-pilot-framer/framer-motion/runtime-telemetry.json +103 -0
  98. package/src/output/runtime-pilot-framer/manifest.json +31 -0
  99. package/src/output/runtime-pilot-framer/motion-spec.json +25 -0
  100. package/src/output/runtime-pilot-framer/runtime-evidence.json +96 -0
  101. package/src/output/runtime-pilot-framer/runtime-telemetry.json +100 -0
  102. package/src/output/runtime-pilot-framer/scene.jsx +52 -0
  103. package/src/output/runtime-pilot-framer/snapshot/.render-meta.json +9 -0
  104. package/src/output/runtime-pilot-framer/snapshot/frame-00.png +0 -0
  105. package/src/output/runtime-pilot-framer/snapshot/frame-100.png +0 -0
  106. package/src/output/runtime-pilot-framer/snapshot/frame-50.png +0 -0
  107. package/tests/scripts/run_tests.py +131 -0
  108. package/tests/scripts/test_ai_pilot_builder.py +128 -0
  109. package/tests/scripts/test_alpha_isolation.py +60 -0
  110. package/tests/scripts/test_artifact_intake.py +146 -0
  111. package/tests/scripts/test_asset_consistency.py +199 -0
  112. package/tests/scripts/test_rig_compatibility.py +105 -0
  113. package/tests/scripts/test_rive_package_gate.py +101 -0
  114. package/tests/scripts/test_runtime_candidate.py +95 -0
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import struct
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import zlib
10
+ from pathlib import Path
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[2]
14
+ BUILDER = ROOT / "scripts" / "build-ai-pilot.py"
15
+
16
+
17
+ def check(condition: bool, message: str) -> None:
18
+ if not condition:
19
+ raise AssertionError(message)
20
+
21
+
22
+ def write_png(path: Path, alpha: bool, offset: int = 0, contamination: bool = False) -> None:
23
+ """Write one compact, non-interlaced 4x4 PNG without third-party libraries."""
24
+ width = height = 4
25
+ channels = 4 if alpha else 3
26
+ rows = bytearray()
27
+ for y in range(height):
28
+ rows.append(0)
29
+ for x in range(width):
30
+ visible = (x == 1 + offset and y in {1, 2}) or (contamination and x == 3 and y == 1)
31
+ rows.extend((244, 124, 21))
32
+ if alpha:
33
+ rows.append(255 if visible else 0)
34
+ def chunk(kind: bytes, data: bytes) -> bytes:
35
+ return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
36
+ color_type = 6 if alpha else 2
37
+ path.write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, color_type, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(bytes(rows))) + chunk(b"IEND", b""))
38
+
39
+
40
+ def build(root: Path, sources: dict[str, Path], output: str, provider: str = "internal-imagegen") -> subprocess.CompletedProcess[str]:
41
+ return subprocess.run(
42
+ [
43
+ sys.executable, str(BUILDER),
44
+ "--idle", str(sources["idle"]),
45
+ "--contact-right", str(sources["contact-right"]),
46
+ "--passing", str(sources["passing"]),
47
+ "--contact-left", str(sources["contact-left"]),
48
+ "--root", str(root),
49
+ "--output", output,
50
+ "--generated-at", "2026-08-15T00:00:00Z",
51
+ "--provider", provider,
52
+ "--overwrite",
53
+ ],
54
+ capture_output=True,
55
+ text=True,
56
+ )
57
+
58
+
59
+ def main() -> int:
60
+ output = ".motionloom/pilots/test-ai-pilot-builder"
61
+ workspace = ROOT / output
62
+ shutil.rmtree(workspace, ignore_errors=True)
63
+ with tempfile.TemporaryDirectory() as td:
64
+ source_dir = Path(td)
65
+ sources: dict[str, Path] = {}
66
+ for index, frame_id in enumerate(("idle", "contact-right", "passing", "contact-left")):
67
+ source = source_dir / f"{frame_id}.png"
68
+ write_png(source, alpha=True, offset=index % 2)
69
+ sources[frame_id] = source
70
+ result = build(ROOT, sources, output)
71
+ check(result.returncode == 0, result.stderr or result.stdout)
72
+ provenance = json.loads((workspace / "provenance.json").read_text())
73
+ candidate = json.loads((workspace / "candidate.json").read_text())
74
+ evidence = json.loads((workspace / "devlab-pilot-evidence.json").read_text())
75
+ check(provenance["authority"] == "ai_generated" and provenance["readiness"] == "runtime_ready", "builder must preserve AI authority/readiness")
76
+ check("human_approval" not in provenance and candidate["runtime"]["review_required"] is True, "builder must not self-approve a candidate")
77
+ check(evidence["state"] == "review_required" and evidence["production_approved"] is False, "Dev Lab evidence must retain review boundary")
78
+ intake = subprocess.run(
79
+ [sys.executable, str(ROOT / "scripts/artifact-intake.py"), "intake", "--root", str(ROOT), "--registry", str(ROOT / "artifact-adapter-registry.json"), "--controls", str(workspace / "controls.json"), "--receipt", str(workspace / "receipt.json"), "--export-manifest", str(workspace / "export.json"), "--json"],
80
+ capture_output=True,
81
+ text=True,
82
+ )
83
+ intake_data = json.loads(intake.stdout)
84
+ check(intake.returncode == 0 and intake_data["status"] == "review_required", intake.stderr or intake.stdout)
85
+ runtime = subprocess.run(
86
+ [sys.executable, str(ROOT / "scripts/runtime-candidate.py"), "validate", "--root", str(ROOT), "--input", str(workspace / "candidate.json"), "--json"],
87
+ capture_output=True,
88
+ text=True,
89
+ )
90
+ runtime_data = json.loads(runtime.stdout)
91
+ check(runtime.returncode == 0 and runtime_data["status"] == "review_required" and runtime_data["runtime_verified"] is False, runtime.stderr or runtime.stdout)
92
+
93
+ chatgpt_output = ".motionloom/pilots/test-ai-pilot-chatgpt"
94
+ chatgpt = build(ROOT, sources, chatgpt_output, provider="chatgpt")
95
+ check(chatgpt.returncode == 0, chatgpt.stderr or chatgpt.stdout)
96
+ chatgpt_workspace = ROOT / chatgpt_output
97
+ chatgpt_provenance = json.loads((chatgpt_workspace / "provenance.json").read_text())
98
+ chatgpt_receipt = json.loads((chatgpt_workspace / "receipt.json").read_text())
99
+ check(chatgpt_provenance["generator"]["source"] == "openai-chatgpt", "ChatGPT import must not be relabelled internal-imagegen")
100
+ check(chatgpt_receipt["provider"]["adapter_id"] == "openai.chatgpt" and chatgpt_receipt["provider"]["invocation_mode"] == "manual", "ChatGPT receipt must preserve user-mediated adapter truth")
101
+
102
+ opaque = source_dir / "opaque.png"
103
+ write_png(opaque, alpha=False)
104
+ opaque_output = ".motionloom/pilots/test-ai-pilot-opaque"
105
+ rejected = build(ROOT, {key: opaque for key in sources}, opaque_output)
106
+ check(rejected.returncode != 0 and "has no alpha channel" in (rejected.stderr + rejected.stdout), "RGB checkered source must be rejected as non-alpha")
107
+ opaque_handoff = json.loads((ROOT / opaque_output / "partial-handoff.json").read_text())
108
+ check(opaque_handoff["state"] == "partial" and opaque_handoff["production_approved"] is False, "early RGB rejection must emit a non-approving partial handoff")
109
+ check(len(opaque_handoff["rejected_source_attempts"][0]["frames"]) == 4, "partial handoff must bind every supplied source hash")
110
+
111
+ contaminated = source_dir / "contaminated.png"
112
+ write_png(contaminated, alpha=True, contamination=True)
113
+ contaminated_output = ".motionloom/pilots/test-ai-pilot-contaminated"
114
+ rejected = build(ROOT, {key: contaminated for key in sources}, contaminated_output, provider="chatgpt")
115
+ check(rejected.returncode != 0 and "clean padding" in (rejected.stderr + rejected.stdout), "detached canvas-edge contamination must be rejected")
116
+ contaminated_handoff = json.loads((ROOT / contaminated_output / "partial-handoff.json").read_text())
117
+ attempt = contaminated_handoff["rejected_source_attempts"][0]
118
+ check(attempt["provider"] == "openai-chatgpt" and attempt["state"] == "rejected_pre_ingest", "partial handoff must preserve ChatGPT provenance")
119
+ shutil.rmtree(workspace, ignore_errors=True)
120
+ shutil.rmtree(ROOT / ".motionloom/pilots/test-ai-pilot-chatgpt", ignore_errors=True)
121
+ shutil.rmtree(ROOT / ".motionloom/pilots/test-ai-pilot-opaque", ignore_errors=True)
122
+ shutil.rmtree(ROOT / ".motionloom/pilots/test-ai-pilot-contaminated", ignore_errors=True)
123
+ print("AI pilot builder tests: PASS")
124
+ return 0
125
+
126
+
127
+ if __name__ == "__main__":
128
+ raise SystemExit(main())
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import struct
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+ import zlib
9
+ from pathlib import Path
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+ ISOLATOR = ROOT / "scripts" / "isolate-alpha-background.py"
14
+
15
+
16
+ def check(condition: bool, message: str) -> None:
17
+ if not condition:
18
+ raise AssertionError(message)
19
+
20
+
21
+ def write_png(path: Path, contamination: bool = False) -> None:
22
+ width = height = 8
23
+ raw = bytearray()
24
+ for y in range(height):
25
+ raw.append(0)
26
+ for x in range(width):
27
+ if 2 <= x <= 5 and 2 <= y <= 5:
28
+ color = (244, 124, 21, 255)
29
+ elif contamination and x == width - 1 and y == 3:
30
+ color = (180, 24, 180, 255)
31
+ else:
32
+ color = (29, 32, 32, 255)
33
+ raw.extend(color)
34
+ def chunk(kind: bytes, content: bytes) -> bytes:
35
+ return struct.pack(">I", len(content)) + kind + content + struct.pack(">I", zlib.crc32(kind + content) & 0xFFFFFFFF)
36
+ payload = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(bytes(raw))) + chunk(b"IEND", b"")
37
+ path.write_bytes(payload)
38
+
39
+
40
+ def main() -> int:
41
+ with tempfile.TemporaryDirectory() as td:
42
+ root = Path(td)
43
+ source, output, report = root / "source.png", root / "isolated.png", root / "report.json"
44
+ write_png(source)
45
+ result = subprocess.run([sys.executable, str(ISOLATOR), str(source), str(output), "--report", str(report), "--tolerance", "24"], capture_output=True, text=True)
46
+ check(result.returncode == 0, result.stderr or result.stdout)
47
+ data = json.loads(report.read_text())
48
+ check(data["removed_pixels"] == 48 and data["foreground_alpha_pixels"] == 16, "isolator must remove only the edge-connected flat background")
49
+ check(data["residual_edge_foreground_pixels"] == 0 and data["human_review_required"] is True and data["production_approved"] is False, "isolation must preserve review/approval boundary")
50
+
51
+ contaminated = root / "contaminated.png"
52
+ write_png(contaminated, contamination=True)
53
+ rejected = subprocess.run([sys.executable, str(ISOLATOR), str(contaminated), str(root / "rejected.png"), "--tolerance", "24"], capture_output=True, text=True)
54
+ check(rejected.returncode != 0 and "residual background contamination" in (rejected.stderr + rejected.stdout), "edge contamination must fail closed")
55
+ print("alpha isolation tests: PASS")
56
+ return 0
57
+
58
+
59
+ if __name__ == "__main__":
60
+ raise SystemExit(main())
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import importlib.util
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+ from types import SimpleNamespace
12
+
13
+
14
+ ROOT = Path(__file__).resolve().parents[2]
15
+ MODULE_PATH = ROOT / "scripts" / "artifact-intake.py"
16
+
17
+
18
+ def load_module():
19
+ spec = importlib.util.spec_from_file_location("artifact_intake", MODULE_PATH)
20
+ if spec is None or spec.loader is None:
21
+ raise RuntimeError("cannot load artifact intake")
22
+ module = importlib.util.module_from_spec(spec)
23
+ sys.modules[spec.name] = module
24
+ spec.loader.exec_module(module)
25
+ return module
26
+
27
+
28
+ MODULE = load_module()
29
+
30
+
31
+ def check(condition: bool, message: str) -> None:
32
+ if not condition:
33
+ raise AssertionError(message)
34
+
35
+
36
+ def sha(path: Path) -> str:
37
+ return hashlib.sha256(path.read_bytes()).hexdigest()
38
+
39
+
40
+ def write(path: Path, document: dict) -> Path:
41
+ path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
42
+ return path
43
+
44
+
45
+ def bundle(root: Path) -> tuple[Path, Path, Path, Path]:
46
+ evidence = root / "evidence.md"
47
+ evidence.write_text("internal capability evidence\n", encoding="utf-8")
48
+ frame_a = root / "frame-00.png"
49
+ frame_b = root / "frame-01.png"
50
+ frame_a.write_bytes(b"frame-zero")
51
+ frame_b.write_bytes(b"frame-one")
52
+ registry = {
53
+ "schema_version": "0.1", "registry_id": "test-registry", "generated_at": "2026-08-15T00:00:00Z",
54
+ "selection_policy": {"require_verified": True, "allow_scaffold_only": False},
55
+ "adapters": [{
56
+ "adapter_id": "fixture.local", "kind": "fixture", "status": "static_validated", "adapter_version": "0.1",
57
+ "invocation_mode": "none", "cost_class": "included", "inputs": ["reference"], "outputs": ["frame_sequence"],
58
+ "compatibility": {"os": ["linux", "macos", "windows"]},
59
+ "evidence": [{"path": "evidence.md", "sha256": sha(evidence), "kind": "static"}],
60
+ "limitations": ["fixture only"], "risk_level": "low", "side_effect_level": "read"
61
+ }]
62
+ }
63
+ controls = {
64
+ "schema_version": "0.1", "control_id": "test-controls", "asset_id": "test-asset", "created_at": "2026-08-15T00:00:00Z",
65
+ "references": [{"id": "identity", "path": "frame-00.png", "sha256": sha(frame_a), "role": "identity"}],
66
+ "tracks": [
67
+ {"id": "identity", "kind": "identity", "binding": "required", "value_hash": "1" * 64},
68
+ {"id": "pose", "kind": "pose", "binding": "required", "value_hash": "2" * 64, "samples": 2}
69
+ ],
70
+ "output_profile": {"kind": "frame_sequence", "fps": 12, "expected_frame_count": 2, "loop": True}
71
+ }
72
+ provenance = {
73
+ "authority": "ai_generated",
74
+ "files": [{"path": "frame-00.png", "sha256": sha(frame_a)}, {"path": "frame-01.png", "sha256": sha(frame_b)}]
75
+ }
76
+ receipt = {
77
+ "schema_version": "0.1", "receipt_id": "test-receipt", "created_at": "2026-08-15T00:00:00Z",
78
+ "asset": {"id": "test-asset", "kind": "frame_sequence", "intended_use": "pilot"}, "authority": "ai_generated",
79
+ "provider": {"adapter_id": "fixture.local", "kind": "fixture", "invocation_mode": "none", "task_id": "task-1", "model": "fixture", "generated_at": "2026-08-15T00:00:00Z", "cost_class": "included"},
80
+ "control_track_ref": "controls.json", "provenance_ref": "provenance.json",
81
+ "outputs": [
82
+ {"path": "frame-00.png", "role": "frame-00", "sha256": sha(frame_a), "bytes": frame_a.stat().st_size},
83
+ {"path": "frame-01.png", "role": "frame-01", "sha256": sha(frame_b), "bytes": frame_b.stat().st_size}
84
+ ]
85
+ }
86
+ export = {
87
+ "schema_version": "0.1", "manifest_id": "test-export", "asset_id": "test-asset", "created_at": "2026-08-15T00:00:00Z",
88
+ "receipt_ref": "receipt.json", "control_track_ref": "controls.json",
89
+ "outputs": [dict(entry, target="preview") for entry in receipt["outputs"]]
90
+ }
91
+ return write(root / "registry.json", registry), write(root / "receipt.json", receipt), write(root / "controls.json", controls), write(root / "export.json", export), write(root / "provenance.json", provenance)
92
+
93
+
94
+ def result(root: Path, registry: Path, receipt: Path, controls: Path, export: Path, strict: bool = False) -> dict:
95
+ return MODULE.evaluate_bundle(SimpleNamespace(root=root, registry=registry, receipt=receipt, controls=controls, export_manifest=export, strict=strict))
96
+
97
+
98
+ def main() -> int:
99
+ with tempfile.TemporaryDirectory() as td:
100
+ root = Path(td)
101
+ registry, receipt, controls, export, provenance = bundle(root)
102
+ valid = result(root, registry, receipt, controls, export)
103
+ check(valid["ready"] and valid["status"] == "review_required", "static adapter must produce review_required evidence, not approval")
104
+ check(not valid["production_eligible"] and not valid["production_approved"], "intake must never grant production authority")
105
+ strict = result(root, registry, receipt, controls, export, strict=True)
106
+ check(not strict["ready"] and any(item["code"] == "adapter_not_runtime_verified" for item in strict["errors"]), "strict intake must fail a non-verified adapter")
107
+
108
+ receipt_doc = json.loads(receipt.read_text())
109
+ receipt_doc["outputs"][0]["sha256"] = "0" * 64
110
+ tampered = write(root / "tampered-receipt.json", receipt_doc)
111
+ bad_hash = result(root, registry, tampered, controls, export)
112
+ check(not bad_hash["ready"] and any(item["code"] == "sha256_mismatch" for item in bad_hash["errors"]), "output hash tampering must fail closed")
113
+
114
+ unknown_doc = copy.deepcopy(json.loads(receipt.read_text()))
115
+ unknown_doc["provider"]["adapter_id"] = "missing.adapter"
116
+ unknown = write(root / "unknown-receipt.json", unknown_doc)
117
+ unknown_result = result(root, registry, unknown, controls, export)
118
+ check(not unknown_result["ready"] and any(item["code"] == "unknown_adapter" for item in unknown_result["errors"]), "unknown adapter must fail closed")
119
+
120
+ approval_doc = copy.deepcopy(json.loads(receipt.read_text()))
121
+ approval_doc["production_approved"] = True
122
+ approval = write(root / "approval-receipt.json", approval_doc)
123
+ approval_result = result(root, registry, approval, controls, export)
124
+ check(not approval_result["ready"] and any(item["code"] == "approval_forbidden" for item in approval_result["errors"]), "receipt cannot self-assert approval")
125
+
126
+ controls_doc = json.loads(controls.read_text())
127
+ controls_doc["tracks"] = [controls_doc["tracks"][0]]
128
+ no_motion = write(root / "no-motion-controls.json", controls_doc)
129
+ no_motion_result = result(root, registry, receipt, no_motion, export)
130
+ check(not no_motion_result["ready"] and any(item["code"] == "missing_motion_control" for item in no_motion_result["errors"]), "animated output requires motion or pose controls")
131
+
132
+ export_doc = json.loads(export.read_text())
133
+ export_doc["outputs"][1]["sha256"] = "f" * 64
134
+ bad_export = write(root / "bad-export.json", export_doc)
135
+ export_result = result(root, registry, receipt, controls, bad_export)
136
+ check(not export_result["ready"] and any(item["code"] in {"sha256_mismatch", "receipt_output_missing"} for item in export_result["errors"]), "export must bind to receipt bytes")
137
+
138
+ cli = subprocess.run([sys.executable, str(MODULE_PATH), "intake", "--root", str(root), "--registry", str(registry), "--receipt", str(receipt), "--controls", str(controls), "--export-manifest", str(export), "--json"], capture_output=True, text=True)
139
+ cli_result = json.loads(cli.stdout)
140
+ check(cli.returncode == 0 and cli_result["status"] == "review_required", "CLI must return evidence without provider invocation")
141
+ print("artifact intake contract tests: PASS")
142
+ return 0
143
+
144
+
145
+ if __name__ == "__main__":
146
+ raise SystemExit(main())
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """Regression suite for MotionLoom's deterministic asset consistency contracts."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import copy
7
+ import hashlib
8
+ import importlib.util
9
+ import json
10
+ import struct
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import zlib
15
+ from pathlib import Path
16
+
17
+
18
+ ROOT = Path(__file__).resolve().parents[2]
19
+ FIXTURE_ROOT = ROOT / "examples/agent-consumer/asset-consistency"
20
+ ANALYZER_PATH = ROOT / "scripts/asset-consistency.py"
21
+
22
+
23
+ def load_analyzer():
24
+ spec = importlib.util.spec_from_file_location("asset_consistency", ANALYZER_PATH)
25
+ if spec is None or spec.loader is None:
26
+ raise RuntimeError("cannot load asset consistency analyzer")
27
+ module = importlib.util.module_from_spec(spec)
28
+ sys.modules[spec.name] = module
29
+ spec.loader.exec_module(module)
30
+ return module
31
+
32
+
33
+ AC = load_analyzer()
34
+
35
+
36
+ def read_json(name: str) -> dict:
37
+ return json.loads((FIXTURE_ROOT / name).read_text(encoding="utf-8"))
38
+
39
+
40
+ def check(condition: bool, message: str) -> None:
41
+ if not condition:
42
+ raise AssertionError(message)
43
+
44
+
45
+ def png_chunk(kind: bytes, payload: bytes) -> bytes:
46
+ return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", zlib.crc32(kind + payload) & 0xFFFFFFFF)
47
+
48
+
49
+ def write_rgba_png(path: Path, width: int, height: int, pixels: list[tuple[int, int, int, int]]) -> None:
50
+ raw = b"".join(b"\x00" + bytes(sum((list(pixel) for pixel in pixels[y * width : (y + 1) * width]), [])) for y in range(height))
51
+ header = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
52
+ path.write_bytes(b"\x89PNG\r\n\x1a\n" + png_chunk(b"IHDR", header) + png_chunk(b"IDAT", zlib.compress(raw, 9)) + png_chunk(b"IEND", b""))
53
+
54
+
55
+ def solid(width: int, height: int, color: tuple[int, int, int, int]) -> list[tuple[int, int, int, int]]:
56
+ return [color] * (width * height)
57
+
58
+
59
+ def test_pass_fixtures() -> None:
60
+ identity = AC.validate_identity(read_json("hero-identity.json"))
61
+ check(identity["ready"], f"identity fixture blocked: {identity}")
62
+
63
+ action_set = AC.validate_action_set(read_json("hero-walk-action-set.json"))
64
+ check(action_set["ready"], f"action-set fixture blocked: {action_set}")
65
+
66
+ geometry = AC.validate_frame_geometry(read_json("hero-walk-frame-geometry.json"), FIXTURE_ROOT)
67
+ check(geometry["ready"], f"frame geometry fixture blocked: {geometry}")
68
+ check(geometry["metrics"]["frame_count"] == 4, "frame geometry must measure all four frames")
69
+
70
+ atlas = AC.validate_atlas(read_json("hero-atlas-contract.json"), FIXTURE_ROOT)
71
+ check(atlas["ready"], f"atlas fixture blocked: {atlas}")
72
+ check(atlas["metrics"]["opaque_pixels_outside_regions"] == 0, "atlas fixture must have no outside pixels")
73
+
74
+ layered_map = AC.validate_layered_map(read_json("forest-layered-map.json"), FIXTURE_ROOT)
75
+ check(layered_map["ready"], f"layered-map fixture blocked: {layered_map}")
76
+ check(layered_map["metrics"]["layer_count"] == 3, "layered-map fixture must measure three layers")
77
+
78
+
79
+ def test_identity_and_action_fail_closed() -> None:
80
+ identity = read_json("hero-identity.json")
81
+ identity["derivation"]["origin"] = "unknown"
82
+ result = AC.validate_identity(identity)
83
+ check(not result["ready"] and any(item["code"] == "unknown_origin" for item in result["errors"]), "unknown identity origin must block")
84
+
85
+ action_set = read_json("hero-walk-action-set.json")
86
+ del action_set["actions"][0]["loop"]
87
+ result = AC.validate_action_set(action_set)
88
+ check(not result["ready"] and any(item["code"] == "invalid_loop" for item in result["errors"]), "implicit loop policy must block")
89
+
90
+
91
+ def test_frame_contamination_and_pivot_drift() -> None:
92
+ geometry = read_json("hero-walk-frame-geometry.json")
93
+ with tempfile.TemporaryDirectory() as td:
94
+ root = Path(td)
95
+ image = [(0, 0, 0, 0)] * 64
96
+ for y in range(1, 6):
97
+ for x in range(2, 6):
98
+ image[y * 8 + x] = (120, 160, 220, 255)
99
+ image[0] = (255, 0, 0, 255)
100
+ write_rgba_png(root / "contaminated.png", 8, 8, image)
101
+ geometry["frames"] = [copy.deepcopy(geometry["frames"][0])]
102
+ geometry["frames"][0]["image"] = "contaminated.png"
103
+ geometry["frames"][0]["rect"] = {"x": 1, "y": 1, "width": 6, "height": 6}
104
+ geometry["frames"][0]["alpha_bbox"] = {"x": 1, "y": 0, "width": 4, "height": 5}
105
+ geometry["frames"][0]["sha256"] = hashlib.sha256((root / "contaminated.png").read_bytes()).hexdigest()
106
+ result = AC.validate_frame_geometry(geometry, root)
107
+ check(not result["ready"] and any(item["code"] == "frame_contamination" for item in result["errors"]), "opaque pixels outside frame rect must block")
108
+
109
+ geometry = read_json("hero-walk-frame-geometry.json")
110
+ geometry["frames"][1]["pivot"]["x"] = 4.5
111
+ result = AC.validate_frame_geometry(geometry, FIXTURE_ROOT)
112
+ check(not result["ready"] and any(item["code"] == "pivot_drift" for item in result["errors"]), "pivot drift must block")
113
+
114
+
115
+ def test_atlas_overlap_and_contamination() -> None:
116
+ atlas = read_json("hero-atlas-contract.json")
117
+ atlas["regions"][1]["rect"]["x"] = 4
118
+ result = AC.validate_atlas(atlas, FIXTURE_ROOT)
119
+ check(not result["ready"] and any(item["code"] == "region_overlap" for item in result["errors"]), "overlapping atlas regions must block")
120
+
121
+ atlas = read_json("hero-atlas-contract.json")
122
+ with tempfile.TemporaryDirectory() as td:
123
+ root = Path(td)
124
+ pixels = solid(16, 8, (0, 0, 0, 0))
125
+ for y in range(1, 7):
126
+ for x in list(range(1, 7)) + list(range(9, 15)):
127
+ pixels[y * 16 + x] = (200, 100, 80, 255)
128
+ pixels[0] = (255, 255, 255, 255)
129
+ contaminated = root / "atlas.png"
130
+ write_rgba_png(contaminated, 16, 8, pixels)
131
+ atlas["image"] = str(contaminated)
132
+ result = AC.validate_atlas(atlas, root)
133
+ check(not result["ready"] and any(item["code"] == "atlas_contamination" for item in result["errors"]), "atlas pixels outside regions must block")
134
+
135
+
136
+ def test_layered_map_order_seam_and_bounds() -> None:
137
+ layered_map = read_json("forest-layered-map.json")
138
+ layered_map["layers"][1]["z_index"] = 0
139
+ result = AC.validate_layered_map(layered_map, FIXTURE_ROOT)
140
+ check(not result["ready"] and any(item["code"] == "duplicate_z_index" for item in result["errors"]), "duplicate z-order must block")
141
+
142
+ layered_map = read_json("forest-layered-map.json")
143
+ layered_map["layers"][1]["parallax"]["x"] = 0.1
144
+ warning = AC.validate_layered_map(layered_map, FIXTURE_ROOT)
145
+ check(warning["ready"] and any(item["code"] == "parallax_order_drift" for item in warning["warnings"]), "non-monotonic parallax must be visible as a warning")
146
+ blocked = AC.validate_layered_map(layered_map, FIXTURE_ROOT, strict=True)
147
+ check(not blocked["ready"] and any(item["code"] == "parallax_order_drift" for item in blocked["errors"]), "strict parallax drift must block")
148
+
149
+ layered_map = read_json("forest-layered-map.json")
150
+ layered_map["camera"]["safe_bounds"] = {"x": 120, "y": 60, "width": 32, "height": 18}
151
+ result = AC.validate_layered_map(layered_map, FIXTURE_ROOT)
152
+ check(not result["ready"] and any(item["code"] == "camera_safe_bounds_outside_world" for item in result["errors"]), "camera safe bounds outside world must block")
153
+
154
+ with tempfile.TemporaryDirectory() as td:
155
+ root = Path(td)
156
+ pixels = solid(4, 4, (0, 0, 0, 255))
157
+ for y in range(4):
158
+ pixels[y * 4 + 3] = (255, 255, 255, 255)
159
+ seam_image = root / "seam.png"
160
+ write_rgba_png(seam_image, 4, 4, pixels)
161
+ seam_map = read_json("forest-layered-map.json")
162
+ seam_map["layers"] = [copy.deepcopy(seam_map["layers"][0])]
163
+ seam_map["layers"][0]["image"] = str(seam_image)
164
+ result = AC.validate_layered_map(seam_map, root)
165
+ check(not result["ready"] and any(item["code"] == "horizontal_seam" for item in result["errors"]), "tileable horizontal seam must block")
166
+
167
+
168
+ def test_missing_file_and_cli_surface() -> None:
169
+ geometry = read_json("hero-walk-frame-geometry.json")
170
+ geometry["frames"][0]["image"] = "does-not-exist.png"
171
+ result = AC.validate_frame_geometry(geometry, FIXTURE_ROOT)
172
+ check(not result["ready"] and any(item["code"] == "image_unreadable" for item in result["errors"]), "missing image must fail closed")
173
+
174
+ cli = subprocess.run([
175
+ "node", str(ROOT / "bin/motionloom.mjs"), "asset-consistency", "validate",
176
+ "--kind", "layered-map", "--input", str(FIXTURE_ROOT / "forest-layered-map.json"),
177
+ "--root", str(FIXTURE_ROOT), "--json",
178
+ ], cwd=ROOT, capture_output=True, text=True)
179
+ data = json.loads(cli.stdout)
180
+ check(cli.returncode == 0 and data.get("ready") is True and data.get("contract") == "layered_map", "CLI must expose the consistency validator")
181
+
182
+
183
+ def main() -> int:
184
+ test_pass_fixtures()
185
+ test_identity_and_action_fail_closed()
186
+ test_frame_contamination_and_pivot_drift()
187
+ test_atlas_overlap_and_contamination()
188
+ test_layered_map_order_seam_and_bounds()
189
+ test_missing_file_and_cli_surface()
190
+ print("asset consistency contract tests: PASS")
191
+ return 0
192
+
193
+
194
+ if __name__ == "__main__":
195
+ try:
196
+ raise SystemExit(main())
197
+ except Exception as exc:
198
+ print(f"asset consistency contract tests: FAIL — {exc}", file=sys.stderr)
199
+ raise
@@ -0,0 +1,105 @@
1
+ import copy
2
+ import importlib.util
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+ from types import SimpleNamespace
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+ MODULE_PATH = ROOT / "scripts" / "rig-compatibility.py"
13
+ REGISTRY = ROOT / "rig-adapter-registry.json"
14
+ FIXTURE = ROOT / "examples/agent-consumer/rig-compatibility/hero-walk-fixture-rig.json"
15
+
16
+
17
+ def load_module():
18
+ spec = importlib.util.spec_from_file_location("rig_compatibility", MODULE_PATH)
19
+ if spec is None or spec.loader is None:
20
+ raise RuntimeError("cannot load rig compatibility")
21
+ module = importlib.util.module_from_spec(spec)
22
+ sys.modules[spec.name] = module
23
+ spec.loader.exec_module(module)
24
+ return module
25
+
26
+
27
+ MODULE = load_module()
28
+
29
+
30
+ def check(condition: bool, message: str) -> None:
31
+ if not condition:
32
+ raise AssertionError(message)
33
+
34
+
35
+ def write(path: Path, document: dict) -> Path:
36
+ path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
37
+ return path
38
+
39
+
40
+ def evaluate(path: Path, strict: bool = False) -> dict:
41
+ return MODULE.validate(SimpleNamespace(root=ROOT, input=path, registry=REGISTRY, strict=strict))
42
+
43
+
44
+ def main() -> int:
45
+ with tempfile.TemporaryDirectory(dir=ROOT) as td:
46
+ temp = Path(td)
47
+ fixture = json.loads(FIXTURE.read_text(encoding="utf-8"))
48
+ valid = evaluate(FIXTURE)
49
+ check(valid["ready"] and valid["status"] == "review_required", "public fixture must be contract-valid and review-required")
50
+ check(not valid["runtime_verified"] and not valid["production_eligible"] and not valid["production_approved"], "valid rig evidence must not self-grant runtime or production authority")
51
+
52
+ strict = evaluate(FIXTURE, strict=True)
53
+ check(not strict["ready"] and any(item["code"] == "runtime_evidence_required" for item in strict["errors"]), "strict rig validation must require matching runtime evidence")
54
+
55
+ bad_hash = copy.deepcopy(fixture)
56
+ bad_hash["source"]["sha256"] = "0" * 64
57
+ bad_hash_path = write(temp / "bad-hash.json", bad_hash)
58
+ hash_result = evaluate(bad_hash_path)
59
+ check(not hash_result["ready"] and any(item["code"] == "sha256_mismatch" for item in hash_result["errors"]), "source hash tampering must fail closed")
60
+
61
+ duplicate_socket = copy.deepcopy(fixture)
62
+ duplicate_socket["sockets"].append(dict(duplicate_socket["sockets"][0]))
63
+ duplicate_socket_path = write(temp / "duplicate-socket.json", duplicate_socket)
64
+ socket_result = evaluate(duplicate_socket_path)
65
+ check(not socket_result["ready"] and any(item["code"] == "invalid_socket" for item in socket_result["errors"]), "duplicate sockets must fail")
66
+
67
+ missing_event = copy.deepcopy(fixture)
68
+ missing_event["actions"][0]["events"] = ["footstep.left"]
69
+ missing_event_path = write(temp / "missing-event.json", missing_event)
70
+ event_result = evaluate(missing_event_path)
71
+ check(not event_result["ready"] and any(item["code"] == "required_event_missing" for item in event_result["errors"]), "required rig events must be explicit")
72
+
73
+ unknown_adapter = copy.deepcopy(fixture)
74
+ unknown_adapter["runtime"]["adapter_id"] = "unknown.adapter"
75
+ unknown_adapter_path = write(temp / "unknown-adapter.json", unknown_adapter)
76
+ adapter_result = evaluate(unknown_adapter_path)
77
+ check(not adapter_result["ready"] and any(item["code"] == "unknown_adapter" for item in adapter_result["errors"]), "unknown rig adapter must fail closed")
78
+
79
+ no_review = copy.deepcopy(fixture)
80
+ no_review["runtime"]["review_required"] = False
81
+ no_review_path = write(temp / "no-review.json", no_review)
82
+ review_result = evaluate(no_review_path)
83
+ check(not review_result["ready"] and any(item["code"] == "review_required" for item in review_result["errors"]), "rig contract cannot suppress human review")
84
+
85
+ evidence = {
86
+ "schema_version": "1.1", "run_id": "rig-fixture", "generated_at": "2026-08-15T00:00:00Z",
87
+ "mode": "runtime", "harness": "fixture", "status": "pass",
88
+ "frameworks": [{"framework": "sprite", "status": "pass", "ready": True, "frames": [0, 50, 100]}]
89
+ }
90
+ evidence_path = write(temp / "runtime-evidence.json", evidence)
91
+ verified = copy.deepcopy(fixture)
92
+ verified["runtime"]["runtime_evidence_ref"] = f"{temp.name}/{evidence_path.name}"
93
+ verified_path = write(temp / "verified.json", verified)
94
+ verified_result = evaluate(verified_path, strict=True)
95
+ check(verified_result["ready"] and verified_result["runtime_verified"], "strict rig pass requires a matching ready runtime evidence artifact")
96
+
97
+ cli = subprocess.run([sys.executable, str(MODULE_PATH), "validate", "--root", str(ROOT), "--input", str(FIXTURE), "--registry", str(REGISTRY), "--json"], capture_output=True, text=True)
98
+ cli_result = json.loads(cli.stdout)
99
+ check(cli.returncode == 0 and cli_result["status"] == "review_required", "rig CLI must return evidence-only review-required state")
100
+ print("rig compatibility contract tests: PASS")
101
+ return 0
102
+
103
+
104
+ if __name__ == "__main__":
105
+ raise SystemExit(main())