motionloom 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/SKILL.md +122 -0
  4. package/agent-card.json +161 -0
  5. package/assets/library/ATTRIBUTION.md +6 -0
  6. package/assets/library/README.md +20 -0
  7. package/assets/library/avatar-base.svg +19 -0
  8. package/assets/library/error-alert.json +1 -0
  9. package/assets/library/rive/ATTRIBUTION.md +12 -0
  10. package/assets/library/rive/state-machine-test.riv +0 -0
  11. package/assets/library/success-check.json +1 -0
  12. package/bin/motionloom.mjs +81 -0
  13. package/docs/BROWSER-REVIEW-E2E.md +78 -0
  14. package/docs/CATEGORIES.md +16 -0
  15. package/docs/CHECKLIST.md +31 -0
  16. package/docs/DEEP-AUDIT-WORKING-NOTES.md +22 -0
  17. package/docs/FRAMEWORK-SELECTION.md +26 -0
  18. package/docs/PROJECT-MANIFEST.md +37 -0
  19. package/docs/ROADMAP-INTELLIGENCE.md +224 -0
  20. package/docs/audits/1.10.0-attestation-research-notes.md +19 -0
  21. package/docs/audits/1.8.0-trust-boundary-hardening.md +56 -0
  22. package/docs/audits/1.9.0-evidence-interoperability-threat-model.md +37 -0
  23. package/docs/audits/2.0.0-attestation-acceptance.md +30 -0
  24. package/docs/releases/1.5.0.md +25 -0
  25. package/docs/releases/1.6.0.md +27 -0
  26. package/docs/releases/1.7.0.md +23 -0
  27. package/docs/releases/1.8.0.md +23 -0
  28. package/docs/releases/1.9.0.md +21 -0
  29. package/docs/releases/2.0.0.md +21 -0
  30. package/docs/releases/npm-publish-from-workstation.md +88 -0
  31. package/docs/research/AGENT-PROTOCOL-FINDINGS.md +43 -0
  32. package/examples/report-demo/REPORT.md +50 -0
  33. package/examples/report-demo/artifact-manifest.json +25 -0
  34. package/examples/report-demo/decision-log.jsonl +0 -0
  35. package/examples/report-demo/execution-report.json +70 -0
  36. package/examples/report-demo/handoff.json +22 -0
  37. package/examples/report-demo/issue-register.json +5 -0
  38. package/examples/report-demo/task.json +13 -0
  39. package/package.json +95 -0
  40. package/project-context.example.json +26 -0
  41. package/references/browser-review-contract.md +32 -0
  42. package/references/dotlottie-source-notes.md +21 -0
  43. package/references/intelligence-core.md +98 -0
  44. package/references/reporting-contract.md +38 -0
  45. package/references/runtime-capability.md +12 -0
  46. package/references/signed-attestation.md +31 -0
  47. package/schemas/artifact-manifest.schema.json +22 -0
  48. package/schemas/browser-review-candidate.schema.json +24 -0
  49. package/schemas/capability-registry.schema.json +61 -0
  50. package/schemas/continuity-report.schema.json +52 -0
  51. package/schemas/evidence-verifier-report.schema.json +35 -0
  52. package/schemas/execution-report.schema.json +35 -0
  53. package/schemas/fix-plan.schema.json +44 -0
  54. package/schemas/handoff.schema.json +19 -0
  55. package/schemas/motion-ir.schema.json +74 -0
  56. package/schemas/project-graph.schema.json +70 -0
  57. package/schemas/provenance.schema.json +76 -0
  58. package/schemas/runtime-evidence.schema.json +48 -0
  59. package/schemas/runtime-telemetry.schema.json +50 -0
  60. package/schemas/scene-manifest.schema.json +45 -0
  61. package/schemas/semantic-benchmark.schema.json +26 -0
  62. package/schemas/semantic-lint-report.schema.json +48 -0
  63. package/schemas/signed-attestation.schema.json +95 -0
  64. package/schemas/task.schema.json +39 -0
  65. package/schemas/trust-policy.schema.json +53 -0
  66. package/scripts/analyze.sh +13 -0
  67. package/scripts/attestation-keygen.py +63 -0
  68. package/scripts/attestation-verifier.py +178 -0
  69. package/scripts/attestation.py +288 -0
  70. package/scripts/capture-runtime-telemetry.sh +37 -0
  71. package/scripts/devlab.sh +77 -0
  72. package/scripts/eval-intelligence.py +377 -0
  73. package/scripts/evidence-verifier.py +222 -0
  74. package/scripts/fetch-library.sh +57 -0
  75. package/scripts/intelligence.py +1543 -0
  76. package/scripts/manifest.py +61 -0
  77. package/scripts/pr.sh +103 -0
  78. package/scripts/quality-gate.py +378 -0
  79. package/scripts/render-node.mjs +53 -0
  80. package/scripts/render.sh +37 -0
  81. package/scripts/report-contract.py +181 -0
  82. package/scripts/report.py +588 -0
  83. package/scripts/review-hook.py +199 -0
  84. package/scripts/runtime-adapters.mjs +187 -0
  85. package/scripts/skill-doctor.py +150 -0
  86. package/scripts/to-dotlottie.mjs +99 -0
  87. package/scripts/to-dotlottie.sh +25 -0
  88. package/scripts/validate-lottie.py +102 -0
  89. package/src/core/analyzer.py +226 -0
  90. package/src/core/snapshot.py +124 -0
  91. package/src/core/spec.py +240 -0
  92. package/src/output/browser-review-smoke/animation.json +57 -0
  93. package/src/output/browser-review-smoke/browser-review.json +21 -0
  94. package/src/output/browser-review-smoke/manifest.json +22 -0
  95. package/src/output/browser-review-smoke/motion-spec.json +28 -0
  96. package/src/output/browser-review-smoke/snapshot/.render-meta.json +10 -0
  97. package/src/output/browser-review-smoke/snapshot/frame-00.png +0 -0
  98. package/src/output/browser-review-smoke/snapshot/frame-100.png +0 -0
  99. package/src/output/browser-review-smoke/snapshot/frame-50.png +0 -0
  100. package/src/rig/README.md +35 -0
  101. package/src/rig/cutout_rig.py +211 -0
  102. package/templates/framer-motion/ui-micro.tsx +50 -0
  103. package/templates/gsap/scroll-scene.js +54 -0
  104. package/templates/lottie/README.md +21 -0
  105. package/templates/lottie/react-component.tsx +82 -0
  106. package/templates/lottie/scaffold/animation.json +57 -0
  107. package/templates/lottie/scaffold/character-rig.svg +19 -0
  108. package/templates/lottie/vanilla.js +68 -0
  109. package/templates/rive/README.md +36 -0
  110. package/tests/evals/intelligence-cases.json +131 -0
  111. package/tests/scripts/run_tests.py +843 -0
  112. package/tests/scripts/test_attestation.py +172 -0
@@ -0,0 +1,843 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ run_tests.py — Deterministic test suite for the skill's core engine.
4
+ Runs without any network or heavy dependencies (pure stdlib).
5
+
6
+ Usage: python3 tests/scripts/run_tests.py
7
+ """
8
+
9
+ import json
10
+ import hashlib
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ import shutil
16
+ import xml.etree.ElementTree as ET
17
+ import zipfile
18
+ from pathlib import Path
19
+
20
+ ROOT = Path(__file__).resolve().parent.parent.parent
21
+ FAILED = []
22
+
23
+
24
+ def check(name: str, condition: bool, detail: str = "") -> None:
25
+ status = "PASS" if condition else "FAIL"
26
+ print(f"[{status}] {name}" + (f" — {detail}" if detail and not condition else ""))
27
+ if not condition:
28
+ FAILED.append(name)
29
+
30
+
31
+ def test_analyzer_on_fixture():
32
+ with tempfile.TemporaryDirectory() as td:
33
+ p = Path(td)
34
+ (p / "package.json").write_text(json.dumps({
35
+ "name": "fixture-app",
36
+ "dependencies": {"framer-motion": "^11.0.0", "react": "^19.0.0"},
37
+ }))
38
+ (p / "tailwind.config.js").write_text(
39
+ "module.exports = { theme: { extend: { colors: { primary: '#2563eb', accent: '#f59e0b' } } } };"
40
+ )
41
+ subprocess.run(
42
+ [sys.executable, str(ROOT / "src/core/analyzer.py"), td],
43
+ check=True, capture_output=True, cwd=td,
44
+ )
45
+ ctx = json.loads((Path(td) / "project-context.json").read_text())
46
+ check("analyzer emits context", ctx.get("name") == "fixture-app")
47
+ check("analyzer detects react stack", ctx.get("stack", {}).get("react") is True)
48
+ check("analyzer detects framer-motion preference", ctx.get("stack", {}).get("framework") == "framer-motion")
49
+ check("analyzer extracts brand primary", ctx.get("brand", {}).get("primary") == "#2563EB")
50
+
51
+
52
+ def test_spec_generate_and_validate():
53
+ with tempfile.TemporaryDirectory() as td:
54
+ ctx = Path(td) / "project-context.json"
55
+ ctx.write_text(json.dumps({
56
+ "schema_version": "1.1",
57
+ "name": "fixture-app",
58
+ "project_root": td,
59
+ "brand": {"primary": "#2563EB", "accent": "#F59E0B"},
60
+ "stack": {"framework": "lottie"},
61
+ "motion_language": {"recommendation": "ease-in-out"},
62
+ "source_authority": "manifest",
63
+ }))
64
+ out = Path(td) / "motion-spec.json"
65
+ subprocess.run(
66
+ [sys.executable, str(ROOT / "src/core/spec.py"), "generate", "loading",
67
+ "--context", str(ctx), "--output", str(out), "--loop"],
68
+ check=True, capture_output=True,
69
+ )
70
+ spec = json.loads(out.read_text())
71
+ check("spec binds category", spec["category"] == "loading")
72
+ check("spec binds framework from stack", spec["framework"] == "lottie")
73
+ check("spec binds brand primary", spec["theme"]["primary"] == "#2563EB")
74
+ check("spec computes total frames", spec["total_frames"] == round(spec["duration_s"] * spec["fps"]))
75
+ check("spec defaults loading to loop", spec["loop"] is True)
76
+ check("spec contains context hash", len(spec.get("context_binding", {}).get("context_sha256", "")) == 64)
77
+ r = subprocess.run([sys.executable, str(ROOT / "src/core/spec.py"), "validate", str(out),
78
+ "--context", str(ctx)],
79
+ capture_output=True, text=True)
80
+ check("spec validates clean", r.returncode == 0, r.stdout.strip())
81
+
82
+
83
+ def test_rig_build_and_pose():
84
+ with tempfile.TemporaryDirectory() as td:
85
+ rigged = Path(td) / "rigged.svg"
86
+ subprocess.run(
87
+ [sys.executable, str(ROOT / "src/rig/cutout_rig.py"), "build",
88
+ "--input", str(ROOT / "assets/library/avatar-base.svg"),
89
+ "--output", str(rigged)],
90
+ check=True, capture_output=True,
91
+ )
92
+ doc = rigged.read_text()
93
+ check("rig contains data-bone markers", 'data-bone="hip"' in doc)
94
+ check("rig wraps in data-rig group", 'data-rig="cutout-v1"' in doc)
95
+ try:
96
+ xml_root = ET.fromstring(doc)
97
+ head = xml_root.find('.//*[@data-bone="head"]')
98
+ check("rig is valid XML", head is not None and len(list(head)) == 1)
99
+ except ET.ParseError as exc:
100
+ check("rig is valid XML", False, str(exc))
101
+
102
+ clip = Path(td) / "walk.json"
103
+ subprocess.run(
104
+ [sys.executable, str(ROOT / "src/rig/cutout_rig.py"), "pose", str(rigged),
105
+ "--pose", "walk", "--duration", "1.2", "--fps", "30", "--out", str(clip)],
106
+ check=True, capture_output=True,
107
+ )
108
+ data = json.loads(clip.read_text())
109
+ check("pose clip has frames", data["frames"] == 36)
110
+ check("clip starts at rest", data["keyframes"][0]["angles"]["l_thigh"] > 0)
111
+ check("clip reverses at seam", data["keyframes"][-1]["angles"]["l_thigh"] > 0)
112
+
113
+
114
+ def test_lottie_scaffold_valid():
115
+ scaffold = ROOT / "templates/lottie/scaffold/animation.json"
116
+ doc = json.loads(scaffold.read_text())
117
+ check("scaffold has version header", doc.get("v") == "5.12.0")
118
+ check("scaffold has frame metadata", doc.get("fr") == 60 and doc.get("op") == 60)
119
+ check("scaffold has at least one layer", len(doc.get("layers", [])) >= 1)
120
+
121
+
122
+ def test_dotlottie_manifest_selection():
123
+ with tempfile.TemporaryDirectory() as td:
124
+ archive = Path(td) / "multi.lottie"
125
+ valid = {"v": "5.12.0", "fr": 60, "ip": 0, "op": 60, "layers": []}
126
+ unrelated = {"not": "an animation"}
127
+ manifest = {"version": "2", "initial": {"animation": "chosen"}, "animations": [{"id": "chosen"}]}
128
+ with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf:
129
+ zf.writestr("manifest.json", json.dumps(manifest))
130
+ zf.writestr("a/chosen.json", json.dumps(valid))
131
+ zf.writestr("random.json", json.dumps(unrelated))
132
+ result = subprocess.run([sys.executable, str(ROOT / "scripts/validate-lottie.py"), str(archive)],
133
+ capture_output=True, text=True)
134
+ check("dotLottie follows manifest animation", result.returncode == 0, result.stdout.strip())
135
+
136
+
137
+ def test_dotlottie_packager():
138
+ with tempfile.TemporaryDirectory() as td:
139
+ check("dotLottie packager script is executable", (ROOT / "scripts/to-dotlottie.sh").is_file())
140
+ smoke = ROOT / "src/output/browser-review-smoke"
141
+ archive = Path(td) / "smoke.lottie"
142
+ result = subprocess.run([
143
+ "bash", str(ROOT / "scripts/to-dotlottie.sh"), "browser-review-smoke", str(archive),
144
+ ], cwd=ROOT, capture_output=True, text=True)
145
+ check("dotLottie packager exits cleanly", result.returncode == 0, result.stderr)
146
+ if result.returncode == 0:
147
+ with zipfile.ZipFile(archive) as zf:
148
+ names = set(zf.namelist())
149
+ manifest = json.loads(zf.read("manifest.json"))
150
+ check("dotLottie packager emits root manifest", "manifest.json" in names)
151
+ check("dotLottie packager emits initial animation", "a/animation.json" in names)
152
+ check("dotLottie packager sets v2 initial id", manifest.get("version") == "2" and manifest.get("initial", {}).get("animation") == "animation")
153
+ validate = subprocess.run([
154
+ sys.executable, str(ROOT / "scripts/validate-lottie.py"), str(archive),
155
+ "--spec", str(smoke / "motion-spec.json"),
156
+ ], capture_output=True, text=True)
157
+ check("packaged dotLottie passes validator", validate.returncode == 0, validate.stdout.strip())
158
+
159
+
160
+ def test_source_binding_contract():
161
+ manifest = json.loads((ROOT / "src/output/browser-review-smoke/manifest.json").read_text())
162
+ binding = manifest.get("source_binding", {})
163
+ source = ROOT / "src/output/browser-review-smoke" / manifest.get("file", "")
164
+ import hashlib
165
+ check("scene manifest includes source binding", all(binding.get(key) for key in ("kind", "source_path", "authority", "license", "sha256")))
166
+ check("source binding path matches scene file", binding.get("source_path") == manifest.get("file"))
167
+ check("source binding checksum matches bytes", binding.get("sha256") == hashlib.sha256(source.read_bytes()).hexdigest())
168
+
169
+ with tempfile.TemporaryDirectory() as td:
170
+ scene = Path(td) / "src/output/unbound"
171
+ scene.mkdir(parents=True)
172
+ context = Path(td) / "project-context.json"
173
+ context.write_text(json.dumps({"name": "unbound", "project_root": td, "brand": {"primary": "#2563EB"}, "stack": {"framework": "lottie"}, "source_authority": "test"}))
174
+ spec = scene / "motion-spec.json"
175
+ subprocess.run([sys.executable, str(ROOT / "src/core/spec.py"), "generate", "loading", "--context", str(context), "--output", str(spec)], check=True, capture_output=True)
176
+ (scene / "animation.json").write_text((ROOT / "templates/lottie/scaffold/animation.json").read_text())
177
+ (scene / "manifest.json").write_text(json.dumps({"framework": "lottie", "category": "loading", "file": "animation.json", "checks": [{"id": "ok", "pass": True}]}))
178
+ snap = scene / "snapshot"
179
+ snap.mkdir()
180
+ for pct in (0, 50, 100): (snap / f"frame-{pct:02d}.png").write_bytes(b"runtime")
181
+ (snap / ".render-meta.json").write_text(json.dumps({"mode": "runtime", "scene": "unbound"}))
182
+ result = subprocess.run([sys.executable, str(ROOT / "scripts/quality-gate.py"), "--root", td, "--scene", "unbound", "--context", str(context)], capture_output=True, text=True)
183
+ check("quality gate rejects missing source binding", result.returncode != 0 and "source_binding" in result.stdout)
184
+
185
+
186
+ def test_placeholder_is_not_runtime_evidence():
187
+ with tempfile.TemporaryDirectory() as td:
188
+ scene = Path(td) / "src/output/placeholder"
189
+ scene.mkdir(parents=True)
190
+ context = Path(td) / "project-context.json"
191
+ context.write_text(json.dumps({
192
+ "name": "placeholder-test", "project_root": td,
193
+ "brand": {"primary": "#2563EB"}, "stack": {"framework": "lottie"},
194
+ "source_authority": "test",
195
+ }))
196
+ spec = Path(scene / "motion-spec.json")
197
+ subprocess.run([sys.executable, str(ROOT / "src/core/spec.py"), "generate", "loading",
198
+ "--context", str(context), "--output", str(spec)], check=True, capture_output=True)
199
+ (scene / "animation.json").write_text((ROOT / "templates/lottie/scaffold/animation.json").read_text())
200
+ (scene / "manifest.json").write_text(json.dumps({
201
+ "framework": "lottie", "category": "loading", "file": "animation.json",
202
+ "checks": [{"id": "check", "pass": True}],
203
+ }))
204
+ snap = scene / "snapshot"
205
+ snap.mkdir()
206
+ for pct in (0, 50, 100):
207
+ (snap / f"frame-{pct:02d}.png").write_bytes(b"not-a-real-png")
208
+ (snap / ".render-meta.json").write_text(json.dumps({"mode": "placeholder", "scene": "placeholder"}))
209
+ result = subprocess.run([sys.executable, str(ROOT / "scripts/quality-gate.py"), "--root", td,
210
+ "--scene", "placeholder", "--context", str(context)],
211
+ capture_output=True, text=True)
212
+ check("quality gate rejects placeholder evidence", result.returncode != 0)
213
+
214
+
215
+ def test_runtime_evidence_binding():
216
+ with tempfile.TemporaryDirectory() as td:
217
+ root = Path(td)
218
+ scene = root / "src/output/browser-review-smoke"
219
+ shutil.copytree(ROOT / "src/output/browser-review-smoke", scene)
220
+ context = root / "project-context.json"
221
+ shutil.copy(ROOT / "artifacts/browser-review-smoke-task/project-context.json", context)
222
+
223
+ spec_path = scene / "motion-spec.json"
224
+ spec = json.loads(spec_path.read_text())
225
+ spec["framework"] = "gsap"
226
+ spec["category"] = "hero-scene"
227
+ spec_path.write_text(json.dumps(spec, indent=2) + "\n")
228
+
229
+ manifest_path = scene / "manifest.json"
230
+ manifest = json.loads(manifest_path.read_text())
231
+ manifest["framework"] = "gsap"
232
+ manifest["category"] = "hero-scene"
233
+ manifest["runtime_evidence"] = "runtime-evidence.json"
234
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
235
+
236
+ source_sha = hashlib.sha256((scene / "animation.json").read_bytes()).hexdigest()
237
+ manifest_sha = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
238
+ evidence = {
239
+ "schema_version": "1.0", "run_id": "test-runtime-run", "mode": "runtime",
240
+ "status": "pass", "scene": scene.name, "source_sha256": source_sha,
241
+ "manifest_sha256": manifest_sha,
242
+ "frameworks": [{"framework": "gsap", "status": "pass", "ready": True}],
243
+ }
244
+ (scene / "runtime-evidence.json").write_text(json.dumps(evidence, indent=2) + "\n")
245
+ accepted = subprocess.run([
246
+ sys.executable, str(ROOT / "scripts/quality-gate.py"), "--root", str(root),
247
+ "--scene", scene.name, "--context", str(context),
248
+ ], capture_output=True, text=True)
249
+ check("quality gate accepts bound runtime evidence", accepted.returncode == 0, accepted.stdout.strip())
250
+
251
+ manifest["description"] = "stale manifest mutation"
252
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
253
+ stale = subprocess.run([
254
+ sys.executable, str(ROOT / "scripts/quality-gate.py"), "--root", str(root),
255
+ "--scene", scene.name, "--context", str(context),
256
+ ], capture_output=True, text=True)
257
+ check("quality gate rejects stale runtime evidence", stale.returncode != 0 and "manifest_sha256" in stale.stdout)
258
+
259
+
260
+ def test_deep_audit_contracts():
261
+ with tempfile.TemporaryDirectory() as td:
262
+ root = Path(td)
263
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", root / "artifacts/browser-review-smoke-task")
264
+ shutil.copytree(ROOT / "artifacts/professional-review-e2e", root / "artifacts/professional-review-e2e")
265
+ scenes = root / "changed-scenes"
266
+ scenes.write_text("browser-review-smoke\n")
267
+ contract = subprocess.run([
268
+ sys.executable, str(ROOT / "scripts/report-contract.py"),
269
+ "--root", str(root), "--scenes-file", str(scenes),
270
+ ], capture_output=True, text=True)
271
+ check(
272
+ "report contract selects deterministic complete scene bundle",
273
+ contract.returncode == 0
274
+ and contract.stdout.count("Selected bundles:") == 1
275
+ and "browser-review-smoke-task" in contract.stdout,
276
+ contract.stdout.strip(),
277
+ )
278
+
279
+ linked_target = root / "outside-task"
280
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", linked_target)
281
+ (root / "artifacts/linked-task").symlink_to(linked_target, target_is_directory=True)
282
+ contract_symlink = subprocess.run([
283
+ sys.executable, str(ROOT / "scripts/report-contract.py"),
284
+ "--root", str(root), "--scenes-file", str(scenes),
285
+ ], capture_output=True, text=True)
286
+ check(
287
+ "report contract ignores symlinked task bundle",
288
+ contract_symlink.returncode == 0 and "linked-task" not in contract_symlink.stdout,
289
+ contract_symlink.stdout.strip(),
290
+ )
291
+
292
+ approval_dir = root / "artifacts/approval-mismatch"
293
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", approval_dir)
294
+ approval_task = json.loads((approval_dir / "task.json").read_text())
295
+ approval_task["state"] = "ready_for_pr"
296
+ (approval_dir / "task.json").write_text(json.dumps(approval_task, indent=2) + "\n")
297
+ approval_review = json.loads((approval_dir / "review.json").read_text())
298
+ approval_review["candidate_id"] = "foreign-candidate"
299
+ (approval_dir / "review.json").write_text(json.dumps(approval_review, indent=2) + "\n")
300
+ approval_check = subprocess.run([
301
+ sys.executable, str(ROOT / "scripts/report.py"), "check", "--task-dir", str(approval_dir),
302
+ ], capture_output=True, text=True)
303
+ check(
304
+ "report check rejects review for foreign candidate",
305
+ approval_check.returncode != 0 and "exact browser-review candidate" in approval_check.stdout,
306
+ approval_check.stdout.strip(),
307
+ )
308
+
309
+ candidate_path = root / "artifacts/browser-review-smoke-task/browser-review.json"
310
+ candidate = json.loads(candidate_path.read_text())
311
+ candidate["expires_at"] = "2000-01-01T00:00:00Z"
312
+ candidate_path.write_text(json.dumps(candidate))
313
+ expired = subprocess.run([
314
+ sys.executable, str(ROOT / "scripts/review-hook.py"), "validate",
315
+ "--task-dir", str(root / "artifacts/browser-review-smoke-task"), "--require-approved",
316
+ ], capture_output=True, text=True)
317
+ check("review hook rejects expired candidate", expired.returncode != 0 and "expired" in expired.stdout)
318
+
319
+ unsafe_runtime = subprocess.run(
320
+ ["node", str(ROOT / "scripts/runtime-adapters.mjs")],
321
+ env={**os.environ, "RUNTIME_FRAMEWORKS": "../../escape"},
322
+ capture_output=True, text=True,
323
+ )
324
+ check("runtime adapter rejects unsupported framework path", unsafe_runtime.returncode != 0 and "unsupported" in (unsafe_runtime.stderr + unsafe_runtime.stdout))
325
+
326
+
327
+ def test_runtime_telemetry_verifier_contract():
328
+ with tempfile.TemporaryDirectory() as td:
329
+ root = Path(td)
330
+ scene = root / "browser-review-smoke"
331
+ task = root / "artifacts/browser-review-smoke-task"
332
+ shutil.copytree(ROOT / "src/output/browser-review-smoke", scene)
333
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", task)
334
+ verifier = ROOT / "scripts/evidence-verifier.py"
335
+ base_args = [sys.executable, str(verifier), "--scene-dir", str(scene), "--task-dir", str(task)]
336
+ clean = subprocess.run(base_args, capture_output=True, text=True)
337
+ check("telemetry verifier accepts bound evidence", clean.returncode == 0 and '"approval": false' in clean.stdout)
338
+
339
+ telemetry_path = task / "runtime-adapters/rive/runtime-telemetry.json"
340
+ telemetry = json.loads(telemetry_path.read_text())
341
+ telemetry["samples"][0]["state"]["tampered"] = True
342
+ telemetry_path.write_text(json.dumps(telemetry, indent=2) + "\n")
343
+ tampered = subprocess.run(base_args, capture_output=True, text=True)
344
+ check("telemetry verifier rejects state tamper", tampered.returncode != 0 and "sha256 mismatch" in tampered.stdout)
345
+
346
+ cross_task = root / "artifacts/other-task"
347
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", cross_task)
348
+ cross_task_doc = json.loads((cross_task / "task.json").read_text())
349
+ cross_task_doc["task_id"] = "other-task"
350
+ (cross_task / "task.json").write_text(json.dumps(cross_task_doc, indent=2) + "\n")
351
+ cross_args = [sys.executable, str(verifier), "--scene-dir", str(scene), "--task-dir", str(cross_task), "--max-age-days", "1"]
352
+ cross = subprocess.run(cross_args, capture_output=True, text=True)
353
+ check("telemetry verifier rejects cross-task identity", cross.returncode != 0 and "task_id" in cross.stdout)
354
+
355
+ stale_task = root / "artifacts/stale-task"
356
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", stale_task)
357
+ stale_evidence = stale_task / "runtime-adapters/runtime-evidence.json"
358
+ stale_doc = json.loads(stale_evidence.read_text())
359
+ stale_doc["generated_at"] = "2020-01-01T00:00:00+00:00"
360
+ stale_evidence.write_text(json.dumps(stale_doc, indent=2) + "\n")
361
+ stale_args = [sys.executable, str(verifier), "--scene-dir", str(scene), "--task-dir", str(stale_task), "--max-age-days", "1"]
362
+ stale = subprocess.run(stale_args, capture_output=True, text=True)
363
+ check("telemetry verifier rejects stale evidence", stale.returncode != 0 and "stale" in stale.stdout)
364
+
365
+ symlink_task = root / "artifacts/symlink-task"
366
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", symlink_task)
367
+ outside = root / "outside-telemetry"
368
+ outside.mkdir()
369
+ (outside / "runtime-telemetry.json").write_text(telemetry_path.read_text())
370
+ linked = symlink_task / "runtime-adapters/linked"
371
+ linked.symlink_to(outside, target_is_directory=True)
372
+ symlink_evidence = symlink_task / "runtime-adapters/runtime-evidence.json"
373
+ symlink_doc = json.loads(symlink_evidence.read_text())
374
+ symlink_doc["frameworks"][0]["telemetry"]["file"] = "linked/runtime-telemetry.json"
375
+ symlink_evidence.write_text(json.dumps(symlink_doc, indent=2) + "\n")
376
+ symlink_args = [sys.executable, str(verifier), "--scene-dir", str(scene), "--task-dir", str(symlink_task), "--max-age-days", "1"]
377
+ symlink = subprocess.run(symlink_args, capture_output=True, text=True)
378
+ check("telemetry verifier rejects symlink escape", symlink.returncode != 0 and "symlink" in symlink.stdout)
379
+
380
+
381
+ def test_approved_browser_review_e2e_contract():
382
+ """Re-run the acceptance side of a real approved task from a clean copy."""
383
+ with tempfile.TemporaryDirectory() as td:
384
+ root = Path(td)
385
+ task_dir = root / "artifacts/professional-review-e2e"
386
+ shutil.copytree(ROOT / "artifacts/professional-review-e2e", task_dir)
387
+ shutil.copytree(ROOT / "src/output/browser-review-smoke", root / "src/output/browser-review-smoke")
388
+ shutil.copy(ROOT / "artifacts/browser-review-smoke-task/project-context.json", root / "project-context.json")
389
+
390
+ candidate_path = task_dir / "browser-review.json"
391
+ candidate = json.loads(candidate_path.read_text())
392
+ candidate["expires_at"] = "2099-01-01T00:00:00Z"
393
+ candidate_path.write_text(json.dumps(candidate, indent=2) + "\n")
394
+
395
+ review = json.loads((task_dir / "review.json").read_text())
396
+ task = json.loads((task_dir / "task.json").read_text())
397
+ check(
398
+ "e2e task binds approved candidate",
399
+ review.get("candidate_id") == candidate.get("candidate_id")
400
+ and candidate.get("task_id") == task.get("task_id")
401
+ and candidate.get("scene") == task.get("scene"),
402
+ )
403
+ check("e2e task is confirmed", task.get("state") == "confirmed")
404
+
405
+ review_hook = subprocess.run([
406
+ sys.executable, str(ROOT / "scripts/review-hook.py"), "validate",
407
+ "--task-dir", str(task_dir), "--require-approved",
408
+ ], capture_output=True, text=True)
409
+ check("e2e review hook accepts approved candidate", review_hook.returncode == 0, review_hook.stdout.strip())
410
+
411
+ quality = subprocess.run([
412
+ sys.executable, str(ROOT / "scripts/quality-gate.py"), "--root", str(root),
413
+ "--scene", "browser-review-smoke", "--context", str(root / "project-context.json"),
414
+ "--task-dir", str(task_dir), "--require-browser-review",
415
+ ], capture_output=True, text=True)
416
+ check("e2e quality gate accepts task evidence", quality.returncode == 0, quality.stdout.strip())
417
+
418
+ report_check = subprocess.run([
419
+ sys.executable, str(ROOT / "scripts/report.py"), "check", "--task-dir", str(task_dir),
420
+ ], capture_output=True, text=True)
421
+ check("e2e report contract accepts confirmed task", report_check.returncode == 0, report_check.stdout.strip())
422
+
423
+
424
+ def test_intelligence_core_contracts():
425
+ """Exercise the deterministic intelligence layer and its adversarial boundary."""
426
+ with tempfile.TemporaryDirectory() as td:
427
+ root = Path(td)
428
+ task_dir = root / "artifacts/professional-review-e2e"
429
+ shutil.copytree(ROOT / "artifacts/professional-review-e2e", task_dir)
430
+
431
+ intelligence = ROOT / "scripts/intelligence.py"
432
+ motion_ir = subprocess.run([
433
+ sys.executable, str(intelligence), "motion-ir", "build", "--task-dir", str(task_dir),
434
+ "--spec", str(ROOT / "src/output/browser-review-smoke/motion-spec.json"),
435
+ ], capture_output=True, text=True)
436
+ check("intelligence Motion IR builds", motion_ir.returncode == 0, motion_ir.stdout.strip())
437
+ motion_ir_check = subprocess.run([
438
+ sys.executable, str(intelligence), "motion-ir", "validate",
439
+ "--path", str(task_dir / "motion-ir.json"),
440
+ ], capture_output=True, text=True)
441
+ check("intelligence Motion IR validates", motion_ir_check.returncode == 0, motion_ir_check.stdout.strip())
442
+ graph = subprocess.run([
443
+ sys.executable, str(intelligence), "graph", "build", "--task-dir", str(task_dir),
444
+ ], capture_output=True, text=True)
445
+ check("intelligence graph builds", graph.returncode == 0, graph.stdout.strip())
446
+ graph_check = subprocess.run([
447
+ sys.executable, str(intelligence), "graph", "validate",
448
+ "--path", str(task_dir / "project-graph.json"),
449
+ ], capture_output=True, text=True)
450
+ check("intelligence graph validates", graph_check.returncode == 0, graph_check.stdout.strip())
451
+
452
+ provenance = subprocess.run([
453
+ sys.executable, str(intelligence), "provenance", "build", "--task-dir", str(task_dir),
454
+ ], capture_output=True, text=True)
455
+ check("intelligence provenance builds", provenance.returncode == 0, provenance.stdout.strip())
456
+ provenance_check = subprocess.run([
457
+ sys.executable, str(intelligence), "provenance", "validate",
458
+ "--task-dir", str(task_dir), "--path", str(task_dir / "provenance.json"),
459
+ ], capture_output=True, text=True)
460
+ check("intelligence provenance validates", provenance_check.returncode == 0, provenance_check.stdout.strip())
461
+
462
+ registry = root / "capability-registry.json"
463
+ capability = subprocess.run([
464
+ sys.executable, str(intelligence), "capabilities", "build",
465
+ "--evidence", str(ROOT / "artifacts/browser-review-smoke-task/quality-report.json"),
466
+ "--evidence-kind", "ci", "--output", str(registry),
467
+ ], capture_output=True, text=True)
468
+ check("intelligence capability registry builds", capability.returncode == 0, capability.stdout.strip())
469
+ registry_check = subprocess.run([
470
+ sys.executable, str(intelligence), "capabilities", "validate", "--path", str(registry),
471
+ ], capture_output=True, text=True)
472
+ check("intelligence capability registry validates", registry_check.returncode == 0, registry_check.stdout.strip())
473
+ selected = subprocess.run([
474
+ sys.executable, str(intelligence), "capabilities", "select",
475
+ "--registry", str(registry), "--capability", "runtime.rive",
476
+ ], capture_output=True, text=True)
477
+ check("intelligence selects verified runtime", selected.returncode == 0 and '"status": "verified"' in selected.stdout)
478
+ scaffold = subprocess.run([
479
+ sys.executable, str(intelligence), "capabilities", "select",
480
+ "--registry", str(registry), "--capability", "runtime.spine",
481
+ ], capture_output=True, text=True)
482
+ check("intelligence blocks scaffold-only runtime", scaffold.returncode != 0)
483
+
484
+ stale_registry = json.loads(registry.read_text())
485
+ for entry in stale_registry["capabilities"]:
486
+ if entry.get("status") == "verified":
487
+ entry["last_verified_at"] = "2000-01-01T00:00:00Z"
488
+ stale_path = root / "stale-capability-registry.json"
489
+ stale_path.write_text(json.dumps(stale_registry, indent=2) + "\n")
490
+ stale_select = subprocess.run([
491
+ sys.executable, str(intelligence), "capabilities", "select",
492
+ "--registry", str(stale_path), "--capability", "runtime.rive",
493
+ ], capture_output=True, text=True)
494
+ check("intelligence blocks stale capability evidence", stale_select.returncode != 0)
495
+
496
+ tampered_registry = json.loads(registry.read_text())
497
+ rive_entry = next(entry for entry in tampered_registry["capabilities"] if entry.get("id") == "runtime.rive")
498
+ rive_entry["evidence"][0]["sha256"] = "0" * 64
499
+ tampered_path = root / "tampered-capability-registry.json"
500
+ tampered_path.write_text(json.dumps(tampered_registry, indent=2) + "\n")
501
+ tampered_select = subprocess.run([
502
+ sys.executable, str(intelligence), "capabilities", "select",
503
+ "--registry", str(tampered_path), "--capability", "runtime.rive",
504
+ ], capture_output=True, text=True)
505
+ check("intelligence blocks tampered capability evidence", tampered_select.returncode != 0)
506
+
507
+ replay = subprocess.run([
508
+ sys.executable, str(intelligence), "replay", "capture",
509
+ "--root", str(root), "--task-dir", str(task_dir),
510
+ ], capture_output=True, text=True)
511
+ check("intelligence replay captures bundle", replay.returncode == 0, replay.stdout.strip())
512
+ replay_check = subprocess.run([
513
+ sys.executable, str(intelligence), "replay", "verify",
514
+ "--root", str(root), "--bundle", str(task_dir / "replay-bundle.json"),
515
+ ], capture_output=True, text=True)
516
+ check("intelligence replay verifies clean bundle", replay_check.returncode == 0, replay_check.stdout.strip())
517
+ review = task_dir / "review.json"
518
+ review.write_text(review.read_text(encoding="utf-8") + "\n", encoding="utf-8")
519
+ tampered = subprocess.run([
520
+ sys.executable, str(intelligence), "replay", "verify",
521
+ "--root", str(root), "--bundle", str(task_dir / "replay-bundle.json"),
522
+ ], capture_output=True, text=True)
523
+ check("intelligence replay rejects tampered artifact", tampered.returncode != 0 and "hash_mismatch" in tampered.stdout)
524
+
525
+ other_task = root / "artifacts/other-task"
526
+ shutil.copytree(task_dir, other_task)
527
+ other_task_data = json.loads((other_task / "task.json").read_text())
528
+ other_task_data["task_id"] = "other-task"
529
+ (other_task / "task.json").write_text(json.dumps(other_task_data, indent=2) + "\n")
530
+ cross_bundle = json.loads((task_dir / "replay-bundle.json").read_text())
531
+ cross_bundle["task_dir"] = "artifacts/other-task"
532
+ cross_bundle_path = root / "cross-task-replay.json"
533
+ cross_bundle_path.write_text(json.dumps(cross_bundle, indent=2) + "\n")
534
+ cross_check = subprocess.run([
535
+ sys.executable, str(intelligence), "replay", "verify",
536
+ "--root", str(root), "--bundle", str(cross_bundle_path),
537
+ ], capture_output=True, text=True)
538
+ check("intelligence replay rejects cross-task bundle binding", cross_check.returncode != 0 and "missing" in cross_check.stdout)
539
+
540
+ outside = root / "outside-artifact.json"
541
+ outside.write_text("outside\n")
542
+ symlink_path = task_dir / "symlink-artifact.json"
543
+ symlink_path.symlink_to(outside)
544
+ symlink_replay = subprocess.run([
545
+ sys.executable, str(intelligence), "replay", "capture",
546
+ "--root", str(root), "--task-dir", str(task_dir),
547
+ ], capture_output=True, text=True)
548
+ check("intelligence replay rejects symlink artifact", symlink_replay.returncode != 0 and "symlinked" in (symlink_replay.stderr + symlink_replay.stdout))
549
+
550
+ external_evidence = root / "external-evidence.json"
551
+ external_evidence.write_text("external\n")
552
+ external_registry = subprocess.run([
553
+ sys.executable, str(intelligence), "capabilities", "build",
554
+ "--evidence", str(external_evidence), "--evidence-kind", "static",
555
+ "--output", str(root / "external-registry.json"),
556
+ ], capture_output=True, text=True)
557
+ check("intelligence blocks capability evidence outside repository", external_registry.returncode != 0 and "inside the repository" in (external_registry.stderr + external_registry.stdout))
558
+
559
+
560
+ def test_malformed_spec_is_rejected_cleanly():
561
+ with tempfile.TemporaryDirectory() as td:
562
+ bad = Path(td) / "bad.json"
563
+ bad.write_text(json.dumps({"category": "loading", "framework": "lottie"}))
564
+ result = subprocess.run([sys.executable, str(ROOT / "src/core/spec.py"), "validate", str(bad)],
565
+ capture_output=True, text=True)
566
+ check("malformed spec exits non-zero", result.returncode != 0)
567
+ check("malformed spec reports issues", "ISSUES:" in result.stdout and "duration_s" in result.stdout)
568
+
569
+
570
+ def test_p1_semantic_continuity_fix_plan():
571
+ with tempfile.TemporaryDirectory() as td:
572
+ root = Path(td)
573
+ task_dir = root / "professional-review-e2e"
574
+ shutil.copytree(ROOT / "artifacts/professional-review-e2e", task_dir)
575
+ intelligence = ROOT / "scripts/intelligence.py"
576
+
577
+ lint = subprocess.run([
578
+ sys.executable, str(intelligence), "semantic-lint", "build", "--task-dir", str(task_dir),
579
+ ], capture_output=True, text=True)
580
+ lint_data = json.loads((task_dir / "semantic-lint-report.json").read_text())
581
+ check("P1 semantic lint builds report", lint.returncode == 0)
582
+ check("P1 semantic lint preserves human keyboard review", lint_data.get("status") == "warn" and any(item.get("id") == "accessibility-keyboard-review" and item.get("basis") == "human" for item in lint_data.get("findings", [])))
583
+ lint_validate = subprocess.run([
584
+ sys.executable, str(intelligence), "semantic-lint", "validate", "--path", str(task_dir / "semantic-lint-report.json"),
585
+ ], capture_output=True, text=True)
586
+ check("P1 semantic lint validates report", lint_validate.returncode == 0)
587
+
588
+ duration_task = root / "duration-budget-task"
589
+ shutil.copytree(task_dir, duration_task)
590
+ duration_ir_path = duration_task / "motion-ir.json"
591
+ duration_ir = json.loads(duration_ir_path.read_text())
592
+ duration_ir["duration_ms"] = 600
593
+ duration_ir_path.write_text(json.dumps(duration_ir, indent=2) + "\n")
594
+ duration_lint = subprocess.run([
595
+ sys.executable, str(intelligence), "semantic-lint", "build", "--task-dir", str(duration_task),
596
+ ], capture_output=True, text=True)
597
+ duration_report = json.loads((duration_task / "semantic-lint-report.json").read_text())
598
+ check("P1 semantic lint warns on UI animation budget", duration_lint.returncode == 0 and any(item.get("id") == "perf-animation-budget" and item.get("severity") == "warning" and not item.get("approval_blocking") for item in duration_report.get("findings", [])))
599
+
600
+ fps_task = root / "fps-task"
601
+ shutil.copytree(task_dir, fps_task)
602
+ fps_ir_path = fps_task / "motion-ir.json"
603
+ fps_ir = json.loads(fps_ir_path.read_text())
604
+ fps_ir["fps"] = 24
605
+ fps_ir_path.write_text(json.dumps(fps_ir, indent=2) + "\n")
606
+ fps_lint = subprocess.run([
607
+ sys.executable, str(intelligence), "semantic-lint", "build", "--task-dir", str(fps_task),
608
+ ], capture_output=True, text=True)
609
+ fps_report = json.loads((fps_task / "semantic-lint-report.json").read_text())
610
+ check("P1 semantic lint warns below 30 FPS", fps_lint.returncode == 0 and any(item.get("id") == "perf-frame-rate" for item in fps_report.get("findings", [])))
611
+
612
+ easing_task = root / "easing-task"
613
+ shutil.copytree(task_dir, easing_task)
614
+ easing_ir_path = easing_task / "motion-ir.json"
615
+ easing_ir = json.loads(easing_ir_path.read_text())
616
+ for keyframe in easing_ir.get("tracks", [])[0].get("keyframes", []):
617
+ keyframe["easing"] = "linear"
618
+ easing_ir_path.write_text(json.dumps(easing_ir, indent=2) + "\n")
619
+ easing_lint = subprocess.run([
620
+ sys.executable, str(intelligence), "semantic-lint", "build", "--task-dir", str(easing_task),
621
+ ], capture_output=True, text=True)
622
+ easing_report = json.loads((easing_task / "semantic-lint-report.json").read_text())
623
+ check("P1 semantic lint warns on linear UI easing", easing_lint.returncode == 0 and any(item.get("id") == "perceptual-easing-linear" and item.get("severity") == "warning" for item in easing_report.get("findings", [])))
624
+
625
+ reduced_task = root / "reduced-motion-task"
626
+ shutil.copytree(task_dir, reduced_task)
627
+ reduced_ir_path = reduced_task / "motion-ir.json"
628
+ reduced_ir = json.loads(reduced_ir_path.read_text())
629
+ reduced_ir.setdefault("accessibility", {})["reduced_motion"] = "none"
630
+ reduced_ir_path.write_text(json.dumps(reduced_ir, indent=2) + "\n")
631
+ reduced_lint = subprocess.run([
632
+ sys.executable, str(intelligence), "semantic-lint", "build", "--task-dir", str(reduced_task),
633
+ ], capture_output=True, text=True)
634
+ reduced_report = json.loads((reduced_task / "semantic-lint-report.json").read_text())
635
+ check("P1 semantic lint warns on absent reduced-motion fallback", reduced_lint.returncode == 0 and any(item.get("id") == "perceptual-reduced-motion-missing" and not item.get("approval_blocking") for item in reduced_report.get("findings", [])))
636
+
637
+ benchmark_path = task_dir / "semantic-lint-benchmark.json"
638
+ benchmark = subprocess.run([
639
+ sys.executable, str(intelligence), "semantic-lint", "benchmark", "--task-dir", str(task_dir),
640
+ "--iterations", "10", "--threshold-ms", "500", "--output", str(benchmark_path),
641
+ ], capture_output=True, text=True)
642
+ benchmark_data = json.loads(benchmark_path.read_text())
643
+ check("P1 semantic lint benchmark passes threshold", benchmark.returncode == 0 and benchmark_data.get("status") == "pass" and benchmark_data.get("p95_ms", 999999) < benchmark_data.get("threshold_ms", 0))
644
+ benchmark_validate = subprocess.run([
645
+ sys.executable, str(intelligence), "semantic-lint", "benchmark", "--task-dir", str(task_dir),
646
+ "--iterations", "2", "--threshold-ms", "500", "--output", str(task_dir / "semantic-lint-benchmark-2.json"),
647
+ ], capture_output=True, text=True)
648
+ check("P1 semantic lint benchmark is repeatable", benchmark_validate.returncode == 0)
649
+
650
+ ir = json.loads((task_dir / "motion-ir.json").read_text())
651
+ ir["intent"] = "motion"
652
+ (task_dir / "motion-ir.json").write_text(json.dumps(ir, indent=2) + "\n")
653
+ generic_lint = subprocess.run([
654
+ sys.executable, str(intelligence), "semantic-lint", "build", "--task-dir", str(task_dir),
655
+ "--output", str(task_dir / "generic-lint.json"),
656
+ ], capture_output=True, text=True)
657
+ generic_data = json.loads((task_dir / "generic-lint.json").read_text())
658
+ check("P1 semantic lint detects low-specificity intent", generic_lint.returncode == 0 and any(item.get("id") == "intent-low-specificity" for item in generic_data.get("findings", [])))
659
+
660
+ fix_plan = subprocess.run([
661
+ sys.executable, str(intelligence), "fix-plan", "build", "--task-dir", str(task_dir),
662
+ "--reports", "generic-lint.json",
663
+ ], capture_output=True, text=True)
664
+ plan = json.loads((task_dir / "fix-plan.json").read_text())
665
+ check("P1 fix plan binds semantic report", fix_plan.returncode == 0 and plan.get("status") == "proposed", f"rc={fix_plan.returncode} stdout={fix_plan.stdout.strip()} stderr={fix_plan.stderr.strip()}")
666
+ check("P1 fix plan declares selective lint rerun", any(issue.get("rerun_scope") == ["lint"] and "intent-low-specificity" in issue.get("finding_ref", "") for issue in plan.get("issues", []) if isinstance(issue, dict)))
667
+ plan_validate = subprocess.run([
668
+ sys.executable, str(intelligence), "fix-plan", "validate", "--path", str(task_dir / "fix-plan.json"),
669
+ ], capture_output=True, text=True)
670
+ check("P1 fix plan validates", plan_validate.returncode == 0)
671
+
672
+ first = root / "scene-a"
673
+ second = root / "scene-b"
674
+ shutil.copytree(task_dir, first)
675
+ shutil.copytree(task_dir, second)
676
+ for path, task_id, scene, order in ((first, "p1-scene-a", "scene-a", 0), (second, "p1-scene-b", "scene-b", 1)):
677
+ task = json.loads((path / "task.json").read_text())
678
+ task.update({"task_id": task_id, "scene": scene, "scene_order": order, "project_name": "p1-continuity"})
679
+ (path / "task.json").write_text(json.dumps(task, indent=2) + "\n")
680
+ ir = json.loads((path / "motion-ir.json").read_text())
681
+ ir.update({"task_id": task_id, "scene": scene, "context_hash": task.get("context_hash")})
682
+ (path / "motion-ir.json").write_text(json.dumps(ir, indent=2) + "\n")
683
+
684
+ continuity = subprocess.run([
685
+ sys.executable, str(intelligence), "continuity", "build", "--task-dirs", str(first), str(second),
686
+ ], capture_output=True, text=True)
687
+ continuity_data = json.loads((first / "continuity-report.json").read_text())
688
+ check("P1 continuity builds multi-scene report", continuity.returncode == 0 and continuity_data.get("summary", {}).get("transition_count") == 1)
689
+ check("P1 continuity passes matching context", continuity_data.get("status") == "pass")
690
+
691
+ second_ir = json.loads((second / "motion-ir.json").read_text())
692
+ second_ir["context_hash"] = "f" * 64
693
+ (second / "motion-ir.json").write_text(json.dumps(second_ir, indent=2) + "\n")
694
+ drift = subprocess.run([
695
+ sys.executable, str(intelligence), "continuity", "build", "--task-dirs", str(first), str(second),
696
+ ], capture_output=True, text=True)
697
+ drift_data = json.loads((first / "continuity-report.json").read_text())
698
+ check("P1 continuity detects context drift", drift.returncode == 0 and drift_data.get("status") == "warn" and "context hash changes between adjacent scenes" in drift_data["transitions"][0].get("findings", []))
699
+
700
+
701
+ def test_category_coverage():
702
+ from src.core.analyzer import CATEGORIES # noqa
703
+ from docs import __file__ as _ # noqa: guard import path
704
+ required = {"ui-micro", "loading", "hero-scene", "character-body", "icon-animation",
705
+ "scroll-linked", "data-viz", "3d-scene"}
706
+ check("all categories defined", required <= set(CATEGORIES))
707
+
708
+
709
+ def test_observability_contract():
710
+ with tempfile.TemporaryDirectory() as td:
711
+ task_dir = Path(td) / "task"
712
+ report_script = ROOT / "scripts/report.py"
713
+ subprocess.run([
714
+ sys.executable, str(report_script), "init", "--task-id", "observability-fixture",
715
+ "--scene", "wave", "--intent", "Test report contract", "--output", str(task_dir),
716
+ ], check=True, capture_output=True)
717
+ subprocess.run([
718
+ sys.executable, str(report_script), "add", "--task-dir", str(task_dir), "--section", "completed",
719
+ "--id", "context", "--summary", "Project context analyzed", "--status", "pass",
720
+ "--evidence", "project-context.json",
721
+ ], check=True, capture_output=True)
722
+ subprocess.run([
723
+ sys.executable, str(report_script), "add", "--task-dir", str(task_dir), "--section", "verified",
724
+ "--id", "runtime", "--summary", "Runtime frame rendered", "--status", "pass",
725
+ "--evidence", "snapshot/frame-50.png",
726
+ ], check=True, capture_output=True)
727
+ subprocess.run([
728
+ sys.executable, str(report_script), "add", "--task-dir", str(task_dir), "--section", "problems",
729
+ "--id", "asset-license", "--summary", "Asset license needs confirmation", "--status", "open",
730
+ "--severity", "P1", "--next-action", "Ask user to confirm source license",
731
+ ], check=True, capture_output=True)
732
+ subprocess.run([
733
+ sys.executable, str(report_script), "add", "--task-dir", str(task_dir), "--section", "next_agent",
734
+ "--id", "review", "--summary", "Review scene in Dev Lab", "--status", "pending",
735
+ "--agent", "animation-review-agent", "--skill", "motionloom",
736
+ "--evidence-needed", "review.json",
737
+ ], check=True, capture_output=True)
738
+ subprocess.run([
739
+ sys.executable, str(report_script), "structure", "--task-dir", str(task_dir),
740
+ "--missing-file", "project-context.json", "--broken-reference", "src/output/wave/animation.json",
741
+ ], check=True, capture_output=True)
742
+
743
+ for state in ("planning", "sourcing", "generating", "rendering", "review_required"):
744
+ subprocess.run([
745
+ sys.executable, str(report_script), "transition", "--task-dir", str(task_dir), "--state", state,
746
+ ], check=True, capture_output=True)
747
+
748
+ (task_dir / "quality-report.json").write_text(json.dumps({"status": "pass", "rules": []}))
749
+ subprocess.run([
750
+ sys.executable, str(report_script), "transition", "--task-dir", str(task_dir), "--state", "validated",
751
+ ], check=True, capture_output=True)
752
+ (task_dir / "browser-review.json").write_text(json.dumps({
753
+ "schema_version": "1.0",
754
+ "candidate_id": "fixture-candidate",
755
+ "task_id": "observability-fixture",
756
+ "scene": "wave",
757
+ "status": "prepared",
758
+ "requires_user_approval": True,
759
+ "expires_at": "2099-01-01T00:00:00Z",
760
+ }))
761
+ subprocess.run([
762
+ sys.executable, str(report_script), "review", "--task-dir", str(task_dir),
763
+ "--candidate-id", "fixture-candidate", "--decision", "approved", "--reviewer", "fixture", "--notes", "Evidence looks consistent.",
764
+ ], check=True, capture_output=True)
765
+ subprocess.run([
766
+ sys.executable, str(report_script), "transition", "--task-dir", str(task_dir), "--state", "ready_for_pr",
767
+ ], check=True, capture_output=True)
768
+ subprocess.run([
769
+ sys.executable, str(report_script), "transition", "--task-dir", str(task_dir), "--state", "confirmed",
770
+ "--commit-sha", "0123456789abcdef0123456789abcdef01234567",
771
+ ], check=True, capture_output=True)
772
+ subprocess.run([
773
+ sys.executable, str(report_script), "collect", "--task-dir", str(task_dir),
774
+ ], check=True, capture_output=True)
775
+ subprocess.run([
776
+ sys.executable, str(report_script), "render", "--task-dir", str(task_dir),
777
+ ], check=True, capture_output=True)
778
+ report_check = subprocess.run([
779
+ sys.executable, str(report_script), "check", "--task-dir", str(task_dir),
780
+ ], capture_output=True, text=True)
781
+
782
+ task = json.loads((task_dir / "task.json").read_text())
783
+ report = (task_dir / "REPORT.md").read_text()
784
+ manifest = json.loads((task_dir / "artifact-manifest.json").read_text())
785
+ check("task lifecycle reaches confirmed", task.get("state") == "confirmed")
786
+ check("confirmed task records commit", task.get("commit_sha", "").startswith("01234567"))
787
+ check("execution report exposes required sections", all(section in report for section in ("## Completed", "## Verified", "## Not completed", "## Problems to fix", "## Structure review")))
788
+ check("execution report includes recorded problem", "Asset license needs confirmation" in report)
789
+ check("report removes initial placeholder after progress", "Task has not run yet." not in report)
790
+ check("report includes structure findings", "project-context.json" in report and "src/output/wave/animation.json" in report)
791
+ check("artifact manifest has checksums", bool(manifest.get("artifacts")) and all(len(item.get("sha256", "")) == 64 for item in manifest["artifacts"]))
792
+ check("semantic report check passes", report_check.returncode == 0 and json.loads(report_check.stdout).get("status") == "pass")
793
+
794
+ candidate = json.loads((task_dir / "browser-review.json").read_text())
795
+ candidate["status"] = "prepared"
796
+ candidate["task_id"] = "foreign-task"
797
+ (task_dir / "browser-review.json").write_text(json.dumps(candidate))
798
+ foreign_review = subprocess.run([
799
+ sys.executable, str(report_script), "review", "--task-dir", str(task_dir),
800
+ "--candidate-id", "fixture-candidate", "--decision", "approved", "--reviewer", "fixture",
801
+ ], capture_output=True, text=True)
802
+ check("review rejects foreign task candidate", foreign_review.returncode != 0 and "task_id" in foreign_review.stderr)
803
+
804
+ doctor = subprocess.run([sys.executable, str(ROOT / "scripts/skill-doctor.py"), "--json"], capture_output=True, text=True)
805
+ doctor_data = json.loads(doctor.stdout)
806
+ check("skill doctor passes package structure", doctor.returncode == 0 and doctor_data.get("status") == "pass")
807
+
808
+ attestation_tests = subprocess.run(
809
+ [sys.executable, str(ROOT / "tests/scripts/test_attestation.py")],
810
+ capture_output=True,
811
+ text=True,
812
+ )
813
+ check(
814
+ "signed attestation trust and revocation contract passes",
815
+ attestation_tests.returncode == 0 and "attestation contract tests: PASS" in attestation_tests.stdout,
816
+ )
817
+
818
+
819
+ if __name__ == "__main__":
820
+ print("== MotionLoom engine tests ==")
821
+ test_analyzer_on_fixture()
822
+ test_spec_generate_and_validate()
823
+ test_rig_build_and_pose()
824
+ test_lottie_scaffold_valid()
825
+ test_dotlottie_manifest_selection()
826
+ test_dotlottie_packager()
827
+ test_source_binding_contract()
828
+ test_placeholder_is_not_runtime_evidence()
829
+ test_runtime_evidence_binding()
830
+ test_deep_audit_contracts()
831
+ test_runtime_telemetry_verifier_contract()
832
+ test_approved_browser_review_e2e_contract()
833
+ test_intelligence_core_contracts()
834
+ test_malformed_spec_is_rejected_cleanly()
835
+ test_p1_semantic_continuity_fix_plan()
836
+ sys.path.insert(0, str(ROOT))
837
+ test_category_coverage()
838
+ test_observability_contract()
839
+ print()
840
+ if FAILED:
841
+ print(f"{len(FAILED)} test(s) FAILED: {', '.join(FAILED)}")
842
+ sys.exit(1)
843
+ print("all tests passed")