motionloom 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -1
- package/README.md +123 -17
- package/ROADMAP.md +8 -3
- package/SKILL.md +34 -3
- package/agent-card.json +48 -5
- package/agent-surfaces.json +8 -1
- package/artifact-adapter-registry.json +53 -0
- package/bin/motionloom.mjs +41 -4
- package/docs/AGENT-INTEGRATION.md +15 -2
- package/docs/CHECKLIST.md +21 -0
- package/docs/STATUS.md +2 -2
- package/docs/audits/data/deep-stress-latest.json +65 -65
- package/docs/releases/2.3.0.md +33 -0
- 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-generated-pilot/hero-male.json +10 -0
- package/examples/agent-consumer/ai-generated-pilot-provenance.json +55 -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 +65 -4
- package/references/agent-interoperability.md +11 -0
- package/references/browser-review-contract.md +7 -1
- package/references/intelligence-core.md +29 -2
- package/rig-adapter-registry.json +39 -0
- package/schemas/action-set.schema.json +42 -0
- package/schemas/agent-surfaces.schema.json +1 -1
- package/schemas/artifact-adapter-registry.schema.json +16 -0
- package/schemas/asset-identity.schema.json +117 -0
- package/schemas/asset-provenance.schema.json +183 -0
- 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 +19 -0
- package/scripts/artifact-intake.py +408 -0
- package/scripts/asset-consistency.py +517 -0
- package/scripts/asset-provenance.py +395 -0
- package/scripts/build-ai-pilot.py +504 -0
- package/scripts/docs-audit.py +14 -2
- package/scripts/isolate-alpha-background.py +147 -0
- package/scripts/pr.py +2 -0
- package/scripts/quality-gate.py +127 -3
- package/scripts/report.py +135 -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 +500 -0
- package/scripts/skill-doctor.py +2 -1
- package/src/output/browser-review-smoke/asset-provenance.json +77 -0
- package/src/output/browser-review-smoke/manifest.json +1 -0
- package/src/output/browser-review-smoke/visual-truth.json +3 -3
- 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 +131 -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,504 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build a hash-bound, review-only MotionLoom AI scout pilot from four PNG frames.
|
|
3
|
+
|
|
4
|
+
The builder is intentionally pilot-specific. It copies source bytes into a local
|
|
5
|
+
``.motionloom`` workspace, measures their actual alpha geometry using the same
|
|
6
|
+
deterministic reader as Asset Consistency, and writes the contracts that feed
|
|
7
|
+
Artifact Intake and Runtime Candidate. It never manufactures a human approval,
|
|
8
|
+
artist authority, or runtime-pass claim.
|
|
9
|
+
|
|
10
|
+
It uses Python's standard library only and runs on Ubuntu, macOS, and Windows.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import hashlib
|
|
17
|
+
import importlib.util
|
|
18
|
+
import json
|
|
19
|
+
import shutil
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
27
|
+
ASSET_ID = "character.scout-robot.ai-pilot"
|
|
28
|
+
PILOT_ID = "ai-pilot-scout-walk"
|
|
29
|
+
FRAME_ORDER = (
|
|
30
|
+
("idle", "idle", "preview"),
|
|
31
|
+
("contact-right", "contact_right", "runtime_candidate"),
|
|
32
|
+
("passing", "passing", "runtime_candidate"),
|
|
33
|
+
("contact-left", "contact_left", "runtime_candidate"),
|
|
34
|
+
)
|
|
35
|
+
PROVIDER_PROFILES = {
|
|
36
|
+
"internal-imagegen": {
|
|
37
|
+
"adapter_id": "internal.imagegen",
|
|
38
|
+
"kind": "internal_skill",
|
|
39
|
+
"invocation_mode": "agent-mediated",
|
|
40
|
+
"cost_class": "included",
|
|
41
|
+
"generator_source": "internal-imagegen",
|
|
42
|
+
"default_model": "default",
|
|
43
|
+
"default_task_id": "motionloom-scout-ai-pilot-ingest-2026",
|
|
44
|
+
"license_source": "Internal ImageGen generation; provider-native receipt was not exported",
|
|
45
|
+
"reconstruction_source": "agent-mediated ImageGen source",
|
|
46
|
+
"lab_limit": "Internal ImageGen adapter is scaffold-only",
|
|
47
|
+
},
|
|
48
|
+
"chatgpt": {
|
|
49
|
+
"adapter_id": "openai.chatgpt",
|
|
50
|
+
"kind": "external_provider",
|
|
51
|
+
"invocation_mode": "manual",
|
|
52
|
+
"cost_class": "external",
|
|
53
|
+
"generator_source": "openai-chatgpt",
|
|
54
|
+
"default_model": "chatgpt-image-generation",
|
|
55
|
+
"default_task_id": "motionloom-scout-chatgpt-pilot-2026",
|
|
56
|
+
"license_source": "User-provided ChatGPT generation; provider-native receipt was not exported",
|
|
57
|
+
"reconstruction_source": "user-mediated ChatGPT source",
|
|
58
|
+
"lab_limit": "ChatGPT import adapter is scaffold-only",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sha256_file(path: Path) -> str:
|
|
64
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sha256_text(value: str) -> str:
|
|
68
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_json(path: Path, value: dict[str, Any]) -> None:
|
|
72
|
+
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_png_reader() -> Any:
|
|
76
|
+
source = REPO_ROOT / "scripts" / "asset-consistency.py"
|
|
77
|
+
spec = importlib.util.spec_from_file_location("motionloom_pilot_png", source)
|
|
78
|
+
if spec is None or spec.loader is None:
|
|
79
|
+
raise RuntimeError(f"could not load deterministic PNG reader: {source}")
|
|
80
|
+
module = importlib.util.module_from_spec(spec)
|
|
81
|
+
sys.modules[spec.name] = module
|
|
82
|
+
spec.loader.exec_module(module)
|
|
83
|
+
return module.PNGImage
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_urls(values: list[str]) -> dict[str, str]:
|
|
87
|
+
result: dict[str, str] = {}
|
|
88
|
+
for value in values:
|
|
89
|
+
frame_id, separator, url = value.partition("=")
|
|
90
|
+
if not separator or frame_id not in {item[0] for item in FRAME_ORDER} or not url.startswith(("/", "https://", "http://")):
|
|
91
|
+
raise ValueError("--frame-url must look like idle=/manus-storage/frame.png")
|
|
92
|
+
result[frame_id] = url
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def measured_frame(PNGImage: Any, path: Path) -> dict[str, Any]:
|
|
97
|
+
image = PNGImage(path)
|
|
98
|
+
supports_alpha = image.color_type in {4, 6} or (image.color_type == 3 and bool(image._transparency))
|
|
99
|
+
if not supports_alpha:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"{path.name} is color type {image.color_type} and has no alpha channel; "
|
|
102
|
+
"do not treat a painted checkerboard as transparency"
|
|
103
|
+
)
|
|
104
|
+
total = image.width * image.height
|
|
105
|
+
alpha_pixels = image.alpha_count(1)
|
|
106
|
+
if alpha_pixels == total:
|
|
107
|
+
raise ValueError(f"{path.name} has no transparent padding; an isolated alpha frame is required")
|
|
108
|
+
bbox = image.alpha_bbox(1)
|
|
109
|
+
if bbox is None:
|
|
110
|
+
raise ValueError(f"{path.name} has no visible alpha pixels")
|
|
111
|
+
return {
|
|
112
|
+
"width": image.width,
|
|
113
|
+
"height": image.height,
|
|
114
|
+
"alpha_pixels": alpha_pixels,
|
|
115
|
+
"alpha_bbox": bbox,
|
|
116
|
+
"sha256": sha256_file(path),
|
|
117
|
+
"bytes": path.stat().st_size,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def require_clean_pilot_geometry(frame_id: str, data: dict[str, Any]) -> None:
|
|
122
|
+
"""Reject canvas-spanning residue for this isolated, single-character pilot.
|
|
123
|
+
|
|
124
|
+
Alpha isolation can deliberately remove only edge-connected background. A
|
|
125
|
+
detached band may therefore remain alpha-opaque without touching an edge.
|
|
126
|
+
The pilot has a narrow, centred single-character contract, so its measured
|
|
127
|
+
alpha bounds must retain padding on every side and cannot occupy most of a
|
|
128
|
+
canvas width. This is a geometry guard, not a human-quality approval.
|
|
129
|
+
"""
|
|
130
|
+
bbox = data["alpha_bbox"]
|
|
131
|
+
width, height = int(data["width"]), int(data["height"])
|
|
132
|
+
minimum_padding = max(1, round(min(width, height) * 0.05))
|
|
133
|
+
paddings = {
|
|
134
|
+
"left": int(bbox["x"]),
|
|
135
|
+
"top": int(bbox["y"]),
|
|
136
|
+
"right": width - (int(bbox["x"]) + int(bbox["width"])),
|
|
137
|
+
"bottom": height - (int(bbox["y"]) + int(bbox["height"])),
|
|
138
|
+
}
|
|
139
|
+
if min(paddings.values()) < minimum_padding:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"{frame_id} does not retain {minimum_padding}px clean padding on every edge; "
|
|
142
|
+
f"measured {paddings}. Reject possible canvas-spanning contamination."
|
|
143
|
+
)
|
|
144
|
+
if int(bbox["width"]) / width > 0.6:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"{frame_id} alpha subject spans {int(bbox['width'])}/{width}px of the canvas width; "
|
|
147
|
+
"reject possible detached horizontal contamination."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def partial_frame_record(frame_id: str, source: Path, data: dict[str, Any] | None, error: str | None) -> dict[str, Any]:
|
|
152
|
+
"""Describe a rejected source without emitting an ingestable candidate."""
|
|
153
|
+
record: dict[str, Any] = {
|
|
154
|
+
"role": frame_id,
|
|
155
|
+
"source": str(source),
|
|
156
|
+
"sha256": sha256_file(source),
|
|
157
|
+
"bytes": source.stat().st_size,
|
|
158
|
+
}
|
|
159
|
+
if data is not None:
|
|
160
|
+
bbox = data["alpha_bbox"]
|
|
161
|
+
record["canvas"] = [data["width"], data["height"]]
|
|
162
|
+
record["alpha_bbox"] = [bbox["x"], bbox["y"], bbox["width"], bbox["height"]]
|
|
163
|
+
record["padding"] = {
|
|
164
|
+
"left": bbox["x"],
|
|
165
|
+
"top": bbox["y"],
|
|
166
|
+
"right": data["width"] - (bbox["x"] + bbox["width"]),
|
|
167
|
+
"bottom": data["height"] - (bbox["y"] + bbox["height"]),
|
|
168
|
+
}
|
|
169
|
+
if error is not None:
|
|
170
|
+
record["error"] = error
|
|
171
|
+
return record
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def write_partial_handoff(
|
|
175
|
+
output: Path,
|
|
176
|
+
provider: dict[str, str],
|
|
177
|
+
provider_model: str,
|
|
178
|
+
provider_task_id: str,
|
|
179
|
+
timestamp: str,
|
|
180
|
+
rejected_frames: list[dict[str, Any]],
|
|
181
|
+
errors: list[str],
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Write source-bound refusal evidence; never create controls or a candidate."""
|
|
184
|
+
handoff = {
|
|
185
|
+
"contract": "motionloom-ai-pilot-partial-handoff",
|
|
186
|
+
"version": 1,
|
|
187
|
+
"asset_id": ASSET_ID,
|
|
188
|
+
"state": "partial",
|
|
189
|
+
"authority": "ai_generated",
|
|
190
|
+
"production_approved": False,
|
|
191
|
+
"created_at": timestamp,
|
|
192
|
+
"blocked": {
|
|
193
|
+
"reason": "Source preflight failed. No canvas resampling, transparent padding, metadata edit, intake, candidate, render, or approval was emitted.",
|
|
194
|
+
"errors": errors,
|
|
195
|
+
"missing_frames": [entry["role"] for entry in rejected_frames if entry.get("error")],
|
|
196
|
+
"not_emitted": [
|
|
197
|
+
"controls.json",
|
|
198
|
+
"generation receipt",
|
|
199
|
+
"export manifest",
|
|
200
|
+
"asset consistency contract",
|
|
201
|
+
"Artifact Intake report",
|
|
202
|
+
"runtime candidate",
|
|
203
|
+
"runtime render evidence",
|
|
204
|
+
"Dev Lab review candidate",
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
"rejected_source_attempts": [
|
|
208
|
+
{
|
|
209
|
+
"provider": provider["generator_source"],
|
|
210
|
+
"adapter_id": provider["adapter_id"],
|
|
211
|
+
"model": provider_model,
|
|
212
|
+
"task_id": provider_task_id,
|
|
213
|
+
"authority": "ai_generated",
|
|
214
|
+
"state": "rejected_pre_ingest",
|
|
215
|
+
"reason": "Measured source bytes failed the pilot preflight; no transform may bypass the gate.",
|
|
216
|
+
"frames": rejected_frames,
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"resume_requirements": [
|
|
220
|
+
"Provide independent source frames that satisfy the measured canvas and clean-padding requirements.",
|
|
221
|
+
"Do not resize, pad, crop, isolate, or relabel rejected bytes to bypass the source gate.",
|
|
222
|
+
"Re-run build-ai-pilot and downstream pipeline stages only after preflight succeeds.",
|
|
223
|
+
],
|
|
224
|
+
}
|
|
225
|
+
write_json(output / "partial-handoff.json", handoff)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def frame_entry(frame_id: str, role: str, target: str, file_name: str, data: dict[str, Any], root_relative_output: str) -> dict[str, Any]:
|
|
229
|
+
return {
|
|
230
|
+
"path": f"{root_relative_output}/frames/{file_name}",
|
|
231
|
+
"role": f"walk_{role}",
|
|
232
|
+
"sha256": data["sha256"],
|
|
233
|
+
"bytes": data["bytes"],
|
|
234
|
+
"target": target,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main(argv: list[str] | None = None) -> int:
|
|
239
|
+
parser = argparse.ArgumentParser(description="Build MotionLoom's review-only scout AI pilot workspace")
|
|
240
|
+
parser.add_argument("--idle", required=True, type=Path, help="Alpha-isolated idle PNG")
|
|
241
|
+
parser.add_argument("--contact-right", required=True, type=Path, help="Alpha-isolated contact-right PNG")
|
|
242
|
+
parser.add_argument("--passing", required=True, type=Path, help="Alpha-isolated passing PNG")
|
|
243
|
+
parser.add_argument("--contact-left", required=True, type=Path, help="Alpha-isolated contact-left PNG")
|
|
244
|
+
parser.add_argument("--root", type=Path, default=REPO_ROOT, help="MotionLoom repository root")
|
|
245
|
+
parser.add_argument(
|
|
246
|
+
"--output",
|
|
247
|
+
default=".motionloom/pilots/ai-pilot-scout",
|
|
248
|
+
help="Safe relative output directory inside --root",
|
|
249
|
+
)
|
|
250
|
+
parser.add_argument("--frame-url", action="append", default=[], help="Optional frame URL: idle=/manus-storage/frame.png")
|
|
251
|
+
parser.add_argument("--generated-at", default=None, help="ISO-8601 evidence timestamp; defaults to build time")
|
|
252
|
+
parser.add_argument("--provider", choices=sorted(PROVIDER_PROFILES), default="internal-imagegen", help="Truthful source provider profile")
|
|
253
|
+
parser.add_argument("--provider-model", default=None, help="Provider/model label for provenance; defaults to profile value")
|
|
254
|
+
parser.add_argument("--provider-task-id", default=None, help="Provider or user-mediated task identifier for provenance")
|
|
255
|
+
parser.add_argument("--overwrite", action="store_true", help="Replace an existing pilot workspace")
|
|
256
|
+
args = parser.parse_args(argv)
|
|
257
|
+
|
|
258
|
+
root = args.root.resolve()
|
|
259
|
+
output = (root / args.output).resolve()
|
|
260
|
+
try:
|
|
261
|
+
output.relative_to(root)
|
|
262
|
+
except ValueError as exc:
|
|
263
|
+
raise SystemExit("--output must stay inside --root") from exc
|
|
264
|
+
if output.exists():
|
|
265
|
+
if not args.overwrite:
|
|
266
|
+
raise SystemExit(f"pilot workspace already exists: {output}; pass --overwrite to replace it")
|
|
267
|
+
shutil.rmtree(output)
|
|
268
|
+
urls = parse_urls(args.frame_url)
|
|
269
|
+
timestamp = args.generated_at or datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
270
|
+
provider = PROVIDER_PROFILES[args.provider]
|
|
271
|
+
provider_model = args.provider_model or provider["default_model"]
|
|
272
|
+
provider_task_id = args.provider_task_id or provider["default_task_id"]
|
|
273
|
+
|
|
274
|
+
sources = {
|
|
275
|
+
"idle": args.idle,
|
|
276
|
+
"contact-right": args.contact_right,
|
|
277
|
+
"passing": args.passing,
|
|
278
|
+
"contact-left": args.contact_left,
|
|
279
|
+
}
|
|
280
|
+
for frame_id, source in sources.items():
|
|
281
|
+
if not source.is_file():
|
|
282
|
+
raise SystemExit(f"missing {frame_id} source image: {source}")
|
|
283
|
+
|
|
284
|
+
output.mkdir(parents=True)
|
|
285
|
+
frames_dir = output / "frames"
|
|
286
|
+
frames_dir.mkdir()
|
|
287
|
+
PNGImage = load_png_reader()
|
|
288
|
+
frames: dict[str, dict[str, Any]] = {}
|
|
289
|
+
rejected_frames: list[dict[str, Any]] = []
|
|
290
|
+
preflight_errors: list[str] = []
|
|
291
|
+
for frame_id, role, _ in FRAME_ORDER:
|
|
292
|
+
destination = frames_dir / f"scout-{frame_id}.png"
|
|
293
|
+
shutil.copy2(sources[frame_id], destination)
|
|
294
|
+
data: dict[str, Any] | None = None
|
|
295
|
+
try:
|
|
296
|
+
data = measured_frame(PNGImage, destination)
|
|
297
|
+
require_clean_pilot_geometry(frame_id, data)
|
|
298
|
+
except ValueError as exc:
|
|
299
|
+
error = str(exc)
|
|
300
|
+
preflight_errors.append(error)
|
|
301
|
+
rejected_frames.append(partial_frame_record(frame_id, destination, data, error))
|
|
302
|
+
continue
|
|
303
|
+
assert data is not None
|
|
304
|
+
frames[frame_id] = data
|
|
305
|
+
rejected_frames.append(partial_frame_record(frame_id, destination, data, None))
|
|
306
|
+
|
|
307
|
+
if not preflight_errors:
|
|
308
|
+
canvas_sizes = {(item["width"], item["height"]) for item in frames.values()}
|
|
309
|
+
if len(canvas_sizes) != 1:
|
|
310
|
+
preflight_errors.append(f"all frames must use one canvas; measured {sorted(canvas_sizes)}")
|
|
311
|
+
|
|
312
|
+
if preflight_errors:
|
|
313
|
+
write_partial_handoff(output, provider, provider_model, provider_task_id, timestamp, rejected_frames, preflight_errors)
|
|
314
|
+
print(json.dumps({"status": "partial", "root": str(root), "output": output.relative_to(root).as_posix(), "asset_id": ASSET_ID, "errors": preflight_errors}, indent=2))
|
|
315
|
+
return 2
|
|
316
|
+
|
|
317
|
+
canvas_sizes = {(item["width"], item["height"]) for item in frames.values()}
|
|
318
|
+
width, height = next(iter(canvas_sizes))
|
|
319
|
+
alpha_bboxes = [item["alpha_bbox"] for item in frames.values()]
|
|
320
|
+
footlines = [bbox["y"] + bbox["height"] - 1 for bbox in alpha_bboxes]
|
|
321
|
+
pivots = [(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] - 1) for bbox in alpha_bboxes]
|
|
322
|
+
reference_pivot = pivots[0]
|
|
323
|
+
pivot_tolerance = max(max(abs(x - reference_pivot[0]), abs(y - reference_pivot[1])) for x, y in pivots)
|
|
324
|
+
footline_tolerance = max(abs(value - footlines[0]) for value in footlines)
|
|
325
|
+
bbox_tolerance = max(
|
|
326
|
+
max(abs(item["width"] - alpha_bboxes[0]["width"]), abs(item["height"] - alpha_bboxes[0]["height"]))
|
|
327
|
+
for item in alpha_bboxes
|
|
328
|
+
)
|
|
329
|
+
min_alpha = min(int(item["alpha_pixels"]) for item in frames.values())
|
|
330
|
+
idle_hash = frames["idle"]["sha256"]
|
|
331
|
+
prompt_capsule = (
|
|
332
|
+
f"MotionLoom scout robot AI pilot imported from {provider['generator_source']}. "
|
|
333
|
+
"Post-hoc ingest capsule only; provider-native prompt and task receipt were not exported. "
|
|
334
|
+
"The capsule binds reviewed output bytes without asserting a human approval or artist authorship."
|
|
335
|
+
)
|
|
336
|
+
prompt_hash = sha256_text(prompt_capsule)
|
|
337
|
+
motion_hash = sha256_text("walk_pilot_4frame:idle,contact-right,passing,contact-left")
|
|
338
|
+
style_hash = sha256_text("cream-orange-black-cyan pixel-art scout robot style lock")
|
|
339
|
+
|
|
340
|
+
relative_output = output.relative_to(root).as_posix()
|
|
341
|
+
geometry_frames: list[dict[str, Any]] = []
|
|
342
|
+
receipt_outputs: list[dict[str, Any]] = []
|
|
343
|
+
export_outputs: list[dict[str, Any]] = []
|
|
344
|
+
provenance_files: list[dict[str, Any]] = []
|
|
345
|
+
for index, (frame_id, role, target) in enumerate(FRAME_ORDER):
|
|
346
|
+
data = frames[frame_id]
|
|
347
|
+
bbox = data["alpha_bbox"]
|
|
348
|
+
pivot = pivots[index]
|
|
349
|
+
filename = f"scout-{frame_id}.png"
|
|
350
|
+
geometry_frames.append(
|
|
351
|
+
{
|
|
352
|
+
"frame_id": f"scout-{frame_id}",
|
|
353
|
+
"image": f"frames/{filename}",
|
|
354
|
+
"rect": {"x": 0, "y": 0, "width": width, "height": height},
|
|
355
|
+
"alpha_bbox": bbox,
|
|
356
|
+
"pivot": {"x": pivot[0], "y": pivot[1], "space": "pixels"},
|
|
357
|
+
"footline_px": footlines[index],
|
|
358
|
+
"safe_rect": bbox,
|
|
359
|
+
"bleed_margin_px": 0,
|
|
360
|
+
"sha256": data["sha256"],
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
record = frame_entry(frame_id, role, target, filename, data, relative_output)
|
|
364
|
+
receipt_outputs.append({key: value for key, value in record.items() if key != "target"})
|
|
365
|
+
export_outputs.append(record)
|
|
366
|
+
provenance_files.append(
|
|
367
|
+
{
|
|
368
|
+
"path": record["path"],
|
|
369
|
+
"role": record["role"],
|
|
370
|
+
"sha256": record["sha256"],
|
|
371
|
+
"bytes": record["bytes"],
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
controls = {
|
|
376
|
+
"schema_version": "0.1",
|
|
377
|
+
"control_id": "scout-ai-pilot-controls-v1",
|
|
378
|
+
"asset_id": ASSET_ID,
|
|
379
|
+
"created_at": timestamp,
|
|
380
|
+
"references": [{"id": "scout-idle-master", "path": f"{relative_output}/frames/scout-idle.png", "sha256": idle_hash, "role": "identity"}],
|
|
381
|
+
"tracks": [
|
|
382
|
+
{"id": "identity-lock", "kind": "identity", "binding": "required", "value_hash": idle_hash, "samples": 1},
|
|
383
|
+
{"id": "style-lock", "kind": "style", "binding": "advisory", "value_hash": style_hash, "samples": 4},
|
|
384
|
+
{"id": "pose-motion", "kind": "pose", "binding": "required", "value_hash": motion_hash, "samples": 4},
|
|
385
|
+
],
|
|
386
|
+
"output_profile": {"kind": "frame_sequence", "fps": 8, "expected_frame_count": 4, "loop": True},
|
|
387
|
+
}
|
|
388
|
+
provenance = {
|
|
389
|
+
"schema_version": "1.0",
|
|
390
|
+
"provenance_id": "scout-ai-pilot-provenance-v1",
|
|
391
|
+
"task_id": provider_task_id,
|
|
392
|
+
"scene": "scout-walk",
|
|
393
|
+
"created_at": timestamp,
|
|
394
|
+
"asset": {"id": ASSET_ID, "path": f"{relative_output}/frames/scout-idle.png", "type": "frame_sequence", "framework": "canvas", "version": "pilot-v1"},
|
|
395
|
+
"authority": "ai_generated",
|
|
396
|
+
"readiness": "runtime_ready",
|
|
397
|
+
"generator": {"model": provider_model, "task_id": provider_task_id, "source": provider["generator_source"], "generated_at": timestamp, "prompt_hash": prompt_hash, "agent": "MotionLoom"},
|
|
398
|
+
"license": {"spdx": "LicenseRef-Generated-Pilot", "source": provider["license_source"], "attribution": "AI-generated scout pilot; human review remains required"},
|
|
399
|
+
"files": provenance_files,
|
|
400
|
+
"provenance_chain": [{"step": "ingest", "actor": "MotionLoom", "source": f"post-hoc hash-bound local ingest from {provider['reconstruction_source']}; not a provider-native receipt", "timestamp": timestamp}],
|
|
401
|
+
"runtime_evidence": {"status": "not_run", "runtime": "canvas", "tested_at": timestamp},
|
|
402
|
+
}
|
|
403
|
+
receipt = {
|
|
404
|
+
"schema_version": "0.1",
|
|
405
|
+
"receipt_id": "scout-ai-pilot-receipt-v1",
|
|
406
|
+
"created_at": timestamp,
|
|
407
|
+
"asset": {"id": ASSET_ID, "kind": "frame_sequence", "intended_use": "runtime_candidate"},
|
|
408
|
+
"authority": "ai_generated",
|
|
409
|
+
"provider": {"adapter_id": provider["adapter_id"], "kind": provider["kind"], "invocation_mode": provider["invocation_mode"], "task_id": provider_task_id, "model": provider_model, "generated_at": timestamp, "cost_class": provider["cost_class"], "prompt_hash": prompt_hash},
|
|
410
|
+
"control_track_ref": f"{relative_output}/controls.json",
|
|
411
|
+
"provenance_ref": f"{relative_output}/provenance.json",
|
|
412
|
+
"outputs": receipt_outputs,
|
|
413
|
+
}
|
|
414
|
+
export = {
|
|
415
|
+
"schema_version": "0.1",
|
|
416
|
+
"manifest_id": "scout-ai-pilot-export-v1",
|
|
417
|
+
"asset_id": ASSET_ID,
|
|
418
|
+
"created_at": timestamp,
|
|
419
|
+
"receipt_ref": f"{relative_output}/receipt.json",
|
|
420
|
+
"control_track_ref": f"{relative_output}/controls.json",
|
|
421
|
+
"outputs": export_outputs,
|
|
422
|
+
}
|
|
423
|
+
identity = {
|
|
424
|
+
"schema_version": "0.1",
|
|
425
|
+
"asset_id": ASSET_ID,
|
|
426
|
+
"asset_kind": "character",
|
|
427
|
+
"identity": {
|
|
428
|
+
"subject_id": "scout-robot",
|
|
429
|
+
"reference_hashes": [idle_hash],
|
|
430
|
+
"camera": {"projection": "orthographic", "width": width, "height": height, "focal_length": 1},
|
|
431
|
+
"coordinate_system": "screen_y_down",
|
|
432
|
+
"scale": 1,
|
|
433
|
+
"pivot": {"x": reference_pivot[0] / width, "y": reference_pivot[1] / height, "space": "normalized_canvas"},
|
|
434
|
+
"palette_lock": {"colors": ["#f7e4c6", "#f47c15", "#121116", "#18d8f8"], "delta_e_tolerance": 28},
|
|
435
|
+
"style_profile": "pixel-art scout robot pilot",
|
|
436
|
+
"lighting_profile": "flat game sprite lighting",
|
|
437
|
+
},
|
|
438
|
+
"derivation": {"origin": "ai_generated", "generator": {"model": provider_model, "task_id": provider_task_id, "prompt_hash": prompt_hash}, "source_refs": ["provenance.json", "receipt-reconstruction.json"]},
|
|
439
|
+
"provenance_ref": "provenance.json",
|
|
440
|
+
"notes": "Identity controls are a post-hoc ingest representation. They bind output hashes but do not prove provider-native generation controls.",
|
|
441
|
+
}
|
|
442
|
+
action_set = {
|
|
443
|
+
"schema_version": "0.1",
|
|
444
|
+
"asset_identity": ASSET_ID,
|
|
445
|
+
"actions": [{"action_id": "walk-pilot", "fps": 8, "frames": [f"frames/scout-{frame_id}.png" for frame_id, _, _ in FRAME_ORDER], "loop": True, "events": ["foot_contact_right", "passing", "foot_contact_left"]}],
|
|
446
|
+
"invariants": {"canvas_width": width, "canvas_height": height, "pivot_tolerance_px": pivot_tolerance, "footline_tolerance_px": footline_tolerance, "bbox_drift_tolerance_px": bbox_tolerance, "min_alpha_pixels": min_alpha},
|
|
447
|
+
}
|
|
448
|
+
frame_geometry = {
|
|
449
|
+
"schema_version": "0.1",
|
|
450
|
+
"asset_identity": ASSET_ID,
|
|
451
|
+
"canvas": {"width": width, "height": height, "color_space": "srgb", "alpha_mode": "straight"},
|
|
452
|
+
"invariants": {"pivot_tolerance_px": pivot_tolerance, "footline_tolerance_px": footline_tolerance, "bbox_drift_tolerance_px": bbox_tolerance, "min_alpha_pixels": min_alpha, "allow_external_opaque_pixels": False},
|
|
453
|
+
"frames": geometry_frames,
|
|
454
|
+
}
|
|
455
|
+
candidate = {
|
|
456
|
+
"schema_version": "0.1",
|
|
457
|
+
"candidate_id": "scout-ai-pilot-candidate-v1",
|
|
458
|
+
"asset_id": ASSET_ID,
|
|
459
|
+
"created_at": timestamp,
|
|
460
|
+
"artifact_intake": {"registry": "artifact-adapter-registry.json", "receipt": f"{relative_output}/receipt.json", "controls": f"{relative_output}/controls.json", "export_manifest": f"{relative_output}/export.json"},
|
|
461
|
+
"consistency": {"asset_identity": f"{relative_output}/asset-identity.json", "action_set": f"{relative_output}/action-set.json", "frame_geometry": f"{relative_output}/frame-geometry.json"},
|
|
462
|
+
"runtime": {"target": "canvas", "review_required": True},
|
|
463
|
+
}
|
|
464
|
+
reconstruction = {
|
|
465
|
+
"artifact": ASSET_ID,
|
|
466
|
+
"status": "incomplete_provider_receipt",
|
|
467
|
+
"created_at": timestamp,
|
|
468
|
+
"known": ["output bytes", "output SHA-256", provider["reconstruction_source"], "post-hoc control capsule hash"],
|
|
469
|
+
"unknown": ["provider-native request ID", "provider-native prompt record", "provider-native seed", "provider-native output receipt"],
|
|
470
|
+
"governance": {"authority": "ai_generated", "human_review": "required", "production_approved": False, "attestation_approval": False},
|
|
471
|
+
}
|
|
472
|
+
lab_evidence = {
|
|
473
|
+
"pilot_id": PILOT_ID,
|
|
474
|
+
"asset_id": ASSET_ID,
|
|
475
|
+
"created_at": timestamp,
|
|
476
|
+
"state": "review_required",
|
|
477
|
+
"authority": "ai_generated",
|
|
478
|
+
"production_approved": False,
|
|
479
|
+
"runtime_verified": False,
|
|
480
|
+
"limits": [provider["lab_limit"], "Provider-native generation receipt was unavailable", "Runtime proof is not yet present", "No human approval exists"],
|
|
481
|
+
"frames": [
|
|
482
|
+
{"id": frame_id, "label": role.replace("_", " "), "path": f"frames/scout-{frame_id}.png", "url": urls.get(frame_id), "sha256": frames[frame_id]["sha256"], "alpha_bbox": frames[frame_id]["alpha_bbox"]}
|
|
483
|
+
for frame_id, role, _ in FRAME_ORDER
|
|
484
|
+
],
|
|
485
|
+
}
|
|
486
|
+
for filename, document in (
|
|
487
|
+
("controls.json", controls),
|
|
488
|
+
("provenance.json", provenance),
|
|
489
|
+
("receipt.json", receipt),
|
|
490
|
+
("export.json", export),
|
|
491
|
+
("asset-identity.json", identity),
|
|
492
|
+
("action-set.json", action_set),
|
|
493
|
+
("frame-geometry.json", frame_geometry),
|
|
494
|
+
("candidate.json", candidate),
|
|
495
|
+
("receipt-reconstruction.json", reconstruction),
|
|
496
|
+
("devlab-pilot-evidence.json", lab_evidence),
|
|
497
|
+
):
|
|
498
|
+
write_json(output / filename, document)
|
|
499
|
+
print(json.dumps({"status": "built", "root": str(root), "output": relative_output, "asset_id": ASSET_ID, "frame_count": len(frames), "alpha_geometry": {"footline_range": [min(footlines), max(footlines)], "pivot_tolerance_px": pivot_tolerance, "footline_tolerance_px": footline_tolerance, "bbox_drift_tolerance_px": bbox_tolerance}}, indent=2))
|
|
500
|
+
return 0
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if __name__ == "__main__":
|
|
504
|
+
raise SystemExit(main())
|
package/scripts/docs-audit.py
CHANGED
|
@@ -23,7 +23,7 @@ for markdown in sorted(ROOT.rglob("*.md")):
|
|
|
23
23
|
if not (markdown.parent / target).resolve().exists():
|
|
24
24
|
errors.append(f"{markdown.relative_to(ROOT)} -> missing {target}")
|
|
25
25
|
|
|
26
|
-
for relative in ["package.json", "agent-card.json", "agent-surfaces.json", "schemas/agent-surfaces.schema.json", "schemas/visual-truth.schema.json", "schemas/remediation-history.schema.json", "project-context.example.json", "tests/evals/project-corpus.json"]:
|
|
26
|
+
for relative in ["package.json", "agent-card.json", "agent-surfaces.json", "schemas/agent-surfaces.schema.json", "schemas/provenance.schema.json", "schemas/asset-provenance.schema.json", "schemas/scene-manifest.schema.json", "schemas/visual-truth.schema.json", "schemas/remediation-history.schema.json", "project-context.example.json", "examples/agent-consumer/ai-generated-pilot-provenance.json", "tests/evals/project-corpus.json"]:
|
|
27
27
|
path = ROOT / relative
|
|
28
28
|
try:
|
|
29
29
|
json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -39,6 +39,12 @@ if package.get("packageManager") != "pnpm@11.20.0":
|
|
|
39
39
|
for required_surface in [".agents", ".claude", ".codex", "AGENTS.md", "agent-surfaces.json"]:
|
|
40
40
|
if required_surface not in package.get("files", []):
|
|
41
41
|
errors.append(f"package.json: files must include Agent surface {required_surface}")
|
|
42
|
+
for required_path in ["scripts/asset-provenance.py", "schemas/asset-provenance.schema.json", "examples/agent-consumer/ai-generated-pilot-provenance.json"]:
|
|
43
|
+
if required_path not in package.get("files", []):
|
|
44
|
+
errors.append(f"package.json: files must include asset provenance contract {required_path}")
|
|
45
|
+
for onboarding_script in ["setup", "setup:dry", "status", "repair"]:
|
|
46
|
+
if onboarding_script not in package.get("scripts", {}):
|
|
47
|
+
errors.append(f"package.json: missing onboarding script {onboarding_script}")
|
|
42
48
|
|
|
43
49
|
sys.path.insert(0, str(ROOT))
|
|
44
50
|
try:
|
|
@@ -54,9 +60,13 @@ for required_doc in ["docs/AGENT-INTEGRATION.md", "references/agent-interoperabi
|
|
|
54
60
|
errors.append(f"missing Agent interoperability document: {required_doc}")
|
|
55
61
|
|
|
56
62
|
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
57
|
-
for heading in ["Why MotionLoom", "Quick start", "Durable Project Memory", "Evidence, trust and review", "Documentation map"]:
|
|
63
|
+
for heading in ["Why MotionLoom", "Quick start", "Durable Project Memory", "Evidence, trust and review", "Asset provenance tiers", "Documentation map"]:
|
|
58
64
|
if f"## {heading}" not in readme:
|
|
59
65
|
errors.append(f"README.md: missing heading {heading}")
|
|
66
|
+
if "npx --yes motionloom setup" not in readme:
|
|
67
|
+
errors.append("README.md: missing one-command onboarding recipe")
|
|
68
|
+
if "npx --yes motionloom setup" not in (ROOT / "docs/AGENT-INTEGRATION.md").read_text(encoding="utf-8"):
|
|
69
|
+
errors.append("docs/AGENT-INTEGRATION.md: missing one-command onboarding recipe")
|
|
60
70
|
|
|
61
71
|
workflow_dir = ROOT / ".github" / "workflows"
|
|
62
72
|
for workflow in sorted(workflow_dir.glob("*.yml")):
|
|
@@ -77,6 +87,8 @@ for workflow in sorted(workflow_dir.glob("*.yml")):
|
|
|
77
87
|
for required in ["name:", "on:", "jobs:", "permissions:"]:
|
|
78
88
|
if required not in text:
|
|
79
89
|
errors.append(f"{workflow.relative_to(ROOT)}: missing {required}")
|
|
90
|
+
if "--require-asset-provenance" not in (workflow_dir / "quality.yml").read_text(encoding="utf-8"):
|
|
91
|
+
errors.append("quality.yml: missing fail-closed asset provenance production gate")
|
|
80
92
|
if "pull_request:" in text and "secrets." in text:
|
|
81
93
|
errors.append(f"{workflow.relative_to(ROOT)}: secrets referenced in pull_request workflow")
|
|
82
94
|
|