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,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())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Regression tests for the fail-closed Rive package gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_gate():
|
|
19
|
+
spec = importlib.util.spec_from_file_location("test_rive_package_gate", ROOT / "scripts" / "rive-package-gate.py")
|
|
20
|
+
assert spec and spec.loader
|
|
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
|
+
GATE = load_gate()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sha(path: Path) -> str:
|
|
31
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RivePackageGateTests(unittest.TestCase):
|
|
35
|
+
def build_bundle(self, directory: Path) -> tuple[Path, Path]:
|
|
36
|
+
source = directory / "hero.riv"
|
|
37
|
+
source.write_bytes(b"RIVE\x07\x00demo-real-test-bytes")
|
|
38
|
+
runtime = directory / "runtime.json"
|
|
39
|
+
runtime.write_text(json.dumps({"schema_version": "1.0", "run_id": "rive-test", "generated_at": "2026-08-15T00:00:00Z", "mode": "runtime", "harness": "test", "source_sha256": sha(source), "status": "pass", "frameworks": [{"run_id": "rive-test", "framework": "rive", "runtime": "rive-web", "status": "pass", "ready": True, "frames": [0, 50, 100]}]}), encoding="utf-8")
|
|
40
|
+
provenance = directory / "provenance.json"
|
|
41
|
+
provenance.write_text(json.dumps({"schema_version": "1.0", "provenance_id": "test-rive", "asset": {"id": "hero-rive", "path": "hero.riv", "type": "character", "framework": "rive"}, "authority": "ai_generated", "readiness": "runtime_ready", "generator": {"model": "internal-imagegen", "task_id": "test", "source": "internal", "generated_at": "2026-08-15T00:00:00Z"}, "files": [{"path": "hero.riv", "role": "runtime-package", "sha256": sha(source), "bytes": source.stat().st_size}], "license": {"spdx": "LicenseRef-AI-Pilot", "source": "internal-imagegen", "attribution": "AI-generated pilot; human review required"}, "provenance_chain": [{"step": "generated", "actor": "agent", "source": "internal-imagegen", "timestamp": "2026-08-15T00:00:00Z"}], "created_at": "2026-08-15T00:00:00Z"}), encoding="utf-8")
|
|
42
|
+
manifest = directory / "manifest.json"
|
|
43
|
+
manifest.write_text(json.dumps({"schema_version": "0.1", "package_id": "hero-rive-package", "package_class": "ai_generated_pilot", "asset": {"id": "hero-rive", "path": "hero.riv", "sha256": sha(source), "bytes": source.stat().st_size}, "provenance_ref": "provenance.json", "runtime": {"adapter_id": "motionloom.rive-runtime", "target": "rive", "required_state_machines": ["Locomotion"], "required_inputs": ["speed"], "required_events": ["footstep"], "runtime_evidence_ref": "runtime.json", "runtime_evidence_sha256": sha(runtime), "review_required": True}, "actions": ["idle", "walk"], "review_required": True}), encoding="utf-8")
|
|
44
|
+
registry = directory / "registry.json"
|
|
45
|
+
adapter_source = directory / "adapter.mjs"
|
|
46
|
+
adapter_source.write_text("export const rive = true;\n", encoding="utf-8")
|
|
47
|
+
registry.write_text(json.dumps({"schema_version": "0.1", "adapters": [{"adapter_id": "motionloom.rive-runtime", "target": "rive", "framework": "rive", "status": "project_integrated", "evidence": [{"path": "adapter.mjs", "sha256": sha(adapter_source)}]}]}), encoding="utf-8")
|
|
48
|
+
return manifest, registry
|
|
49
|
+
|
|
50
|
+
def validate(self, manifest: Path, registry: Path, strict: bool = True):
|
|
51
|
+
return GATE.validate(argparse.Namespace(input=manifest, root=manifest.parent, registry=registry, strict=strict))
|
|
52
|
+
|
|
53
|
+
def test_valid_ai_pilot_is_runtime_test_ready_but_not_approved(self):
|
|
54
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
55
|
+
manifest, registry = self.build_bundle(Path(raw))
|
|
56
|
+
result = self.validate(manifest, registry)
|
|
57
|
+
self.assertEqual(result["status"], "pass")
|
|
58
|
+
self.assertTrue(result["runtime_test_ready"])
|
|
59
|
+
self.assertTrue(result["review_required"])
|
|
60
|
+
self.assertFalse(result["production_approved"])
|
|
61
|
+
|
|
62
|
+
def test_rejects_tampered_riv_bytes(self):
|
|
63
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
64
|
+
manifest, registry = self.build_bundle(Path(raw))
|
|
65
|
+
(Path(raw) / "hero.riv").write_bytes(b"RIVETwitter")
|
|
66
|
+
result = self.validate(manifest, registry)
|
|
67
|
+
self.assertEqual(result["status"], "blocked")
|
|
68
|
+
self.assertTrue(any(item["code"] == "asset_sha256_mismatch" for item in result["issues"]))
|
|
69
|
+
|
|
70
|
+
def test_strict_rejects_missing_runtime_proof(self):
|
|
71
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
72
|
+
manifest, registry = self.build_bundle(Path(raw))
|
|
73
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
74
|
+
data["runtime"].pop("runtime_evidence_ref")
|
|
75
|
+
data["runtime"].pop("runtime_evidence_sha256")
|
|
76
|
+
manifest.write_text(json.dumps(data), encoding="utf-8")
|
|
77
|
+
result = self.validate(manifest, registry)
|
|
78
|
+
self.assertEqual(result["status"], "blocked")
|
|
79
|
+
self.assertTrue(any(item["code"] == "runtime_evidence_required" for item in result["issues"]))
|
|
80
|
+
|
|
81
|
+
def test_rejects_suppressed_review_and_unknown_adapter(self):
|
|
82
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
83
|
+
manifest, registry = self.build_bundle(Path(raw))
|
|
84
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
85
|
+
data["review_required"] = False
|
|
86
|
+
data["runtime"]["adapter_id"] = "untrusted-rive"
|
|
87
|
+
manifest.write_text(json.dumps(data), encoding="utf-8")
|
|
88
|
+
result = self.validate(manifest, registry)
|
|
89
|
+
self.assertEqual(result["status"], "blocked")
|
|
90
|
+
codes = {item["code"] for item in result["issues"]}
|
|
91
|
+
self.assertIn("review_required", codes)
|
|
92
|
+
self.assertIn("unknown_adapter", codes)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
runner = unittest.TextTestRunner(verbosity=2)
|
|
97
|
+
result = runner.run(unittest.defaultTestLoader.loadTestsFromTestCase(RivePackageGateTests))
|
|
98
|
+
if result.wasSuccessful():
|
|
99
|
+
print("rive package gate contract tests: PASS")
|
|
100
|
+
raise SystemExit(0)
|
|
101
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import importlib.util
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
13
|
+
MODULE_PATH = ROOT / "scripts" / "runtime-candidate.py"
|
|
14
|
+
CANDIDATE_PATH = ROOT / "examples/agent-consumer/runtime-candidate/hero-walk-candidate.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_module():
|
|
18
|
+
spec = importlib.util.spec_from_file_location("runtime_candidate", MODULE_PATH)
|
|
19
|
+
if spec is None or spec.loader is None:
|
|
20
|
+
raise RuntimeError("cannot load runtime candidate")
|
|
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 evaluate(path: Path, strict: bool = False) -> dict:
|
|
36
|
+
return MODULE.validate_candidate(SimpleNamespace(root=ROOT, input=path, strict=strict))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def write(path: Path, document: dict) -> Path:
|
|
40
|
+
path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
|
|
41
|
+
return path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> int:
|
|
45
|
+
with tempfile.TemporaryDirectory(dir=ROOT) as td:
|
|
46
|
+
temp = Path(td)
|
|
47
|
+
candidate = json.loads(CANDIDATE_PATH.read_text(encoding="utf-8"))
|
|
48
|
+
public = evaluate(CANDIDATE_PATH)
|
|
49
|
+
check(public["runtime_test_ready"] and public["status"] == "review_required", "hash-bound public candidate must be runtime-test-ready and review-required")
|
|
50
|
+
check(not public["runtime_verified"], "candidate without runtime evidence must not self-assert runtime verification")
|
|
51
|
+
check(not public["production_eligible"] and not public["production_approved"], "candidate bridge must never grant production authority")
|
|
52
|
+
|
|
53
|
+
strict = evaluate(CANDIDATE_PATH, strict=True)
|
|
54
|
+
check(not strict["runtime_test_ready"] and any(item["code"] == "runtime_evidence_required" for item in strict["errors"]), "strict candidate validation must require passing runtime evidence")
|
|
55
|
+
|
|
56
|
+
wrong_asset = copy.deepcopy(candidate)
|
|
57
|
+
wrong_asset["asset_id"] = "character/not-hero"
|
|
58
|
+
wrong_asset_path = write(temp / "wrong-asset.json", wrong_asset)
|
|
59
|
+
mismatch = evaluate(wrong_asset_path)
|
|
60
|
+
check(not mismatch["runtime_test_ready"] and any(item["code"] in {"asset_id_mismatch", "consistency_asset_id_mismatch"} for item in mismatch["errors"]), "candidate asset ID must bind receipt, export and consistency contracts")
|
|
61
|
+
|
|
62
|
+
missing_geometry = copy.deepcopy(candidate)
|
|
63
|
+
del missing_geometry["consistency"]["frame_geometry"]
|
|
64
|
+
missing_geometry_path = write(temp / "missing-geometry.json", missing_geometry)
|
|
65
|
+
geometry = evaluate(missing_geometry_path)
|
|
66
|
+
check(not geometry["runtime_test_ready"] and any(item["code"] == "missing_profile_contract" for item in geometry["errors"]), "frame sequence candidate must require frame geometry evidence")
|
|
67
|
+
|
|
68
|
+
no_review = copy.deepcopy(candidate)
|
|
69
|
+
no_review["runtime"]["review_required"] = False
|
|
70
|
+
no_review_path = write(temp / "no-review.json", no_review)
|
|
71
|
+
review = evaluate(no_review_path)
|
|
72
|
+
check(not review["runtime_test_ready"] and any(item["code"] == "review_required" for item in review["errors"]), "candidate cannot suppress human review")
|
|
73
|
+
|
|
74
|
+
evidence = {
|
|
75
|
+
"schema_version": "1.0", "run_id": "fixture-runtime", "generated_at": "2026-08-15T00:00:00Z",
|
|
76
|
+
"mode": "runtime", "harness": "fixture", "status": "pass",
|
|
77
|
+
"frameworks": [{"run_id": "fixture-runtime", "framework": "sprite", "runtime": "fixture", "status": "pass", "ready": True, "frames": [0, 50, 100]}]
|
|
78
|
+
}
|
|
79
|
+
evidence_path = temp / "runtime-evidence.json"
|
|
80
|
+
write(evidence_path, evidence)
|
|
81
|
+
verified_candidate = copy.deepcopy(candidate)
|
|
82
|
+
verified_candidate["runtime"]["runtime_evidence_ref"] = f"{temp.name}/runtime-evidence.json"
|
|
83
|
+
verified_path = write(temp / "verified.json", verified_candidate)
|
|
84
|
+
verified = evaluate(verified_path, strict=True)
|
|
85
|
+
check(verified["runtime_test_ready"] and verified["runtime_verified"], "strict candidate may be runtime-verified only with passing evidence")
|
|
86
|
+
|
|
87
|
+
cli = subprocess.run([sys.executable, str(MODULE_PATH), "validate", "--root", str(ROOT), "--input", str(CANDIDATE_PATH), "--json"], capture_output=True, text=True)
|
|
88
|
+
cli_result = json.loads(cli.stdout)
|
|
89
|
+
check(cli.returncode == 0 and cli_result["status"] == "review_required", "CLI must expose review-required candidate evidence")
|
|
90
|
+
print("runtime candidate contract tests: PASS")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
raise SystemExit(main())
|