motionloom 2.6.0 → 2.7.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 (71) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +68 -0
  3. package/CONTRIBUTING.md +3 -1
  4. package/README.md +20 -4
  5. package/SECURITY.md +5 -5
  6. package/SKILL.md +15 -5
  7. package/agent-card.json +77 -14
  8. package/agent-surfaces.json +30 -7
  9. package/artifact-adapter-registry.json +568 -20
  10. package/bin/motionloom.mjs +25 -2
  11. package/capability-registry.json +58 -234
  12. package/dev-lab/public/devlab.js +36 -3
  13. package/dev-lab/public/index.html +6 -2
  14. package/docs/ACTION-SEPARATION.md +104 -0
  15. package/docs/ASSET-GENERATION-PLANNER.md +101 -0
  16. package/docs/BRANCH-PROTECTION.md +44 -0
  17. package/docs/EXTERNAL-CORPUS.md +26 -0
  18. package/docs/STATUS.md +3 -2
  19. package/docs/audits/field-test-after-hardening-2026-08-21.md +25 -0
  20. package/docs/releases/2.6.1.md +57 -0
  21. package/docs/releases/2.7.0.md +98 -0
  22. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.00.json +32 -0
  23. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.01.json +32 -0
  24. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.02.json +32 -0
  25. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.03.json +32 -0
  26. package/examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json +81 -0
  27. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.00.json +26 -0
  28. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.01.json +26 -0
  29. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.02.json +26 -0
  30. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.03.json +26 -0
  31. package/examples/agent-consumer/asset-planning/pixellab-hero-256x448-request.json +43 -0
  32. package/examples/agent-consumer/devlab-live-sprite/README.md +30 -0
  33. package/examples/agent-consumer/devlab-live-sprite/devlab-runtime.json +61 -0
  34. package/examples/agent-consumer/devlab-live-sprite/frames/idle-00.png +0 -0
  35. package/examples/agent-consumer/devlab-live-sprite/frames/idle-01.png +0 -0
  36. package/examples/agent-consumer/devlab-live-sprite/frames/idle-02.png +0 -0
  37. package/examples/agent-consumer/devlab-live-sprite/frames/reverse-00.png +0 -0
  38. package/examples/agent-consumer/devlab-live-sprite/frames/reverse-01.png +0 -0
  39. package/examples/agent-consumer/devlab-live-sprite/frames/reverse-02.png +0 -0
  40. package/examples/agent-consumer/frame-generation-lock/hero-walk-lock.json +97 -0
  41. package/package.json +23 -9
  42. package/references/multi-frame-asset-generation.md +144 -0
  43. package/schemas/action-separation-verifier-evidence.schema.json +43 -0
  44. package/schemas/action-sequence-manifest.schema.json +48 -0
  45. package/schemas/artifact-adapter-registry.schema.json +1 -1
  46. package/schemas/asset-adaptation.schema.json +21 -0
  47. package/schemas/asset-generation-plan.schema.json +77 -0
  48. package/schemas/asset-generation-request.schema.json +104 -0
  49. package/schemas/frame-envelope.schema.json +62 -0
  50. package/schemas/frame-generation-lock.schema.json +189 -0
  51. package/scripts/action-separation.py +410 -0
  52. package/scripts/asset-adapt.mjs +92 -0
  53. package/scripts/asset-generation-plan.py +613 -0
  54. package/scripts/browser_review_consistency.py +64 -0
  55. package/scripts/fetch-project-corpus.py +91 -0
  56. package/scripts/frame-generation-lock.py +358 -0
  57. package/scripts/frame-set-preflight.py +264 -0
  58. package/scripts/package-consumer-smoke.mjs +20 -0
  59. package/scripts/quality-gate.py +7 -0
  60. package/scripts/release-verify.py +25 -0
  61. package/scripts/report-contract.py +1 -1
  62. package/scripts/report.py +19 -4
  63. package/scripts/resolve-task-bundle.py +11 -3
  64. package/scripts/review-hook.py +51 -2
  65. package/scripts/skill-doctor.py +42 -0
  66. package/src/output/browser-review-smoke/browser-review.json +6 -6
  67. package/tests/scripts/run_tests.py +45 -2
  68. package/tests/scripts/test_asset_adapt.py +47 -0
  69. package/tests/scripts/test_asset_consistency.py +77 -4
  70. package/tests/scripts/test_asset_generation_plan.py +250 -0
  71. package/tests/scripts/test_attestation.py +24 -8
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schema_version": "1.0",
3
- "candidate_id": "88a2f2f18ba45a07f56e",
4
- "task_id": "professional-review-e2e",
3
+ "candidate_id": "956caeb2f56397430c4f",
4
+ "task_id": "browser-review-smoke-task",
5
5
  "scene": "browser-review-smoke",
6
- "url": "https://3300-i8isn2ocahcvfq0jk51ta-68c15abe.sg1.manus.computer/?scene=browser-review-smoke&task_id=professional-review-e2e&candidate_id=88a2f2f18ba45a07f56e&artifact_base=https%3A%2F%2F3300-i8isn2ocahcvfq0jk51ta-68c15abe.sg1.manus.computer%2Fscenes%2Fbrowser-review-smoke&task_base=https%3A%2F%2F3300-i8isn2ocahcvfq0jk51ta-68c15abe.sg1.manus.computer%2Ftasks%2Fprofessional-review-e2e",
6
+ "url": "http://127.0.0.1:3300/?scene=browser-review-smoke&task_id=browser-review-smoke-task&candidate_id=956caeb2f56397430c4f",
7
7
  "status": "approved",
8
8
  "context_sha256": "cbbd43a53a0ddd5cf5452df5f6d521f06829465bd58c6d764a59d501ed1710fa",
9
9
  "source_sha256": "c5ab312427678b17ae903d280d89db9d202670f031f10ad6fe774fc9aaecee8a",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "review_artifact": "review.json",
17
17
  "requires_user_approval": true,
18
- "prepared_at": "2026-08-12T17:42:31Z",
19
- "expires_at": "2026-08-13T17:42:31Z",
20
- "reviewed_at": "2026-08-12T17:48:54Z"
18
+ "prepared_at": "2026-08-12T14:29:17Z",
19
+ "expires_at": "2099-01-01T00:00:00Z",
20
+ "reviewed_at": "2026-08-12T14:32:51Z"
21
21
  }
@@ -511,6 +511,9 @@ def test_approved_browser_review_e2e_contract():
511
511
  candidate = json.loads(candidate_path.read_text())
512
512
  candidate["expires_at"] = "2099-01-01T00:00:00Z"
513
513
  candidate_path.write_text(json.dumps(candidate, indent=2) + "\n")
514
+ scene_candidate_path = root / "src/output/browser-review-smoke/browser-review.json"
515
+ scene_candidate = dict(candidate)
516
+ scene_candidate_path.write_text(json.dumps(scene_candidate, indent=2) + "\n")
514
517
 
515
518
  review = json.loads((task_dir / "review.json").read_text())
516
519
  task = json.loads((task_dir / "task.json").read_text())
@@ -524,7 +527,7 @@ def test_approved_browser_review_e2e_contract():
524
527
 
525
528
  review_hook = subprocess.run([
526
529
  sys.executable, str(ROOT / "scripts/review-hook.py"), "validate",
527
- "--task-dir", str(task_dir), "--require-approved",
530
+ "--task-dir", str(task_dir), "--root", str(root), "--require-approved",
528
531
  ], capture_output=True, text=True)
529
532
  check("e2e review hook accepts approved candidate", review_hook.returncode == 0, review_hook.stdout.strip())
530
533
 
@@ -536,7 +539,7 @@ def test_approved_browser_review_e2e_contract():
536
539
  check("e2e quality gate accepts task evidence", quality.returncode == 0, quality.stdout.strip())
537
540
 
538
541
  report_check = subprocess.run([
539
- sys.executable, str(ROOT / "scripts/report.py"), "check", "--task-dir", str(task_dir),
542
+ sys.executable, str(ROOT / "scripts/report.py"), "check", "--task-dir", str(task_dir), "--root", str(root),
540
543
  ], capture_output=True, text=True)
541
544
  check("e2e report contract accepts confirmed task", report_check.returncode == 0, report_check.stdout.strip())
542
545
 
@@ -1162,6 +1165,46 @@ if __name__ == "__main__":
1162
1165
  test_category_coverage()
1163
1166
  test_observability_contract()
1164
1167
  test_quality_workflow_rebuilds_replay_after_generated_artifacts()
1168
+ browser_review_consistency_tests = subprocess.run(
1169
+ [sys.executable, str(ROOT / "tests/scripts/test_browser_review_consistency.py")],
1170
+ capture_output=True,
1171
+ text=True,
1172
+ )
1173
+ check(
1174
+ "browser review scene/task candidate consistency is fail-closed",
1175
+ browser_review_consistency_tests.returncode == 0 and "browser review consistency tests: PASS" in browser_review_consistency_tests.stdout,
1176
+ browser_review_consistency_tests.stdout.strip() or browser_review_consistency_tests.stderr.strip(),
1177
+ )
1178
+ action_separation_tests = subprocess.run(
1179
+ [sys.executable, str(ROOT / "tests/scripts/test_action_separation.py")],
1180
+ capture_output=True,
1181
+ text=True,
1182
+ )
1183
+ check(
1184
+ "action-scoped frame manifests reject cross-action and ambiguous frames",
1185
+ action_separation_tests.returncode == 0 and "action separation tests: PASS" in action_separation_tests.stdout,
1186
+ action_separation_tests.stdout.strip() or action_separation_tests.stderr.strip(),
1187
+ )
1188
+ asset_generation_plan_tests = subprocess.run(
1189
+ [sys.executable, str(ROOT / "tests/scripts/test_asset_generation_plan.py")],
1190
+ capture_output=True,
1191
+ text=True,
1192
+ )
1193
+ check(
1194
+ "asset planner recommends explicit provider and canvas adaptations",
1195
+ asset_generation_plan_tests.returncode == 0 and "asset generation planner contract tests: PASS" in asset_generation_plan_tests.stdout,
1196
+ asset_generation_plan_tests.stdout.strip() or asset_generation_plan_tests.stderr.strip(),
1197
+ )
1198
+ asset_adapt_tests = subprocess.run(
1199
+ [sys.executable, str(ROOT / "tests/scripts/test_asset_adapt.py")],
1200
+ capture_output=True,
1201
+ text=True,
1202
+ )
1203
+ check(
1204
+ "asset adaptation preserves target geometry without crop or stretch",
1205
+ asset_adapt_tests.returncode == 0 and "asset adaptation contract tests: PASS" in asset_adapt_tests.stdout,
1206
+ asset_adapt_tests.stdout.strip() or asset_adapt_tests.stderr.strip(),
1207
+ )
1165
1208
  print()
1166
1209
  if FAILED:
1167
1210
  print(f"{len(FAILED)} test(s) FAILED: {', '.join(FAILED)}")
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ ROOT = Path(__file__).resolve().parents[2]
10
+ SCRIPT = ROOT / "scripts" / "asset-adapt.mjs"
11
+ SOURCE = ROOT / "examples/agent-consumer/asset-consistency/assets/hero-frame-00.png"
12
+
13
+
14
+ def check(condition: bool, message: str) -> None:
15
+ if not condition:
16
+ raise AssertionError(message)
17
+
18
+
19
+ def main() -> int:
20
+ with tempfile.TemporaryDirectory() as td:
21
+ root = Path(td)
22
+ output = root / "padded.png"
23
+ report = root / "adaptation.json"
24
+ result = subprocess.run([
25
+ "node", str(SCRIPT), "pad", "--input", str(SOURCE), "--output", str(output),
26
+ "--width", "16", "--height", "16", "--scale", "1", "--anchor", "center",
27
+ "--report", str(report), "--json",
28
+ ], capture_output=True, text=True)
29
+ check(result.returncode == 0, result.stderr)
30
+ doc = json.loads(report.read_text())
31
+ check(doc["approval"] is False and doc["production_approved"] is False, "adaptation cannot grant approval")
32
+ check(doc["source"]["canvas"] == [8, 8] and doc["output"]["canvas"] == [16, 16], "adaptation must bind source/output geometry")
33
+ check(doc["transform"]["crop"] is False and doc["transform"]["stretch"] is False, "crop/stretch must remain false")
34
+ check(doc["transform"]["interpolation"] == "nearest-neighbour", "pixel art must use nearest-neighbour")
35
+ check(output.is_file() and output.stat().st_size > 0, "adapter must emit target PNG")
36
+
37
+ blocked = subprocess.run([
38
+ "node", str(SCRIPT), "pad", "--input", str(SOURCE), "--output", str(root / "blocked.png"),
39
+ "--width", "4", "--height", "4", "--json",
40
+ ], capture_output=True, text=True)
41
+ check(blocked.returncode != 0 and "crop is forbidden" in blocked.stderr, "oversized source must fail closed")
42
+ print("asset adaptation contract tests: PASS")
43
+ return 0
44
+
45
+
46
+ if __name__ == "__main__":
47
+ raise SystemExit(main())
@@ -18,19 +18,21 @@ from pathlib import Path
18
18
  ROOT = Path(__file__).resolve().parents[2]
19
19
  FIXTURE_ROOT = ROOT / "examples/agent-consumer/asset-consistency"
20
20
  ANALYZER_PATH = ROOT / "scripts/asset-consistency.py"
21
+ PREFLIGHT_PATH = ROOT / "scripts/frame-set-preflight.py"
21
22
 
22
23
 
23
- def load_analyzer():
24
- spec = importlib.util.spec_from_file_location("asset_consistency", ANALYZER_PATH)
24
+ def load_module(path: Path, name: str):
25
+ spec = importlib.util.spec_from_file_location(name, path)
25
26
  if spec is None or spec.loader is None:
26
- raise RuntimeError("cannot load asset consistency analyzer")
27
+ raise RuntimeError(f"cannot load {path.name}")
27
28
  module = importlib.util.module_from_spec(spec)
28
29
  sys.modules[spec.name] = module
29
30
  spec.loader.exec_module(module)
30
31
  return module
31
32
 
32
33
 
33
- AC = load_analyzer()
34
+ AC = load_module(ANALYZER_PATH, "asset_consistency")
35
+ PREFLIGHT = load_module(PREFLIGHT_PATH, "frame_set_preflight")
34
36
 
35
37
 
36
38
  def read_json(name: str) -> dict:
@@ -112,6 +114,65 @@ def test_frame_contamination_and_pivot_drift() -> None:
112
114
  check(not result["ready"] and any(item["code"] == "pivot_drift" for item in result["errors"]), "pivot drift must block")
113
115
 
114
116
 
117
+ def test_generated_frame_set_preflight() -> None:
118
+ geometry = read_json("hero-walk-frame-geometry.json")
119
+ result = PREFLIGHT.validate(geometry, FIXTURE_ROOT)
120
+ check(result["ready"], f"isolated generated frame fixture must pass preflight: {result}")
121
+ check(result.get("approval") is False, "preflight must never emit approval")
122
+
123
+ shared = read_json("hero-walk-frame-geometry.json")
124
+ shared["frames"][1]["image"] = shared["frames"][0]["image"]
125
+ shared["frames"][1]["sha256"] = shared["frames"][0]["sha256"]
126
+ result = PREFLIGHT.validate(shared, FIXTURE_ROOT)
127
+ check(
128
+ not result["ready"] and any(item["code"] == "shared_source_image" for item in result["errors"]),
129
+ "generated multi-frame source must reject a shared pose-sheet image",
130
+ )
131
+
132
+ guard = read_json("hero-walk-frame-geometry.json")
133
+ guard["frames"][0]["safe_rect"] = {"x": 2, "y": 1, "width": 4, "height": 5}
134
+ guard["frames"][0]["bleed_margin_px"] = 1
135
+ result = PREFLIGHT.validate(guard, FIXTURE_ROOT)
136
+ check(
137
+ not result["ready"] and any(item["code"] == "guard_band_violation" for item in result["errors"]),
138
+ "generated frame alpha must preserve the declared transparent guard band",
139
+ )
140
+
141
+ with tempfile.TemporaryDirectory() as td:
142
+ root = Path(td)
143
+ geometry = read_json("hero-walk-frame-geometry.json")
144
+ geometry["frames"] = [copy.deepcopy(geometry["frames"][0]), copy.deepcopy(geometry["frames"][1])]
145
+ geometry["invariants"]["bbox_drift_tolerance_px"] = 0
146
+ for frame in geometry["frames"]:
147
+ frame["rect"] = {"x": 0, "y": 0, "width": 8, "height": 8}
148
+ frame["safe_rect"] = {"x": 0, "y": 0, "width": 8, "height": 8}
149
+ frame["bleed_margin_px"] = 0
150
+
151
+ first_pixels = solid(8, 8, (0, 0, 0, 0))
152
+ for y in range(1, 6):
153
+ for x in range(2, 6):
154
+ first_pixels[y * 8 + x] = (120, 160, 220, 255)
155
+ second_pixels = solid(8, 8, (0, 0, 0, 0))
156
+ for y in range(1, 6):
157
+ for x in range(1, 7):
158
+ second_pixels[y * 8 + x] = (120, 160, 220, 255)
159
+ write_rgba_png(root / "frame-00.png", 8, 8, first_pixels)
160
+ write_rgba_png(root / "frame-01.png", 8, 8, second_pixels)
161
+
162
+ geometry["frames"][0]["image"] = "frame-00.png"
163
+ geometry["frames"][0]["alpha_bbox"] = {"x": 2, "y": 1, "width": 4, "height": 5}
164
+ geometry["frames"][0]["sha256"] = hashlib.sha256((root / "frame-00.png").read_bytes()).hexdigest()
165
+ geometry["frames"][1]["image"] = "frame-01.png"
166
+ geometry["frames"][1]["alpha_bbox"] = {"x": 1, "y": 1, "width": 6, "height": 5}
167
+ geometry["frames"][1]["sha256"] = hashlib.sha256((root / "frame-01.png").read_bytes()).hexdigest()
168
+
169
+ result = PREFLIGHT.validate(geometry, root)
170
+ check(
171
+ not result["ready"] and any(item["code"] == "bbox_drift" for item in result["errors"]),
172
+ "generated frame apparent-size drift beyond tolerance must block instead of warning",
173
+ )
174
+
175
+
115
176
  def test_atlas_overlap_and_contamination() -> None:
116
177
  atlas = read_json("hero-atlas-contract.json")
117
178
  atlas["regions"][1]["rect"]["x"] = 4
@@ -179,11 +240,23 @@ def test_missing_file_and_cli_surface() -> None:
179
240
  data = json.loads(cli.stdout)
180
241
  check(cli.returncode == 0 and data.get("ready") is True and data.get("contract") == "layered_map", "CLI must expose the consistency validator")
181
242
 
243
+ preflight_cli = subprocess.run([
244
+ sys.executable, str(PREFLIGHT_PATH),
245
+ "--input", str(FIXTURE_ROOT / "hero-walk-frame-geometry.json"),
246
+ "--root", str(FIXTURE_ROOT), "--json",
247
+ ], cwd=ROOT, capture_output=True, text=True)
248
+ preflight_data = json.loads(preflight_cli.stdout)
249
+ check(
250
+ preflight_cli.returncode == 0 and preflight_data.get("ready") is True and preflight_data.get("approval") is False,
251
+ "generated frame-set preflight CLI must pass isolated fixture without granting approval",
252
+ )
253
+
182
254
 
183
255
  def main() -> int:
184
256
  test_pass_fixtures()
185
257
  test_identity_and_action_fail_closed()
186
258
  test_frame_contamination_and_pivot_drift()
259
+ test_generated_frame_set_preflight()
187
260
  test_atlas_overlap_and_contamination()
188
261
  test_layered_map_order_seam_and_bounds()
189
262
  test_missing_file_and_cli_surface()
@@ -0,0 +1,250 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import importlib.util
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+ MODULE_PATH = ROOT / "scripts" / "asset-generation-plan.py"
13
+
14
+
15
+ def load_module():
16
+ spec = importlib.util.spec_from_file_location("asset_generation_plan", MODULE_PATH)
17
+ if spec is None or spec.loader is None:
18
+ raise RuntimeError("cannot load asset generation planner")
19
+ module = importlib.util.module_from_spec(spec)
20
+ sys.modules[spec.name] = module
21
+ spec.loader.exec_module(module)
22
+ return module
23
+
24
+
25
+ MODULE = load_module()
26
+
27
+
28
+ def check(condition: bool, message: str) -> None:
29
+ if not condition:
30
+ raise AssertionError(message)
31
+
32
+
33
+ def request(canvas=(256, 448), frame_count=8, isolation="required", preferences=None):
34
+ value = {
35
+ "schema_version": "0.1",
36
+ "request_id": "test-request",
37
+ "asset_id": "test-hero-attack",
38
+ "asset_kind": "frame_sequence",
39
+ "target": {
40
+ "canvas": {"width": canvas[0], "height": canvas[1]},
41
+ "frame_count": frame_count,
42
+ "fps": 8,
43
+ "alpha_mode": "straight",
44
+ "pixel_art": True,
45
+ "anchor": "footline",
46
+ },
47
+ "generation_policy": {
48
+ "frame_isolation": isolation,
49
+ "allow_crop": False,
50
+ "allow_stretch": False,
51
+ "allow_silent_resize": False,
52
+ "allow_provider_batch_as_provisional": True,
53
+ "integer_scale_only": True,
54
+ },
55
+ "actions": [{"action_id": "attack", "positive_cues": ["sword swing"], "negative_cues": ["walk", "jump"]}],
56
+ }
57
+ if preferences is not None:
58
+ value["provider_preferences"] = preferences
59
+ return value
60
+
61
+
62
+ def verified_adapter(canvas=(256, 448)):
63
+ return {
64
+ "adapter_id": "verified.single-frame",
65
+ "kind": "external_provider",
66
+ "status": "verified",
67
+ "adapter_version": "0.1",
68
+ "invocation_mode": "api",
69
+ "cost_class": "external",
70
+ "inputs": ["reference"],
71
+ "outputs": ["frame_sequence"],
72
+ "compatibility": {"os": ["linux"]},
73
+ "availability": {"status": "available", "environment": "test"},
74
+ "capabilities": {
75
+ "canvas": {"shapes": ["portrait" if canvas[1] > canvas[0] else "landscape" if canvas[0] > canvas[1] else "square"], "allowed_sizes": [[canvas[0], canvas[1]]], "max_width": canvas[0], "max_height": canvas[1]},
76
+ "frame_behavior": {"mode": "single_frame", "single_frame": True, "max_frames_per_request": 1},
77
+ "adaptation": [],
78
+ },
79
+ "evidence": [{"path": "evidence.md", "sha256": "a" * 64, "kind": "static"}],
80
+ "limitations": [],
81
+ "risk_level": "low",
82
+ "side_effect_level": "read",
83
+ }
84
+
85
+
86
+ def registry_with(adapters, require_verified=True, allow_scaffold_only=False):
87
+ return {
88
+ "schema_version": "0.1",
89
+ "registry_id": "test-registry",
90
+ "generated_at": "2026-08-22T00:00:00Z",
91
+ "selection_policy": {"require_verified": require_verified, "allow_scaffold_only": allow_scaffold_only},
92
+ "adapters": adapters,
93
+ }
94
+
95
+
96
+ def by_id(plan, adapter_id):
97
+ return next(item for item in plan["providers"] if item["adapter_id"] == adapter_id)
98
+
99
+
100
+ def main() -> int:
101
+ production_registry = json.loads((ROOT / "artifact-adapter-registry.json").read_text())
102
+ plan_schema = json.loads((ROOT / "schemas" / "asset-generation-plan.schema.json").read_text())
103
+ check(plan_schema["properties"]["producer"]["const"] == "MotionLoom" and plan_schema["properties"]["schema_version"]["const"] == "0.2", "published plan schema must bind MotionLoom identity and version")
104
+ with tempfile.TemporaryDirectory() as td:
105
+ root = Path(td)
106
+ request_path = root / "request.json"
107
+ request_path.write_text(json.dumps(request()))
108
+ registry_path = root / "registry.json"
109
+ registry_path.write_text(json.dumps(production_registry))
110
+
111
+ plan = MODULE.build_plan(request(), production_registry, root)
112
+ check(plan["contract"] == "motionloom-asset-generation-plan" and plan["schema_version"] == "0.2", "plan contract must identify MotionLoom plan 0.2")
113
+ check(plan["producer"] == "MotionLoom" and plan["identity"]["product"] == "MotionLoom", "structured plan must preserve MotionLoom identity")
114
+ check(plan["approval"] is False and plan["production_approved"] is False, "planner must never grant approval")
115
+ check(plan["decision"] == "recommendations_available", "normal planning must remain useful without verified execution provider")
116
+ check(plan["execution_decision"] == "no_execution_eligible_route", "default verified-only execution must remain fail-closed")
117
+ check(plan["selection"]["eligible_count"] == 0, "default registry must have no eligible execution provider")
118
+ check(plan["selection"]["provisional_count"] >= 4, "scaffold/manual routes must remain visible as provisional")
119
+ check(plan["selection"]["recommendation_count"] >= 3, "normal planning must expose useful ranked recommendations")
120
+ check("fixture.local-artifact-intake" not in plan["selection"]["recommendation_adapter_ids"], "regression fixture must not be recommended as a generation route")
121
+ check(plan["recommendations"], "normal planning must not collapse to no provider available")
122
+ check(all(item["recommendation_status"] in {"recommended", "acceptable"} for item in plan["recommendations"]), "recommendation list must exclude not-recommended routes")
123
+ check(all(item["approval"] is False for item in plan["recommendations"]), "recommendations must preserve approval=false")
124
+ check(plan["agent_guidance"]["recommended_by"] == "MotionLoom", "plan-level guidance must identify MotionLoom")
125
+ check(plan["project"]["runtime"] is None and plan["project"]["framework"] is None, "synthetic test request without project context should remain explicit")
126
+ check(any("MotionLoom" in step for step in plan["next_steps"]), "next steps must use MotionLoom workflow identity")
127
+
128
+ pix = by_id(plan, "pixellab.animate-skeleton")
129
+ check(pix["recommendation_status"] == "recommended" and pix["execution_status"] == "provisional", "PixelLab may be recommended while execution remains provisional")
130
+ check(pix["execution_eligible"] is False, "PixelLab must not be execution-eligible under verified-only policy")
131
+ check(pix["canvas"]["status"] == "adaptation_required", "PixelLab skeleton must reject non-square native target")
132
+ check(pix["frames"]["status"] == "provisional_batch_only", "batch PixelLab output must remain provisional under required isolation")
133
+ pad = next(option for option in pix["adaptation_options"] if option["id"] == "deterministic-pad-to-target")
134
+ check(pad["source_canvas"] == [256, 256] and pad["target_canvas"] == [256, 448], "planner must propose explicit 256x256 to 256x448 padding")
135
+ check(pad["crop"] is False and pad["stretch"] is False, "planner must forbid crop and stretch")
136
+ check("action-separation" in pad["requires_validation"], "adaptation must require action separation validation")
137
+ check(pix["availability"]["status"] == "unknown", "PixelLab availability must not be fabricated")
138
+ check(pix["agent_guidance"]["recommended_by"] == "MotionLoom", "route guidance must identify MotionLoom")
139
+
140
+ manual = by_id(plan, "manual.import-frame-sequence")
141
+ check(manual["recommendation_status"] in {"recommended", "acceptable"}, "manual fallback must remain a normal planning option")
142
+ check(manual["execution_status"] == "provisional", "manual route remains provisional without runtime evidence")
143
+ check(manual["availability"]["status"] == "known", "manual route availability should be explicit")
144
+
145
+ create_text = by_id(plan, "pixellab.create-animated-object-character")
146
+ check(create_text["frames"]["frame_count_policy"] == "depends_on_canvas_size", "PixelLab create-from-text frame policy must be dynamic")
147
+ create_text_adapter = next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.create-animated-object-character")
148
+ small_canvas_frames = MODULE.frame_assessment(create_text_adapter, MODULE.target_summary(request(canvas=(128, 128), frame_count=4)), request()["generation_policy"])
149
+ check(small_canvas_frames["max_frames_per_request"] == 4 and small_canvas_frames["limits_by_canvas"], "planner must resolve declared dynamic frame limit for an exact PixelLab canvas")
150
+ existing_text_adapter = next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.animate-with-text")
151
+ existing_text = by_id(plan, "pixellab.animate-with-text")
152
+ check(existing_text["frames"]["frame_count_policy"] == "fixed" and existing_text["frames"]["max_frames_per_request"] == 4, "PixelLab animate-existing-reference route must model its documented four-frame output")
153
+
154
+ preferred_request = request(preferences={"preferred_adapter_ids": ["pixellab.animate-skeleton"]})
155
+ preferred_plan = MODULE.build_plan(preferred_request, production_registry, root)
156
+ preferred_pix = by_id(preferred_plan, "pixellab.animate-skeleton")
157
+ check(preferred_pix["user_preference"] == {"state": "preferred", "requested": True}, "explicit user preference must be visible")
158
+ check(preferred_pix["recommendation_status"] == "recommended", "preferred provisional provider may be recommended")
159
+ check(preferred_pix["execution_status"] == "provisional" and preferred_pix["execution_eligible"] is False, "preference must not promote provisional execution")
160
+ check(any("preferred by the user" in reason for reason in preferred_pix["rationale"]), "rationale must explain preferred route")
161
+ check(preferred_pix["ranking_score"] >= 8 and any(item["adapter_id"] == "pixellab.animate-skeleton" for item in preferred_plan["recommendations"]), "preferred compatible provisional route should receive a strong explainable ranking signal without overriding a safer native route")
162
+
163
+ incompatible_request = request(preferences={"preferred_adapter_ids": ["internal.imagegen"]})
164
+ incompatible_plan = MODULE.build_plan(incompatible_request, production_registry, root)
165
+ incompatible = by_id(incompatible_plan, "internal.imagegen")
166
+ check(incompatible["user_preference"]["state"] == "preferred", "incompatible preference must remain visible")
167
+ check(incompatible["recommendation_status"] == "not_recommended" and incompatible["execution_status"] == "blocked", "hard-incompatible preferred route must be blocked")
168
+ check("internal.imagegen" not in incompatible_plan["selection"]["recommendation_adapter_ids"], "incompatible preferred route must not be recommended")
169
+ check(incompatible_plan["recommendations"], "safer alternatives must remain available")
170
+ check(any("does not declare canvas capability" in reason for reason in incompatible["rationale"]), "incompatibility rationale must explain missing canvas capability")
171
+
172
+ excluded_request = request(preferences={"excluded_adapter_ids": ["manual.import-frame-sequence"]})
173
+ excluded_plan = MODULE.build_plan(excluded_request, production_registry, root)
174
+ excluded = by_id(excluded_plan, "manual.import-frame-sequence")
175
+ check(excluded["user_preference"]["state"] == "excluded" and excluded["recommendation_status"] == "not_recommended", "excluded preference must be honored without affecting other routes")
176
+ check("manual.import-frame-sequence" not in excluded_plan["selection"]["recommendation_adapter_ids"], "excluded route must not be recommended")
177
+
178
+ unavailable_registry = registry_with([copy.deepcopy(next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.animate-skeleton"))], require_verified=False, allow_scaffold_only=True)
179
+ unavailable_registry["adapters"][0]["availability"] = {"status": "unavailable", "reason": "connector not configured"}
180
+ unavailable_plan = MODULE.build_plan(request(), unavailable_registry, root)
181
+ unavailable = unavailable_plan["providers"][0]
182
+ check(unavailable["recommendation_status"] == "recommended" and unavailable["execution_status"] == "blocked", "unavailable compatible route may remain a recommendation but cannot execute")
183
+ check(unavailable_plan["recommendations"][0]["route"] == "resolve_availability_first", "unavailable route must instruct the Agent to resolve availability")
184
+
185
+ unknown_preference = request(preferences={"preferred_adapter_ids": ["future.provider"]})
186
+ unknown_plan = MODULE.build_plan(unknown_preference, production_registry, root)
187
+ check(unknown_plan["selection"]["unknown_preferred_adapter_ids"] == ["future.provider"], "unknown preferred provider must be surfaced without fabrication")
188
+ check(any("future.provider" in warning for warning in unknown_plan["warnings"]), "unknown preference warning must be human-readable")
189
+
190
+ cli = subprocess.run([
191
+ sys.executable, str(MODULE_PATH), "plan", "--request", str(request_path), "--registry", str(registry_path), "--project-root", str(root), "--json", "--strict"
192
+ ], capture_output=True, text=True)
193
+ strict_output = json.loads(cli.stdout)
194
+ check(cli.returncode == 2 and strict_output["execution_decision"] == "no_execution_eligible_route", "strict mode must fail without execution-eligible provider")
195
+
196
+ human = subprocess.run([
197
+ sys.executable, str(MODULE_PATH), "plan", "--request", str(request_path), "--registry", str(registry_path), "--project-root", str(root)
198
+ ], capture_output=True, text=True)
199
+ check(human.returncode == 0 and "MotionLoom Project Assessment" in human.stdout and "MotionLoom Recommendations" in human.stdout and "MotionLoom Agent Guidance" in human.stdout, "human CLI must expose MotionLoom identity and guidance")
200
+
201
+ permitted = copy.deepcopy(production_registry)
202
+ permitted["selection_policy"] = {"require_verified": False, "allow_scaffold_only": True}
203
+ next(item for item in permitted["adapters"] if item["adapter_id"] == "pixellab.animate-skeleton")["availability"] = {"status": "available", "environment": "test-connector"}
204
+ permitted_plan = MODULE.build_plan(request(), permitted, root)
205
+ permitted_pix = by_id(permitted_plan, "pixellab.animate-skeleton")
206
+ check(permitted_pix["execution_status"] == "provisional" and permitted_pix["execution_eligible"] is True, "policy may authorize scaffold execution while preserving provisional status")
207
+ check(any(item["adapter_id"] == "pixellab.animate-skeleton" for item in permitted_plan["recommendations"]), "permitted scaffold provider may be ranked")
208
+
209
+ verified = verified_adapter()
210
+ verified_plan = MODULE.build_plan(request(), registry_with([verified]), root)
211
+ check(verified_plan["decision"] == "compatible_execution_route_available", "verified native provider must produce execution route")
212
+ check(verified_plan["selection"]["eligible_adapter_ids"] == ["verified.single-frame"], "verified provider must be execution-eligible")
213
+ check(verified_plan["recommendations"][0]["execution_status"] == "verified" and verified_plan["recommendations"][0]["route"] == "use_native", "verified native route must be recommended with verified status")
214
+
215
+ unknown = copy.deepcopy(production_registry)
216
+ unknown["adapters"] = [next(item for item in production_registry["adapters"] if item["adapter_id"] == "internal.imagegen")]
217
+ unknown_plan = MODULE.build_plan(request(), unknown, root)
218
+ unknown_item = unknown_plan["providers"][0]
219
+ check(unknown_plan["decision"] == "no_safe_recommendation_available", "unknown-only provider set must not produce a false recommendation")
220
+ check(unknown_item["execution_status"] == "blocked" and any("does not declare canvas capability" in warning for warning in unknown_item["warnings"]), "unknown capabilities must be blocked and surfaced")
221
+
222
+ oversized = copy.deepcopy(next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.animate-skeleton"))
223
+ oversized["adapter_id"] = "provider.oversized-square"
224
+ oversized["capabilities"]["canvas"]["allowed_sizes"] = [[384, 384]]
225
+ oversized_plan = MODULE.build_plan(request(), registry_with([oversized], require_verified=False, allow_scaffold_only=True), root)
226
+ oversized_assessment = oversized_plan["providers"][0]
227
+ check(oversized_assessment["execution_status"] == "blocked", "source exceeding target width must be blocked")
228
+ check(not oversized_assessment["adaptation_options"] and "no declared safe canvas adaptation strategy" in oversized_assessment["hard_failures"], "384x384 source must not be recommended for 256x448 padding")
229
+
230
+ landscape_plan = MODULE.build_plan(request(canvas=(448, 256), frame_count=2), registry_with([existing_text_adapter], require_verified=False, allow_scaffold_only=True), root)
231
+ landscape = landscape_plan["providers"][0]
232
+ check(any(option["target_canvas"] == [448, 256] for option in landscape["adaptation_options"]), "landscape target must receive explicit adaptation assessment")
233
+
234
+ request_path.write_text(json.dumps(request(canvas=(128, 128), frame_count=1)))
235
+ contextual = copy.deepcopy(request())
236
+ contextual["project_context"] = {"runtime": "game-runtime", "framework": "canvas", "existing_assets": ["assets/hero.png"], "rig_requirements": ["footline"], "provenance_requirements": ["receipt"], "validation_requirements": ["MotionLoom Dev Lab"]}
237
+ contextual_plan = MODULE.build_plan(contextual, production_registry, root)
238
+ check(contextual_plan["project"]["runtime"] == "game-runtime" and contextual_plan["project"]["framework"] == "canvas", "project context must flow into MotionLoom assessment")
239
+ check(contextual_plan["project"]["requested_existing_assets"] == ["assets/hero.png"] and contextual_plan["project"]["validation_requirements"] == ["MotionLoom Dev Lab"], "project asset and validation requirements must remain visible")
240
+
241
+ cli_native = subprocess.run([
242
+ sys.executable, str(MODULE_PATH), "plan", "--request", str(request_path), "--registry", str(registry_path), "--project-root", str(root), "--json"
243
+ ], capture_output=True, text=True)
244
+ check(cli_native.returncode == 0 and json.loads(cli_native.stdout)["approval"] is False, "CLI must preserve approval=false")
245
+ print("asset generation planner contract tests: PASS")
246
+ return 0
247
+
248
+
249
+ if __name__ == "__main__":
250
+ raise SystemExit(main())
@@ -6,6 +6,8 @@ from __future__ import annotations
6
6
  import base64
7
7
  import hashlib
8
8
  import json
9
+ import os
10
+ import shutil
9
11
  import subprocess
10
12
  import sys
11
13
  import tempfile
@@ -134,19 +136,33 @@ def main() -> int:
134
136
 
135
137
  scene_dir = ROOT / "src" / "output" / "browser-review-smoke"
136
138
  task_dir = ROOT / "artifacts" / "browser-review-smoke-task"
139
+ smoke_task_dir = temp / "professional-review-e2e-attestation"
140
+ shutil.copytree(task_dir, smoke_task_dir)
141
+ shutil.copy(
142
+ ROOT / "artifacts" / "browser-review-smoke-task" / "project-context.json",
143
+ smoke_task_dir / "project-context.json",
144
+ )
145
+ shutil.copy(
146
+ ROOT / "artifacts" / "browser-review-smoke-task" / "evidence-verifier-report.json",
147
+ smoke_task_dir / "evidence-verifier-report.json",
148
+ )
149
+ candidate_path = smoke_task_dir / "browser-review.json"
150
+ candidate = json.loads(candidate_path.read_text(encoding="utf-8"))
151
+ candidate["expires_at"] = "2099-01-01T00:00:00Z"
152
+ candidate_path.write_text(json.dumps(candidate, indent=2) + "\n", encoding="utf-8")
137
153
  task_statement = make_statement(
138
154
  task_id="browser-review-smoke-task",
139
155
  scene="browser-review-smoke",
140
- context_hash=sha256_file(task_dir / "project-context.json"),
156
+ context_hash=sha256_file(smoke_task_dir / "project-context.json"),
141
157
  source_sha256=sha256_file(scene_dir / "animation.json"),
142
158
  manifest_sha256=sha256_file(scene_dir / "manifest.json"),
143
- motion_ir_sha256=sha256_file(task_dir / "motion-ir.json"),
159
+ motion_ir_sha256=sha256_file(smoke_task_dir / "motion-ir.json"),
144
160
  evidence={
145
- "runtime_evidence_sha256": sha256_file(task_dir / "runtime-adapters" / "runtime-evidence.json"),
146
- "runtime_telemetry_sha256": telemetry_bundle_sha256(task_dir),
147
- "verifier_report_sha256": sha256_file(task_dir / "evidence-verifier-report.json"),
161
+ "runtime_evidence_sha256": sha256_file(smoke_task_dir / "runtime-adapters" / "runtime-evidence.json"),
162
+ "runtime_telemetry_sha256": telemetry_bundle_sha256(smoke_task_dir),
163
+ "verifier_report_sha256": sha256_file(smoke_task_dir / "evidence-verifier-report.json"),
148
164
  },
149
- provenance_chain_hash=sha256_file(task_dir / "provenance.json"),
165
+ provenance_chain_hash=sha256_file(smoke_task_dir / "provenance.json"),
150
166
  )
151
167
  smoke_statement_file = temp / "smoke-statement.json"
152
168
  smoke_statement_file.write_text(json.dumps(task_statement, indent=2) + "\n", encoding="utf-8")
@@ -157,8 +173,8 @@ def main() -> int:
157
173
  sys.executable,
158
174
  str(ROOT / "scripts" / "quality-gate.py"),
159
175
  "--scene", "browser-review-smoke",
160
- "--context", str(task_dir / "project-context.json"),
161
- "--task-dir", str(task_dir),
176
+ "--context", str(smoke_task_dir / "project-context.json"),
177
+ "--task-dir", str(smoke_task_dir),
162
178
  "--require-attestation",
163
179
  "--attestation", str(smoke_bundle_file),
164
180
  "--trust-policy", str(policy_file),