motionloom 2.6.1 → 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 +2 -2
- package/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +19 -3
- package/SECURITY.md +5 -5
- package/SKILL.md +6 -3
- package/agent-card.json +18 -4
- package/agent-surfaces.json +1 -1
- package/artifact-adapter-registry.json +568 -20
- package/bin/motionloom.mjs +21 -4
- 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.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 +14 -1
- package/package.json +15 -1
- package/references/multi-frame-asset-generation.md +37 -2
- 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 +24 -1
- 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 +35 -5
- package/scripts/frame-set-preflight.py +52 -2
- 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_generation_plan.py +250 -0
- package/tests/scripts/test_attestation.py +24 -8
package/scripts/review-hook.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
import hashlib
|
|
8
|
+
import os
|
|
8
9
|
import json
|
|
9
10
|
import re
|
|
10
11
|
import shutil
|
|
@@ -14,6 +15,12 @@ from datetime import datetime, timedelta, timezone
|
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
16
17
|
|
|
18
|
+
try:
|
|
19
|
+
from browser_review_consistency import candidate_consistency_errors
|
|
20
|
+
except ModuleNotFoundError: # Support importlib-based contract tests.
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
+
from browser_review_consistency import candidate_consistency_errors
|
|
23
|
+
|
|
17
24
|
ROOT = Path(__file__).resolve().parents[1]
|
|
18
25
|
SAFE_SCENE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
19
26
|
SAFE_ANIMATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
@@ -182,6 +189,20 @@ def runtime_bundle(scene_dir: Path) -> dict | None:
|
|
|
182
189
|
review_policy = descriptor.get("review_policy")
|
|
183
190
|
if not isinstance(review_policy, dict) or not isinstance(review_policy.get("require_all_animations"), bool):
|
|
184
191
|
raise ValueError("devlab-runtime.json review_policy.require_all_animations must be boolean")
|
|
192
|
+
action_separation = descriptor.get("action_separation")
|
|
193
|
+
if action_separation is not None:
|
|
194
|
+
if not isinstance(action_separation, dict):
|
|
195
|
+
raise ValueError("devlab-runtime.json action_separation must be an object")
|
|
196
|
+
if action_separation.get("status") not in {"pass", "quarantined"}:
|
|
197
|
+
raise ValueError("devlab-runtime.json action_separation.status must be pass or quarantined")
|
|
198
|
+
if not isinstance(action_separation.get("action_id"), str) or not SAFE_ANIMATION.fullmatch(action_separation["action_id"]):
|
|
199
|
+
raise ValueError("devlab-runtime.json action_separation.action_id is invalid")
|
|
200
|
+
frame_count = action_separation.get("frame_count")
|
|
201
|
+
passing_count = action_separation.get("passing_frame_count")
|
|
202
|
+
if not isinstance(frame_count, int) or frame_count < 1 or not isinstance(passing_count, int) or passing_count < 0 or passing_count > frame_count:
|
|
203
|
+
raise ValueError("devlab-runtime.json action_separation frame counts are invalid")
|
|
204
|
+
if not isinstance(action_separation.get("forbidden_action_ids"), list):
|
|
205
|
+
raise ValueError("devlab-runtime.json action_separation.forbidden_action_ids must be an array")
|
|
185
206
|
|
|
186
207
|
digest = hashlib.sha256()
|
|
187
208
|
digest.update(b"motionloom-devlab-runtime-v1\0")
|
|
@@ -198,6 +219,7 @@ def runtime_bundle(scene_dir: Path) -> dict | None:
|
|
|
198
219
|
"mode": mode,
|
|
199
220
|
"files": sorted(resolved_files),
|
|
200
221
|
"review_policy": {"require_all_animations": review_policy["require_all_animations"]},
|
|
222
|
+
"action_separation": action_separation,
|
|
201
223
|
}
|
|
202
224
|
|
|
203
225
|
|
|
@@ -215,6 +237,7 @@ def runtime_review_payload(bundle: dict | None) -> dict:
|
|
|
215
237
|
"bundle_sha256": bundle["bundle_sha256"],
|
|
216
238
|
"animations": bundle["animations"],
|
|
217
239
|
"review_policy": bundle["review_policy"],
|
|
240
|
+
"action_separation": bundle.get("action_separation"),
|
|
218
241
|
}
|
|
219
242
|
|
|
220
243
|
|
|
@@ -334,8 +357,24 @@ def prepare(args: argparse.Namespace) -> int:
|
|
|
334
357
|
)),
|
|
335
358
|
})
|
|
336
359
|
handoff_path.write_text(json.dumps(handoff, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
337
|
-
|
|
338
|
-
|
|
360
|
+
project_env = os.environ.copy()
|
|
361
|
+
project_env["MOTIONLOOM_PROJECT_ROOT"] = str(ROOT)
|
|
362
|
+
subprocess.run(
|
|
363
|
+
[sys.executable, str(ROOT / "scripts/devlab.py"), scene, "--prepare-only", "--task-dir", str(task_dir)],
|
|
364
|
+
check=True,
|
|
365
|
+
capture_output=True,
|
|
366
|
+
text=True,
|
|
367
|
+
cwd=ROOT,
|
|
368
|
+
env=project_env,
|
|
369
|
+
)
|
|
370
|
+
subprocess.run(
|
|
371
|
+
[sys.executable, str(ROOT / "scripts/report.py"), "collect", "--task-dir", str(task_dir)],
|
|
372
|
+
check=True,
|
|
373
|
+
capture_output=True,
|
|
374
|
+
text=True,
|
|
375
|
+
cwd=ROOT,
|
|
376
|
+
env=project_env,
|
|
377
|
+
)
|
|
339
378
|
print(json.dumps({
|
|
340
379
|
"status": "review_required",
|
|
341
380
|
"task_id": task["task_id"],
|
|
@@ -363,6 +402,12 @@ def validate(args: argparse.Namespace) -> int:
|
|
|
363
402
|
bundle["bundle_sha256"] if bundle else None,
|
|
364
403
|
)
|
|
365
404
|
errors = []
|
|
405
|
+
errors.extend(candidate_consistency_errors(
|
|
406
|
+
scene_dir / "browser-review.json",
|
|
407
|
+
task_dir / "browser-review.json",
|
|
408
|
+
expected_task_id=task.get("task_id"),
|
|
409
|
+
expected_scene=task.get("scene"),
|
|
410
|
+
))
|
|
366
411
|
if not candidate.get("expires_at"):
|
|
367
412
|
errors.append("browser-review candidate has no expiry")
|
|
368
413
|
else:
|
|
@@ -446,17 +491,21 @@ def validate(args: argparse.Namespace) -> int:
|
|
|
446
491
|
|
|
447
492
|
|
|
448
493
|
def main() -> int:
|
|
494
|
+
global ROOT
|
|
449
495
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
450
496
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
451
497
|
p = sub.add_parser("prepare")
|
|
452
498
|
p.add_argument("--task-dir", required=True)
|
|
499
|
+
p.add_argument("--root", default=str(ROOT), help="Repository root containing the canonical scene artifacts")
|
|
453
500
|
p.add_argument("--lab-url", default="http://127.0.0.1:3300")
|
|
454
501
|
p.set_defaults(func=prepare)
|
|
455
502
|
v = sub.add_parser("validate")
|
|
456
503
|
v.add_argument("--task-dir", required=True)
|
|
504
|
+
v.add_argument("--root", default=str(ROOT), help="Repository root containing the canonical scene artifacts")
|
|
457
505
|
v.add_argument("--require-approved", action="store_true")
|
|
458
506
|
v.set_defaults(func=validate)
|
|
459
507
|
args = parser.parse_args()
|
|
508
|
+
ROOT = Path(args.root).expanduser().resolve()
|
|
460
509
|
try:
|
|
461
510
|
return args.func(args)
|
|
462
511
|
except (KeyError, FileNotFoundError, json.JSONDecodeError, ValueError, subprocess.CalledProcessError) as exc:
|
package/scripts/skill-doctor.py
CHANGED
|
@@ -7,6 +7,7 @@ import argparse
|
|
|
7
7
|
import importlib.util
|
|
8
8
|
import json
|
|
9
9
|
import re
|
|
10
|
+
import subprocess
|
|
10
11
|
import sys
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
@@ -71,9 +72,35 @@ def parse_frontmatter(text: str) -> dict[str, str] | None:
|
|
|
71
72
|
return values
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
def chromium_executable() -> tuple[Path | None, str | None]:
|
|
76
|
+
probe = (
|
|
77
|
+
"import { chromium } from 'playwright'; "
|
|
78
|
+
"process.stdout.write(chromium.executablePath());"
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
["node", "--input-type=module", "--eval", probe],
|
|
83
|
+
cwd=ROOT,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
timeout=10,
|
|
87
|
+
)
|
|
88
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
89
|
+
return None, str(exc)
|
|
90
|
+
if result.returncode != 0:
|
|
91
|
+
return None, (result.stderr or result.stdout).strip() or "Playwright import failed"
|
|
92
|
+
path = Path(result.stdout.strip())
|
|
93
|
+
return (path if path else None), None
|
|
94
|
+
|
|
95
|
+
|
|
74
96
|
def run() -> int:
|
|
75
97
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
76
98
|
parser.add_argument("--json", action="store_true", dest="as_json")
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--runtime",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="also verify that the Playwright Chromium executable is installed",
|
|
103
|
+
)
|
|
77
104
|
args = parser.parse_args()
|
|
78
105
|
errors: list[dict] = []
|
|
79
106
|
warnings: list[dict] = []
|
|
@@ -156,6 +183,21 @@ def run() -> int:
|
|
|
156
183
|
except (FileNotFoundError, json.JSONDecodeError) as exc:
|
|
157
184
|
errors.append({"code": "invalid_package_json", "message": str(exc)})
|
|
158
185
|
|
|
186
|
+
if args.runtime:
|
|
187
|
+
chromium_path, chromium_error = chromium_executable()
|
|
188
|
+
chromium_ready = chromium_path is not None and chromium_path.is_file()
|
|
189
|
+
checks.append({
|
|
190
|
+
"id": "runtime:chromium",
|
|
191
|
+
"status": "pass" if chromium_ready else "fail",
|
|
192
|
+
"path": str(chromium_path) if chromium_path else None,
|
|
193
|
+
})
|
|
194
|
+
if not chromium_ready:
|
|
195
|
+
detail = chromium_error or f"Chromium executable is missing: {chromium_path}"
|
|
196
|
+
errors.append({
|
|
197
|
+
"code": "missing_browser_executable",
|
|
198
|
+
"message": f"Playwright Chromium is unavailable ({detail}); run `npx playwright install chromium`.",
|
|
199
|
+
})
|
|
200
|
+
|
|
159
201
|
cryptography_available = importlib.util.find_spec("cryptography") is not None
|
|
160
202
|
checks.append({"id": "python-dependency:cryptography", "status": "pass" if cryptography_available else "fail"})
|
|
161
203
|
if not cryptography_available:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.0",
|
|
3
|
-
"candidate_id": "
|
|
4
|
-
"task_id": "
|
|
3
|
+
"candidate_id": "956caeb2f56397430c4f",
|
|
4
|
+
"task_id": "browser-review-smoke-task",
|
|
5
5
|
"scene": "browser-review-smoke",
|
|
6
|
-
"url": "
|
|
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-
|
|
19
|
-
"expires_at": "
|
|
20
|
-
"reviewed_at": "2026-08-
|
|
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())
|
|
@@ -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(
|
|
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(
|
|
159
|
+
motion_ir_sha256=sha256_file(smoke_task_dir / "motion-ir.json"),
|
|
144
160
|
evidence={
|
|
145
|
-
"runtime_evidence_sha256": sha256_file(
|
|
146
|
-
"runtime_telemetry_sha256": telemetry_bundle_sha256(
|
|
147
|
-
"verifier_report_sha256": sha256_file(
|
|
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(
|
|
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(
|
|
161
|
-
"--task-dir", str(
|
|
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),
|