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,37 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ SCENE="${1:?scene is required}"
6
+ TASK_DIR="${2:?task directory is required}"
7
+
8
+ if [[ ! "$SCENE" =~ ^[A-Za-z0-9._-]+$ ]]; then
9
+ echo "capture-runtime-telemetry: unsafe scene identifier: $SCENE" >&2
10
+ exit 2
11
+ fi
12
+ if [[ ! -f "$ROOT/src/output/$SCENE/manifest.json" || ! -f "$ROOT/$TASK_DIR/task.json" ]]; then
13
+ echo "capture-runtime-telemetry: missing scene manifest or task.json" >&2
14
+ exit 2
15
+ fi
16
+
17
+ MANIFEST="$ROOT/src/output/$SCENE/manifest.json"
18
+ TASK_JSON="$ROOT/$TASK_DIR/task.json"
19
+ SOURCE_FILE="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["file"])' "$MANIFEST")"
20
+ TASK_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["task_id"])' "$TASK_JSON")"
21
+ OUTPUT_DIR="$ROOT/$TASK_DIR/runtime-adapters"
22
+
23
+ rm -rf "$OUTPUT_DIR"
24
+ RUNTIME_EVIDENCE_DIR="$OUTPUT_DIR" \
25
+ RUNTIME_SCENE="$SCENE" \
26
+ RUNTIME_TASK_ID="$TASK_ID" \
27
+ RUNTIME_SOURCE_PATH="$ROOT/src/output/$SCENE/$SOURCE_FILE" \
28
+ RUNTIME_MANIFEST_PATH="$MANIFEST" \
29
+ RUNTIME_MOTION_IR_PATH="$ROOT/$TASK_DIR/motion-ir.json" \
30
+ npm run runtime:test
31
+
32
+ python3 "$ROOT/scripts/evidence-verifier.py" \
33
+ --scene-dir "$ROOT/src/output/$SCENE" \
34
+ --task-dir "$ROOT/$TASK_DIR" \
35
+ --runtime-evidence runtime-adapters/runtime-evidence.json \
36
+ --max-age-days 1 \
37
+ --output "$ROOT/$TASK_DIR/evidence-verifier-report.json"
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # devlab.sh — Step 5 of the pipeline: boot the Dev Lab for a scene.
3
+ # Copies the scene into the Dev Lab's public scenes folder, installs its
4
+ # dependencies, and serves the workbench so the user can scrub, inspect,
5
+ # run the checklist and iterate fixes before confirming.
6
+ #
7
+ # Usage: bash scripts/devlab.sh <scene> [mode] [task-dir]
8
+ set -euo pipefail
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ REPO="$(dirname "$SCRIPT_DIR")"
11
+ SCENE="${1:?usage: bash scripts/devlab.sh <scene>}"
12
+ MODE="${2:-serve}"
13
+ TASK_DIR="${3:-}"
14
+ SCENE_DIR="$REPO/src/output/$SCENE"
15
+ LAB="$REPO/dev-lab"
16
+
17
+ if [[ ! "$SCENE" =~ ^[A-Za-z0-9._-]+$ || "$SCENE" == "." || "$SCENE" == ".." ]]; then
18
+ echo "error: scene id contains unsafe path characters: $SCENE" >&2
19
+ exit 1
20
+ fi
21
+
22
+ if [ ! -d "$SCENE_DIR" ]; then
23
+ echo "error: scene directory not found: $SCENE_DIR"
24
+ echo "run the generator first and render the scene into src/output/<scene>/"
25
+ exit 1
26
+ fi
27
+
28
+ if [ ! -f "$SCENE_DIR/browser-review.json" ]; then
29
+ echo "error: browser-review.json is required; run scripts/review-hook.py prepare after runtime render" >&2
30
+ exit 1
31
+ fi
32
+
33
+ rm -rf "$LAB/public/scenes/$SCENE"
34
+ mkdir -p "$LAB/public/scenes/$SCENE"
35
+ cp -R "$SCENE_DIR"/. "$LAB/public/scenes/$SCENE/"
36
+
37
+ if [ -n "$TASK_DIR" ]; then
38
+ TASK_DIR="$(cd "$TASK_DIR" && pwd -P)"
39
+ case "$TASK_DIR" in
40
+ "$REPO"/*) ;;
41
+ *) echo "error: task bundle must be inside the repository" >&2; exit 1 ;;
42
+ esac
43
+ TASK_ID="$(python3 - "$TASK_DIR/task.json" <<'PY'
44
+ import json, sys
45
+ from pathlib import Path
46
+ data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
47
+ print(data.get("task_id", ""))
48
+ PY
49
+ )"
50
+ if [[ -z "$TASK_ID" || ! "$TASK_ID" =~ ^[A-Za-z0-9._-]+$ || "$TASK_ID" == "." || "$TASK_ID" == ".." ]]; then
51
+ echo "error: task bundle has unsafe or missing task_id" >&2
52
+ exit 1
53
+ fi
54
+ rm -rf "$LAB/public/tasks/$TASK_ID"
55
+ mkdir -p "$LAB/public/tasks/$TASK_ID"
56
+ for artifact in task.json browser-review.json review.json execution-report.json handoff.json quality-report.json artifact-manifest.json issue-register.json decision-log.jsonl project-graph.json provenance.json capability-registry.json motion-ir.json replay-bundle.json semantic-lint-report.json continuity-report.json fix-plan.json browser-observation.md; do
57
+ if [ -f "$TASK_DIR/$artifact" ]; then
58
+ cp "$TASK_DIR/$artifact" "$LAB/public/tasks/$TASK_ID/$artifact"
59
+ fi
60
+ done
61
+ fi
62
+
63
+ if [ "$MODE" = "--prepare-only" ]; then
64
+ echo "== Dev Lab scene prepared: $LAB/public/scenes/$SCENE =="
65
+ if [ -n "$TASK_DIR" ]; then echo "== Dev Lab task bundle prepared: $LAB/public/tasks/$TASK_ID =="; fi
66
+ exit 0
67
+ fi
68
+
69
+ if [ ! -d "$LAB/node_modules" ]; then
70
+ echo "== installing Dev Lab dependencies (first run) =="
71
+ (cd "$LAB" && pnpm install --silent)
72
+ fi
73
+
74
+ PORT="${PORT:-3300}"
75
+ echo "== Dev Lab ready for scene: $SCENE =="
76
+ echo " http://localhost:${PORT}/?scene=$SCENE (use the candidate URL emitted by review-hook.py for task-bound review)"
77
+ exec python3 -m http.server "$PORT" --directory "$LAB/public"
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env python3
2
+ """Run the deterministic Intelligence Core eval corpus.
3
+
4
+ The runner intentionally uses clean temporary roots and subprocesses the same
5
+ CLI entrypoints used by Agents and CI. It reports each case as pass/fail and
6
+ never turns a negative case into a successful acceptance.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import hashlib
13
+ import json
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ from datetime import datetime, timedelta, timezone
19
+ from pathlib import Path
20
+
21
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
22
+
23
+
24
+ ROOT = Path(__file__).resolve().parents[1]
25
+ INTELLIGENCE = ROOT / "scripts/intelligence.py"
26
+ REPORT = ROOT / "scripts/report.py"
27
+ VERIFIER = ROOT / "scripts/evidence-verifier.py"
28
+ ATTESTATION = ROOT / "scripts/attestation.py"
29
+ ATTESTATION_VERIFIER = ROOT / "scripts/attestation-verifier.py"
30
+ CASES = ROOT / "tests/evals/intelligence-cases.json"
31
+
32
+
33
+ def invoke(args: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
34
+ return subprocess.run([sys.executable, *args], cwd=cwd or ROOT, capture_output=True, text=True)
35
+
36
+
37
+ def record(results: list[dict[str, object]], case_id: str, passed: bool, detail: str = "") -> None:
38
+ results.append({"id": case_id, "status": "pass" if passed else "fail", "detail": detail.strip()[-500:]})
39
+
40
+
41
+ def iso(value: datetime) -> str:
42
+ return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
43
+
44
+
45
+ def attestation_policy(key_id: str, public_key: bytes, status: str = "active") -> dict[str, object]:
46
+ now = datetime.now(timezone.utc)
47
+ key: dict[str, object] = {
48
+ "key_id": key_id,
49
+ "algorithm": "ed25519",
50
+ "public_key_base64": base64.b64encode(public_key).decode("ascii"),
51
+ "status": status,
52
+ "valid_from": iso(now - timedelta(days=1)),
53
+ }
54
+ if status == "revoked":
55
+ key["revoked_at"] = iso(now - timedelta(hours=1))
56
+ key["revocation_reason"] = "eval fixture revocation"
57
+ return {
58
+ "schema_version": "1.0",
59
+ "policy_id": "motionloom-eval-policy",
60
+ "trust_domain": "https://motionloom.dev/trust/eval",
61
+ "keys": [key],
62
+ "rotation": {"max_key_age_days": 90, "overlap_days": 7, "require_active_signer": True},
63
+ "revocation": {"mode": "local-policy", "fail_closed": True, "sources": ["eval-fixture"]},
64
+ }
65
+
66
+
67
+ def run_attestation_cases(root: Path, results: list[dict[str, object]]) -> None:
68
+ """Exercise the signed-attestation boundary with stable verifier outcomes."""
69
+ case_root = root / "attestation-eval"
70
+ case_root.mkdir(parents=True, exist_ok=True)
71
+ private_key = Ed25519PrivateKey.generate()
72
+ key_id = "eval-signer-v1"
73
+ private_key_path = case_root / "private.key"
74
+ private_key_path.write_text(base64.b64encode(private_key.private_bytes_raw()).decode("ascii") + "\n", encoding="utf-8")
75
+ policy_path = case_root / "trust-policy.json"
76
+ policy_path.write_text(json.dumps(attestation_policy(key_id, private_key.public_key().public_bytes_raw()), indent=2) + "\n", encoding="utf-8")
77
+ statement = {
78
+ "type": "https://motionloom.dev/attestation/v1",
79
+ "predicate_type": "https://motionloom.dev/predicate/animation-evidence/v1",
80
+ "subject": [{"name": "eval-scene", "digest": {"sha256": "a" * 64}}],
81
+ "predicate": {
82
+ "task_id": "attestation-eval-task",
83
+ "scene": "eval-scene",
84
+ "context_hash": "b" * 64,
85
+ "source_sha256": "c" * 64,
86
+ "manifest_sha256": "d" * 64,
87
+ "motion_ir_sha256": "e" * 64,
88
+ "evidence": {
89
+ "runtime_evidence_sha256": "f" * 64,
90
+ "runtime_telemetry_sha256": "0" * 64,
91
+ "verifier_report_sha256": "1" * 64,
92
+ },
93
+ "provenance_chain_hash": "2" * 64,
94
+ "policy_version": "1.0",
95
+ "generated_at": iso(datetime.now(timezone.utc)),
96
+ "builder": {"name": "motionloom-eval", "version": "1.0.0"},
97
+ },
98
+ }
99
+ statement_path = case_root / "statement.json"
100
+ statement_path.write_text(json.dumps(statement, indent=2) + "\n", encoding="utf-8")
101
+ bundle_path = case_root / "attestation.json"
102
+ built = invoke([
103
+ str(ATTESTATION), "build", "--statement", str(statement_path), "--private-key", str(private_key_path),
104
+ "--key-id", key_id, "--output", str(bundle_path),
105
+ ])
106
+ clean = invoke([
107
+ str(ATTESTATION_VERIFIER), "--attestation", str(bundle_path), "--trust-policy", str(policy_path),
108
+ "--expected-task-id", "attestation-eval-task", "--expected-scene", "eval-scene",
109
+ ])
110
+ clean_doc = json.loads(clean.stdout) if clean.stdout.strip().startswith("{") else {}
111
+ record(results, "p2-attestation-clean", built.returncode == 0 and clean.returncode == 0 and clean_doc.get("verified") is True and clean_doc.get("approval") is False, clean.stdout + clean.stderr)
112
+
113
+ tampered = json.loads(bundle_path.read_text(encoding="utf-8"))
114
+ tampered["envelope"]["payload_base64"] = base64.b64encode(b"tampered").decode("ascii")
115
+ tampered_path = case_root / "tampered.json"
116
+ tampered_path.write_text(json.dumps(tampered, indent=2) + "\n", encoding="utf-8")
117
+ tamper_result = invoke([str(ATTESTATION_VERIFIER), "--attestation", str(tampered_path), "--trust-policy", str(policy_path)])
118
+ record(results, "p2-attestation-payload-tamper", tamper_result.returncode == 11, tamper_result.stdout + tamper_result.stderr)
119
+
120
+ binding_result = invoke([
121
+ str(ATTESTATION_VERIFIER), "--attestation", str(bundle_path), "--trust-policy", str(policy_path),
122
+ "--expected-task-id", "foreign-task",
123
+ ])
124
+ record(results, "p2-attestation-binding-mismatch", binding_result.returncode == 14, binding_result.stdout + binding_result.stderr)
125
+
126
+ revoked_policy = case_root / "revoked-policy.json"
127
+ revoked_policy.write_text(json.dumps(attestation_policy(key_id, private_key.public_key().public_bytes_raw(), "revoked"), indent=2) + "\n", encoding="utf-8")
128
+ revoked_result = invoke([str(ATTESTATION_VERIFIER), "--attestation", str(bundle_path), "--trust-policy", str(revoked_policy)])
129
+ record(results, "p2-attestation-revoked-signer", revoked_result.returncode == 13, revoked_result.stdout + revoked_result.stderr)
130
+
131
+ unknown_policy = case_root / "unknown-policy.json"
132
+ unknown_policy.write_text(json.dumps(attestation_policy("other-signer-v1", private_key.public_key().public_bytes_raw()), indent=2) + "\n", encoding="utf-8")
133
+ unknown_result = invoke([str(ATTESTATION_VERIFIER), "--attestation", str(bundle_path), "--trust-policy", str(unknown_policy)])
134
+ record(results, "p2-attestation-unknown-signer", unknown_result.returncode == 13, unknown_result.stdout + unknown_result.stderr)
135
+
136
+
137
+ def run_p1_cases(root: Path, task_dir: Path, results: list[dict[str, object]]) -> None:
138
+ """Exercise P1 semantic, continuity and feedback contracts in isolated copies."""
139
+ lint_task = root / "p1-human-review"
140
+ shutil.copytree(task_dir, lint_task)
141
+ lint_result = invoke([str(INTELLIGENCE), "semantic-lint", "build", "--task-dir", str(lint_task)])
142
+ lint_data: dict[str, object] = {}
143
+ if (lint_task / "semantic-lint-report.json").is_file():
144
+ lint_data = json.loads((lint_task / "semantic-lint-report.json").read_text(encoding="utf-8"))
145
+ human_warning = any(
146
+ isinstance(item, dict) and item.get("basis") == "human" and not item.get("approval_blocking")
147
+ for item in lint_data.get("findings", []) if isinstance(lint_data.get("findings"), list)
148
+ )
149
+ record(results, "p1-human-review-warning-preserved", lint_result.returncode == 0 and human_warning, lint_result.stdout + lint_result.stderr)
150
+
151
+ generic_task = root / "p1-generic-intent"
152
+ shutil.copytree(task_dir, generic_task)
153
+ generic_ir_path = generic_task / "motion-ir.json"
154
+ generic_ir = json.loads(generic_ir_path.read_text(encoding="utf-8"))
155
+ generic_ir["intent"] = "motion"
156
+ generic_ir_path.write_text(json.dumps(generic_ir, indent=2) + "\n", encoding="utf-8")
157
+ generic_result = invoke([str(INTELLIGENCE), "semantic-lint", "build", "--task-dir", str(generic_task)])
158
+ generic_report = json.loads((generic_task / "semantic-lint-report.json").read_text(encoding="utf-8"))
159
+ generic_warning = any(item.get("id") == "intent-low-specificity" for item in generic_report.get("findings", []))
160
+ record(results, "p1-generic-intent-warning", generic_result.returncode == 0 and generic_warning and generic_report.get("status") == "warn", generic_result.stdout + generic_result.stderr)
161
+
162
+ continuity_a = root / "p1-continuity-a"
163
+ continuity_b = root / "p1-continuity-b"
164
+ shutil.copytree(task_dir, continuity_a)
165
+ shutil.copytree(task_dir, continuity_b)
166
+ task_b_path = continuity_b / "task.json"
167
+ task_b = json.loads(task_b_path.read_text(encoding="utf-8"))
168
+ task_b["task_id"] = "professional-review-followup"
169
+ task_b["scene"] = "browser-review-followup"
170
+ task_b["scene_order"] = 1
171
+ task_b_path.write_text(json.dumps(task_b, indent=2) + "\n", encoding="utf-8")
172
+ ir_b_path = continuity_b / "motion-ir.json"
173
+ ir_b = json.loads(ir_b_path.read_text(encoding="utf-8"))
174
+ ir_b["task_id"] = task_b["task_id"]
175
+ ir_b["scene"] = task_b["scene"]
176
+ ir_b["context_hash"] = "f" * 64
177
+ ir_b_path.write_text(json.dumps(ir_b, indent=2) + "\n", encoding="utf-8")
178
+ continuity_output = root / "p1-continuity-drift.json"
179
+ continuity_result = invoke([
180
+ str(INTELLIGENCE), "continuity", "build", "--task-dirs", str(continuity_a), str(continuity_b), "--output", str(continuity_output)
181
+ ])
182
+ continuity_report = json.loads(continuity_output.read_text(encoding="utf-8")) if continuity_output.is_file() else {}
183
+ transitions = continuity_report.get("transitions", []) if isinstance(continuity_report, dict) else []
184
+ drift_found = bool(transitions) and "context hash changes between adjacent scenes" in transitions[0].get("findings", [])
185
+ record(results, "p1-continuity-context-drift", continuity_result.returncode == 0 and continuity_report.get("status") == "warn" and drift_found, continuity_result.stdout + continuity_result.stderr)
186
+
187
+ fix_plan_path = task_dir / "fix-plan.json"
188
+ fix_plan_result = invoke([str(INTELLIGENCE), "fix-plan", "validate", "--path", str(fix_plan_path)])
189
+ fix_plan = json.loads(fix_plan_path.read_text(encoding="utf-8")) if fix_plan_path.is_file() else {}
190
+ selective = any(
191
+ isinstance(issue, dict) and "lint" in issue.get("rerun_scope", []) and issue.get("finding_ref")
192
+ for issue in fix_plan.get("issues", []) if isinstance(fix_plan.get("issues"), list)
193
+ )
194
+ handoff = json.loads((task_dir / "handoff.json").read_text(encoding="utf-8"))
195
+ synced = handoff.get("fix_plan", {}).get("path") == "fix-plan.json" and "semantic-lint-report.json" in handoff.get("required_artifacts", [])
196
+ record(results, "p1-fix-plan-selective-rerun", fix_plan_result.returncode == 0 and selective and synced, fix_plan_result.stdout + fix_plan_result.stderr)
197
+
198
+
199
+ def run_performance_perceptual_cases(root: Path, task_dir: Path, results: list[dict[str, object]]) -> None:
200
+ """Exercise non-blocking performance/perceptual findings and the benchmark contract."""
201
+ duration_task = root / "p1-perf-duration-budget"
202
+ shutil.copytree(task_dir, duration_task)
203
+ duration_ir_path = duration_task / "motion-ir.json"
204
+ duration_ir = json.loads(duration_ir_path.read_text(encoding="utf-8"))
205
+ duration_ir["duration_ms"] = 600
206
+ duration_ir_path.write_text(json.dumps(duration_ir, indent=2) + "\n", encoding="utf-8")
207
+ duration_result = invoke([str(INTELLIGENCE), "semantic-lint", "build", "--task-dir", str(duration_task)])
208
+ duration_report = json.loads((duration_task / "semantic-lint-report.json").read_text(encoding="utf-8")) if (duration_task / "semantic-lint-report.json").is_file() else {}
209
+ duration_warning = 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", []))
210
+ record(results, "p1-perf-duration-budget-warning", duration_result.returncode == 0 and duration_warning, duration_result.stdout + duration_result.stderr)
211
+
212
+ fps_task = root / "p1-perf-fps"
213
+ shutil.copytree(task_dir, fps_task)
214
+ fps_ir_path = fps_task / "motion-ir.json"
215
+ fps_ir = json.loads(fps_ir_path.read_text(encoding="utf-8"))
216
+ fps_ir["fps"] = 24
217
+ fps_ir_path.write_text(json.dumps(fps_ir, indent=2) + "\n", encoding="utf-8")
218
+ fps_result = invoke([str(INTELLIGENCE), "semantic-lint", "build", "--task-dir", str(fps_task)])
219
+ fps_report = json.loads((fps_task / "semantic-lint-report.json").read_text(encoding="utf-8")) if (fps_task / "semantic-lint-report.json").is_file() else {}
220
+ fps_warning = any(item.get("id") == "perf-frame-rate" and item.get("severity") == "warning" for item in fps_report.get("findings", []))
221
+ record(results, "p1-perf-fps-warning", fps_result.returncode == 0 and fps_warning, fps_result.stdout + fps_result.stderr)
222
+
223
+ easing_task = root / "p1-perceptual-easing"
224
+ shutil.copytree(task_dir, easing_task)
225
+ easing_ir_path = easing_task / "motion-ir.json"
226
+ easing_ir = json.loads(easing_ir_path.read_text(encoding="utf-8"))
227
+ for keyframe in easing_ir.get("tracks", [])[0].get("keyframes", []):
228
+ keyframe["easing"] = "linear"
229
+ easing_ir_path.write_text(json.dumps(easing_ir, indent=2) + "\n", encoding="utf-8")
230
+ easing_result = invoke([str(INTELLIGENCE), "semantic-lint", "build", "--task-dir", str(easing_task)])
231
+ easing_report = json.loads((easing_task / "semantic-lint-report.json").read_text(encoding="utf-8")) if (easing_task / "semantic-lint-report.json").is_file() else {}
232
+ easing_warning = any(item.get("id") == "perceptual-easing-linear" and item.get("severity") == "warning" for item in easing_report.get("findings", []))
233
+ record(results, "p1-perceptual-easing-linear-warning", easing_result.returncode == 0 and easing_warning, easing_result.stdout + easing_result.stderr)
234
+
235
+ reduced_task = root / "p1-perceptual-reduced-motion"
236
+ shutil.copytree(task_dir, reduced_task)
237
+ reduced_ir_path = reduced_task / "motion-ir.json"
238
+ reduced_ir = json.loads(reduced_ir_path.read_text(encoding="utf-8"))
239
+ reduced_ir.setdefault("accessibility", {})["reduced_motion"] = "none"
240
+ reduced_ir_path.write_text(json.dumps(reduced_ir, indent=2) + "\n", encoding="utf-8")
241
+ reduced_result = invoke([str(INTELLIGENCE), "semantic-lint", "build", "--task-dir", str(reduced_task)])
242
+ reduced_report = json.loads((reduced_task / "semantic-lint-report.json").read_text(encoding="utf-8")) if (reduced_task / "semantic-lint-report.json").is_file() else {}
243
+ reduced_warning = any(item.get("id") == "perceptual-reduced-motion-missing" and item.get("severity") == "warning" and not item.get("approval_blocking") for item in reduced_report.get("findings", []))
244
+ record(results, "p1-perceptual-reduced-motion-warning", reduced_result.returncode == 0 and reduced_warning, reduced_result.stdout + reduced_result.stderr)
245
+
246
+ benchmark_task = root / "p1-benchmark"
247
+ shutil.copytree(task_dir, benchmark_task)
248
+ benchmark_path = benchmark_task / "semantic-lint-benchmark.json"
249
+ benchmark_result = invoke([
250
+ str(INTELLIGENCE), "semantic-lint", "benchmark", "--task-dir", str(benchmark_task),
251
+ "--iterations", "10", "--threshold-ms", "500", "--output", str(benchmark_path),
252
+ ])
253
+ benchmark = json.loads(benchmark_path.read_text(encoding="utf-8")) if benchmark_path.is_file() else {}
254
+ benchmark_ok = benchmark.get("status") == "pass" and benchmark.get("p95_ms", 999999) < benchmark.get("threshold_ms", 0) and benchmark.get("rule_count", 0) >= 10
255
+ record(results, "p1-benchmark-execution-time", benchmark_result.returncode == 0 and benchmark_ok, benchmark_result.stdout + benchmark_result.stderr)
256
+
257
+
258
+ def run_runtime_verifier_cases(root: Path, results: list[dict[str, object]]) -> None:
259
+ """Exercise external verification without granting approval or trusting paths."""
260
+ scene = root / "telemetry-scene/browser-review-smoke"
261
+ task = root / "telemetry-task"
262
+ shutil.copytree(ROOT / "src/output/browser-review-smoke", scene)
263
+ shutil.copytree(ROOT / "artifacts/browser-review-smoke-task", task)
264
+ base = [str(VERIFIER), "--scene-dir", str(scene), "--task-dir", str(task)]
265
+
266
+ clean = invoke(base)
267
+ clean_doc = json.loads(clean.stdout) if clean.stdout.strip().startswith("{") else {}
268
+ record(results, "p2-runtime-verifier-clean", clean.returncode == 0 and clean_doc.get("verified") is True and clean_doc.get("approval") is False, clean.stdout + clean.stderr)
269
+
270
+ tampered_task = root / "telemetry-tampered"
271
+ shutil.copytree(task, tampered_task)
272
+ tampered_path = tampered_task / "runtime-adapters/rive/runtime-telemetry.json"
273
+ tampered = json.loads(tampered_path.read_text(encoding="utf-8"))
274
+ tampered["samples"][0]["state"]["eval_tamper"] = True
275
+ tampered_path.write_text(json.dumps(tampered, indent=2) + "\n", encoding="utf-8")
276
+ tampered_result = invoke([str(VERIFIER), "--scene-dir", str(scene), "--task-dir", str(tampered_task)])
277
+ record(results, "p2-runtime-verifier-tamper", tampered_result.returncode != 0 and "sha256 mismatch" in tampered_result.stdout, tampered_result.stdout + tampered_result.stderr)
278
+
279
+ foreign_task = root / "telemetry-foreign-task"
280
+ shutil.copytree(task, foreign_task)
281
+ foreign_doc = json.loads((foreign_task / "task.json").read_text(encoding="utf-8"))
282
+ foreign_doc["task_id"] = "telemetry-foreign-task"
283
+ (foreign_task / "task.json").write_text(json.dumps(foreign_doc, indent=2) + "\n", encoding="utf-8")
284
+ foreign_result = invoke([str(VERIFIER), "--scene-dir", str(scene), "--task-dir", str(foreign_task)])
285
+ record(results, "p2-runtime-verifier-cross-task", foreign_result.returncode != 0 and "task_id" in foreign_result.stdout, foreign_result.stdout + foreign_result.stderr)
286
+
287
+ symlink_task = root / "telemetry-symlink-task"
288
+ shutil.copytree(task, symlink_task)
289
+ outside = root / "telemetry-outside"
290
+ outside.mkdir()
291
+ (outside / "runtime-telemetry.json").write_text((task / "runtime-adapters/rive/runtime-telemetry.json").read_text(encoding="utf-8"), encoding="utf-8")
292
+ (symlink_task / "runtime-adapters/linked").symlink_to(outside, target_is_directory=True)
293
+ symlink_evidence_path = symlink_task / "runtime-adapters/runtime-evidence.json"
294
+ symlink_evidence = json.loads(symlink_evidence_path.read_text(encoding="utf-8"))
295
+ symlink_evidence["frameworks"][0]["telemetry"]["file"] = "linked/runtime-telemetry.json"
296
+ symlink_evidence_path.write_text(json.dumps(symlink_evidence, indent=2) + "\n", encoding="utf-8")
297
+ symlink_result = invoke([str(VERIFIER), "--scene-dir", str(scene), "--task-dir", str(symlink_task)])
298
+ record(results, "p2-runtime-verifier-symlink", symlink_result.returncode != 0 and "symlink" in symlink_result.stdout, symlink_result.stdout + symlink_result.stderr)
299
+
300
+
301
+ def main() -> int:
302
+ corpus = json.loads(CASES.read_text(encoding="utf-8"))
303
+ expected = {case["id"] for case in corpus.get("cases", [])}
304
+ results: list[dict[str, object]] = []
305
+ with tempfile.TemporaryDirectory(prefix="motionloom-eval-") as td:
306
+ root = Path(td)
307
+ task_dir = root / "artifacts/professional-review-e2e"
308
+ shutil.copytree(ROOT / "artifacts/professional-review-e2e", task_dir)
309
+
310
+ registry = root / "capability-registry.json"
311
+ build = invoke([str(INTELLIGENCE), "capabilities", "build", "--output", str(registry)])
312
+ if build.returncode != 0:
313
+ record(results, "verified-runtime-selection", False, build.stdout + build.stderr)
314
+ else:
315
+ selected = invoke([str(INTELLIGENCE), "capabilities", "select", "--registry", str(registry), "--capability", "runtime.rive"])
316
+ record(results, "verified-runtime-selection", selected.returncode == 0, selected.stdout + selected.stderr)
317
+
318
+ scaffold = invoke([str(INTELLIGENCE), "capabilities", "select", "--registry", str(registry), "--capability", "runtime.spine"])
319
+ record(results, "scaffold-runtime-blocked", scaffold.returncode != 0, scaffold.stdout + scaffold.stderr)
320
+
321
+ stale = json.loads(registry.read_text(encoding="utf-8"))
322
+ for entry in stale["capabilities"]:
323
+ if entry.get("id") == "runtime.rive":
324
+ entry["last_verified_at"] = "2000-01-01T00:00:00Z"
325
+ stale_path = root / "stale-capability-registry.json"
326
+ stale_path.write_text(json.dumps(stale, indent=2) + "\n", encoding="utf-8")
327
+ stale_result = invoke([str(INTELLIGENCE), "capabilities", "select", "--registry", str(stale_path), "--capability", "runtime.rive"])
328
+ record(results, "stale-capability-evidence", stale_result.returncode != 0, stale_result.stdout + stale_result.stderr)
329
+
330
+ tampered = json.loads(registry.read_text(encoding="utf-8"))
331
+ for entry in tampered["capabilities"]:
332
+ if entry.get("id") == "runtime.rive":
333
+ entry["evidence"][0]["sha256"] = "0" * 64
334
+ tampered_path = root / "tampered-capability-registry.json"
335
+ tampered_path.write_text(json.dumps(tampered, indent=2) + "\n", encoding="utf-8")
336
+ tampered_result = invoke([str(INTELLIGENCE), "capabilities", "select", "--registry", str(tampered_path), "--capability", "runtime.rive"])
337
+ record(results, "tampered-capability-evidence", tampered_result.returncode != 0, tampered_result.stdout + tampered_result.stderr)
338
+
339
+ graph_build = invoke([str(INTELLIGENCE), "graph", "build", "--task-dir", str(task_dir)])
340
+ graph = json.loads((task_dir / "project-graph.json").read_text(encoding="utf-8"))
341
+ graph["edges"].append({"from": graph["roots"][0], "to": "artifact:missing", "relation": "uses"})
342
+ graph_path = root / "corrupt-project-graph.json"
343
+ graph_path.write_text(json.dumps(graph, indent=2) + "\n", encoding="utf-8")
344
+ graph_result = invoke([str(INTELLIGENCE), "graph", "validate", "--path", str(graph_path)])
345
+ record(results, "graph-edge-corruption", graph_build.returncode == 0 and graph_result.returncode != 0, graph_result.stdout + graph_result.stderr)
346
+
347
+ replay_build = invoke([str(INTELLIGENCE), "replay", "capture", "--root", str(root), "--task-dir", str(task_dir)])
348
+ replay_verify = invoke([str(INTELLIGENCE), "replay", "verify", "--root", str(root), "--bundle", str(task_dir / "replay-bundle.json")])
349
+ review_path = task_dir / "review.json"
350
+ review_path.write_text(review_path.read_text(encoding="utf-8") + "\n", encoding="utf-8")
351
+ replay_tamper = invoke([str(INTELLIGENCE), "replay", "verify", "--root", str(root), "--bundle", str(task_dir / "replay-bundle.json")])
352
+ record(results, "replay-artifact-tamper", replay_build.returncode == 0 and replay_verify.returncode == 0 and replay_tamper.returncode != 0, replay_tamper.stdout + replay_tamper.stderr)
353
+
354
+ foreign_task = root / "foreign-task"
355
+ shutil.copytree(ROOT / "artifacts/professional-review-e2e", foreign_task)
356
+ candidate = json.loads((foreign_task / "browser-review.json").read_text(encoding="utf-8"))
357
+ candidate["status"] = "prepared"
358
+ candidate["task_id"] = "foreign-task"
359
+ (foreign_task / "browser-review.json").write_text(json.dumps(candidate, indent=2) + "\n", encoding="utf-8")
360
+ foreign = invoke([str(REPORT), "review", "--task-dir", str(foreign_task), "--candidate-id", str(candidate.get("candidate_id")), "--decision", "approved", "--reviewer", "eval"])
361
+ record(results, "foreign-task-candidate", foreign.returncode != 0, foreign.stdout + foreign.stderr)
362
+
363
+ run_p1_cases(root, task_dir, results)
364
+ run_performance_perceptual_cases(root, task_dir, results)
365
+ run_runtime_verifier_cases(root, results)
366
+ run_attestation_cases(root, results)
367
+
368
+ missing = expected - {str(item["id"]) for item in results}
369
+ for case_id in sorted(missing):
370
+ record(results, case_id, False, "case was declared but not executed")
371
+ failed = [item for item in results if item["status"] != "pass"]
372
+ print(json.dumps({"status": "fail" if failed else "pass", "suite": corpus.get("suite"), "case_count": len(results), "results": results}, indent=2))
373
+ return 1 if failed else 0
374
+
375
+
376
+ if __name__ == "__main__":
377
+ raise SystemExit(main())