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.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +68 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +20 -4
- package/SECURITY.md +5 -5
- package/SKILL.md +15 -5
- package/agent-card.json +77 -14
- package/agent-surfaces.json +30 -7
- package/artifact-adapter-registry.json +568 -20
- package/bin/motionloom.mjs +25 -2
- package/capability-registry.json +58 -234
- package/dev-lab/public/devlab.js +36 -3
- package/dev-lab/public/index.html +6 -2
- package/docs/ACTION-SEPARATION.md +104 -0
- package/docs/ASSET-GENERATION-PLANNER.md +101 -0
- package/docs/BRANCH-PROTECTION.md +44 -0
- package/docs/EXTERNAL-CORPUS.md +26 -0
- package/docs/STATUS.md +3 -2
- package/docs/audits/field-test-after-hardening-2026-08-21.md +25 -0
- package/docs/releases/2.6.1.md +57 -0
- package/docs/releases/2.7.0.md +98 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.00.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.01.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.02.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.03.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json +81 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.00.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.01.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.02.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.03.json +26 -0
- package/examples/agent-consumer/asset-planning/pixellab-hero-256x448-request.json +43 -0
- package/examples/agent-consumer/devlab-live-sprite/README.md +30 -0
- package/examples/agent-consumer/devlab-live-sprite/devlab-runtime.json +61 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-00.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-01.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-02.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-00.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-01.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-02.png +0 -0
- package/examples/agent-consumer/frame-generation-lock/hero-walk-lock.json +97 -0
- package/package.json +23 -9
- package/references/multi-frame-asset-generation.md +144 -0
- package/schemas/action-separation-verifier-evidence.schema.json +43 -0
- package/schemas/action-sequence-manifest.schema.json +48 -0
- package/schemas/artifact-adapter-registry.schema.json +1 -1
- package/schemas/asset-adaptation.schema.json +21 -0
- package/schemas/asset-generation-plan.schema.json +77 -0
- package/schemas/asset-generation-request.schema.json +104 -0
- package/schemas/frame-envelope.schema.json +62 -0
- package/schemas/frame-generation-lock.schema.json +189 -0
- package/scripts/action-separation.py +410 -0
- package/scripts/asset-adapt.mjs +92 -0
- package/scripts/asset-generation-plan.py +613 -0
- package/scripts/browser_review_consistency.py +64 -0
- package/scripts/fetch-project-corpus.py +91 -0
- package/scripts/frame-generation-lock.py +358 -0
- package/scripts/frame-set-preflight.py +264 -0
- package/scripts/package-consumer-smoke.mjs +20 -0
- package/scripts/quality-gate.py +7 -0
- package/scripts/release-verify.py +25 -0
- package/scripts/report-contract.py +1 -1
- package/scripts/report.py +19 -4
- package/scripts/resolve-task-bundle.py +11 -3
- package/scripts/review-hook.py +51 -2
- package/scripts/skill-doctor.py +42 -0
- package/src/output/browser-review-smoke/browser-review.json +6 -6
- package/tests/scripts/run_tests.py +45 -2
- package/tests/scripts/test_asset_adapt.py +47 -0
- package/tests/scripts/test_asset_consistency.py +77 -4
- package/tests/scripts/test_asset_generation_plan.py +250 -0
- package/tests/scripts/test_attestation.py +24 -8
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build a MotionLoom project-aware asset generation recommendation.
|
|
3
|
+
|
|
4
|
+
The planner is advisory: it understands a request, compares it with declared
|
|
5
|
+
adapter capabilities, and produces explainable routes for an Agent. It never
|
|
6
|
+
transfers credentials, calls a provider, changes image bytes, or grants
|
|
7
|
+
approval. Recommendation status and execution status are deliberately
|
|
8
|
+
separate.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CONTRACT = "motionloom-asset-generation-plan"
|
|
19
|
+
SCHEMA_VERSION = "0.2"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_json(path: Path) -> dict[str, Any]:
|
|
23
|
+
try:
|
|
24
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
25
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
26
|
+
raise ValueError(f"cannot read JSON {path}: {exc}") from exc
|
|
27
|
+
if not isinstance(value, dict):
|
|
28
|
+
raise ValueError(f"JSON document must be an object: {path}")
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write_json(path: Path, value: dict[str, Any]) -> None:
|
|
33
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def detect_project_contracts(root: Path, requested_context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
38
|
+
checks = {
|
|
39
|
+
"action_separation": root / "scripts" / "action-separation.py",
|
|
40
|
+
"frame_generation_lock": root / "schemas" / "frame-generation-lock.schema.json",
|
|
41
|
+
"asset_identity": root / "schemas" / "asset-identity.schema.json",
|
|
42
|
+
"frame_geometry": root / "schemas" / "frame-geometry.schema.json",
|
|
43
|
+
"asset_provenance": root / "schemas" / "asset-provenance.schema.json",
|
|
44
|
+
"artifact_intake": root / "schemas" / "generation-receipt.schema.json",
|
|
45
|
+
"devlab": root / "dev-lab" / "public" / "devlab.js",
|
|
46
|
+
}
|
|
47
|
+
present = [name for name, path in checks.items() if path.exists()]
|
|
48
|
+
missing = [name for name, path in checks.items() if not path.exists()]
|
|
49
|
+
requested_context = requested_context or {}
|
|
50
|
+
asset_roots = []
|
|
51
|
+
for relative in ("assets", "assets/library", "src/assets", "src/output"):
|
|
52
|
+
if (root / relative).exists():
|
|
53
|
+
asset_roots.append(relative)
|
|
54
|
+
return {
|
|
55
|
+
"root": str(root),
|
|
56
|
+
"runtime": requested_context.get("runtime"),
|
|
57
|
+
"framework": requested_context.get("framework"),
|
|
58
|
+
"existing_asset_roots": asset_roots,
|
|
59
|
+
"requested_existing_assets": requested_context.get("existing_assets", []),
|
|
60
|
+
"rig_requirements": requested_context.get("rig_requirements", []),
|
|
61
|
+
"provenance_requirements": requested_context.get("provenance_requirements", []),
|
|
62
|
+
"validation_requirements": requested_context.get("validation_requirements", []),
|
|
63
|
+
"detected_contracts": present,
|
|
64
|
+
"missing_contracts": missing,
|
|
65
|
+
"recommendations": [
|
|
66
|
+
"bind generation receipt, control track and export manifest before ingest" if "artifact_intake" in present else "add artifact-intake contracts before provider integration",
|
|
67
|
+
"require action manifest and independent frame envelopes" if "action_separation" in present else "add action-scoped envelopes for multi-action frame sequences",
|
|
68
|
+
"measure real PNG geometry before runtime" if "frame_geometry" in present else "add deterministic frame-geometry measurement",
|
|
69
|
+
"keep generated output review_required and approval=false" if "asset_provenance" in present else "add provenance and human-review boundary",
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def target_summary(request: dict[str, Any]) -> dict[str, Any]:
|
|
75
|
+
target = request.get("target") or {}
|
|
76
|
+
canvas = target.get("canvas") or {}
|
|
77
|
+
width = int(canvas.get("width", 0))
|
|
78
|
+
height = int(canvas.get("height", 0))
|
|
79
|
+
return {
|
|
80
|
+
"width": width,
|
|
81
|
+
"height": height,
|
|
82
|
+
"shape": "square" if width == height else "portrait" if height > width else "landscape",
|
|
83
|
+
"area": width * height,
|
|
84
|
+
"frame_count": int(target.get("frame_count", 1)),
|
|
85
|
+
"fps": target.get("fps"),
|
|
86
|
+
"alpha_mode": target.get("alpha_mode"),
|
|
87
|
+
"pixel_art": bool(target.get("pixel_art", False)),
|
|
88
|
+
"anchor": target.get("anchor", "footline"),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def allowed_size_match(canvas: dict[str, Any], width: int, height: int) -> bool | None:
|
|
93
|
+
allowed = canvas.get("allowed_sizes")
|
|
94
|
+
if allowed is None:
|
|
95
|
+
return None
|
|
96
|
+
pairs = {(int(item[0]), int(item[1])) for item in allowed if isinstance(item, list) and len(item) == 2}
|
|
97
|
+
return (width, height) in pairs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def source_fits(source_width: int, source_height: int, target: dict[str, Any], scale: int = 1) -> bool:
|
|
101
|
+
return source_width * scale <= target["width"] and source_height * scale <= target["height"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def canvas_assessment(adapter: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]:
|
|
105
|
+
capability = adapter.get("capabilities") or {}
|
|
106
|
+
canvas = capability.get("canvas") or {}
|
|
107
|
+
width, height = target["width"], target["height"]
|
|
108
|
+
if not canvas:
|
|
109
|
+
return {
|
|
110
|
+
"status": "unknown",
|
|
111
|
+
"native": False,
|
|
112
|
+
"reason": "adapter does not declare canvas capability",
|
|
113
|
+
"adaptation_required": True,
|
|
114
|
+
}
|
|
115
|
+
shape_ok = not canvas.get("shapes") or target["shape"] in canvas.get("shapes", [])
|
|
116
|
+
max_ok = width <= int(canvas.get("max_width", width)) and height <= int(canvas.get("max_height", height))
|
|
117
|
+
exact = allowed_size_match(canvas, width, height)
|
|
118
|
+
if exact is False:
|
|
119
|
+
native = False
|
|
120
|
+
else:
|
|
121
|
+
native = shape_ok and max_ok
|
|
122
|
+
if exact is True:
|
|
123
|
+
reason = "target canvas is explicitly supported"
|
|
124
|
+
elif not shape_ok:
|
|
125
|
+
reason = f"target shape {target['shape']} is not in provider-supported shapes {canvas.get('shapes', [])}"
|
|
126
|
+
elif not max_ok:
|
|
127
|
+
reason = "target exceeds provider-declared maximum canvas"
|
|
128
|
+
elif exact is False:
|
|
129
|
+
reason = "target canvas is not in provider's explicit allowed sizes"
|
|
130
|
+
else:
|
|
131
|
+
reason = "provider declares a compatible canvas range but not an exact size list"
|
|
132
|
+
return {
|
|
133
|
+
"status": "native" if native else "adaptation_required",
|
|
134
|
+
"native": native,
|
|
135
|
+
"reason": reason,
|
|
136
|
+
"provider_canvas": canvas,
|
|
137
|
+
"adaptation_required": not native,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def frame_assessment(adapter: dict[str, Any], target: dict[str, Any], policy: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
behavior = (adapter.get("capabilities") or {}).get("frame_behavior") or {}
|
|
143
|
+
count = target["frame_count"]
|
|
144
|
+
if not behavior:
|
|
145
|
+
return {
|
|
146
|
+
"status": "unknown",
|
|
147
|
+
"single_frame": False,
|
|
148
|
+
"max_frames_per_request": None,
|
|
149
|
+
"frame_count_policy": "unknown",
|
|
150
|
+
"reason": "adapter does not declare frame isolation behavior",
|
|
151
|
+
}
|
|
152
|
+
max_frames = behavior.get("max_frames_per_request")
|
|
153
|
+
limits_by_canvas = behavior.get("limits_by_canvas") or []
|
|
154
|
+
if max_frames is None:
|
|
155
|
+
for limit in limits_by_canvas:
|
|
156
|
+
if isinstance(limit, dict) and limit.get("canvas") == [target["width"], target["height"]]:
|
|
157
|
+
max_frames = limit.get("max_frames_per_request")
|
|
158
|
+
break
|
|
159
|
+
single = bool(behavior.get("single_frame", False))
|
|
160
|
+
enough = single or (isinstance(max_frames, int) and max_frames >= count)
|
|
161
|
+
if policy.get("frame_isolation") == "required" and not single:
|
|
162
|
+
status = "provisional_batch_only" if policy.get("allow_provider_batch_as_provisional") else "blocked_for_isolation"
|
|
163
|
+
elif enough:
|
|
164
|
+
status = "native"
|
|
165
|
+
else:
|
|
166
|
+
status = "batch_split_required"
|
|
167
|
+
return {
|
|
168
|
+
"status": status,
|
|
169
|
+
"single_frame": single,
|
|
170
|
+
"max_frames_per_request": max_frames,
|
|
171
|
+
"frame_count_policy": behavior.get("frame_count_policy", "fixed" if max_frames is not None else "unknown"),
|
|
172
|
+
"limits_by_canvas": limits_by_canvas,
|
|
173
|
+
"declared_mode": behavior.get("mode"),
|
|
174
|
+
"reason": "provider can emit one source frame per request" if single else "provider emits multiple frames; per-frame envelopes must be created after export",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def adaptation_options(adapter: dict[str, Any], target: dict[str, Any], policy: dict[str, Any]) -> list[dict[str, Any]]:
|
|
179
|
+
options: list[dict[str, Any]] = []
|
|
180
|
+
declared = ((adapter.get("capabilities") or {}).get("adaptation") or [])
|
|
181
|
+
canvas = ((adapter.get("capabilities") or {}).get("canvas") or {})
|
|
182
|
+
allowed = canvas.get("allowed_sizes") or []
|
|
183
|
+
square_sizes = sorted({int(pair[0]) for pair in allowed if isinstance(pair, list) and len(pair) == 2 and pair[0] == pair[1]})
|
|
184
|
+
fitting_sizes = [size for size in square_sizes if source_fits(size, size, target, 1)]
|
|
185
|
+
source_size = max(fitting_sizes, default=0)
|
|
186
|
+
if target["width"] != target["height"] and source_size and ("pad_to_target" in declared or canvas):
|
|
187
|
+
options.append({
|
|
188
|
+
"id": "deterministic-pad-to-target",
|
|
189
|
+
"kind": "deterministic_adaptation",
|
|
190
|
+
"source_canvas": [source_size, source_size],
|
|
191
|
+
"target_canvas": [target["width"], target["height"]],
|
|
192
|
+
"operation": "generate on a compatible source canvas, preserve aspect ratio, then place on transparent target canvas",
|
|
193
|
+
"anchor": target["anchor"],
|
|
194
|
+
"stretch": False,
|
|
195
|
+
"crop": False,
|
|
196
|
+
"approval": False,
|
|
197
|
+
"requires_validation": ["alpha-bounds", "pivot-footline", "frame-geometry", "action-separation"],
|
|
198
|
+
})
|
|
199
|
+
if policy.get("integer_scale_only") and source_size:
|
|
200
|
+
max_scale = min(target["width"] // source_size, target["height"] // source_size)
|
|
201
|
+
if max_scale >= 2:
|
|
202
|
+
options.append({
|
|
203
|
+
"id": "integer-upscale-and-pad",
|
|
204
|
+
"kind": "deterministic_adaptation",
|
|
205
|
+
"operation": "integer nearest-neighbour upscale only, followed by transparent padding",
|
|
206
|
+
"source_canvas": [source_size, source_size],
|
|
207
|
+
"target_canvas": [target["width"], target["height"]],
|
|
208
|
+
"scale": max_scale,
|
|
209
|
+
"stretch": False,
|
|
210
|
+
"crop": False,
|
|
211
|
+
"approval": False,
|
|
212
|
+
"requires_validation": ["pixel-grid", "alpha-bounds", "frame-geometry"],
|
|
213
|
+
})
|
|
214
|
+
if "provider_native_resize" in declared:
|
|
215
|
+
options.append({
|
|
216
|
+
"id": "provider-native-resize",
|
|
217
|
+
"kind": "provider_operation",
|
|
218
|
+
"operation": "use provider-native resize only when the selected adapter declares API evidence for it",
|
|
219
|
+
"stretch": False,
|
|
220
|
+
"crop": False,
|
|
221
|
+
"requires_provider_evidence": True,
|
|
222
|
+
"approval": False,
|
|
223
|
+
})
|
|
224
|
+
if "tile_stitch" in declared:
|
|
225
|
+
options.append({
|
|
226
|
+
"id": "tile-and-stitch",
|
|
227
|
+
"kind": "multi_request_adaptation",
|
|
228
|
+
"operation": "generate bounded tiles and stitch only with explicit overlap, seam and identity contracts",
|
|
229
|
+
"stretch": False,
|
|
230
|
+
"crop": False,
|
|
231
|
+
"requires_validation": ["tile-seam", "identity", "action-separation"],
|
|
232
|
+
"risk": "high",
|
|
233
|
+
"approval": False,
|
|
234
|
+
})
|
|
235
|
+
return options
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def preference_context(request: dict[str, Any]) -> dict[str, set[str]]:
|
|
239
|
+
raw = request.get("provider_preferences") or {}
|
|
240
|
+
return {
|
|
241
|
+
"preferred": {str(value) for value in raw.get("preferred_adapter_ids", []) if isinstance(value, str)},
|
|
242
|
+
"excluded": {str(value) for value in raw.get("excluded_adapter_ids", []) if isinstance(value, str)},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def availability_assessment(adapter: dict[str, Any]) -> dict[str, Any]:
|
|
247
|
+
raw = adapter.get("availability") if isinstance(adapter.get("availability"), dict) else {}
|
|
248
|
+
status = raw.get("status", "unknown")
|
|
249
|
+
if status not in {"known", "available", "unavailable", "unknown"}:
|
|
250
|
+
status = "unknown"
|
|
251
|
+
result = {
|
|
252
|
+
"status": status,
|
|
253
|
+
"known_to_motionloom": True,
|
|
254
|
+
"executable_in_current_environment": status == "available" if status != "unknown" else None,
|
|
255
|
+
}
|
|
256
|
+
for key in ("environment", "checked_at", "reason"):
|
|
257
|
+
if key in raw:
|
|
258
|
+
result[key] = raw[key]
|
|
259
|
+
if status == "unknown" and "reason" not in result:
|
|
260
|
+
result["reason"] = "registry does not declare current tool connectivity or installation"
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def project_fit(canvas: dict[str, Any], frames: dict[str, Any], hard_failures: list[str], options: list[dict[str, Any]]) -> tuple[str, int]:
|
|
265
|
+
if hard_failures:
|
|
266
|
+
return "low", 0
|
|
267
|
+
score = 0
|
|
268
|
+
if canvas.get("native"):
|
|
269
|
+
score += 4
|
|
270
|
+
elif options:
|
|
271
|
+
score += 2
|
|
272
|
+
if frames.get("status") == "native":
|
|
273
|
+
score += 4
|
|
274
|
+
elif frames.get("status") in {"provisional_batch_only", "batch_split_required"}:
|
|
275
|
+
score += 1
|
|
276
|
+
if not canvas.get("native") and not options:
|
|
277
|
+
score -= 4
|
|
278
|
+
if score >= 7:
|
|
279
|
+
return "high", score
|
|
280
|
+
if score >= 3:
|
|
281
|
+
return "medium", score
|
|
282
|
+
return "low", score
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def agent_guidance(adapter: dict[str, Any], target: dict[str, Any], frames: dict[str, Any], options: list[dict[str, Any]], availability: dict[str, Any], execution_status: str) -> dict[str, Any]:
|
|
286
|
+
adapter_id = adapter.get("adapter_id", "selected route")
|
|
287
|
+
steps = ["Use the MotionLoom Project Assessment and preserve this route's declared limitations."]
|
|
288
|
+
if availability["status"] in {"unknown", "unavailable"}:
|
|
289
|
+
steps.append("Resolve tool availability or connector access before execution; MotionLoom cannot claim it can invoke this route from registry metadata alone.")
|
|
290
|
+
if options:
|
|
291
|
+
primary = options[0]
|
|
292
|
+
source = primary.get("source_canvas")
|
|
293
|
+
target_canvas = primary.get("target_canvas")
|
|
294
|
+
if source and target_canvas:
|
|
295
|
+
steps.append(f"Generate or import at source canvas {source[0]}x{source[1]}, then run MotionLoom asset adaptation to {target_canvas[0]}x{target_canvas[1]} with the declared anchor; do not crop or stretch.")
|
|
296
|
+
else:
|
|
297
|
+
steps.append("Apply the declared adaptation only after recording source and target geometry in the MotionLoom export manifest.")
|
|
298
|
+
if frames.get("status") != "native":
|
|
299
|
+
steps.append("Treat batch output as provisional and bind one frame envelope plus independent verifier evidence per accepted frame when isolation is required.")
|
|
300
|
+
if execution_status == "blocked":
|
|
301
|
+
steps.append("Do not execute this route under the current hard constraints; use a MotionLoom-ranked alternative or revise the request explicitly.")
|
|
302
|
+
steps.extend([
|
|
303
|
+
"Run MotionLoom frame geometry and asset consistency validation before packing an atlas.",
|
|
304
|
+
"Run MotionLoom action separation, then review the candidate in MotionLoom Dev Lab.",
|
|
305
|
+
"Keep approval and production_approved false until human review is recorded.",
|
|
306
|
+
])
|
|
307
|
+
return {
|
|
308
|
+
"recommended_by": "MotionLoom",
|
|
309
|
+
"summary": f"MotionLoom recommends evaluating {adapter_id} through its declared project route; execution status remains {execution_status}.",
|
|
310
|
+
"steps": steps,
|
|
311
|
+
"validation_route": ["MotionLoom frame geometry", "MotionLoom action separation", "MotionLoom Dev Lab review"],
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def assess_adapter(adapter: dict[str, Any], target: dict[str, Any], policy: dict[str, Any], requested_kind: str, selection_policy: dict[str, Any], preferences: dict[str, set[str]]) -> dict[str, Any]:
|
|
316
|
+
capabilities = adapter.get("capabilities") or {}
|
|
317
|
+
outputs = set(adapter.get("outputs") or [])
|
|
318
|
+
adapter_id = adapter.get("adapter_id")
|
|
319
|
+
preference = "excluded" if adapter_id in preferences["excluded"] else "preferred" if adapter_id in preferences["preferred"] else "neutral"
|
|
320
|
+
kind_ok = requested_kind in outputs or requested_kind.replace("_", "-") in outputs or requested_kind in {"image", "frame_sequence"} and "image" in outputs
|
|
321
|
+
canvas = canvas_assessment(adapter, target)
|
|
322
|
+
frames = frame_assessment(adapter, target, policy)
|
|
323
|
+
hard_failures: list[str] = []
|
|
324
|
+
warnings: list[str] = []
|
|
325
|
+
if not kind_ok:
|
|
326
|
+
hard_failures.append(f"adapter does not declare output kind {requested_kind}")
|
|
327
|
+
if policy.get("frame_isolation") == "required" and frames["status"] == "blocked_for_isolation":
|
|
328
|
+
hard_failures.append("required per-frame isolation is not supported by the declared provider behavior")
|
|
329
|
+
if canvas["adaptation_required"]:
|
|
330
|
+
warnings.append(canvas["reason"])
|
|
331
|
+
status = adapter.get("status")
|
|
332
|
+
if status not in {"verified", "project_integrated"}:
|
|
333
|
+
warnings.append(f"adapter status is {status}; real provider/runtime evidence is still required")
|
|
334
|
+
options = adaptation_options(adapter, target, policy)
|
|
335
|
+
if canvas["adaptation_required"] and not options:
|
|
336
|
+
hard_failures.append("no declared safe canvas adaptation strategy")
|
|
337
|
+
availability = availability_assessment(adapter)
|
|
338
|
+
if availability["status"] == "unavailable":
|
|
339
|
+
warnings.append("adapter is currently marked unavailable; resolve availability before execution")
|
|
340
|
+
elif availability["status"] == "unknown":
|
|
341
|
+
warnings.append("adapter availability is unknown; the Agent must resolve connectivity or installation before execution")
|
|
342
|
+
|
|
343
|
+
evidence_status = "verified" if status == "verified" else "provisional"
|
|
344
|
+
if hard_failures or preference == "excluded" or status == "disabled" or availability["status"] == "unavailable":
|
|
345
|
+
execution_status = "blocked"
|
|
346
|
+
elif status == "verified" and availability["status"] == "available":
|
|
347
|
+
execution_status = "verified"
|
|
348
|
+
else:
|
|
349
|
+
execution_status = "provisional"
|
|
350
|
+
execution_eligible = not hard_failures and availability["status"] == "available" and status == "verified"
|
|
351
|
+
if not hard_failures and availability["status"] == "available" and not selection_policy.get("require_verified", True):
|
|
352
|
+
allowed_statuses = {"verified", "project_integrated", "static_validated"}
|
|
353
|
+
if status in allowed_statuses or (status == "scaffold_only" and selection_policy.get("allow_scaffold_only", False)):
|
|
354
|
+
execution_eligible = True
|
|
355
|
+
non_generation_fixture = adapter.get("kind") == "fixture"
|
|
356
|
+
if non_generation_fixture:
|
|
357
|
+
warnings.append("fixture adapter is regression evidence only and is not a user-facing generation route")
|
|
358
|
+
recommendation_status = "not_recommended" if hard_failures or preference == "excluded" or non_generation_fixture else "recommended" if preference == "preferred" or canvas.get("native") or options else "acceptable"
|
|
359
|
+
fit_label, fit_score = project_fit(canvas, frames, hard_failures, options)
|
|
360
|
+
ranking_factors = {
|
|
361
|
+
"project_fit": fit_score,
|
|
362
|
+
"native_canvas": 4 if canvas.get("native") else 0,
|
|
363
|
+
"frame_isolation": 4 if frames.get("status") == "native" else 1 if frames.get("status") in {"provisional_batch_only", "batch_split_required"} else 0,
|
|
364
|
+
"execution_evidence": 3 if status == "verified" else 1,
|
|
365
|
+
"user_preference": 5 if preference == "preferred" else -100 if preference == "excluded" else 0,
|
|
366
|
+
"availability": 1 if availability["status"] == "available" else 0,
|
|
367
|
+
"risk": {"low": 1, "medium": 0, "high": -2}.get(adapter.get("risk_level"), 0),
|
|
368
|
+
"adaptation_cost": -1 if options and not canvas.get("native") else 0,
|
|
369
|
+
}
|
|
370
|
+
ranking_score = sum(ranking_factors.values()) if recommendation_status != "not_recommended" else -1000
|
|
371
|
+
rationale = []
|
|
372
|
+
rationale.append("matches the requested asset kind" if kind_ok else f"does not declare the requested asset kind {requested_kind}")
|
|
373
|
+
rationale.append("matches the target canvas natively" if canvas.get("native") else ("has a declared safe adaptation route" if options else "has no declared safe canvas route"))
|
|
374
|
+
rationale.append("supports isolated frames natively" if frames.get("status") == "native" else "requires provisional batch handling and per-frame evidence")
|
|
375
|
+
rationale.append(f"execution evidence is {execution_status}")
|
|
376
|
+
if preference == "preferred":
|
|
377
|
+
rationale.append("explicitly preferred by the user; preference does not override hard constraints")
|
|
378
|
+
elif preference == "excluded":
|
|
379
|
+
rationale.append("explicitly excluded by the user")
|
|
380
|
+
rationale.append(f"availability is {availability['status']}")
|
|
381
|
+
rationale.extend(warnings)
|
|
382
|
+
legacy_selection = execution_status if execution_status != "verified" else "eligible"
|
|
383
|
+
return {
|
|
384
|
+
"adapter_id": adapter_id,
|
|
385
|
+
"status": status,
|
|
386
|
+
"recommendation_status": recommendation_status,
|
|
387
|
+
"execution_status": execution_status,
|
|
388
|
+
"execution_evidence_status": evidence_status,
|
|
389
|
+
"execution_eligible": execution_eligible,
|
|
390
|
+
"selection_status": legacy_selection,
|
|
391
|
+
"eligible": execution_eligible,
|
|
392
|
+
"user_preference": {"state": preference, "requested": preference != "neutral"},
|
|
393
|
+
"availability": availability,
|
|
394
|
+
"project_fit": fit_label,
|
|
395
|
+
"ranking_score": ranking_score,
|
|
396
|
+
"ranking_factors": ranking_factors,
|
|
397
|
+
"kind": adapter.get("kind"),
|
|
398
|
+
"invocation_mode": adapter.get("invocation_mode"),
|
|
399
|
+
"cost_class": adapter.get("cost_class"),
|
|
400
|
+
"kind_compatible": kind_ok,
|
|
401
|
+
"canvas": canvas,
|
|
402
|
+
"frames": frames,
|
|
403
|
+
"hard_failures": hard_failures,
|
|
404
|
+
"warnings": warnings,
|
|
405
|
+
"adaptation_options": options,
|
|
406
|
+
"limitations": adapter.get("limitations", []),
|
|
407
|
+
"evidence": adapter.get("evidence", []),
|
|
408
|
+
"docs": capabilities.get("official_docs", []),
|
|
409
|
+
"rationale": rationale,
|
|
410
|
+
"agent_guidance": agent_guidance(adapter, target, frames, options, availability, execution_status),
|
|
411
|
+
"approval": False,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def recommendation_view(item: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]:
|
|
416
|
+
native = item["canvas"].get("native")
|
|
417
|
+
frame_native = item["frames"].get("status") == "native"
|
|
418
|
+
if item["availability"].get("status") == "unavailable" and item["recommendation_status"] != "not_recommended":
|
|
419
|
+
route = "resolve_availability_first"
|
|
420
|
+
elif item["execution_status"] == "blocked":
|
|
421
|
+
route = "blocked_use_alternative"
|
|
422
|
+
elif native and frame_native and item["execution_status"] == "verified":
|
|
423
|
+
route = "use_native"
|
|
424
|
+
elif native and frame_native:
|
|
425
|
+
route = "use_native_after_review"
|
|
426
|
+
elif item["adaptation_options"]:
|
|
427
|
+
route = "use_with_explicit_adaptation"
|
|
428
|
+
else:
|
|
429
|
+
route = "research_or_manual_review"
|
|
430
|
+
return {
|
|
431
|
+
"adapter_id": item["adapter_id"],
|
|
432
|
+
"rank": item["ranking_score"],
|
|
433
|
+
"recommendation_status": item["recommendation_status"],
|
|
434
|
+
"execution_status": item["execution_status"],
|
|
435
|
+
"execution_evidence_status": item["execution_evidence_status"],
|
|
436
|
+
"execution_eligible": item["execution_eligible"],
|
|
437
|
+
"user_preference": item["user_preference"],
|
|
438
|
+
"availability": item["availability"],
|
|
439
|
+
"project_fit": item["project_fit"],
|
|
440
|
+
"route": route,
|
|
441
|
+
"why": item["rationale"],
|
|
442
|
+
"rationale": item["rationale"],
|
|
443
|
+
"agent_guidance": item["agent_guidance"],
|
|
444
|
+
"adaptation_options": item["adaptation_options"],
|
|
445
|
+
"hard_failures": item["hard_failures"],
|
|
446
|
+
"approval": False,
|
|
447
|
+
"target_canvas": [target["width"], target["height"]],
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def build_plan(request: dict[str, Any], registry: dict[str, Any], root: Path) -> dict[str, Any]:
|
|
452
|
+
target = target_summary(request)
|
|
453
|
+
policy = request.get("generation_policy") or {}
|
|
454
|
+
adapters = registry.get("adapters") if isinstance(registry.get("adapters"), list) else []
|
|
455
|
+
selection_policy = registry.get("selection_policy") if isinstance(registry.get("selection_policy"), dict) else {}
|
|
456
|
+
preferences = preference_context(request)
|
|
457
|
+
assessments = [assess_adapter(adapter, target, policy, request.get("asset_kind", "image"), selection_policy, preferences) for adapter in adapters if isinstance(adapter, dict)]
|
|
458
|
+
known_ids = {item["adapter_id"] for item in assessments}
|
|
459
|
+
unknown_preferred = sorted(preferences["preferred"] - known_ids)
|
|
460
|
+
unknown_excluded = sorted(preferences["excluded"] - known_ids)
|
|
461
|
+
rec_candidates = [item for item in assessments if item["recommendation_status"] != "not_recommended"]
|
|
462
|
+
rec_candidates.sort(key=lambda item: (-item["ranking_score"], item["adapter_id"]))
|
|
463
|
+
recommendations = [recommendation_view(item, target) for item in rec_candidates[:5]]
|
|
464
|
+
execution_eligible = [item for item in assessments if item["execution_eligible"]]
|
|
465
|
+
provisional = [item for item in assessments if item["execution_status"] == "provisional"]
|
|
466
|
+
blocked = [item for item in assessments if item["execution_status"] == "blocked"]
|
|
467
|
+
if execution_eligible and any(item["canvas"]["native"] and item["frames"]["status"] == "native" for item in execution_eligible):
|
|
468
|
+
execution_decision = "compatible_execution_route_available"
|
|
469
|
+
elif execution_eligible:
|
|
470
|
+
execution_decision = "execution_route_requires_review_or_adaptation"
|
|
471
|
+
else:
|
|
472
|
+
execution_decision = "no_execution_eligible_route"
|
|
473
|
+
decision = "recommendations_available" if recommendations else "no_safe_recommendation_available"
|
|
474
|
+
if execution_eligible:
|
|
475
|
+
decision = execution_decision
|
|
476
|
+
warnings = [
|
|
477
|
+
"Provider capability metadata is advisory until backed by real export and target-runtime evidence.",
|
|
478
|
+
"A provider-native batch animation is not equivalent to isolated per-frame generation.",
|
|
479
|
+
"Padding preserves content; crop and non-uniform stretch are forbidden by this request.",
|
|
480
|
+
]
|
|
481
|
+
if unknown_preferred:
|
|
482
|
+
warnings.append(f"requested adapter IDs are unknown to this registry: {', '.join(unknown_preferred)}")
|
|
483
|
+
if unknown_excluded:
|
|
484
|
+
warnings.append(f"excluded adapter IDs are unknown to this registry: {', '.join(unknown_excluded)}")
|
|
485
|
+
if not execution_eligible:
|
|
486
|
+
warnings.append("Normal planning still exposes compatible provisional/manual options; strict execution remains fail-closed because no route satisfies the active execution policy.")
|
|
487
|
+
next_steps = [
|
|
488
|
+
"Keep this MotionLoom assessment as a plan only; do not send credentials or invoke a provider from the planner.",
|
|
489
|
+
"Resolve availability or connector access for any route marked unknown/unavailable before execution.",
|
|
490
|
+
"If adaptation is selected, record source_canvas, target_canvas, anchor and transform in the export manifest.",
|
|
491
|
+
"Generate or import each frame with its own hash-bound envelope when frame_isolation is required.",
|
|
492
|
+
"Run MotionLoom frame geometry, asset consistency and action separation validation before Dev Lab review.",
|
|
493
|
+
"Keep production_approved and approval false until a human reviews the runtime candidate.",
|
|
494
|
+
]
|
|
495
|
+
return {
|
|
496
|
+
"contract": CONTRACT,
|
|
497
|
+
"schema_version": SCHEMA_VERSION,
|
|
498
|
+
"producer": "MotionLoom",
|
|
499
|
+
"source": "MotionLoom project-aware asset generation planner",
|
|
500
|
+
"identity": {"product": "MotionLoom", "role": "project-aware decision and guidance layer"},
|
|
501
|
+
"status": "plan_only",
|
|
502
|
+
"decision": decision,
|
|
503
|
+
"execution_decision": execution_decision,
|
|
504
|
+
"approval": False,
|
|
505
|
+
"production_approved": False,
|
|
506
|
+
"request": {
|
|
507
|
+
"request_id": request.get("request_id"),
|
|
508
|
+
"asset_id": request.get("asset_id"),
|
|
509
|
+
"asset_kind": request.get("asset_kind"),
|
|
510
|
+
"target": target,
|
|
511
|
+
"generation_policy": policy,
|
|
512
|
+
"actions": request.get("actions", []),
|
|
513
|
+
"project_context": request.get("project_context", {}),
|
|
514
|
+
"provider_preferences": request.get("provider_preferences", {}),
|
|
515
|
+
},
|
|
516
|
+
"project": detect_project_contracts(root, request.get("project_context")),
|
|
517
|
+
"registry": {
|
|
518
|
+
"path": str((root / "artifact-adapter-registry.json").resolve()),
|
|
519
|
+
"selection_policy": selection_policy,
|
|
520
|
+
},
|
|
521
|
+
"providers": assessments,
|
|
522
|
+
"selection": {
|
|
523
|
+
"policy": selection_policy,
|
|
524
|
+
"eligible_count": len(execution_eligible),
|
|
525
|
+
"eligible_adapter_ids": [item["adapter_id"] for item in execution_eligible],
|
|
526
|
+
"provisional_count": len(provisional),
|
|
527
|
+
"provisional_adapter_ids": [item["adapter_id"] for item in provisional],
|
|
528
|
+
"blocked_count": len(blocked),
|
|
529
|
+
"blocked_adapter_ids": [item["adapter_id"] for item in blocked],
|
|
530
|
+
"recommendation_count": len(recommendations),
|
|
531
|
+
"recommendation_adapter_ids": [item["adapter_id"] for item in recommendations],
|
|
532
|
+
"unknown_preferred_adapter_ids": unknown_preferred,
|
|
533
|
+
"unknown_excluded_adapter_ids": unknown_excluded,
|
|
534
|
+
},
|
|
535
|
+
"recommendations": recommendations,
|
|
536
|
+
"agent_guidance": {
|
|
537
|
+
"recommended_by": "MotionLoom",
|
|
538
|
+
"summary": "MotionLoom assessed the project requirements first, then compared available metadata, user preference and execution policy.",
|
|
539
|
+
"next_step": next_steps[1],
|
|
540
|
+
"validation_route": ["MotionLoom frame geometry", "MotionLoom action separation", "MotionLoom Dev Lab review"],
|
|
541
|
+
},
|
|
542
|
+
"next_steps": next_steps,
|
|
543
|
+
"warnings": warnings,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def render_human(result: dict[str, Any]) -> str:
|
|
548
|
+
target = result["request"]["target"]
|
|
549
|
+
lines = [
|
|
550
|
+
"MotionLoom Project Assessment",
|
|
551
|
+
f"Target: {target['width']}x{target['height']} · {target['frame_count']} frames · isolation={result['request']['generation_policy'].get('frame_isolation')}",
|
|
552
|
+
f"Decision: {result['decision']} · Execution: {result['execution_decision']}",
|
|
553
|
+
"",
|
|
554
|
+
"MotionLoom Recommendations",
|
|
555
|
+
]
|
|
556
|
+
if not result["recommendations"]:
|
|
557
|
+
lines.append("No safe recommendation is available under the declared project constraints.")
|
|
558
|
+
for index, item in enumerate(result["recommendations"], start=1):
|
|
559
|
+
lines.extend([
|
|
560
|
+
f"{index}. {item['adapter_id']}",
|
|
561
|
+
f" Project fit: {item['project_fit']}",
|
|
562
|
+
f" Recommendation: {item['recommendation_status']}",
|
|
563
|
+
f" Execution status: {item['execution_status']}",
|
|
564
|
+
f" Availability: {item['availability']['status']}",
|
|
565
|
+
f" Why MotionLoom recommends it: {'; '.join(item['why'][:4])}",
|
|
566
|
+
f" Agent route: {item['route']}",
|
|
567
|
+
f" Guidance: {item['agent_guidance']['summary']}",
|
|
568
|
+
])
|
|
569
|
+
not_recommended = [item for item in result["providers"] if item.get("recommendation_status") == "not_recommended"]
|
|
570
|
+
if not_recommended:
|
|
571
|
+
lines.extend(["", "MotionLoom Evaluated but Not Recommended"])
|
|
572
|
+
for item in not_recommended:
|
|
573
|
+
reason = (item.get("rationale") or item.get("hard_failures") or ["hard constraint or user preference boundary"])[0]
|
|
574
|
+
lines.append(f"- {item['adapter_id']}: execution={item.get('execution_status')} · {reason}")
|
|
575
|
+
lines.extend(["", "MotionLoom Agent Guidance", f"{result['agent_guidance']['summary']}", f"MotionLoom next step: {result['next_steps'][1]}"])
|
|
576
|
+
return "\n".join(lines)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def main(argv: list[str] | None = None) -> int:
|
|
580
|
+
parser = argparse.ArgumentParser(description="Build a MotionLoom project-aware asset generation recommendation without invoking providers")
|
|
581
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
582
|
+
plan = sub.add_parser("plan", help="build a provider/canvas recommendation and guarded execution plan")
|
|
583
|
+
plan.add_argument("--request", required=True, type=Path)
|
|
584
|
+
plan.add_argument("--registry", type=Path, default=None)
|
|
585
|
+
plan.add_argument("--project-root", type=Path, default=Path.cwd())
|
|
586
|
+
plan.add_argument("--output", type=Path, default=None)
|
|
587
|
+
plan.add_argument("--strict", action="store_true", help="return non-zero when no execution-eligible route meets hard constraints")
|
|
588
|
+
plan.add_argument("--json", action="store_true", help="emit machine-readable JSON; without it print a human-readable MotionLoom assessment")
|
|
589
|
+
args = parser.parse_args(argv)
|
|
590
|
+
root = args.project_root.resolve()
|
|
591
|
+
registry_path = (args.registry or (root / "artifact-adapter-registry.json")).resolve()
|
|
592
|
+
try:
|
|
593
|
+
request_path = args.request.resolve()
|
|
594
|
+
try:
|
|
595
|
+
request_path.relative_to(root)
|
|
596
|
+
except ValueError as exc:
|
|
597
|
+
raise ValueError("--request must resolve inside --project-root") from exc
|
|
598
|
+
request = read_json(request_path)
|
|
599
|
+
registry = read_json(registry_path)
|
|
600
|
+
result = build_plan(request, registry, root)
|
|
601
|
+
except ValueError as exc:
|
|
602
|
+
print(json.dumps({"contract": CONTRACT, "schema_version": SCHEMA_VERSION, "producer": "MotionLoom", "status": "invalid", "approval": False, "errors": [str(exc)]}, indent=2))
|
|
603
|
+
return 2
|
|
604
|
+
if args.output:
|
|
605
|
+
write_json(args.output.resolve(), result)
|
|
606
|
+
print(json.dumps(result, indent=2, sort_keys=True) if args.json else render_human(result))
|
|
607
|
+
if args.strict and result["execution_decision"] == "no_execution_eligible_route":
|
|
608
|
+
return 2
|
|
609
|
+
return 0
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
if __name__ == "__main__":
|
|
613
|
+
raise SystemExit(main())
|