motionloom 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +68 -0
  3. package/CONTRIBUTING.md +3 -1
  4. package/README.md +20 -4
  5. package/SECURITY.md +5 -5
  6. package/SKILL.md +15 -5
  7. package/agent-card.json +77 -14
  8. package/agent-surfaces.json +30 -7
  9. package/artifact-adapter-registry.json +568 -20
  10. package/bin/motionloom.mjs +25 -2
  11. package/capability-registry.json +58 -234
  12. package/dev-lab/public/devlab.js +36 -3
  13. package/dev-lab/public/index.html +6 -2
  14. package/docs/ACTION-SEPARATION.md +104 -0
  15. package/docs/ASSET-GENERATION-PLANNER.md +101 -0
  16. package/docs/BRANCH-PROTECTION.md +44 -0
  17. package/docs/EXTERNAL-CORPUS.md +26 -0
  18. package/docs/STATUS.md +3 -2
  19. package/docs/audits/field-test-after-hardening-2026-08-21.md +25 -0
  20. package/docs/releases/2.6.1.md +57 -0
  21. package/docs/releases/2.7.0.md +98 -0
  22. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.00.json +32 -0
  23. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.01.json +32 -0
  24. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.02.json +32 -0
  25. package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.03.json +32 -0
  26. package/examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json +81 -0
  27. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.00.json +26 -0
  28. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.01.json +26 -0
  29. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.02.json +26 -0
  30. package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.03.json +26 -0
  31. package/examples/agent-consumer/asset-planning/pixellab-hero-256x448-request.json +43 -0
  32. package/examples/agent-consumer/devlab-live-sprite/README.md +30 -0
  33. package/examples/agent-consumer/devlab-live-sprite/devlab-runtime.json +61 -0
  34. package/examples/agent-consumer/devlab-live-sprite/frames/idle-00.png +0 -0
  35. package/examples/agent-consumer/devlab-live-sprite/frames/idle-01.png +0 -0
  36. package/examples/agent-consumer/devlab-live-sprite/frames/idle-02.png +0 -0
  37. package/examples/agent-consumer/devlab-live-sprite/frames/reverse-00.png +0 -0
  38. package/examples/agent-consumer/devlab-live-sprite/frames/reverse-01.png +0 -0
  39. package/examples/agent-consumer/devlab-live-sprite/frames/reverse-02.png +0 -0
  40. package/examples/agent-consumer/frame-generation-lock/hero-walk-lock.json +97 -0
  41. package/package.json +23 -9
  42. package/references/multi-frame-asset-generation.md +144 -0
  43. package/schemas/action-separation-verifier-evidence.schema.json +43 -0
  44. package/schemas/action-sequence-manifest.schema.json +48 -0
  45. package/schemas/artifact-adapter-registry.schema.json +1 -1
  46. package/schemas/asset-adaptation.schema.json +21 -0
  47. package/schemas/asset-generation-plan.schema.json +77 -0
  48. package/schemas/asset-generation-request.schema.json +104 -0
  49. package/schemas/frame-envelope.schema.json +62 -0
  50. package/schemas/frame-generation-lock.schema.json +189 -0
  51. package/scripts/action-separation.py +410 -0
  52. package/scripts/asset-adapt.mjs +92 -0
  53. package/scripts/asset-generation-plan.py +613 -0
  54. package/scripts/browser_review_consistency.py +64 -0
  55. package/scripts/fetch-project-corpus.py +91 -0
  56. package/scripts/frame-generation-lock.py +358 -0
  57. package/scripts/frame-set-preflight.py +264 -0
  58. package/scripts/package-consumer-smoke.mjs +20 -0
  59. package/scripts/quality-gate.py +7 -0
  60. package/scripts/release-verify.py +25 -0
  61. package/scripts/report-contract.py +1 -1
  62. package/scripts/report.py +19 -4
  63. package/scripts/resolve-task-bundle.py +11 -3
  64. package/scripts/review-hook.py +51 -2
  65. package/scripts/skill-doctor.py +42 -0
  66. package/src/output/browser-review-smoke/browser-review.json +6 -6
  67. package/tests/scripts/run_tests.py +45 -2
  68. package/tests/scripts/test_asset_adapt.py +47 -0
  69. package/tests/scripts/test_asset_consistency.py +77 -4
  70. package/tests/scripts/test_asset_generation_plan.py +250 -0
  71. package/tests/scripts/test_attestation.py +24 -8
@@ -40,6 +40,10 @@ const PYTHON_COMMANDS = {
40
40
  "remediation-learning": "scripts/remediation-learning.py",
41
41
  "asset-provenance": "scripts/asset-provenance.py",
42
42
  "asset-consistency": "scripts/asset-consistency.py",
43
+ "frame-generation-lock": "scripts/frame-generation-lock.py",
44
+ "frame-set-preflight": "scripts/frame-set-preflight.py",
45
+ "action-separation": "scripts/action-separation.py",
46
+ "asset-generation-plan": "scripts/asset-generation-plan.py",
43
47
  "artifact-intake": "scripts/artifact-intake.py",
44
48
  "runtime-candidate": "scripts/runtime-candidate.py",
45
49
  "rig-compatibility": "scripts/rig-compatibility.py",
@@ -48,6 +52,10 @@ const PYTHON_COMMANDS = {
48
52
  "alpha-isolate": "scripts/isolate-alpha-background.py",
49
53
  };
50
54
 
55
+ const DIRECT_NODE_COMMANDS = {
56
+ "asset-adapt": "scripts/asset-adapt.mjs",
57
+ };
58
+
51
59
  const NODE_COMMANDS = {
52
60
  init: "scripts/setup.mjs",
53
61
  setup: "scripts/setup.mjs",
@@ -69,6 +77,7 @@ Start here:
69
77
  init One safe project setup; no animation gate runs
70
78
  status Short read-only readiness report
71
79
  doctor Validate the installed Skill package
80
+ doctor --runtime Also verify the Playwright Chromium executable
72
81
 
73
82
  Use when an animation task needs it:
74
83
  analyze Run project analysis and refresh Project Memory
@@ -92,6 +101,11 @@ Use when an animation task needs it:
92
101
  remediation-learning Record or summarize user-confirmed remediation and benchmark history
93
102
  asset-provenance Validate, classify or report asset origin and production readiness
94
103
  asset-consistency Validate frame geometry, atlas contamination and layered-map contracts
104
+ frame-generation-lock Validate or compose locked per-frame generation instructions
105
+ frame-set-preflight Fail closed on shared canvases, scale drift, frame contamination and action manifest binding
106
+ action-separation Validate action-scoped frame manifests and independent competitor-action evidence
107
+ asset-generation-plan Produce a MotionLoom project assessment, ranked routes and explicit canvas/frame guidance without invoking APIs
108
+ asset-adapt Apply deterministic transparent padding/integer upscale with a hash-bound report
95
109
  artifact-intake Bind generation controls, provenance, adapter metadata and exported bytes
96
110
  runtime-candidate Bind intake exports to consistency contracts before runtime testing
97
111
  rig-compatibility Validate rig bones, sockets, actions, events and runtime adapter evidence
@@ -108,12 +122,19 @@ Cross-platform examples:
108
122
  motionloom memory record-decision --project-root . --id ui-easing \\
109
123
  --status accepted --summary "Use ease-out for UI entry" --user-confirmed
110
124
  motionloom discovery check --root . --json
125
+ motionloom doctor --runtime --json
111
126
  motionloom discovery install-matrix --root . --json
112
127
  motionloom capability card --format json
113
128
  motionloom visual-truth validate --root . --input src/output/<scene>/visual-truth.json
114
129
  motionloom remediation-learning summary --history artifacts/remediation-history.jsonl --json
115
130
  motionloom asset-provenance check --input <asset-provenance.json> --root <scene-dir> --mode runtime --json
116
131
  motionloom asset-consistency validate --kind frame-geometry --input <frame-geometry.json> --root <scene-dir> --json
132
+ motionloom frame-generation-lock compose --input <frame-generation-lock.json> --root <asset-root> --frame-id walk.00 --json
133
+ motionloom frame-set-preflight --input <frame-geometry.json> --root <asset-root> --action-manifest <action-sequence.json> --json
134
+ motionloom action-separation validate --input <action-sequence.json> --root <asset-root> --json
135
+ motionloom asset-generation-plan plan --request <asset-generation-request.json> --project-root .
136
+ motionloom asset-generation-plan plan --request <asset-generation-request.json> --project-root . --json --strict
137
+ motionloom asset-adapt pad --input source.png --output target.png --width 256 --height 448 --anchor footline --report adaptation.json --json
117
138
  motionloom artifact-intake intake --root <asset-dir> --registry artifact-adapter-registry.json \\
118
139
  --receipt <generation-receipt.json> --controls <control-track.json> --export-manifest <export-manifest.json> --json
119
140
  motionloom alpha-isolate <opaque.png> <isolated.png> --report <alpha-report.json>
@@ -132,7 +153,7 @@ if (!command || command === "help" || command === "--help" || command === "-h")
132
153
  }
133
154
 
134
155
  const alias = COMMAND_ALIASES[command];
135
- const script = alias?.script || NODE_COMMANDS[command] || PYTHON_COMMANDS[command];
156
+ const script = alias?.script || DIRECT_NODE_COMMANDS[command] || NODE_COMMANDS[command] || PYTHON_COMMANDS[command];
136
157
  if (!script) {
137
158
  console.error(`Unknown MotionLoom command: ${command}`);
138
159
  printHelp();
@@ -142,7 +163,9 @@ if (!script) {
142
163
  const executable = script.endsWith(".mjs") ? process.execPath : PYTHON;
143
164
  const delegatedArgs = alias
144
165
  ? [...alias.args, ...args]
145
- : NODE_COMMANDS[command] && !["setup", "init"].includes(command)
166
+ : DIRECT_NODE_COMMANDS[command]
167
+ ? args
168
+ : NODE_COMMANDS[command] && !["setup", "init"].includes(command)
146
169
  ? [command, ...args]
147
170
  : command === "init"
148
171
  ? ["init", ...args]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "0.1",
3
- "registry_id": "registry-motionloom-2.5.1",
4
- "generated_at": "2026-08-17T23:50:53Z",
3
+ "registry_id": "registry-motionloom-2.7.0",
4
+ "generated_at": "2026-08-22T06:55:00Z",
5
5
  "selection_policy": {
6
6
  "require_verified": true,
7
7
  "allow_scaffold_only": false,
@@ -12,35 +12,13 @@
12
12
  "id": "runtime.lottie-json",
13
13
  "kind": "renderer",
14
14
  "status": "verified",
15
- "adapter_version": "2.5.1",
16
- "inputs": [
17
- "project-context",
18
- "motion-spec",
19
- "source-binding"
20
- ],
21
- "outputs": [
22
- "runtime-evidence"
23
- ],
24
- "compatibility": {
25
- "browsers": [
26
- "chromium"
27
- ],
28
- "os": [
29
- "linux"
30
- ],
31
- "node": ">=22"
32
- },
33
- "last_verified_at": "2026-08-17T23:50:53Z",
34
- "evidence": [
35
- {
36
- "path": "scripts/runtime-adapters.mjs",
37
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
38
- "kind": "ci"
39
- }
40
- ],
41
- "limitations": [
42
- "Capability evidence is refreshed by runtime:test."
43
- ],
15
+ "adapter_version": "2.7.0",
16
+ "inputs": ["project-context", "motion-spec", "source-binding"],
17
+ "outputs": ["runtime-evidence"],
18
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
19
+ "last_verified_at": "2026-08-21T18:23:59Z",
20
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
21
+ "limitations": ["Capability evidence is refreshed by runtime:test."],
44
22
  "fallback": "",
45
23
  "risk_level": "low",
46
24
  "side_effect_level": "local_write"
@@ -49,35 +27,13 @@
49
27
  "id": "runtime.dotlottie-package",
50
28
  "kind": "packager",
51
29
  "status": "verified",
52
- "adapter_version": "2.5.1",
53
- "inputs": [
54
- "project-context",
55
- "motion-spec",
56
- "source-binding"
57
- ],
58
- "outputs": [
59
- "runtime-evidence"
60
- ],
61
- "compatibility": {
62
- "browsers": [
63
- "chromium"
64
- ],
65
- "os": [
66
- "linux"
67
- ],
68
- "node": ">=22"
69
- },
70
- "last_verified_at": "2026-08-17T23:50:53Z",
71
- "evidence": [
72
- {
73
- "path": "scripts/runtime-adapters.mjs",
74
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
75
- "kind": "ci"
76
- }
77
- ],
78
- "limitations": [
79
- "Capability evidence is refreshed by runtime:test."
80
- ],
30
+ "adapter_version": "2.7.0",
31
+ "inputs": ["project-context", "motion-spec", "source-binding"],
32
+ "outputs": ["runtime-evidence"],
33
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
34
+ "last_verified_at": "2026-08-21T18:23:59Z",
35
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
36
+ "limitations": ["Capability evidence is refreshed by runtime:test."],
81
37
  "fallback": "",
82
38
  "risk_level": "low",
83
39
  "side_effect_level": "local_write"
@@ -86,35 +42,13 @@
86
42
  "id": "runtime.svg-cutout-rig",
87
43
  "kind": "renderer",
88
44
  "status": "verified",
89
- "adapter_version": "2.5.1",
90
- "inputs": [
91
- "project-context",
92
- "motion-spec",
93
- "source-binding"
94
- ],
95
- "outputs": [
96
- "runtime-evidence"
97
- ],
98
- "compatibility": {
99
- "browsers": [
100
- "chromium"
101
- ],
102
- "os": [
103
- "linux"
104
- ],
105
- "node": ">=22"
106
- },
107
- "last_verified_at": "2026-08-17T23:50:53Z",
108
- "evidence": [
109
- {
110
- "path": "scripts/runtime-adapters.mjs",
111
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
112
- "kind": "ci"
113
- }
114
- ],
115
- "limitations": [
116
- "Capability evidence is refreshed by runtime:test."
117
- ],
45
+ "adapter_version": "2.7.0",
46
+ "inputs": ["project-context", "motion-spec", "source-binding"],
47
+ "outputs": ["runtime-evidence"],
48
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
49
+ "last_verified_at": "2026-08-21T18:23:59Z",
50
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
51
+ "limitations": ["Capability evidence is refreshed by runtime:test."],
118
52
  "fallback": "",
119
53
  "risk_level": "low",
120
54
  "side_effect_level": "local_write"
@@ -123,35 +57,13 @@
123
57
  "id": "runtime.rive",
124
58
  "kind": "runtime-adapter",
125
59
  "status": "verified",
126
- "adapter_version": "2.5.1",
127
- "inputs": [
128
- "project-context",
129
- "motion-spec",
130
- "source-binding"
131
- ],
132
- "outputs": [
133
- "runtime-evidence"
134
- ],
135
- "compatibility": {
136
- "browsers": [
137
- "chromium"
138
- ],
139
- "os": [
140
- "linux"
141
- ],
142
- "node": ">=22"
143
- },
144
- "last_verified_at": "2026-08-17T23:50:53Z",
145
- "evidence": [
146
- {
147
- "path": "scripts/runtime-adapters.mjs",
148
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
149
- "kind": "ci"
150
- }
151
- ],
152
- "limitations": [
153
- "Capability evidence is refreshed by runtime:test."
154
- ],
60
+ "adapter_version": "2.7.0",
61
+ "inputs": ["project-context", "motion-spec", "source-binding"],
62
+ "outputs": ["runtime-evidence"],
63
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
64
+ "last_verified_at": "2026-08-21T18:23:59Z",
65
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
66
+ "limitations": ["Capability evidence is refreshed by runtime:test."],
155
67
  "fallback": "",
156
68
  "risk_level": "low",
157
69
  "side_effect_level": "local_write"
@@ -160,35 +72,13 @@
160
72
  "id": "runtime.gsap",
161
73
  "kind": "runtime-adapter",
162
74
  "status": "verified",
163
- "adapter_version": "2.5.1",
164
- "inputs": [
165
- "project-context",
166
- "motion-spec",
167
- "source-binding"
168
- ],
169
- "outputs": [
170
- "runtime-evidence"
171
- ],
172
- "compatibility": {
173
- "browsers": [
174
- "chromium"
175
- ],
176
- "os": [
177
- "linux"
178
- ],
179
- "node": ">=22"
180
- },
181
- "last_verified_at": "2026-08-17T23:50:53Z",
182
- "evidence": [
183
- {
184
- "path": "scripts/runtime-adapters.mjs",
185
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
186
- "kind": "ci"
187
- }
188
- ],
189
- "limitations": [
190
- "Capability evidence is refreshed by runtime:test."
191
- ],
75
+ "adapter_version": "2.7.0",
76
+ "inputs": ["project-context", "motion-spec", "source-binding"],
77
+ "outputs": ["runtime-evidence"],
78
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
79
+ "last_verified_at": "2026-08-21T18:23:59Z",
80
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
81
+ "limitations": ["Capability evidence is refreshed by runtime:test."],
192
82
  "fallback": "",
193
83
  "risk_level": "low",
194
84
  "side_effect_level": "local_write"
@@ -197,35 +87,13 @@
197
87
  "id": "runtime.framer-motion",
198
88
  "kind": "runtime-adapter",
199
89
  "status": "verified",
200
- "adapter_version": "2.5.1",
201
- "inputs": [
202
- "project-context",
203
- "motion-spec",
204
- "source-binding"
205
- ],
206
- "outputs": [
207
- "runtime-evidence"
208
- ],
209
- "compatibility": {
210
- "browsers": [
211
- "chromium"
212
- ],
213
- "os": [
214
- "linux"
215
- ],
216
- "node": ">=22"
217
- },
218
- "last_verified_at": "2026-08-17T23:50:53Z",
219
- "evidence": [
220
- {
221
- "path": "scripts/runtime-adapters.mjs",
222
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
223
- "kind": "ci"
224
- }
225
- ],
226
- "limitations": [
227
- "Capability evidence is refreshed by runtime:test."
228
- ],
90
+ "adapter_version": "2.7.0",
91
+ "inputs": ["project-context", "motion-spec", "source-binding"],
92
+ "outputs": ["runtime-evidence"],
93
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
94
+ "last_verified_at": "2026-08-21T18:23:59Z",
95
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
96
+ "limitations": ["Capability evidence is refreshed by runtime:test."],
229
97
  "fallback": "",
230
98
  "risk_level": "low",
231
99
  "side_effect_level": "local_write"
@@ -234,35 +102,13 @@
234
102
  "id": "runtime.spine",
235
103
  "kind": "renderer",
236
104
  "status": "scaffold_only",
237
- "adapter_version": "2.5.1",
238
- "inputs": [
239
- "project-context",
240
- "motion-spec",
241
- "source-binding"
242
- ],
243
- "outputs": [
244
- "runtime-evidence"
245
- ],
246
- "compatibility": {
247
- "browsers": [
248
- "chromium"
249
- ],
250
- "os": [
251
- "linux"
252
- ],
253
- "node": ">=22"
254
- },
255
- "last_verified_at": "2026-08-17T23:50:53Z",
256
- "evidence": [
257
- {
258
- "path": "scripts/runtime-adapters.mjs",
259
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
260
- "kind": "ci"
261
- }
262
- ],
263
- "limitations": [
264
- "Scaffold only; do not use for production acceptance."
265
- ],
105
+ "adapter_version": "2.7.0",
106
+ "inputs": ["project-context", "motion-spec", "source-binding"],
107
+ "outputs": ["runtime-evidence"],
108
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
109
+ "last_verified_at": "2026-08-21T18:23:59Z",
110
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
111
+ "limitations": ["Scaffold only; do not use for production acceptance."],
266
112
  "fallback": "runtime.lottie-json",
267
113
  "risk_level": "high",
268
114
  "side_effect_level": "local_write"
@@ -271,35 +117,13 @@
271
117
  "id": "runtime.threejs",
272
118
  "kind": "renderer",
273
119
  "status": "scaffold_only",
274
- "adapter_version": "2.5.1",
275
- "inputs": [
276
- "project-context",
277
- "motion-spec",
278
- "source-binding"
279
- ],
280
- "outputs": [
281
- "runtime-evidence"
282
- ],
283
- "compatibility": {
284
- "browsers": [
285
- "chromium"
286
- ],
287
- "os": [
288
- "linux"
289
- ],
290
- "node": ">=22"
291
- },
292
- "last_verified_at": "2026-08-17T23:50:53Z",
293
- "evidence": [
294
- {
295
- "path": "scripts/runtime-adapters.mjs",
296
- "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d",
297
- "kind": "ci"
298
- }
299
- ],
300
- "limitations": [
301
- "Scaffold only; do not use for production acceptance."
302
- ],
120
+ "adapter_version": "2.7.0",
121
+ "inputs": ["project-context", "motion-spec", "source-binding"],
122
+ "outputs": ["runtime-evidence"],
123
+ "compatibility": {"browsers": ["chromium"], "os": ["linux"], "node": ">=22"},
124
+ "last_verified_at": "2026-08-21T18:23:59Z",
125
+ "evidence": [{"path": "scripts/runtime-adapters.mjs", "sha256": "6f135e75e700a7bb105903db59445fcd3577669b595082a2b542db639f5fe56d", "kind": "ci"}],
126
+ "limitations": ["Scaffold only; do not use for production acceptance."],
303
127
  "fallback": "runtime.lottie-json",
304
128
  "risk_level": "high",
305
129
  "side_effect_level": "local_write"
@@ -9,6 +9,7 @@ let manifest;
9
9
  let spec;
10
10
  let candidate;
11
11
  let runtimeDescriptor = null;
12
+ let actionSeparation = null;
12
13
  let review;
13
14
  let driver;
14
15
  let currentAnimationId = null;
@@ -117,6 +118,17 @@ function validateRuntimeDescriptor(value) {
117
118
  return value;
118
119
  }
119
120
 
121
+ function validateActionSeparation(value) {
122
+ if (!value) return null;
123
+ if (typeof value !== "object") throw new Error("Action-separation evidence must be an object");
124
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/.test(value.action_id || "")) throw new Error("Action-separation evidence requires a safe action_id");
125
+ if (!new Set(["pass", "quarantined"]).has(value.status)) throw new Error("Action-separation evidence has an invalid status");
126
+ if (!Number.isInteger(value.frame_count) || value.frame_count < 1) throw new Error("Action-separation evidence requires a positive frame_count");
127
+ if (!Number.isInteger(value.passing_frame_count) || value.passing_frame_count < 0 || value.passing_frame_count > value.frame_count) throw new Error("Action-separation passing_frame_count is invalid");
128
+ if (!Array.isArray(value.forbidden_action_ids)) throw new Error("Action-separation evidence requires forbidden_action_ids");
129
+ return value;
130
+ }
131
+
120
132
  function controlEnabled(name, capability = true) {
121
133
  if (!runtimeDescriptor) return false;
122
134
  return runtimeDescriptor.controls?.[name] !== false && capability !== false;
@@ -386,7 +398,8 @@ function currentReview(decision) {
386
398
  mode: runtimeDescriptor ? "live-runtime" : "captured-evidence",
387
399
  descriptor_sha256: candidate.runtime_review?.bundle_sha256 || null,
388
400
  selected_animation: currentAnimationId,
389
- state
401
+ state,
402
+ action_separation: actionSeparation
390
403
  },
391
404
  spec: { framework: spec.framework, category: spec.category, context_binding: spec.context_binding },
392
405
  candidate: { source_sha256: candidate.source_sha256, context_sha256: candidate.context_sha256 }
@@ -422,6 +435,20 @@ function renderMetadata() {
422
435
  }));
423
436
  }
424
437
 
438
+ function renderActionSeparation() {
439
+ const node = $("action-separation");
440
+ if (!node) return;
441
+ if (!actionSeparation) {
442
+ node.className = "progress-note";
443
+ node.textContent = "No action-separation evidence bound; legacy review compatibility mode.";
444
+ return;
445
+ }
446
+ const pass = actionSeparation.status === "pass";
447
+ node.className = `progress-note ${pass ? "pass" : "quarantine"}`;
448
+ const forbidden = actionSeparation.forbidden_action_ids.join(", ") || "none";
449
+ node.textContent = `${pass ? "PASS" : "QUARANTINED"} · expected ${actionSeparation.action_id} · ${actionSeparation.passing_frame_count}/${actionSeparation.frame_count} frames separated · competitors: ${forbidden}`;
450
+ }
451
+
425
452
  function renderChecks() {
426
453
  const nodes = (manifest.checks || []).map((check) => {
427
454
  const label = document.createElement("label"); label.className = "check";
@@ -474,12 +501,17 @@ function updateReviewGate() {
474
501
  const checks = currentChecks();
475
502
  const checksPass = checks.length > 0 && checks.every((item) => item.pass);
476
503
  const coverage = reviewCoverageComplete();
504
+ const actionGate = !actionSeparation || actionSeparation.status === "pass";
477
505
  const liveRuntimeBlocked = candidate?.runtime_review?.live === true && driver instanceof SnapshotDriver;
478
- $("confirm").disabled = !(checksPass && coverage && !liveRuntimeBlocked);
506
+ $("confirm").disabled = !(checksPass && coverage && actionGate && !liveRuntimeBlocked);
479
507
  if (liveRuntimeBlocked) {
480
508
  setStatus("warn", "LIVE RUNTIME UNAVAILABLE · approval is blocked; captured evidence remains inspectable and changes may still be requested.");
481
509
  return;
482
510
  }
511
+ if (!actionGate) {
512
+ setStatus("warn", "ACTION SEPARATION QUARANTINED · regenerate or independently verify the ambiguous frame before approval.");
513
+ return;
514
+ }
483
515
  const missing = requiredAnimationIds().filter((id) => !inspectedAnimations.has(id));
484
516
  if (!coverage && missing.length) setStatus("info", `Inspect required animations before approval: ${missing.join(", ")}`);
485
517
  }
@@ -669,6 +701,7 @@ async function load() {
669
701
  ]);
670
702
  validateCandidate();
671
703
  runtimeDescriptor = validateRuntimeDescriptor(runtimeDescriptor);
704
+ actionSeparation = validateActionSeparation(runtimeDescriptor?.action_separation || null);
672
705
  const expectedLiveRuntimeMissing = candidate.runtime_review?.live === true && !runtimeDescriptor;
673
706
 
674
707
  $("title").textContent = `${manifest.name || scene}`;
@@ -680,7 +713,7 @@ async function load() {
680
713
  const saved = JSON.parse(localStorage.getItem(`devlab:${taskId}:${candidateId}`) || "null");
681
714
  review = saved || { checks: (manifest.checks || []).map((check) => ({ id: check.id, pass: false })), notes: "", decision: "pending", animations_inspected: [] };
682
715
  for (const id of review.animations_inspected || []) inspectedAnimations.add(id);
683
- renderChecks(); $("notes").value = review.notes || ""; expose(review);
716
+ renderActionSeparation(); renderChecks(); $("notes").value = review.notes || ""; expose(review);
684
717
  wireControls(); applyViewportOverlays(); setZoom(1);
685
718
 
686
719
  if (expectedLiveRuntimeMissing) {
@@ -93,6 +93,8 @@
93
93
  #status { margin-top:10px; font:11px/1.4 ui-monospace,monospace; }
94
94
  .ok { color:var(--good); }.warn { color:var(--bad); }.info { color:var(--blue); }
95
95
  .progress-note { margin-top:9px; color:var(--muted); font:11px ui-monospace,monospace; }
96
+ #action-separation.pass { color:var(--good); }
97
+ #action-separation.quarantine { color:var(--bad); }
96
98
  @media (max-width:1050px) { .app { grid-template-columns:180px minmax(0,1fr) 285px; } }
97
99
  @media (max-width:820px) { header { height:auto; min-height:58px; flex-wrap:wrap; padding:10px 12px; } .app { height:auto; min-height:0; grid-template-columns:1fr; } nav { border-right:0; border-bottom:1px solid var(--line); } #animations { flex-direction:row; overflow:auto; } .animation-btn { width:auto; min-width:130px; } aside { border-left:0; border-top:1px solid var(--line); } .workbench { min-height:620px; } }
98
100
  </style>
@@ -150,8 +152,10 @@
150
152
  <aside>
151
153
  <h1 class="inspector-title" id="title">Scene</h1><p id="description">Loading manifest and candidate binding.</p>
152
154
  <div class="meta-grid" id="meta"></div>
153
- <h2>Runtime inspector</h2><pre class="runtime-state" id="runtime-state">Waiting for runtime…</pre>
154
- <h2>Quality checklist</h2><div id="checks"></div>
155
+ <h2>Runtime inspector</h2><pre class="runtime-state" id="runtime-state">Waiting for runtime…</pre>
156
+ <h2>Action separation</h2><div class="progress-note" id="action-separation">No action-separation evidence bound.</div>
157
+ <h2>Quality checklist</h2>
158
+ <div id="checks"></div>
155
159
  <h2>Fix notes</h2><textarea id="notes" placeholder="Describe a precise fix request, timing issue, visual defect, or confirmation…"></textarea>
156
160
  <div class="review-actions"><button class="danger" id="request-changes" type="button">Request changes</button><button class="primary" id="confirm" type="button">Approve candidate</button><button id="reset" type="button">Reset review</button></div>
157
161
  <div id="status"></div>
@@ -0,0 +1,104 @@
1
+ # Action-scoped isolated frame pipeline
2
+
3
+ MotionLoom keeps generated source frames isolated: one source PNG per frame, no pose sheet as production input, no shared-canvas crop and no post-generation resize. Isolation prevents pixel contamination, but it does not by itself prove that a frame belongs to the intended action. This document adds the action-level contract that closes that gap.
4
+
5
+ ## Model
6
+
7
+ A frame belongs to an immutable `sequence_id` and `action_id`. The sequence manifest declares the expected action, explicitly forbidden competitor actions, the ordered frame set, the identity-lock hash and one envelope for every image. A verifier result is evidence only; it cannot grant approval or move a file.
8
+
9
+ The lock and manifest deliberately use two separate layers. Verifier identity is not the same thing as verifier provenance: a different `verifier_id` alone does not prove that a separate verifier ran.
10
+
11
+ | Layer | Source of truth | Purpose |
12
+ |---|---|---|
13
+ | Generation lock | `frame-generation-lock.schema.json` / `frame-generation-lock.py` | Compose one isolated request per frame with shared identity, geometry and action cues |
14
+ | Action manifest | `action-sequence-manifest.schema.json` / `action-separation.py` | Bind frame order, image bytes, envelope metadata and competitor-action separation |
15
+ | Geometry preflight | `frame-set-preflight.py` | Measure actual PNG geometry and optionally require the action manifest to pass |
16
+ | Human review | Dev Lab | Inspect the live sequence and see whether action evidence is pass or quarantined |
17
+
18
+ ## CLI
19
+
20
+ Validate the action manifest directly:
21
+
22
+ ```bash
23
+ motionloom action-separation validate \
24
+ --input examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json \
25
+ --root examples/agent-consumer/asset-consistency --json
26
+ ```
27
+
28
+ Run geometry and action checks together:
29
+
30
+ ```bash
31
+ motionloom frame-set-preflight \
32
+ --input examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json \
33
+ --root examples/agent-consumer/asset-consistency \
34
+ --action-manifest examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json \
35
+ --json
36
+ ```
37
+
38
+ The equivalent npm scripts are `npm run action:separation` and `npm run frame:preflight`. To create one envelope from a manifest and a real PNG, use:
39
+
40
+ ```bash
41
+ motionloom action-separation envelope \
42
+ --manifest examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json \
43
+ --root examples/agent-consumer/asset-consistency \
44
+ --frame-id walk.00 --expected-action walk --top-competitor run \
45
+ --margin 0.42 --threshold 0.20 \
46
+ --output examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.00.json --json
47
+ ```
48
+
49
+ The `envelope` command is generator-side convenience only: it emits `verification_mode: declared`, keeps the result `quarantined` and returns non-zero even when the supplied numbers meet the rubric. A separate verifier process/agent must create the evidence artifact and a review workflow must bind it into an `independently_bound` envelope. The validator checks those bindings, but cryptographic hashes prove artifact integrity and provenance claims, not the truthfulness of a claimed producer identity.
50
+
51
+ ## Frame envelope
52
+
53
+ Every manifest frame points to an envelope. The envelope binds the frame to the exact sequence, action, frame index, identity lock and image bytes. The verifier must provide an expected action, a top competitor, a numeric confidence margin and a declared threshold. Schema 0.2 also classifies the result as `declared` or `independently_bound`. A `declared` result may describe a check, but it is never accepted as independent proof. An `independently_bound` result must reference a separate verifier artifact whose bytes, canonical result, producer provenance and frame binding all match the envelope. A margin below threshold is quarantined; it is not silently assigned to the nearest action.
54
+
55
+ ```json
56
+ {
57
+ "sequence_id": "hero-walk-v1",
58
+ "action_id": "walk",
59
+ "frame_id": "walk.01",
60
+ "frame_index": 1,
61
+ "image": "assets/hero-frame-01.png",
62
+ "image_sha256": "<sha256>",
63
+ "identity_lock_sha256": "<lock-sha256>",
64
+ "verifier": {
65
+ "verifier_id": "motionloom-separation-verifier-v1",
66
+ "verification_mode": "independently_bound",
67
+ "expected_action": "walk",
68
+ "top_competitor": "run",
69
+ "margin": 0.42,
70
+ "threshold": 0.20,
71
+ "status": "pass",
72
+ "method": "independent-action-rubric-v1",
73
+ "evidence": {
74
+ "artifact": "action-sequence/verifier-evidence/walk.01.json",
75
+ "artifact_sha256": "<sha256>",
76
+ "result_sha256": "<sha256>",
77
+ "provenance": {
78
+ "kind": "separate_verifier_artifact",
79
+ "producer_id": "motionloom-independent-verifier-v1",
80
+ "run_id": "verifier-run-2026-08-22"
81
+ }
82
+ }
83
+ },
84
+ "approval": false
85
+ }
86
+ ```
87
+
88
+ The validator rejects duplicate images, non-contiguous frame indexes, path escapes, mismatched envelope fields, stale image hashes, missing/tampered verifier provenance, verifier-generator collisions, undeclared competitors, low margins and any verifier status other than `pass`. It never repairs, renames, relocates or approves an asset.
89
+
90
+ ## Generation lock 0.2
91
+
92
+ `frame-generation-lock` accepts legacy schema `0.1` and enhanced schema `0.2`. A `0.2` lock requires `sequence_id`, `forbidden_action_ids` and `action_contract.positive_cues`/`negative_cues`. `compose` and `compose-all` include those cues in every provider-facing instruction while preserving the one-image-per-frame rule.
93
+
94
+ The canonical example is [`examples/agent-consumer/frame-generation-lock/hero-walk-lock.json`](../examples/agent-consumer/frame-generation-lock/hero-walk-lock.json). The action manifest and envelopes are under [`examples/agent-consumer/asset-consistency/action-sequence/`](../examples/agent-consumer/asset-consistency/action-sequence/).
95
+
96
+ ## Status and approval boundary
97
+
98
+ The manifest may be `review_required` or `quarantined`; it must never claim approval. A passing verifier means that the evidence is sufficiently separated for the next gate, not that the user approved the action. Dev Lab still requires animation coverage and explicit checklist confirmation before enabling Approve.
99
+
100
+ For live runtime fixtures, the optional `action_separation` summary in `devlab-runtime.json` is displayed in Dev Lab. A quarantined summary disables approval and instructs the reviewer to regenerate or independently verify the ambiguous frame. Contact sheets remain review projections only; production packing must use the manifest-approved isolated images.
101
+
102
+ ## Adversarial coverage
103
+
104
+ The regression suite covers a valid four-frame sequence with independently bound evidence and rejects cross-action envelopes, verifier-generator collisions, missing provenance, modified verifier results, modified evidence hashes, low-confidence margins, wrong expected actions, undeclared competitors, quarantined evidence, reused images and approval assertions. The canonical fixture also runs through geometry preflight, so a sequence can only pass when both the measured PNG contract and the action-scoped evidence pass.