motionloom 2.1.0 → 2.3.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 (46) hide show
  1. package/.agents/skills/motionloom/SKILL.md +14 -0
  2. package/.claude/skills/motionloom.md +5 -0
  3. package/.codex/skills/motionloom.md +11 -0
  4. package/AGENTS.md +17 -0
  5. package/CHANGELOG.md +51 -0
  6. package/README.md +60 -15
  7. package/ROADMAP.md +11 -5
  8. package/SECURITY.md +3 -2
  9. package/SKILL.md +40 -5
  10. package/agent-card.json +42 -4
  11. package/agent-surfaces.json +86 -0
  12. package/bin/motionloom.mjs +26 -3
  13. package/docs/AGENT-INTEGRATION.md +60 -0
  14. package/docs/CHECKLIST.md +7 -1
  15. package/docs/STATUS.md +2 -2
  16. package/docs/audits/ci-replay-remediation-2026-08-13.md +33 -0
  17. package/docs/releases/2.2.0.md +35 -0
  18. package/docs/releases/2.3.0.md +33 -0
  19. package/docs/releases/npm-publish-from-workstation.md +6 -6
  20. package/examples/agent-consumer/README.md +18 -0
  21. package/examples/agent-consumer/ai-generated-pilot/hero-male.json +10 -0
  22. package/examples/agent-consumer/ai-generated-pilot-provenance.json +55 -0
  23. package/examples/agent-consumer/fixture-manifest.json +82 -0
  24. package/package.json +31 -7
  25. package/references/agent-interoperability.md +40 -0
  26. package/references/intelligence-core.md +12 -2
  27. package/schemas/agent-surfaces.schema.json +78 -0
  28. package/schemas/asset-provenance.schema.json +183 -0
  29. package/schemas/remediation-history.schema.json +23 -0
  30. package/schemas/scene-manifest.schema.json +2 -0
  31. package/schemas/visual-truth.schema.json +80 -0
  32. package/scripts/asset-provenance.py +390 -0
  33. package/scripts/devlab.py +1 -1
  34. package/scripts/discovery.py +257 -0
  35. package/scripts/docs-audit.py +30 -2
  36. package/scripts/pr.py +3 -0
  37. package/scripts/quality-gate.py +81 -3
  38. package/scripts/remediation-learning.py +326 -0
  39. package/scripts/report.py +66 -0
  40. package/scripts/setup.mjs +472 -0
  41. package/scripts/skill-doctor.py +2 -1
  42. package/scripts/visual-truth.py +310 -0
  43. package/src/output/browser-review-smoke/asset-provenance.json +77 -0
  44. package/src/output/browser-review-smoke/manifest.json +2 -0
  45. package/src/output/browser-review-smoke/visual-truth.json +68 -0
  46. package/tests/scripts/run_tests.py +83 -0
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python3
2
+ """Build and validate MotionLoom's provenance-bound visual truth contract.
3
+
4
+ The contract is deliberately a review aid, not an approval engine. It compares
5
+ real rendered PNG frames, records image dimensions and SHA-256 digests, emits a
6
+ small deterministic perceptual summary, and keeps user approval false until a
7
+ separate browser review artifact records a human decision.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import hashlib
14
+ import json
15
+ import struct
16
+ import sys
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+
20
+
21
+ SCHEMA_VERSION = "1.0"
22
+ PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
23
+ SHA256_RE = 64
24
+
25
+
26
+ def sha256(path: Path) -> str:
27
+ digest = hashlib.sha256()
28
+ with path.open("rb") as handle:
29
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
30
+ digest.update(chunk)
31
+ return digest.hexdigest()
32
+
33
+
34
+ def json_sha256(path: Path) -> str:
35
+ return sha256(path)
36
+
37
+
38
+ def load_json(path: Path) -> dict:
39
+ try:
40
+ value = json.loads(path.read_text(encoding="utf-8"))
41
+ except (OSError, json.JSONDecodeError) as exc:
42
+ raise ValueError(f"invalid JSON {path}: {exc}") from exc
43
+ if not isinstance(value, dict):
44
+ raise ValueError(f"expected JSON object: {path}")
45
+ return value
46
+
47
+
48
+ def png_dimensions(path: Path) -> tuple[int, int]:
49
+ """Read PNG dimensions without requiring Pillow or a system image tool."""
50
+ with path.open("rb") as handle:
51
+ if handle.read(8) != PNG_SIGNATURE:
52
+ raise ValueError(f"not a PNG file: {path}")
53
+ length = struct.unpack(">I", handle.read(4))[0]
54
+ chunk = handle.read(4)
55
+ if chunk != b"IHDR" or length < 8:
56
+ raise ValueError(f"PNG has no IHDR: {path}")
57
+ width, height = struct.unpack(">II", handle.read(8))
58
+ if width <= 0 or height <= 0:
59
+ raise ValueError(f"PNG dimensions are invalid: {path}")
60
+ return width, height
61
+
62
+
63
+ def relpath(path: Path, root: Path) -> str:
64
+ resolved = path.resolve()
65
+ try:
66
+ return resolved.relative_to(root.resolve()).as_posix()
67
+ except ValueError:
68
+ raise ValueError(f"path must be inside repository root: {path}")
69
+
70
+
71
+ def display_path(path: Path, root: Path) -> str:
72
+ """Render an output path without weakening repository-bound evidence paths."""
73
+ try:
74
+ return relpath(path, root)
75
+ except ValueError:
76
+ return path.resolve().as_posix()
77
+
78
+
79
+ def frame_record(path: Path, root: Path, role: str, percent: int) -> dict:
80
+ if not path.is_file() or path.stat().st_size == 0:
81
+ raise ValueError(f"{role} frame is missing or empty: {path}")
82
+ width, height = png_dimensions(path)
83
+ return {
84
+ "role": role,
85
+ "percent": percent,
86
+ "path": relpath(path, root),
87
+ "sha256": sha256(path),
88
+ "bytes": path.stat().st_size,
89
+ "width": width,
90
+ "height": height,
91
+ }
92
+
93
+
94
+ def perceptual_summary(baseline: dict, candidate: dict) -> dict:
95
+ """Return a conservative perceptual signal based on byte identity.
96
+
97
+ A byte-identical PNG is a strong deterministic equality signal. A changed
98
+ PNG is intentionally reported as `review_required`, not as a failed quality
99
+ judgment, because visual acceptability is a user/runtime review decision.
100
+ """
101
+ same_dimensions = (baseline["width"], baseline["height"]) == (candidate["width"], candidate["height"])
102
+ identical = baseline["sha256"] == candidate["sha256"]
103
+ return {
104
+ "metric": "sha256-image-identity",
105
+ "method": "PNG metadata plus byte identity; no visual approval inferred",
106
+ "same_dimensions": same_dimensions,
107
+ "byte_identical": identical,
108
+ "changed": not identical or not same_dimensions,
109
+ "distance": 0.0 if identical and same_dimensions else 1.0,
110
+ "threshold": 0.0,
111
+ "interpretation": "equal" if identical and same_dimensions else "review_required",
112
+ }
113
+
114
+
115
+ def build(args: argparse.Namespace) -> int:
116
+ root = Path(args.root).resolve()
117
+ output = Path(args.output)
118
+ if not output.is_absolute():
119
+ output = (root / output).resolve()
120
+ scene = str(args.scene)
121
+ if not scene or scene in {".", ".."} or "/" in scene or "\\" in scene:
122
+ print("visual-truth: unsafe scene identifier", file=sys.stderr)
123
+ return 2
124
+ try:
125
+ baseline = Path(args.baseline)
126
+ candidate = Path(args.candidate)
127
+ if not baseline.is_absolute():
128
+ baseline = (root / baseline).resolve()
129
+ if not candidate.is_absolute():
130
+ candidate = (root / candidate).resolve()
131
+ source = Path(args.source)
132
+ manifest = Path(args.manifest)
133
+ runtime_evidence = Path(args.runtime_evidence) if args.runtime_evidence else None
134
+ motion_ir = Path(args.motion_ir) if args.motion_ir else None
135
+ for value in (source, manifest, runtime_evidence, motion_ir):
136
+ if value is not None and not value.is_absolute():
137
+ value = (root / value).resolve()
138
+ # Rebind optional paths after the loop because Path is immutable.
139
+ if args.runtime_evidence:
140
+ runtime_evidence = (root / args.runtime_evidence).resolve() if not Path(args.runtime_evidence).is_absolute() else Path(args.runtime_evidence).resolve()
141
+ if args.motion_ir:
142
+ motion_ir = (root / args.motion_ir).resolve() if not Path(args.motion_ir).is_absolute() else Path(args.motion_ir).resolve()
143
+ source = (root / args.source).resolve() if not Path(args.source).is_absolute() else Path(args.source).resolve()
144
+ manifest = (root / args.manifest).resolve() if not Path(args.manifest).is_absolute() else Path(args.manifest).resolve()
145
+ baseline_record = frame_record(baseline, root, "baseline", args.percent)
146
+ candidate_record = frame_record(candidate, root, "candidate", args.percent)
147
+ manifest_data = load_json(manifest)
148
+ if not source.is_file() or not manifest.is_file():
149
+ raise ValueError("source and manifest must exist")
150
+ runtime_data = load_json(runtime_evidence) if runtime_evidence else None
151
+ motion_data = load_json(motion_ir) if motion_ir else None
152
+ source_hash = sha256(source)
153
+ manifest_hash = sha256(manifest)
154
+ motion_hash = sha256(motion_ir) if motion_ir else None
155
+ runtime_hash = sha256(runtime_evidence) if runtime_evidence else None
156
+ if manifest_data.get("file") and not str(manifest_data["file"]).strip():
157
+ raise ValueError("manifest.file must be non-empty")
158
+ if runtime_data is not None:
159
+ if runtime_data.get("status") != "pass":
160
+ raise ValueError("runtime evidence status must be pass")
161
+ if args.task_id and runtime_data.get("task_id") not in {None, args.task_id}:
162
+ raise ValueError("runtime evidence task_id does not match requested task")
163
+ if runtime_data.get("scene") not in {None, scene}:
164
+ raise ValueError("runtime evidence scene does not match requested scene")
165
+ comparison = perceptual_summary(baseline_record, candidate_record)
166
+ changed_regions = []
167
+ if comparison["changed"]:
168
+ changed_regions.append({
169
+ "id": "full-frame",
170
+ "label": "Full rendered frame",
171
+ "reason": "Baseline and candidate PNG identities differ; inspect the exact candidate in Dev Lab.",
172
+ "severity": "review",
173
+ "evidence": [baseline_record["path"], candidate_record["path"]],
174
+ })
175
+ report = {
176
+ "schema_version": SCHEMA_VERSION,
177
+ "contract": "motionloom-visual-truth",
178
+ "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
179
+ "status": "review_required" if comparison["changed"] else "pass",
180
+ "scene": scene,
181
+ "task_id": args.task_id or None,
182
+ "frames": {
183
+ "baseline": baseline_record,
184
+ "candidate": candidate_record,
185
+ },
186
+ "comparison": comparison,
187
+ "regions": changed_regions,
188
+ "provenance": {
189
+ "source_path": relpath(source, root),
190
+ "source_sha256": source_hash,
191
+ "manifest_path": relpath(manifest, root),
192
+ "manifest_sha256": manifest_hash,
193
+ "runtime_evidence_path": relpath(runtime_evidence, root) if runtime_evidence else None,
194
+ "runtime_evidence_sha256": runtime_hash,
195
+ "motion_ir_path": relpath(motion_ir, root) if motion_ir else None,
196
+ "motion_ir_sha256": motion_hash,
197
+ "runtime_status": runtime_data.get("status") if runtime_data else None,
198
+ "motion_ir_schema_version": motion_data.get("schema_version") if motion_data else None,
199
+ },
200
+ "review_boundary": {
201
+ "approval": False,
202
+ "user_review_required": True,
203
+ "decision": "pending",
204
+ "pr_side_effects": "explicit-confirmation",
205
+ },
206
+ }
207
+ output.parent.mkdir(parents=True, exist_ok=True)
208
+ output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
209
+ print(json.dumps(report, ensure_ascii=False, indent=2))
210
+ return 0
211
+ except (OSError, ValueError) as exc:
212
+ print(f"visual-truth: {exc}", file=sys.stderr)
213
+ return 1
214
+
215
+
216
+ def validate_report(path: Path, root: Path | None = None, expected_scene: str | None = None, expected_task_id: str | None = None, expected_source_sha256: str | None = None, expected_manifest_sha256: str | None = None, expected_motion_ir_sha256: str | None = None) -> list[str]:
217
+ root = (root or path.parent).resolve()
218
+ errors: list[str] = []
219
+ try:
220
+ data = load_json(path)
221
+ except ValueError as exc:
222
+ return [str(exc)]
223
+ if data.get("schema_version") != SCHEMA_VERSION:
224
+ errors.append("visual truth schema_version must be 1.0")
225
+ if data.get("contract") != "motionloom-visual-truth":
226
+ errors.append("visual truth contract identifier is invalid")
227
+ if data.get("status") not in {"pass", "review_required"}:
228
+ errors.append("visual truth status must be pass or review_required")
229
+ if expected_scene and data.get("scene") != expected_scene:
230
+ errors.append("visual truth scene does not match requested scene")
231
+ if expected_task_id and data.get("task_id") != expected_task_id:
232
+ errors.append("visual truth task_id does not match task bundle")
233
+ boundary = data.get("review_boundary")
234
+ if not isinstance(boundary, dict) or boundary.get("approval") is not False or boundary.get("user_review_required") is not True:
235
+ errors.append("visual truth must preserve approval=false and user_review_required=true")
236
+ frames = data.get("frames")
237
+ if not isinstance(frames, dict) or not isinstance(frames.get("baseline"), dict) or not isinstance(frames.get("candidate"), dict):
238
+ errors.append("visual truth must contain baseline and candidate frame records")
239
+ else:
240
+ for role in ("baseline", "candidate"):
241
+ frame = frames[role]
242
+ if frame.get("role") != role or not isinstance(frame.get("sha256"), str) or len(frame.get("sha256", "")) != SHA256_RE:
243
+ errors.append(f"visual truth {role} frame hash is invalid")
244
+ frame_path = root / str(frame.get("path", ""))
245
+ try:
246
+ if not frame_path.is_file() or sha256(frame_path) != frame.get("sha256"):
247
+ errors.append(f"visual truth {role} frame hash/path binding is stale")
248
+ elif png_dimensions(frame_path) != (frame.get("width"), frame.get("height")):
249
+ errors.append(f"visual truth {role} frame dimensions are stale")
250
+ except (OSError, ValueError):
251
+ errors.append(f"visual truth {role} frame is not a readable PNG")
252
+ provenance = data.get("provenance")
253
+ if not isinstance(provenance, dict):
254
+ errors.append("visual truth provenance is required")
255
+ else:
256
+ for label, expected in (("source_sha256", expected_source_sha256), ("manifest_sha256", expected_manifest_sha256), ("motion_ir_sha256", expected_motion_ir_sha256)):
257
+ if expected and provenance.get(label) != expected:
258
+ errors.append(f"visual truth {label} does not match current artifact")
259
+ for label in ("source_path", "manifest_path"):
260
+ path_value = provenance.get(label)
261
+ if not isinstance(path_value, str) or Path(path_value).is_absolute() or ".." in Path(path_value).parts:
262
+ errors.append(f"visual truth {label} must be a safe repository-relative path")
263
+ comparison = data.get("comparison")
264
+ if not isinstance(comparison, dict) or comparison.get("interpretation") not in {"equal", "review_required"}:
265
+ errors.append("visual truth comparison interpretation is invalid")
266
+ return errors
267
+
268
+
269
+ def validate(args: argparse.Namespace) -> int:
270
+ root = Path(args.root).resolve()
271
+ path = Path(args.input)
272
+ if not path.is_absolute():
273
+ path = (root / path).resolve()
274
+ errors = validate_report(path, root, args.scene, args.task_id, args.source_sha256, args.manifest_sha256, args.motion_ir_sha256)
275
+ result = {"status": "pass" if not errors else "fail", "path": display_path(path, root), "errors": errors}
276
+ print(json.dumps(result, ensure_ascii=False, indent=2))
277
+ return 0 if not errors else 1
278
+
279
+
280
+ def main() -> int:
281
+ parser = argparse.ArgumentParser(description=__doc__)
282
+ sub = parser.add_subparsers(dest="command", required=True)
283
+ build_parser = sub.add_parser("build")
284
+ build_parser.add_argument("--root", default=".")
285
+ build_parser.add_argument("--scene", required=True)
286
+ build_parser.add_argument("--baseline", required=True)
287
+ build_parser.add_argument("--candidate", required=True)
288
+ build_parser.add_argument("--percent", type=int, default=100)
289
+ build_parser.add_argument("--source", required=True)
290
+ build_parser.add_argument("--manifest", required=True)
291
+ build_parser.add_argument("--runtime-evidence")
292
+ build_parser.add_argument("--motion-ir")
293
+ build_parser.add_argument("--task-id")
294
+ build_parser.add_argument("--output", required=True)
295
+ build_parser.set_defaults(func=build)
296
+ validate_parser = sub.add_parser("validate")
297
+ validate_parser.add_argument("--root", default=".")
298
+ validate_parser.add_argument("--input", required=True)
299
+ validate_parser.add_argument("--scene")
300
+ validate_parser.add_argument("--task-id")
301
+ validate_parser.add_argument("--source-sha256")
302
+ validate_parser.add_argument("--manifest-sha256")
303
+ validate_parser.add_argument("--motion-ir-sha256")
304
+ validate_parser.set_defaults(func=validate)
305
+ args = parser.parse_args()
306
+ return args.func(args)
307
+
308
+
309
+ if __name__ == "__main__":
310
+ raise SystemExit(main())
@@ -0,0 +1,77 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "provenance_id": "browser-review-smoke-asset-provenance",
4
+ "task_id": "browser-review-smoke-task",
5
+ "scene": "browser-review-smoke",
6
+ "created_at": "2026-08-14T00:00:00Z",
7
+ "asset": {
8
+ "id": "browser-review-smoke-animation",
9
+ "path": "animation.json",
10
+ "type": "deterministic-fixture",
11
+ "framework": "lottie",
12
+ "version": "fixture-2.2.0"
13
+ },
14
+ "authority": "ai_assisted_human_reviewed",
15
+ "readiness": "production_eligible",
16
+ "generator": {
17
+ "model": "motionloom-deterministic-fixture-builder",
18
+ "task_id": "browser-review-smoke-task",
19
+ "source": "repository-fixture",
20
+ "generated_at": "2026-08-14T00:00:00Z",
21
+ "agent": "motionloom-ci"
22
+ },
23
+ "human_review": {
24
+ "reviewer": "MotionLoom fixture maintainer",
25
+ "decision": "approved",
26
+ "scope": "deterministic runtime and contract fixture, not production art approval",
27
+ "reviewed_at": "2026-08-14T00:00:00Z",
28
+ "user_confirmed": true,
29
+ "notes": "The fixture is eligible for repository CI and PR evidence; final product approval remains outside this artifact."
30
+ },
31
+ "license": {
32
+ "spdx": "MIT",
33
+ "source": "MotionLoom repository fixture",
34
+ "attribution": "MotionLoom deterministic browser-review smoke fixture."
35
+ },
36
+ "files": [
37
+ {
38
+ "path": "animation.json",
39
+ "role": "runtime-scene",
40
+ "sha256": "c5ab312427678b17ae903d280d89db9d202670f031f10ad6fe774fc9aaecee8a"
41
+ }
42
+ ],
43
+ "provenance_chain": [
44
+ {
45
+ "step": "generate",
46
+ "actor": "agent:motionloom-ci",
47
+ "source": "repository-fixture",
48
+ "timestamp": "2026-08-14T00:00:00Z"
49
+ },
50
+ {
51
+ "step": "runtime-test",
52
+ "actor": "runtime:lottie",
53
+ "source": "browser-review-smoke runtime evidence",
54
+ "timestamp": "2026-08-14T00:00:00Z"
55
+ },
56
+ {
57
+ "step": "human-review",
58
+ "actor": "user:fixture-maintainer",
59
+ "source": "deterministic fixture review boundary",
60
+ "timestamp": "2026-08-14T00:00:00Z"
61
+ }
62
+ ],
63
+ "runtime_evidence": {
64
+ "status": "pass",
65
+ "runtime": "lottie-runtime",
66
+ "tested_at": "2026-08-14T00:00:00Z",
67
+ "evidence_path": "snapshot/.render-meta.json"
68
+ },
69
+ "full_gate": {
70
+ "status": "pass",
71
+ "quality_gate": "pass",
72
+ "visual_truth": "pass",
73
+ "license": "pass",
74
+ "checked_at": "2026-08-14T00:00:00Z",
75
+ "report_path": "artifacts/browser-review-smoke-task/quality-report.json"
76
+ }
77
+ }
@@ -4,6 +4,8 @@
4
4
  "framework": "lottie",
5
5
  "category": "loading",
6
6
  "file": "animation.json",
7
+ "visual_truth": "visual-truth.json",
8
+ "asset_provenance": "asset-provenance.json",
7
9
  "source_binding": {
8
10
  "kind": "fixture",
9
11
  "source_path": "animation.json",
@@ -0,0 +1,68 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "contract": "motionloom-visual-truth",
4
+ "generated_at": "2026-08-13T17:17:31.327320Z",
5
+ "status": "review_required",
6
+ "scene": "browser-review-smoke",
7
+ "task_id": "browser-review-smoke-task",
8
+ "frames": {
9
+ "baseline": {
10
+ "role": "baseline",
11
+ "percent": 100,
12
+ "path": "src/output/browser-review-smoke/snapshot/frame-00.png",
13
+ "sha256": "8a3bf18101d395ee00a6c6899c31910fbd2c405aee7e83143ce08832e740c5da",
14
+ "bytes": 3588,
15
+ "width": 512,
16
+ "height": 512
17
+ },
18
+ "candidate": {
19
+ "role": "candidate",
20
+ "percent": 100,
21
+ "path": "src/output/browser-review-smoke/snapshot/frame-100.png",
22
+ "sha256": "b3dac3308b0b7295ee3d5d4d87225017692b4d1f6d46828b482948cdd8979e2e",
23
+ "bytes": 4228,
24
+ "width": 512,
25
+ "height": 512
26
+ }
27
+ },
28
+ "comparison": {
29
+ "metric": "sha256-image-identity",
30
+ "method": "PNG metadata plus byte identity; no visual approval inferred",
31
+ "same_dimensions": true,
32
+ "byte_identical": false,
33
+ "changed": true,
34
+ "distance": 1.0,
35
+ "threshold": 0.0,
36
+ "interpretation": "review_required"
37
+ },
38
+ "regions": [
39
+ {
40
+ "id": "full-frame",
41
+ "label": "Full rendered frame",
42
+ "reason": "Baseline and candidate PNG identities differ; inspect the exact candidate in Dev Lab.",
43
+ "severity": "review",
44
+ "evidence": [
45
+ "src/output/browser-review-smoke/snapshot/frame-00.png",
46
+ "src/output/browser-review-smoke/snapshot/frame-100.png"
47
+ ]
48
+ }
49
+ ],
50
+ "provenance": {
51
+ "source_path": "src/output/browser-review-smoke/animation.json",
52
+ "source_sha256": "c5ab312427678b17ae903d280d89db9d202670f031f10ad6fe774fc9aaecee8a",
53
+ "manifest_path": "src/output/browser-review-smoke/manifest.json",
54
+ "manifest_sha256": "8d1e07c81dbbcbcc060415ac71dfcae7ce88f2e7aa91347b226dbf4f554e5623",
55
+ "runtime_evidence_path": "artifacts/browser-review-smoke-task/runtime-adapters/runtime-evidence.json",
56
+ "runtime_evidence_sha256": "88194b8ed8e8eb9780a80bc55114b9cf52886c911b70a7f4331c18fa39a9b090",
57
+ "motion_ir_path": "artifacts/browser-review-smoke-task/motion-ir.json",
58
+ "motion_ir_sha256": "bdb2305737e5dee9b9ee078dd5657a4a5717bf4184f112a1d01ac82361c5a23f",
59
+ "runtime_status": "pass",
60
+ "motion_ir_schema_version": "0.1"
61
+ },
62
+ "review_boundary": {
63
+ "approval": false,
64
+ "user_review_required": true,
65
+ "decision": "pending",
66
+ "pr_side_effects": "explicit-confirmation"
67
+ }
68
+ }
@@ -256,6 +256,10 @@ def test_runtime_evidence_binding():
256
256
  root = Path(td)
257
257
  scene = root / "src/output/browser-review-smoke"
258
258
  shutil.copytree(ROOT / "src/output/browser-review-smoke", scene)
259
+ candidate_path = scene / "browser-review.json"
260
+ candidate = json.loads(candidate_path.read_text())
261
+ candidate["expires_at"] = "2099-01-01T00:00:00Z"
262
+ candidate_path.write_text(json.dumps(candidate, indent=2) + "\n")
259
263
  context = root / "project-context.json"
260
264
  shutil.copy(ROOT / "artifacts/browser-review-smoke-task/project-context.json", context)
261
265
 
@@ -840,6 +844,18 @@ def test_observability_contract():
840
844
  ], capture_output=True, text=True)
841
845
  check("review rejects foreign task candidate", foreign_review.returncode != 0 and "task_id" in foreign_review.stderr)
842
846
 
847
+
848
+ def test_quality_workflow_rebuilds_replay_after_generated_artifacts():
849
+ workflow = (ROOT / ".github/workflows/quality.yml").read_text(encoding="utf-8")
850
+ capture = workflow.find("python3 scripts/intelligence.py replay capture")
851
+ render = workflow.find("- name: Render runtime snapshot frames for changed scenes")
852
+ gate = workflow.find("- name: Enforce context-bound quality gate")
853
+ check("quality workflow rebuilds replay bundle", capture >= 0)
854
+ check("replay rebuild follows runtime rendering", render >= 0 and render < capture)
855
+ check("replay rebuild precedes quality gate", gate >= 0 and capture < gate)
856
+ check("replay rebuild binds task directory and root", '--task-dir "$task_dir" --root .' in workflow)
857
+ check("replay rebuild writes canonical task bundle path", '--output "$task_dir/replay-bundle.json"' in workflow)
858
+
843
859
  doctor = subprocess.run([sys.executable, str(ROOT / "scripts/skill-doctor.py"), "--json"], capture_output=True, text=True)
844
860
  doctor_data = json.loads(doctor.stdout)
845
861
  check("skill doctor passes package structure", doctor.returncode == 0 and doctor_data.get("status") == "pass")
@@ -863,6 +879,72 @@ def test_observability_contract():
863
879
  "project memory recovery and cross-platform contract passes",
864
880
  memory_tests.returncode == 0 and "project memory contract tests: PASS" in memory_tests.stdout,
865
881
  )
882
+ discovery_tests = subprocess.run(
883
+ [sys.executable, str(ROOT / "tests/scripts/test_discovery.py")],
884
+ capture_output=True,
885
+ text=True,
886
+ )
887
+ check(
888
+ "Agent discovery and installation contract passes",
889
+ discovery_tests.returncode == 0 and "discovery contract tests: PASS" in discovery_tests.stdout,
890
+ )
891
+ consumer_tests = subprocess.run(
892
+ [sys.executable, str(ROOT / "tests/scripts/test_consumer_fixtures.py")],
893
+ capture_output=True,
894
+ text=True,
895
+ )
896
+ check(
897
+ "Agent consumer fixture contract passes",
898
+ consumer_tests.returncode == 0 and "consumer fixture tests: PASS" in consumer_tests.stdout,
899
+ )
900
+ matrix_tests = subprocess.run(
901
+ [sys.executable, str(ROOT / "tests/scripts/test_installation_matrix.py")],
902
+ capture_output=True,
903
+ text=True,
904
+ )
905
+ check(
906
+ "cross-platform installation matrix contract passes",
907
+ matrix_tests.returncode == 0 and "installation matrix tests: PASS" in matrix_tests.stdout,
908
+ )
909
+ setup_tests = subprocess.run(
910
+ [sys.executable, str(ROOT / "tests/scripts/test_setup.py")],
911
+ capture_output=True,
912
+ text=True,
913
+ )
914
+ check(
915
+ "one-command onboarding is idempotent and project-bound",
916
+ setup_tests.returncode == 0 and "setup onboarding tests: PASS" in setup_tests.stdout,
917
+ setup_tests.stdout.strip() or setup_tests.stderr.strip(),
918
+ )
919
+ visual_tests = subprocess.run(
920
+ [sys.executable, str(ROOT / "tests/scripts/test_visual_truth.py")],
921
+ capture_output=True,
922
+ text=True,
923
+ )
924
+ check(
925
+ "Visual Truth contract preserves provenance and review boundary",
926
+ visual_tests.returncode == 0 and "visual truth contract tests: PASS" in visual_tests.stdout,
927
+ )
928
+ remediation_tests = subprocess.run(
929
+ [sys.executable, str(ROOT / "tests/scripts/test_remediation_learning.py")],
930
+ capture_output=True,
931
+ text=True,
932
+ )
933
+ check(
934
+ "Remediation Learning preserves append-only history and first-pass metrics",
935
+ remediation_tests.returncode == 0 and "remediation learning tests: PASS" in remediation_tests.stdout,
936
+ remediation_tests.stdout.strip() or remediation_tests.stderr.strip(),
937
+ )
938
+ asset_provenance_tests = subprocess.run(
939
+ [sys.executable, str(ROOT / "tests/scripts/test_asset_provenance.py")],
940
+ capture_output=True,
941
+ text=True,
942
+ )
943
+ check(
944
+ "Asset provenance preserves runtime-ready and human-governed production boundaries",
945
+ asset_provenance_tests.returncode == 0 and "asset provenance contract tests: PASS" in asset_provenance_tests.stdout,
946
+ asset_provenance_tests.stdout.strip() or asset_provenance_tests.stderr.strip(),
947
+ )
866
948
 
867
949
 
868
950
  if __name__ == "__main__":
@@ -887,6 +969,7 @@ if __name__ == "__main__":
887
969
  sys.path.insert(0, str(ROOT))
888
970
  test_category_coverage()
889
971
  test_observability_contract()
972
+ test_quality_workflow_rebuilds_replay_after_generated_artifacts()
890
973
  print()
891
974
  if FAILED:
892
975
  print(f"{len(FAILED)} test(s) FAILED: {', '.join(FAILED)}")