motionloom 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +78 -11
- package/ROADMAP.md +8 -5
- package/SKILL.md +24 -5
- package/agent-card.json +31 -5
- package/agent-surfaces.json +1 -1
- package/artifact-adapter-registry.json +53 -0
- package/bin/motionloom.mjs +29 -5
- package/docs/CHECKLIST.md +16 -0
- package/docs/STATUS.md +2 -2
- package/docs/audits/data/deep-stress-latest.json +65 -65
- package/docs/releases/2.4.0.md +31 -0
- package/docs/releases/npm-publish-from-workstation.md +19 -8
- package/docs/research/agent-skill-ecosystem-notes.md +47 -0
- package/docs/research/ai-animation-tools-2026-notes.md +81 -0
- package/docs/research/ai-animation-tools-2026-report.md +168 -0
- package/docs/research/ai-pilot-ingest-notes.md +29 -0
- package/examples/agent-consumer/ai-pilot-scout/CHATGPT-HANDOFF.md +70 -0
- package/examples/agent-consumer/ai-pilot-scout/CODEX-HANDOFF.md +80 -0
- package/examples/agent-consumer/ai-pilot-scout/README.md +39 -0
- package/examples/agent-consumer/ai-pilot-scout/chatgpt-geometry-correction-prompt.md +55 -0
- package/examples/agent-consumer/ai-pilot-scout/chatgpt-pose-generation-prompt.md +78 -0
- package/examples/agent-consumer/ai-pilot-scout/partial-handoff.json +73 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-controls.json +15 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-export.json +14 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-provenance.json +20 -0
- package/examples/agent-consumer/artifact-intake/hero-motion-receipt.json +16 -0
- package/examples/agent-consumer/asset-consistency/assets/forest-back.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/forest-front.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/forest-mid.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-atlas.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-00.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-01.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-02.png +0 -0
- package/examples/agent-consumer/asset-consistency/assets/hero-frame-03.png +0 -0
- package/examples/agent-consumer/asset-consistency/forest-layered-map.json +53 -0
- package/examples/agent-consumer/asset-consistency/hero-atlas-contract.json +24 -0
- package/examples/agent-consumer/asset-consistency/hero-identity.json +51 -0
- package/examples/agent-consumer/asset-consistency/hero-walk-action-set.json +27 -0
- package/examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json +63 -0
- package/examples/agent-consumer/rig-compatibility/hero-walk-fixture-rig.json +13 -0
- package/examples/agent-consumer/rive-package-gate/README.md +20 -0
- package/examples/agent-consumer/runtime-candidate/hero-walk-candidate.json +18 -0
- package/examples/agent-consumer/runtime-pilot/action-set.json +19 -0
- package/examples/agent-consumer/runtime-pilot/asset-identity.json +44 -0
- package/examples/agent-consumer/runtime-pilot/candidate.json +21 -0
- package/examples/agent-consumer/runtime-pilot/controls.json +35 -0
- package/examples/agent-consumer/runtime-pilot/export.json +17 -0
- package/examples/agent-consumer/runtime-pilot/provenance.json +38 -0
- package/examples/agent-consumer/runtime-pilot/receipt.json +30 -0
- package/package.json +56 -4
- package/references/browser-review-contract.md +7 -1
- package/references/intelligence-core.md +22 -1
- package/rig-adapter-registry.json +39 -0
- package/schemas/action-set.schema.json +42 -0
- package/schemas/artifact-adapter-registry.schema.json +16 -0
- package/schemas/asset-identity.schema.json +117 -0
- package/schemas/asset-provenance.schema.json +1 -1
- package/schemas/atlas-contract.schema.json +48 -0
- package/schemas/control-track.schema.json +18 -0
- package/schemas/export-manifest.schema.json +19 -0
- package/schemas/frame-geometry.schema.json +79 -0
- package/schemas/generation-receipt.schema.json +20 -0
- package/schemas/layered-map.schema.json +75 -0
- package/schemas/rig-adapter-registry.schema.json +14 -0
- package/schemas/rig-compatibility.schema.json +53 -0
- package/schemas/rive-package-manifest.schema.json +43 -0
- package/schemas/runtime-candidate.schema.json +48 -0
- package/schemas/scene-manifest.schema.json +18 -0
- package/scripts/artifact-intake.py +408 -0
- package/scripts/asset-consistency.py +517 -0
- package/scripts/asset-provenance.py +8 -3
- package/scripts/build-ai-pilot.py +504 -0
- package/scripts/isolate-alpha-background.py +147 -0
- package/scripts/quality-gate.py +88 -2
- package/scripts/report.py +85 -0
- package/scripts/resolve-task-bundle.py +84 -0
- package/scripts/review-hook.py +26 -4
- package/scripts/rig-compatibility.py +251 -0
- package/scripts/rive-package-gate.py +245 -0
- package/scripts/runtime-candidate.py +301 -0
- package/scripts/setup.mjs +54 -26
- package/src/output/runtime-pilot-framer/asset-provenance.json +38 -0
- package/src/output/runtime-pilot-framer/browser-review.json +20 -0
- package/src/output/runtime-pilot-framer/framer-motion/runtime-telemetry.json +103 -0
- package/src/output/runtime-pilot-framer/manifest.json +31 -0
- package/src/output/runtime-pilot-framer/motion-spec.json +25 -0
- package/src/output/runtime-pilot-framer/runtime-evidence.json +96 -0
- package/src/output/runtime-pilot-framer/runtime-telemetry.json +100 -0
- package/src/output/runtime-pilot-framer/scene.jsx +52 -0
- package/src/output/runtime-pilot-framer/snapshot/.render-meta.json +9 -0
- package/src/output/runtime-pilot-framer/snapshot/frame-00.png +0 -0
- package/src/output/runtime-pilot-framer/snapshot/frame-100.png +0 -0
- package/src/output/runtime-pilot-framer/snapshot/frame-50.png +0 -0
- package/tests/scripts/run_tests.py +107 -0
- package/tests/scripts/test_ai_pilot_builder.py +128 -0
- package/tests/scripts/test_alpha_isolation.py +60 -0
- package/tests/scripts/test_artifact_intake.py +146 -0
- package/tests/scripts/test_asset_consistency.py +199 -0
- package/tests/scripts/test_rig_compatibility.py +105 -0
- package/tests/scripts/test_rive_package_gate.py +101 -0
- package/tests/scripts/test_runtime_candidate.py +95 -0
package/scripts/quality-gate.py
CHANGED
|
@@ -63,6 +63,24 @@ def _load_asset_provenance():
|
|
|
63
63
|
return module
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def _load_asset_consistency():
|
|
67
|
+
path = ROOT / "scripts" / "asset-consistency.py"
|
|
68
|
+
loader = importlib.util.spec_from_file_location("asset_consistency", path)
|
|
69
|
+
module = importlib.util.module_from_spec(loader)
|
|
70
|
+
sys.modules[loader.name] = module
|
|
71
|
+
loader.loader.exec_module(module)
|
|
72
|
+
return module
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_artifact_intake():
|
|
76
|
+
path = ROOT / "scripts" / "artifact-intake.py"
|
|
77
|
+
loader = importlib.util.spec_from_file_location("artifact_intake", path)
|
|
78
|
+
module = importlib.util.module_from_spec(loader)
|
|
79
|
+
sys.modules[loader.name] = module
|
|
80
|
+
loader.loader.exec_module(module)
|
|
81
|
+
return module
|
|
82
|
+
|
|
83
|
+
|
|
66
84
|
def _json(path: Path):
|
|
67
85
|
try:
|
|
68
86
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -82,7 +100,7 @@ def _telemetry_bundle_sha256(task_dir: Path) -> str:
|
|
|
82
100
|
return hashlib.sha256(json.dumps(entries, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
|
|
83
101
|
|
|
84
102
|
|
|
85
|
-
def validate_scene(scene_dir: Path, context_path: Path, require_review: bool = False, task_dir: Path | None = None, require_intelligence: bool = False, require_p1: bool = False, require_benchmark: bool = False, require_telemetry: bool = False, require_attestation: bool = False, attestation_path: Path | None = None, trust_policy_path: Path | None = None, require_visual_truth: bool = False, require_asset_provenance: bool = False, asset_provenance_path: Path | None = None) -> list[str]:
|
|
103
|
+
def validate_scene(scene_dir: Path, context_path: Path, require_review: bool = False, task_dir: Path | None = None, require_intelligence: bool = False, require_p1: bool = False, require_benchmark: bool = False, require_telemetry: bool = False, require_attestation: bool = False, attestation_path: Path | None = None, trust_policy_path: Path | None = None, require_visual_truth: bool = False, require_asset_provenance: bool = False, asset_provenance_path: Path | None = None, require_asset_consistency: bool = False, asset_consistency_path: Path | None = None, require_artifact_intake: bool = False) -> list[str]:
|
|
86
104
|
issues = []
|
|
87
105
|
manifest_path = scene_dir / "manifest.json"
|
|
88
106
|
spec_path = scene_dir / "motion-spec.json"
|
|
@@ -138,6 +156,66 @@ def validate_scene(scene_dir: Path, context_path: Path, require_review: bool = F
|
|
|
138
156
|
except (OSError, ValueError, AttributeError) as exc:
|
|
139
157
|
issues.append(f"asset provenance contract: {exc}")
|
|
140
158
|
|
|
159
|
+
consistency_name = manifest.get("consistency_ref")
|
|
160
|
+
if require_asset_consistency and not isinstance(consistency_name, str) and asset_consistency_path is None:
|
|
161
|
+
issues.append("asset consistency gate requires manifest.consistency_ref")
|
|
162
|
+
if consistency_name or asset_consistency_path:
|
|
163
|
+
resolved_consistency = asset_consistency_path or (scene_dir / str(consistency_name)).resolve()
|
|
164
|
+
if scene_dir.resolve() not in resolved_consistency.parents or not resolved_consistency.is_file():
|
|
165
|
+
issues.append("manifest.consistency_ref must point to an existing file inside the scene directory")
|
|
166
|
+
else:
|
|
167
|
+
try:
|
|
168
|
+
consistency_kind = manifest.get("consistency_kind", "frame-geometry")
|
|
169
|
+
consistency_module = _load_asset_consistency()
|
|
170
|
+
consistency_document = _json(resolved_consistency)
|
|
171
|
+
consistency_result = consistency_module.run(
|
|
172
|
+
consistency_kind,
|
|
173
|
+
consistency_document,
|
|
174
|
+
scene_dir,
|
|
175
|
+
strict=require_asset_consistency,
|
|
176
|
+
)
|
|
177
|
+
issues.extend(
|
|
178
|
+
f"asset consistency: {item.get('code', 'issue')} — {item.get('message', 'contract violation')}"
|
|
179
|
+
for item in consistency_result.get("errors", [])
|
|
180
|
+
)
|
|
181
|
+
if require_asset_consistency and not consistency_result.get("ready"):
|
|
182
|
+
issues.append("asset consistency contract is not ready")
|
|
183
|
+
except (OSError, ValueError, AttributeError, KeyError, TypeError) as exc:
|
|
184
|
+
issues.append(f"asset consistency contract: {exc}")
|
|
185
|
+
|
|
186
|
+
intake = manifest.get("artifact_intake")
|
|
187
|
+
if require_artifact_intake and not isinstance(intake, dict):
|
|
188
|
+
issues.append("artifact intake gate requires manifest.artifact_intake")
|
|
189
|
+
if intake is not None:
|
|
190
|
+
required_intake_keys = ("registry", "receipt", "controls", "export_manifest")
|
|
191
|
+
if not isinstance(intake, dict) or any(not isinstance(intake.get(key), str) for key in required_intake_keys):
|
|
192
|
+
issues.append("manifest.artifact_intake requires registry, receipt, controls and export_manifest relative paths")
|
|
193
|
+
else:
|
|
194
|
+
resolved_intake = {key: (scene_dir / intake[key]).resolve() for key in required_intake_keys}
|
|
195
|
+
if any(scene_dir.resolve() not in path.parents or not path.is_file() for path in resolved_intake.values()):
|
|
196
|
+
issues.append("manifest.artifact_intake paths must point to existing files inside the scene directory")
|
|
197
|
+
else:
|
|
198
|
+
try:
|
|
199
|
+
intake_module = _load_artifact_intake()
|
|
200
|
+
intake_result = intake_module.evaluate_bundle(
|
|
201
|
+
argparse.Namespace(
|
|
202
|
+
root=scene_dir,
|
|
203
|
+
registry=resolved_intake["registry"],
|
|
204
|
+
receipt=resolved_intake["receipt"],
|
|
205
|
+
controls=resolved_intake["controls"],
|
|
206
|
+
export_manifest=resolved_intake["export_manifest"],
|
|
207
|
+
strict=require_artifact_intake,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
issues.extend(
|
|
211
|
+
f"artifact intake: {item.get('code', 'issue')} — {item.get('message', 'contract violation')}"
|
|
212
|
+
for item in intake_result.get("errors", [])
|
|
213
|
+
)
|
|
214
|
+
if require_artifact_intake and not intake_result.get("ready"):
|
|
215
|
+
issues.append("artifact intake contract is not runtime-ready")
|
|
216
|
+
except (OSError, ValueError, AttributeError, KeyError, TypeError) as exc:
|
|
217
|
+
issues.append(f"artifact intake contract: {exc}")
|
|
218
|
+
|
|
141
219
|
checks = manifest.get("checks")
|
|
142
220
|
if not isinstance(checks, list) or not checks:
|
|
143
221
|
issues.append("manifest.checks must contain the Dev Lab quality checklist")
|
|
@@ -414,6 +492,9 @@ def main() -> int:
|
|
|
414
492
|
parser.add_argument("--require-visual-truth", action="store_true")
|
|
415
493
|
parser.add_argument("--require-asset-provenance", action="store_true")
|
|
416
494
|
parser.add_argument("--asset-provenance")
|
|
495
|
+
parser.add_argument("--require-asset-consistency", action="store_true")
|
|
496
|
+
parser.add_argument("--asset-consistency")
|
|
497
|
+
parser.add_argument("--require-artifact-intake", action="store_true")
|
|
417
498
|
parser.add_argument("--attestation")
|
|
418
499
|
parser.add_argument("--trust-policy")
|
|
419
500
|
args = parser.parse_args()
|
|
@@ -429,13 +510,14 @@ def main() -> int:
|
|
|
429
510
|
attestation_path = Path(args.attestation).resolve() if args.attestation else None
|
|
430
511
|
trust_policy_path = Path(args.trust_policy).resolve() if args.trust_policy else None
|
|
431
512
|
asset_provenance_path = Path(args.asset_provenance).resolve() if args.asset_provenance else None
|
|
513
|
+
asset_consistency_path = Path(args.asset_consistency).resolve() if args.asset_consistency else None
|
|
432
514
|
scenes = [root / "src" / "output" / args.scene] if args.scene else sorted(p for p in output_root.iterdir() if p.is_dir()) if output_root.exists() else []
|
|
433
515
|
if not scenes:
|
|
434
516
|
print("QUALITY GATE: no scene outputs found")
|
|
435
517
|
return 0
|
|
436
518
|
failed = False
|
|
437
519
|
for scene_dir in scenes:
|
|
438
|
-
issues = validate_scene(scene_dir, context, args.require_browser_review, task_dir, args.require_intelligence, args.require_p1, args.require_benchmark, args.require_telemetry, args.require_attestation, attestation_path, trust_policy_path, args.require_visual_truth, args.require_asset_provenance, asset_provenance_path)
|
|
520
|
+
issues = validate_scene(scene_dir, context, args.require_browser_review, task_dir, args.require_intelligence, args.require_p1, args.require_benchmark, args.require_telemetry, args.require_attestation, attestation_path, trust_policy_path, args.require_visual_truth, args.require_asset_provenance, asset_provenance_path, args.require_asset_consistency, asset_consistency_path, args.require_artifact_intake)
|
|
439
521
|
if issues:
|
|
440
522
|
failed = True
|
|
441
523
|
print(f"REJECTED {scene_dir.name}:")
|
|
@@ -447,6 +529,10 @@ def main() -> int:
|
|
|
447
529
|
suffixes.append("visual-truth contract")
|
|
448
530
|
if args.require_asset_provenance:
|
|
449
531
|
suffixes.append("asset provenance production eligibility")
|
|
532
|
+
if args.require_asset_consistency:
|
|
533
|
+
suffixes.append("asset consistency contract")
|
|
534
|
+
if args.require_artifact_intake:
|
|
535
|
+
suffixes.append("runtime-ready artifact intake")
|
|
450
536
|
suffix = f" + {' + '.join(suffixes)}" if suffixes else ""
|
|
451
537
|
print(f"ACCEPTED {scene_dir.name}: context + spec + runtime snapshots + browser-review candidate + checklist{suffix}")
|
|
452
538
|
return 1 if failed else 0
|
package/scripts/report.py
CHANGED
|
@@ -63,6 +63,24 @@ def asset_provenance_module():
|
|
|
63
63
|
return module
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def asset_consistency_module():
|
|
67
|
+
path = ROOT / "scripts" / "asset-consistency.py"
|
|
68
|
+
loader = importlib.util.spec_from_file_location("motionloom_asset_consistency", path)
|
|
69
|
+
module = importlib.util.module_from_spec(loader)
|
|
70
|
+
sys.modules[loader.name] = module
|
|
71
|
+
loader.loader.exec_module(module)
|
|
72
|
+
return module
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def artifact_intake_module():
|
|
76
|
+
path = ROOT / "scripts" / "artifact-intake.py"
|
|
77
|
+
loader = importlib.util.spec_from_file_location("motionloom_artifact_intake", path)
|
|
78
|
+
module = importlib.util.module_from_spec(loader)
|
|
79
|
+
sys.modules[loader.name] = module
|
|
80
|
+
loader.loader.exec_module(module)
|
|
81
|
+
return module
|
|
82
|
+
|
|
83
|
+
|
|
66
84
|
def asset_provenance_result(scene_manifest_path: Path, scene_manifest: dict, mode: str = "runtime") -> dict:
|
|
67
85
|
name = scene_manifest.get("asset_provenance")
|
|
68
86
|
if not name:
|
|
@@ -81,6 +99,44 @@ def asset_provenance_result(scene_manifest_path: Path, scene_manifest: dict, mod
|
|
|
81
99
|
return {"status": "fail", "errors": [f"asset provenance contract: {exc}"]}
|
|
82
100
|
|
|
83
101
|
|
|
102
|
+
def asset_consistency_result(scene_manifest_path: Path, scene_manifest: dict, strict: bool = False) -> dict:
|
|
103
|
+
name = scene_manifest.get("consistency_ref")
|
|
104
|
+
if not name:
|
|
105
|
+
return {"status": "not-run", "ready": False, "errors": []}
|
|
106
|
+
scene_dir = scene_manifest_path.parent.resolve()
|
|
107
|
+
consistency_path = (scene_dir / str(name)).resolve()
|
|
108
|
+
if scene_dir not in consistency_path.parents or not consistency_path.is_file():
|
|
109
|
+
return {"status": "fail", "ready": False, "errors": ["scene manifest consistency_ref points to a missing or unsafe artifact"]}
|
|
110
|
+
try:
|
|
111
|
+
document = read_json(consistency_path)
|
|
112
|
+
kind = scene_manifest.get("consistency_kind", "frame-geometry")
|
|
113
|
+
result = asset_consistency_module().run(kind, document, scene_dir, strict=strict)
|
|
114
|
+
result["status"] = "pass" if result.get("ready") else "fail"
|
|
115
|
+
return result
|
|
116
|
+
except (OSError, ValueError, AttributeError, KeyError, TypeError) as exc:
|
|
117
|
+
return {"status": "fail", "ready": False, "errors": [f"asset consistency contract: {exc}"]}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def artifact_intake_result(scene_manifest_path: Path, scene_manifest: dict, strict: bool = False) -> dict:
|
|
121
|
+
intake = scene_manifest.get("artifact_intake")
|
|
122
|
+
if not intake:
|
|
123
|
+
return {"status": "not-run", "ready": False, "errors": []}
|
|
124
|
+
required = ("registry", "receipt", "controls", "export_manifest")
|
|
125
|
+
scene_dir = scene_manifest_path.parent.resolve()
|
|
126
|
+
if not isinstance(intake, dict) or any(not isinstance(intake.get(key), str) for key in required):
|
|
127
|
+
return {"status": "fail", "ready": False, "errors": ["scene manifest artifact_intake is incomplete"]}
|
|
128
|
+
paths = {key: (scene_dir / intake[key]).resolve() for key in required}
|
|
129
|
+
if any(scene_dir not in path.parents or not path.is_file() for path in paths.values()):
|
|
130
|
+
return {"status": "fail", "ready": False, "errors": ["scene manifest artifact_intake points to a missing or unsafe artifact"]}
|
|
131
|
+
try:
|
|
132
|
+
result = artifact_intake_module().evaluate_bundle(
|
|
133
|
+
argparse.Namespace(root=scene_dir, registry=paths["registry"], receipt=paths["receipt"], controls=paths["controls"], export_manifest=paths["export_manifest"], strict=strict)
|
|
134
|
+
)
|
|
135
|
+
return result
|
|
136
|
+
except (OSError, ValueError, AttributeError, KeyError, TypeError) as exc:
|
|
137
|
+
return {"status": "fail", "ready": False, "errors": [f"artifact intake contract: {exc}"]}
|
|
138
|
+
|
|
139
|
+
|
|
84
140
|
def memory_summary() -> dict | None:
|
|
85
141
|
path = project_memory_path()
|
|
86
142
|
if not path.is_file():
|
|
@@ -475,6 +531,18 @@ def check_report(args: argparse.Namespace) -> int:
|
|
|
475
531
|
errors.append("ready-for-PR or confirmed task requires a passing asset provenance production check")
|
|
476
532
|
elif not provenance.get("summary", {}).get("production_eligible"):
|
|
477
533
|
errors.append("ready-for-PR or confirmed task requires asset provenance production_eligible")
|
|
534
|
+
consistency = asset_consistency_result(scene_manifest_path, scene_manifest, strict=state in {"ready_for_pr", "confirmed"})
|
|
535
|
+
if consistency.get("status") == "fail":
|
|
536
|
+
errors.extend(f"asset consistency: {error}" for error in consistency.get("errors", []))
|
|
537
|
+
if state in {"ready_for_pr", "confirmed"} and scene_manifest_path.is_file() and scene_manifest.get("consistency_ref"):
|
|
538
|
+
if consistency.get("status") != "pass" or not consistency.get("ready"):
|
|
539
|
+
errors.append("ready-for-PR or confirmed task requires a passing asset consistency contract")
|
|
540
|
+
intake = artifact_intake_result(scene_manifest_path, scene_manifest, strict=state in {"ready_for_pr", "confirmed"})
|
|
541
|
+
if intake.get("status") == "fail":
|
|
542
|
+
errors.extend(f"artifact intake: {error}" for error in intake.get("errors", []))
|
|
543
|
+
if state in {"ready_for_pr", "confirmed"} and scene_manifest_path.is_file() and scene_manifest.get("artifact_intake"):
|
|
544
|
+
if intake.get("status") != "pass" or not intake.get("ready"):
|
|
545
|
+
errors.append("ready-for-PR or confirmed task requires a runtime-ready artifact intake contract")
|
|
478
546
|
if state in {"validated", "ready_for_pr", "confirmed"}:
|
|
479
547
|
quality = read_json(task_dir / "quality-report.json")
|
|
480
548
|
if quality.get("status") != "pass":
|
|
@@ -576,6 +644,18 @@ def render(args: argparse.Namespace) -> int:
|
|
|
576
644
|
scene_manifest,
|
|
577
645
|
"production" if task.get("state") in {"ready_for_pr", "confirmed"} else "runtime",
|
|
578
646
|
)
|
|
647
|
+
consistency = asset_consistency_result(
|
|
648
|
+
ROOT / "src" / "output" / str(task.get("scene", "")) / "manifest.json",
|
|
649
|
+
scene_manifest,
|
|
650
|
+
strict=task.get("state") in {"ready_for_pr", "confirmed"},
|
|
651
|
+
)
|
|
652
|
+
intake = artifact_intake_result(
|
|
653
|
+
ROOT / "src" / "output" / str(task.get("scene", "")) / "manifest.json",
|
|
654
|
+
scene_manifest,
|
|
655
|
+
strict=task.get("state") in {"ready_for_pr", "confirmed"},
|
|
656
|
+
)
|
|
657
|
+
intake_adapter = intake.get("adapter") or {}
|
|
658
|
+
intake_evidence = intake.get("evidence") or {}
|
|
579
659
|
lines = [
|
|
580
660
|
f"# Animation Task Report — {task.get('task_id', task_dir.name)}",
|
|
581
661
|
"",
|
|
@@ -609,6 +689,11 @@ def render(args: argparse.Namespace) -> int:
|
|
|
609
689
|
"## Asset provenance",
|
|
610
690
|
f"- Status: **{provenance.get('status', 'not-run')}**; authority: **{provenance.get('summary', {}).get('authority', 'unknown')}**; declared readiness: **{provenance.get('summary', {}).get('declared_readiness', 'blocked')}**; effective readiness: **{provenance.get('summary', {}).get('effective_readiness', 'blocked')}**",
|
|
611
691
|
f"- Production eligible: **{provenance.get('summary', {}).get('production_eligible', False)}**; production approved: **{provenance.get('summary', {}).get('production_approved', False)}**; errors: **{len(provenance.get('errors', []))}**",
|
|
692
|
+
"## Asset consistency",
|
|
693
|
+
f"- Status: **{consistency.get('status', 'not-run')}**; contract: **{consistency.get('contract', scene_manifest.get('consistency_kind', 'not-declared'))}**; ready: **{consistency.get('ready', False)}**; errors: **{len(consistency.get('errors', []))}**; warnings: **{len(consistency.get('warnings', []))}**",
|
|
694
|
+
"## Artifact Intake",
|
|
695
|
+
f"- Status: **{intake.get('status', 'not-run')}**; adapter: **{intake_adapter.get('adapter_id', 'not-declared')}**; adapter status: **{intake_adapter.get('status', 'not-declared')}**; ready: **{intake.get('ready', False)}**; production eligible: **{intake.get('production_eligible', False)}**; production approved: **{intake.get('production_approved', False)}**",
|
|
696
|
+
f"- Receipt: `{intake_evidence.get('receipt', '')}`; controls: `{intake_evidence.get('controls', '')}`; export: `{intake_evidence.get('export_manifest', '')}`; errors: **{len(intake.get('errors', []))}**; warnings: **{len(intake.get('warnings', []))}**",
|
|
612
697
|
"## Semantic motion lint",
|
|
613
698
|
f"- Status: **{lint.get('status', 'not-run')}**; errors: **{lint.get('summary', {}).get('errors', 0)}**; warnings: **{lint.get('summary', {}).get('warnings', 0)}**; blocking: **{lint.get('summary', {}).get('blocking', 0)}**",
|
|
614
699
|
md_table(lint.get("findings", []), [("Rule", "rule_id"), ("Severity", "severity"), ("Confidence", "confidence"), ("Message", "message"), ("Basis", "basis")]),
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolve one safe artifact task bundle bound to a scene identity.
|
|
3
|
+
|
|
4
|
+
The resolver intentionally does not infer a task path from a scene slug. A
|
|
5
|
+
bundle is eligible only when its direct ``artifacts/<task-id>/task.json``
|
|
6
|
+
declares the requested scene and remains inside the repository artifacts root.
|
|
7
|
+
Multiple matching bundles are rejected rather than ranked implicitly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SAFE_SCENE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_json(path: Path) -> dict:
|
|
23
|
+
try:
|
|
24
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
25
|
+
except (OSError, json.JSONDecodeError):
|
|
26
|
+
return {}
|
|
27
|
+
return data if isinstance(data, dict) else {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def has_symlink_component(path: Path, root: Path) -> bool:
|
|
31
|
+
current = path
|
|
32
|
+
while True:
|
|
33
|
+
if current.is_symlink():
|
|
34
|
+
return True
|
|
35
|
+
if current == root:
|
|
36
|
+
return False
|
|
37
|
+
if current == current.parent:
|
|
38
|
+
return True
|
|
39
|
+
current = current.parent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_task_dirs(root: Path, scene: str) -> list[Path]:
|
|
43
|
+
artifacts = root / "artifacts"
|
|
44
|
+
if not artifacts.is_dir() or artifacts.is_symlink():
|
|
45
|
+
return []
|
|
46
|
+
resolved_artifacts = artifacts.resolve()
|
|
47
|
+
matches: list[Path] = []
|
|
48
|
+
for task_path in sorted(artifacts.glob("*/task.json")):
|
|
49
|
+
if has_symlink_component(task_path, root):
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
if not task_path.resolve().is_relative_to(resolved_artifacts):
|
|
53
|
+
continue
|
|
54
|
+
except (OSError, RuntimeError):
|
|
55
|
+
continue
|
|
56
|
+
if read_json(task_path).get("scene") == scene:
|
|
57
|
+
matches.append(task_path.parent)
|
|
58
|
+
return matches
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> int:
|
|
62
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
63
|
+
parser.add_argument("--scene", required=True)
|
|
64
|
+
parser.add_argument("--root", default=".")
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
if not SAFE_SCENE.fullmatch(args.scene) or args.scene in {".", ".."}:
|
|
67
|
+
print("FAIL: unsafe scene identifier", file=sys.stderr)
|
|
68
|
+
return 2
|
|
69
|
+
|
|
70
|
+
root = Path(args.root).resolve()
|
|
71
|
+
matches = resolve_task_dirs(root, args.scene)
|
|
72
|
+
if not matches:
|
|
73
|
+
print(f"FAIL: no safe task bundle bound to scene: {args.scene}", file=sys.stderr)
|
|
74
|
+
return 3
|
|
75
|
+
if len(matches) != 1:
|
|
76
|
+
joined = ", ".join(str(path.relative_to(root)) for path in matches)
|
|
77
|
+
print(f"FAIL: ambiguous task bundles bound to scene {args.scene}: {joined}", file=sys.stderr)
|
|
78
|
+
return 4
|
|
79
|
+
print(matches[0].relative_to(root).as_posix())
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
raise SystemExit(main())
|
package/scripts/review-hook.py
CHANGED
|
@@ -12,7 +12,7 @@ import subprocess
|
|
|
12
12
|
import sys
|
|
13
13
|
from datetime import datetime, timedelta, timezone
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from urllib.parse import urlencode
|
|
15
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
16
16
|
|
|
17
17
|
ROOT = Path(__file__).resolve().parents[1]
|
|
18
18
|
SAFE_SCENE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
@@ -42,6 +42,29 @@ def candidate_id(task_id: str, scene: str, context_hash: str, source_hash: str,
|
|
|
42
42
|
return hashlib.sha256(raw).hexdigest()[:20]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def review_urls(lab_url: str, scene: str, task_id: str, candidate: str) -> tuple[str, str, str]:
|
|
46
|
+
"""Build an exact review route while anchoring artifact paths at its origin.
|
|
47
|
+
|
|
48
|
+
`--lab-url` is the actual browser route, e.g. `https://host/lab` for the
|
|
49
|
+
React Dev Lab or `http://127.0.0.1:3300/` for the bundled static lab.
|
|
50
|
+
Artifact and task paths intentionally remain same-origin root paths.
|
|
51
|
+
"""
|
|
52
|
+
parsed = urlsplit(lab_url.strip())
|
|
53
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.query or parsed.fragment:
|
|
54
|
+
raise ValueError("lab-url must be an absolute http(s) Dev Lab route without query or fragment")
|
|
55
|
+
origin = urlunsplit((parsed.scheme, parsed.netloc, "", "", ""))
|
|
56
|
+
review_path = parsed.path.rstrip("/") or "/"
|
|
57
|
+
review_base = f"{origin}{review_path}" if review_path != "/" else origin
|
|
58
|
+
query = urlencode({
|
|
59
|
+
"scene": scene,
|
|
60
|
+
"task_id": task_id,
|
|
61
|
+
"candidate_id": candidate,
|
|
62
|
+
"artifact_base": f"{origin}/scenes/{scene}",
|
|
63
|
+
"task_base": f"{origin}/tasks/{task_id}",
|
|
64
|
+
})
|
|
65
|
+
return f"{review_base}/?{query}" if review_path != "/" else f"{origin}/?{query}", f"{origin}/scenes/{scene}", f"{origin}/tasks/{task_id}"
|
|
66
|
+
|
|
67
|
+
|
|
45
68
|
def paths(task_dir: Path, task: dict) -> tuple[Path, Path, Path, Path]:
|
|
46
69
|
scene = task.get("scene", "")
|
|
47
70
|
if not scene or scene in {".", ".."} or not SAFE_SCENE.fullmatch(scene):
|
|
@@ -79,9 +102,8 @@ def prepare(args: argparse.Namespace) -> int:
|
|
|
79
102
|
source_hash = sha256(source_path)
|
|
80
103
|
render_hash = sha256(render_meta)
|
|
81
104
|
cid = candidate_id(task["task_id"], scene, context_hash, source_hash, render_hash)
|
|
82
|
-
base = args.lab_url.rstrip("/")
|
|
83
105
|
task_id = task["task_id"]
|
|
84
|
-
url =
|
|
106
|
+
url, artifact_base, task_base = review_urls(args.lab_url, scene, task_id, cid)
|
|
85
107
|
candidate = {
|
|
86
108
|
"schema_version": "1.0",
|
|
87
109
|
"candidate_id": cid,
|
|
@@ -120,7 +142,7 @@ def prepare(args: argparse.Namespace) -> int:
|
|
|
120
142
|
handoff = read_json(handoff_path)
|
|
121
143
|
if memory_path.is_file():
|
|
122
144
|
shutil.copy2(memory_path, task_dir / "project-memory.json")
|
|
123
|
-
handoff.update({"state": "review_required", "to_agent": "browser-review-agent", "summary": "Open the exact rendered candidate in the internal Dev Lab and obtain user approval before PR.", "next_actions": [{"action": "Open internal Dev Lab candidate", "kind": "browser_review", "agent": "browser-review-agent", "skill": "browser-review", "url": url, "candidate_id": cid, "requires_user_approval": True, "evidence_needed": ["review.json"], "output_artifacts": ["review.json"]}], "required_artifacts": sorted(set(handoff.get("required_artifacts", []) + ["browser-review.json", "review.json", "semantic-lint-benchmark.json", "evidence-verifier-report.json", "runtime-adapters/runtime-evidence.json"] + (["project-memory.json"] if memory_path.is_file() else [])))})
|
|
145
|
+
handoff.update({"state": "review_required", "to_agent": "browser-review-agent", "summary": "Open the exact rendered candidate in the internal Dev Lab and obtain user approval before PR.", "next_actions": [{"action": "Open internal Dev Lab candidate", "kind": "browser_review", "agent": "browser-review-agent", "skill": "browser-review", "url": url, "artifact_base": artifact_base, "task_base": task_base, "candidate_id": cid, "requires_user_approval": True, "evidence_needed": ["review.json"], "output_artifacts": ["review.json"]}], "required_artifacts": sorted(set(handoff.get("required_artifacts", []) + ["browser-review.json", "review.json", "semantic-lint-benchmark.json", "evidence-verifier-report.json", "runtime-adapters/runtime-evidence.json"] + (["project-memory.json"] if memory_path.is_file() else [])))})
|
|
124
146
|
handoff_path.write_text(json.dumps(handoff, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
125
147
|
subprocess.run([sys.executable, str(ROOT / "scripts/devlab.py"), scene, "--prepare-only", "--task-dir", str(task_dir)], check=True, capture_output=True, text=True)
|
|
126
148
|
subprocess.run([sys.executable, str(ROOT / "scripts/report.py"), "collect", "--task-dir", str(task_dir)], check=True, capture_output=True, text=True)
|