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.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +68 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +20 -4
- package/SECURITY.md +5 -5
- package/SKILL.md +15 -5
- package/agent-card.json +77 -14
- package/agent-surfaces.json +30 -7
- package/artifact-adapter-registry.json +568 -20
- package/bin/motionloom.mjs +25 -2
- package/capability-registry.json +58 -234
- package/dev-lab/public/devlab.js +36 -3
- package/dev-lab/public/index.html +6 -2
- package/docs/ACTION-SEPARATION.md +104 -0
- package/docs/ASSET-GENERATION-PLANNER.md +101 -0
- package/docs/BRANCH-PROTECTION.md +44 -0
- package/docs/EXTERNAL-CORPUS.md +26 -0
- package/docs/STATUS.md +3 -2
- package/docs/audits/field-test-after-hardening-2026-08-21.md +25 -0
- package/docs/releases/2.6.1.md +57 -0
- package/docs/releases/2.7.0.md +98 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.00.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.01.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.02.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.03.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json +81 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.00.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.01.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.02.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.03.json +26 -0
- package/examples/agent-consumer/asset-planning/pixellab-hero-256x448-request.json +43 -0
- package/examples/agent-consumer/devlab-live-sprite/README.md +30 -0
- package/examples/agent-consumer/devlab-live-sprite/devlab-runtime.json +61 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-00.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-01.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-02.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-00.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-01.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-02.png +0 -0
- package/examples/agent-consumer/frame-generation-lock/hero-walk-lock.json +97 -0
- package/package.json +23 -9
- package/references/multi-frame-asset-generation.md +144 -0
- package/schemas/action-separation-verifier-evidence.schema.json +43 -0
- package/schemas/action-sequence-manifest.schema.json +48 -0
- package/schemas/artifact-adapter-registry.schema.json +1 -1
- package/schemas/asset-adaptation.schema.json +21 -0
- package/schemas/asset-generation-plan.schema.json +77 -0
- package/schemas/asset-generation-request.schema.json +104 -0
- package/schemas/frame-envelope.schema.json +62 -0
- package/schemas/frame-generation-lock.schema.json +189 -0
- package/scripts/action-separation.py +410 -0
- package/scripts/asset-adapt.mjs +92 -0
- package/scripts/asset-generation-plan.py +613 -0
- package/scripts/browser_review_consistency.py +64 -0
- package/scripts/fetch-project-corpus.py +91 -0
- package/scripts/frame-generation-lock.py +358 -0
- package/scripts/frame-set-preflight.py +264 -0
- package/scripts/package-consumer-smoke.mjs +20 -0
- package/scripts/quality-gate.py +7 -0
- package/scripts/release-verify.py +25 -0
- package/scripts/report-contract.py +1 -1
- package/scripts/report.py +19 -4
- package/scripts/resolve-task-bundle.py +11 -3
- package/scripts/review-hook.py +51 -2
- package/scripts/skill-doctor.py +42 -0
- package/src/output/browser-review-smoke/browser-review.json +6 -6
- package/tests/scripts/run_tests.py +45 -2
- package/tests/scripts/test_asset_adapt.py +47 -0
- package/tests/scripts/test_asset_consistency.py +77 -4
- package/tests/scripts/test_asset_generation_plan.py +250 -0
- package/tests/scripts/test_attestation.py +24 -8
package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.01.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"contract": "action-separation-verifier-evidence",
|
|
4
|
+
"sequence_id": "hero-walk-v1",
|
|
5
|
+
"action_id": "walk",
|
|
6
|
+
"frame_id": "walk.01",
|
|
7
|
+
"frame_index": 1,
|
|
8
|
+
"image_sha256": "1eaf21df83ed49e490ceb7db1e1a14c58e5227bcb4f1fd79a70e8ad3ee7d38ad",
|
|
9
|
+
"identity_lock_sha256": "59d6b09622b19839b8c9342fb043965ea3f515c8979d769284d413f60a5f7e2d",
|
|
10
|
+
"generator_agent_id": "motionloom-generator-v1",
|
|
11
|
+
"verifier_id": "motionloom-separation-verifier-v1",
|
|
12
|
+
"result": {
|
|
13
|
+
"expected_action": "walk",
|
|
14
|
+
"top_competitor": "jump",
|
|
15
|
+
"margin": 0.39,
|
|
16
|
+
"threshold": 0.2,
|
|
17
|
+
"status": "pass",
|
|
18
|
+
"method": "independent-action-rubric-v1"
|
|
19
|
+
},
|
|
20
|
+
"result_sha256": "0ad134eda50c1d6a161d0395d03f43a51a15767665bc06b203564dd327ce0904",
|
|
21
|
+
"provenance": {
|
|
22
|
+
"kind": "separate_verifier_artifact",
|
|
23
|
+
"producer_id": "motionloom-independent-verifier-v1",
|
|
24
|
+
"run_id": "fixture-run-2026-08-22"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.02.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"contract": "action-separation-verifier-evidence",
|
|
4
|
+
"sequence_id": "hero-walk-v1",
|
|
5
|
+
"action_id": "walk",
|
|
6
|
+
"frame_id": "walk.02",
|
|
7
|
+
"frame_index": 2,
|
|
8
|
+
"image_sha256": "de88659cb7a57ae9c10b6e3e84e04e54bdc07e9971d5c42160a315d41b38fbc3",
|
|
9
|
+
"identity_lock_sha256": "59d6b09622b19839b8c9342fb043965ea3f515c8979d769284d413f60a5f7e2d",
|
|
10
|
+
"generator_agent_id": "motionloom-generator-v1",
|
|
11
|
+
"verifier_id": "motionloom-separation-verifier-v1",
|
|
12
|
+
"result": {
|
|
13
|
+
"expected_action": "walk",
|
|
14
|
+
"top_competitor": "attack",
|
|
15
|
+
"margin": 0.36,
|
|
16
|
+
"threshold": 0.2,
|
|
17
|
+
"status": "pass",
|
|
18
|
+
"method": "independent-action-rubric-v1"
|
|
19
|
+
},
|
|
20
|
+
"result_sha256": "529c083ac2959e80524234910750c015377d0d7be59b27ca5b14edf6585cd1cc",
|
|
21
|
+
"provenance": {
|
|
22
|
+
"kind": "separate_verifier_artifact",
|
|
23
|
+
"producer_id": "motionloom-independent-verifier-v1",
|
|
24
|
+
"run_id": "fixture-run-2026-08-22"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.03.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"contract": "action-separation-verifier-evidence",
|
|
4
|
+
"sequence_id": "hero-walk-v1",
|
|
5
|
+
"action_id": "walk",
|
|
6
|
+
"frame_id": "walk.03",
|
|
7
|
+
"frame_index": 3,
|
|
8
|
+
"image_sha256": "161d906dc850cd3d3ee970b97406fc36bcdf1c34222ac94171a05672bf64d957",
|
|
9
|
+
"identity_lock_sha256": "59d6b09622b19839b8c9342fb043965ea3f515c8979d769284d413f60a5f7e2d",
|
|
10
|
+
"generator_agent_id": "motionloom-generator-v1",
|
|
11
|
+
"verifier_id": "motionloom-separation-verifier-v1",
|
|
12
|
+
"result": {
|
|
13
|
+
"expected_action": "walk",
|
|
14
|
+
"top_competitor": "run",
|
|
15
|
+
"margin": 0.32999999999999996,
|
|
16
|
+
"threshold": 0.2,
|
|
17
|
+
"status": "pass",
|
|
18
|
+
"method": "independent-action-rubric-v1"
|
|
19
|
+
},
|
|
20
|
+
"result_sha256": "259be183f714d57cec7721127c0dfe61b552e8fbcbb189b782b662ed57ac138b",
|
|
21
|
+
"provenance": {
|
|
22
|
+
"kind": "separate_verifier_artifact",
|
|
23
|
+
"producer_id": "motionloom-independent-verifier-v1",
|
|
24
|
+
"run_id": "fixture-run-2026-08-22"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"request_id": "hero-attack-pixellab-256x448",
|
|
4
|
+
"asset_id": "hero.character.attack",
|
|
5
|
+
"asset_kind": "frame_sequence",
|
|
6
|
+
"target": {
|
|
7
|
+
"canvas": {"width": 256, "height": 448},
|
|
8
|
+
"frame_count": 8,
|
|
9
|
+
"fps": 8,
|
|
10
|
+
"alpha_mode": "straight",
|
|
11
|
+
"pixel_art": true,
|
|
12
|
+
"anchor": "footline"
|
|
13
|
+
},
|
|
14
|
+
"generation_policy": {
|
|
15
|
+
"frame_isolation": "required",
|
|
16
|
+
"allow_crop": false,
|
|
17
|
+
"allow_stretch": false,
|
|
18
|
+
"allow_silent_resize": false,
|
|
19
|
+
"allow_provider_batch_as_provisional": true,
|
|
20
|
+
"integer_scale_only": true
|
|
21
|
+
},
|
|
22
|
+
"project_context": {
|
|
23
|
+
"runtime": "custom-2d-game-runtime",
|
|
24
|
+
"framework": "canvas-sprite-runtime",
|
|
25
|
+
"existing_assets": ["examples/agent-consumer/asset-consistency/assets/hero-frame-00.png"],
|
|
26
|
+
"rig_requirements": ["preserve footline and pivot across frames", "keep action-specific frame identity"],
|
|
27
|
+
"provenance_requirements": ["generation receipt", "hash-bound export manifest", "human review"],
|
|
28
|
+
"validation_requirements": ["MotionLoom frame geometry", "MotionLoom action separation", "MotionLoom Dev Lab review"]
|
|
29
|
+
},
|
|
30
|
+
"actions": [
|
|
31
|
+
{
|
|
32
|
+
"action_id": "attack",
|
|
33
|
+
"positive_cues": ["forward sword swing", "weight shifts into strike", "weapon leads the silhouette"],
|
|
34
|
+
"negative_cues": ["walking cycle", "running cycle", "jumping", "idle stance"],
|
|
35
|
+
"frame_count": 8
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"references": [
|
|
39
|
+
{"id": "hero-identity", "role": "identity"},
|
|
40
|
+
{"id": "hero-style", "role": "style"},
|
|
41
|
+
{"id": "attack-pose-reference", "role": "pose"}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Canonical live Dev Lab sprite fixture
|
|
2
|
+
|
|
3
|
+
This fixture is a small, hash-bindable sprite-sequence runtime for exercising the Dev Lab live path from a clean checkout. It deliberately contains two review-required actions, `idle` and `reverse`, so the Action Library coverage gate can be observed moving from `1/2` to `2/2`.
|
|
4
|
+
|
|
5
|
+
The PNG files are deterministic repository fixtures. They are suitable for runtime-control and provenance-contract testing; they are **not** a claim about production art quality, identity fidelity, licensing, authorship or user approval.
|
|
6
|
+
|
|
7
|
+
## Run the live binding check
|
|
8
|
+
|
|
9
|
+
From the repository root:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python3 - <<'PY'
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
root = Path(".").resolve()
|
|
18
|
+
sys.path.insert(0, str(root / "scripts"))
|
|
19
|
+
path = root / "scripts/review-hook.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("motionloom_review_hook", path)
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(module)
|
|
23
|
+
bundle = module.runtime_bundle(root / "examples/agent-consumer/devlab-live-sprite")
|
|
24
|
+
assert bundle and bundle["mode"] == "sprite-sequence"
|
|
25
|
+
assert bundle["animations"] == ["idle", "reverse"]
|
|
26
|
+
print(bundle["bundle_sha256"])
|
|
27
|
+
PY
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To use the fixture in an actual review task, copy it into a rendered scene directory as `devlab-runtime.json` and run `motionloom review-hook prepare --task-dir <task-dir> --lab-url <dev-lab-url>`. Open the exact emitted candidate URL, inspect both actions, exercise play/pause/restart/seek/step/speed/loop, and obtain explicit user approval before any PR workflow.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"mode": "sprite-sequence",
|
|
4
|
+
"files": [
|
|
5
|
+
"frames/idle-00.png",
|
|
6
|
+
"frames/idle-01.png",
|
|
7
|
+
"frames/idle-02.png",
|
|
8
|
+
"frames/reverse-00.png",
|
|
9
|
+
"frames/reverse-01.png",
|
|
10
|
+
"frames/reverse-02.png"
|
|
11
|
+
],
|
|
12
|
+
"default_animation": "idle",
|
|
13
|
+
"animations": [
|
|
14
|
+
{
|
|
15
|
+
"id": "idle",
|
|
16
|
+
"fps": 6,
|
|
17
|
+
"frames": [
|
|
18
|
+
"frames/idle-00.png",
|
|
19
|
+
"frames/idle-01.png",
|
|
20
|
+
"frames/idle-02.png"
|
|
21
|
+
],
|
|
22
|
+
"loop": true,
|
|
23
|
+
"review_required": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "reverse",
|
|
27
|
+
"fps": 6,
|
|
28
|
+
"frames": [
|
|
29
|
+
"frames/reverse-00.png",
|
|
30
|
+
"frames/reverse-01.png",
|
|
31
|
+
"frames/reverse-02.png"
|
|
32
|
+
],
|
|
33
|
+
"loop": false,
|
|
34
|
+
"review_required": true
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"controls": {
|
|
38
|
+
"play": true,
|
|
39
|
+
"pause": true,
|
|
40
|
+
"restart": true,
|
|
41
|
+
"seek": true,
|
|
42
|
+
"step": true,
|
|
43
|
+
"speed": true,
|
|
44
|
+
"loop": true
|
|
45
|
+
},
|
|
46
|
+
"viewport": {
|
|
47
|
+
"canvas_width": 1920,
|
|
48
|
+
"canvas_height": 1920,
|
|
49
|
+
"pixel_art": false
|
|
50
|
+
},
|
|
51
|
+
"review_policy": {
|
|
52
|
+
"require_all_animations": true
|
|
53
|
+
},
|
|
54
|
+
"action_separation": {
|
|
55
|
+
"status": "pass",
|
|
56
|
+
"action_id": "sprite-sequence",
|
|
57
|
+
"frame_count": 6,
|
|
58
|
+
"passing_frame_count": 6,
|
|
59
|
+
"forbidden_action_ids": ["cross-action-mix"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.2",
|
|
3
|
+
"lock_id": "hero-walk-v1",
|
|
4
|
+
"asset_identity": "character/hero-male",
|
|
5
|
+
"action_id": "walk",
|
|
6
|
+
"sequence_id": "hero-walk-v1",
|
|
7
|
+
"forbidden_action_ids": ["run", "jump", "attack"],
|
|
8
|
+
"action_manifest": "asset-consistency/action-sequence/hero-walk-action-manifest.json",
|
|
9
|
+
"action_contract": {
|
|
10
|
+
"positive_cues": [
|
|
11
|
+
"alternating contact and passing phases with an upright body",
|
|
12
|
+
"weight transfers along the shared footline with a natural counter-swing"
|
|
13
|
+
],
|
|
14
|
+
"negative_cues": [
|
|
15
|
+
"no airborne jump, attack wind-up, or running stride",
|
|
16
|
+
"no casting, hurt, dodge, or second character silhouette"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"reference": {
|
|
20
|
+
"image": "asset-consistency/assets/hero-frame-00.png",
|
|
21
|
+
"sha256": "75ce90653a01a23bcfab1aef83647d6364d3edbf6e4a44b84c635338c12b9248",
|
|
22
|
+
"role": "accepted_frame_anchor"
|
|
23
|
+
},
|
|
24
|
+
"canvas": {
|
|
25
|
+
"width": 8,
|
|
26
|
+
"height": 8,
|
|
27
|
+
"color_space": "srgb",
|
|
28
|
+
"alpha_mode": "straight"
|
|
29
|
+
},
|
|
30
|
+
"geometry": {
|
|
31
|
+
"center_x": 4,
|
|
32
|
+
"pivot": { "x": 4, "y": 7, "space": "pixels" },
|
|
33
|
+
"footline_px": 6,
|
|
34
|
+
"safe_rect": { "x": 1, "y": 0, "width": 6, "height": 7 },
|
|
35
|
+
"min_padding_px": 1,
|
|
36
|
+
"target_alpha_bbox": { "width": 4, "height": 5 },
|
|
37
|
+
"tolerances": {
|
|
38
|
+
"pivot_px": 0.01,
|
|
39
|
+
"footline_px": 0.01,
|
|
40
|
+
"bbox_width_px": 0,
|
|
41
|
+
"bbox_height_px": 0
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"appearance": {
|
|
45
|
+
"preserve": [
|
|
46
|
+
"same character identity and silhouette",
|
|
47
|
+
"same camera and left-to-right orientation",
|
|
48
|
+
"same palette, outline weight and apparent scale"
|
|
49
|
+
],
|
|
50
|
+
"forbid": [
|
|
51
|
+
"second character or neighboring pose",
|
|
52
|
+
"background, shadow, text, prop or crop",
|
|
53
|
+
"camera change, mirror, recolor or post-generation resize"
|
|
54
|
+
],
|
|
55
|
+
"pixel_art": {
|
|
56
|
+
"enabled": true,
|
|
57
|
+
"nearest_neighbor_only": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"source_policy": {
|
|
61
|
+
"isolated_frames": true,
|
|
62
|
+
"max_frames_per_image": 1,
|
|
63
|
+
"allow_pose_sheet": false,
|
|
64
|
+
"allow_post_resize": false,
|
|
65
|
+
"reuse_reference": true
|
|
66
|
+
},
|
|
67
|
+
"frames": [
|
|
68
|
+
{
|
|
69
|
+
"frame_id": "walk.00",
|
|
70
|
+
"pose": "Contact-right: right foot forward on the shared footline, left leg trailing, natural counter-swing.",
|
|
71
|
+
"output": "frame-generation-lock/generated/hero-walk-00.png"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"frame_id": "walk.01",
|
|
75
|
+
"pose": "Passing: one leg passes beneath the upright body with compact stride and natural counter-swing.",
|
|
76
|
+
"output": "frame-generation-lock/generated/hero-walk-01.png"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"frame_id": "walk.02",
|
|
80
|
+
"pose": "Contact-left: left foot forward on the shared footline, right leg trailing, natural counter-swing.",
|
|
81
|
+
"output": "frame-generation-lock/generated/hero-walk-02.png"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"frame_id": "walk.03",
|
|
85
|
+
"pose": "Passing complement: opposite leg passes beneath the upright body; preserve the same body scale and baseline.",
|
|
86
|
+
"output": "frame-generation-lock/generated/hero-walk-03.png"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"postflight": {
|
|
90
|
+
"frame_geometry": "asset-consistency/hero-walk-frame-geometry.json"
|
|
91
|
+
},
|
|
92
|
+
"trust": {
|
|
93
|
+
"authority": "ai_generated",
|
|
94
|
+
"review_only": true,
|
|
95
|
+
"approval": false
|
|
96
|
+
}
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "motionloom",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@11.20.0",
|
|
6
6
|
"description": "MotionLoom: project-aware animation Skill with Agent interoperability, graph, provenance, Motion IR, runtime telemetry, Visual Truth, Remediation Learning, DSSE-compatible signed attestation, external evidence verification and Dev Lab review.",
|
|
@@ -60,9 +60,18 @@
|
|
|
60
60
|
"schemas/control-track.schema.json",
|
|
61
61
|
"schemas/export-manifest.schema.json",
|
|
62
62
|
"schemas/artifact-adapter-registry.schema.json",
|
|
63
|
+
"schemas/action-separation-verifier-evidence.schema.json",
|
|
64
|
+
"schemas/asset-generation-request.schema.json",
|
|
65
|
+
"schemas/asset-generation-plan.schema.json",
|
|
66
|
+
"schemas/asset-adaptation.schema.json",
|
|
67
|
+
"scripts/asset-adapt.mjs",
|
|
63
68
|
"artifact-adapter-registry.json",
|
|
64
69
|
"examples/agent-consumer/artifact-intake",
|
|
65
70
|
"tests/scripts/test_artifact_intake.py",
|
|
71
|
+
"scripts/asset-generation-plan.py",
|
|
72
|
+
"examples/agent-consumer/asset-planning",
|
|
73
|
+
"tests/scripts/test_asset_generation_plan.py",
|
|
74
|
+
"tests/scripts/test_asset_adapt.py",
|
|
66
75
|
"scripts/runtime-candidate.py",
|
|
67
76
|
"schemas/runtime-candidate.schema.json",
|
|
68
77
|
"examples/agent-consumer/runtime-candidate",
|
|
@@ -76,11 +85,11 @@
|
|
|
76
85
|
"scripts/rive-package-gate.py",
|
|
77
86
|
"schemas/rive-package-manifest.schema.json",
|
|
78
87
|
"examples/agent-consumer/rive-package-gate",
|
|
79
|
-
"tests/scripts/test_rive_package_gate.py"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
"tests/scripts/test_rive_package_gate.py",
|
|
89
|
+
"scripts/build-ai-pilot.py",
|
|
90
|
+
"scripts/isolate-alpha-background.py",
|
|
91
|
+
"tests/scripts/test_ai_pilot_builder.py",
|
|
92
|
+
"tests/scripts/test_alpha_isolation.py"
|
|
84
93
|
],
|
|
85
94
|
"keywords": [
|
|
86
95
|
"animation",
|
|
@@ -133,6 +142,7 @@
|
|
|
133
142
|
"test": "python3 tests/scripts/run_tests.py",
|
|
134
143
|
"validate": "npm run quality:attestation",
|
|
135
144
|
"doctor": "node bin/motionloom.mjs doctor --json",
|
|
145
|
+
"doctor:runtime": "node bin/motionloom.mjs doctor --runtime --json",
|
|
136
146
|
"init": "node bin/motionloom.mjs init",
|
|
137
147
|
"setup": "node bin/motionloom.mjs setup",
|
|
138
148
|
"setup:dry": "node bin/motionloom.mjs setup --dry-run",
|
|
@@ -181,12 +191,16 @@
|
|
|
181
191
|
"asset-provenance:check": "node bin/motionloom.mjs asset-provenance check --input examples/agent-consumer/ai-generated-pilot-provenance.json --root examples/agent-consumer --mode runtime --json",
|
|
182
192
|
"asset:consistency": "node bin/motionloom.mjs asset-consistency validate --kind frame-geometry --input examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json --root examples/agent-consumer/asset-consistency --json",
|
|
183
193
|
"asset:audit": "node bin/motionloom.mjs asset-consistency report --kind frame-geometry --input examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json --root examples/agent-consumer/asset-consistency --json",
|
|
194
|
+
"action:separation": "node bin/motionloom.mjs action-separation validate --input examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json --root examples/agent-consumer/asset-consistency --json",
|
|
195
|
+
"asset:plan": "node bin/motionloom.mjs asset-generation-plan plan --request examples/agent-consumer/asset-planning/pixellab-hero-256x448-request.json --project-root . --json",
|
|
196
|
+
"asset:adapt:smoke": "node bin/motionloom.mjs asset-adapt pad --input examples/agent-consumer/asset-consistency/assets/hero-frame-00.png --output /tmp/motionloom-adapt-smoke.png --width 16 --height 16 --anchor center --report /tmp/motionloom-adapt-smoke.json --json",
|
|
197
|
+
"frame:preflight": "node bin/motionloom.mjs frame-set-preflight --input examples/agent-consumer/asset-consistency/hero-walk-frame-geometry.json --root examples/agent-consumer/asset-consistency --action-manifest examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json --json",
|
|
184
198
|
"artifact:intake": "node bin/motionloom.mjs artifact-intake intake --root . --registry artifact-adapter-registry.json --receipt examples/agent-consumer/artifact-intake/hero-motion-receipt.json --controls examples/agent-consumer/artifact-intake/hero-motion-controls.json --export-manifest examples/agent-consumer/artifact-intake/hero-motion-export.json --json",
|
|
185
199
|
"runtime:candidate": "node bin/motionloom.mjs runtime-candidate validate --root . --input examples/agent-consumer/runtime-candidate/hero-walk-candidate.json --json",
|
|
186
200
|
"rig:compatibility": "node bin/motionloom.mjs rig-compatibility validate --root . --registry rig-adapter-registry.json --input examples/agent-consumer/rig-compatibility/hero-walk-fixture-rig.json --json",
|
|
187
|
-
"rive:gate": "node bin/motionloom.mjs rive-package-gate --help"
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
"rive:gate": "node bin/motionloom.mjs rive-package-gate --help",
|
|
202
|
+
"pilot:build": "node bin/motionloom.mjs pilot-build --help",
|
|
203
|
+
"alpha:isolate": "node bin/motionloom.mjs alpha-isolate --help"
|
|
190
204
|
},
|
|
191
205
|
"dependencies": {
|
|
192
206
|
"@lottiefiles/dotlottie-web": "^0.79.0",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Multi-frame asset generation discipline
|
|
2
|
+
|
|
3
|
+
This contract applies whenever MotionLoom or an Agent creates two or more source frames for the same character, object, effect or other frame-based animation. It is automatic: the user should not have to ask for consistent frame geometry, isolated source frames or atlas hygiene.
|
|
4
|
+
|
|
5
|
+
## Plan the provider before generating
|
|
6
|
+
|
|
7
|
+
Before calling PixelLab, ImageGen, ChatGPT or another provider, create a provider-neutral request that describes the real runtime target. The request must include the target canvas, frame count, alpha mode, pixel-art policy, action cues and whether one source frame per request is required:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
motionloom asset-generation-plan plan \
|
|
11
|
+
--request examples/agent-consumer/asset-planning/pixellab-hero-256x448-request.json \
|
|
12
|
+
--project-root . --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Treat the result as a MotionLoom recommendation, not an approval. The planner first assesses the project/task, then separates `recommendation_status` from `execution_status`. A provisional or scaffold route may remain a useful normal-planning recommendation when its project fit is strong; strict execution still requires the active execution policy. User preference is visible and may influence ranking, but cannot override a hard incompatibility. For example, PixelLab animation tools documented with square canvases can be used as a provisional source for a `256x448` target only through an explicit transparent-padding plan and post-export action/frame validation; the planner must not silently crop, stretch, or claim that batch output satisfies per-frame isolation. If no route is execution-eligible, normal planning should still expose safe provisional/manual options while strict mode fails closed.
|
|
16
|
+
|
|
17
|
+
The registry is provider-neutral and metadata-driven. A provider adapter remains `scaffold_only` until real export bytes, generation receipt, target-runtime evidence and human review have been recorded. Availability is separate from registry existence: an unknown or unavailable route may be shown as a MotionLoom option, but the Agent must resolve connectivity before execution. Bearer tokens belong in the connector/secret layer and never in requests, prompts, receipts or project manifests.
|
|
18
|
+
|
|
19
|
+
## Default source policy
|
|
20
|
+
|
|
21
|
+
Generated source frames are **isolated-frame assets**, not a pose sheet.
|
|
22
|
+
|
|
23
|
+
- Generate exactly one animation source frame per image/canvas by default.
|
|
24
|
+
- For long actions (including actions with six or more frames), never ask an image generator for one contact sheet, sprite sheet or multi-pose canvas and then crop the poses out as production source frames.
|
|
25
|
+
- Do not pack an atlas or sprite sheet until every isolated source frame has passed identity and frame-geometry validation.
|
|
26
|
+
- Imported third-party atlases may still use the atlas contract; this rule is about Agent-generated source material.
|
|
27
|
+
|
|
28
|
+
If a generator cannot preserve isolated source canvases reliably, regenerate in smaller isolated calls or use provider outputs that are already separate files. Do not convert one contaminated multi-pose canvas into accepted source frames merely because crops can be extracted from it.
|
|
29
|
+
|
|
30
|
+
This avoids neighboring poses leaking into a crop, shared-canvas bleed, accidental partial limbs from adjacent frames and ambiguous ownership of opaque pixels.
|
|
31
|
+
|
|
32
|
+
## Lock geometry before the second frame
|
|
33
|
+
|
|
34
|
+
Before generating frame 2, establish one canonical geometry lock from the accepted identity/reference frame and keep it unchanged for the rest of the action:
|
|
35
|
+
|
|
36
|
+
- exact canvas width and height;
|
|
37
|
+
- transparent alpha mode and color space;
|
|
38
|
+
- camera and left/right orientation;
|
|
39
|
+
- target apparent character scale;
|
|
40
|
+
- pivot point;
|
|
41
|
+
- baseline / footline;
|
|
42
|
+
- safe rectangle and transparent guard band;
|
|
43
|
+
- palette/style/identity reference;
|
|
44
|
+
- frame naming and action order.
|
|
45
|
+
|
|
46
|
+
Create or update the matching `action-set` and `frame-geometry` contracts before continuing the sequence. The first accepted frame is the geometry anchor; later poses may move limbs, but they must not silently change canvas size, camera, character scale, pivot or baseline.
|
|
47
|
+
|
|
48
|
+
For AI-generated frames, every subsequent generation request should carry the same locked identity reference and geometry requirements. When the image tool can accept an image reference, reuse the accepted identity/anchor frame rather than relying on prose alone.
|
|
49
|
+
|
|
50
|
+
## Frame Generation Lock
|
|
51
|
+
|
|
52
|
+
Do not rely on the Agent remembering the same geometry prose across many provider calls. Persist the generation-side constraints in `frame-generation-lock.json` using `schemas/frame-generation-lock.schema.json`.
|
|
53
|
+
|
|
54
|
+
The lock binds:
|
|
55
|
+
|
|
56
|
+
- the exact identity/accepted-frame reference path and SHA-256;
|
|
57
|
+
- canvas dimensions, color space and alpha mode;
|
|
58
|
+
- center, pivot, footline, safe rectangle and minimum transparent padding;
|
|
59
|
+
- target measured alpha-bounds plus permitted width/height drift;
|
|
60
|
+
- appearance features that must be preserved and changes that are forbidden;
|
|
61
|
+
- a hard source policy of one isolated frame per image, no pose sheet and no silent post-generation resize;
|
|
62
|
+
- every `frame_id`, its pose intent and its unique PNG output path;
|
|
63
|
+
- the post-generation `frame-geometry` contract used by deterministic preflight;
|
|
64
|
+
- a review-only trust boundary with `approval: false`.
|
|
65
|
+
|
|
66
|
+
Before a provider call, validate the lock and compose the exact frame instruction from it:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
motionloom frame-generation-lock validate \
|
|
70
|
+
--input src/output/<scene>/<action>-frame-generation-lock.json \
|
|
71
|
+
--root src/output/<scene> --json
|
|
72
|
+
|
|
73
|
+
motionloom frame-generation-lock compose \
|
|
74
|
+
--input src/output/<scene>/<action>-frame-generation-lock.json \
|
|
75
|
+
--root src/output/<scene> --frame-id <action.frame-id> --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For batch planning, `compose-all` emits one independent instruction per frame while preserving the same lock hash and reference hash. It does **not** authorize asking a provider for one multi-frame canvas; each returned instruction still represents one isolated source image. If the selected provider only emits a batch animation, record it as provisional and create independent envelopes after export; do not promote it to an isolated-source pass automatically.
|
|
79
|
+
|
|
80
|
+
If the reference bytes change, an output path escapes the asset root, two frames target the same PNG, the lock permits a pose sheet/post-resize, or the trust boundary is weakened, validation fails closed before generation.
|
|
81
|
+
|
|
82
|
+
## Keep action identity across isolated frames
|
|
83
|
+
|
|
84
|
+
Isolated canvases prevent neighboring pixels from leaking into a crop, but they do not prove that a frame belongs to the intended action. For every multi-frame action, use an action-scoped manifest with one immutable `sequence_id`, `action_id`, `identity_lock_sha256`, ordered `frame_index` values and explicit `forbidden_action_ids`. Each image must have a frame envelope binding those fields to the image SHA-256.
|
|
85
|
+
|
|
86
|
+
The action verifier must record `expected_action`, `top_competitor`, `margin`, `threshold` and `status`. A generator-created envelope is only declared evidence; independent verification requires a separate hash-bound verifier artifact. A low margin, competitor mismatch or missing verifier provenance is **quarantined**, not silently relabeled or moved to another action. Validate the manifest before packing:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
motionloom action-separation validate \\
|
|
90
|
+
--input src/output/<scene>/<action>-action-manifest.json \\
|
|
91
|
+
--root src/output/<scene> --json
|
|
92
|
+
|
|
93
|
+
motionloom frame-set-preflight \\
|
|
94
|
+
--input src/output/<scene>/<action>-frame-geometry.json \\
|
|
95
|
+
--root src/output/<scene> \\
|
|
96
|
+
--action-manifest src/output/<scene>/<action>-action-manifest.json --json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A contact sheet may be generated after isolated frames pass as a review projection, but it is never the production source. When one frame is ambiguous, regenerate that frame from the accepted anchor and the same action lock; do not auto-move it, use a neighboring action as a reference, or let the agent infer its action from the filename.
|
|
100
|
+
|
|
101
|
+
See [`docs/ACTION-SEPARATION.md`](../docs/ACTION-SEPARATION.md) for the manifest/envelope contract and quarantine policy.
|
|
102
|
+
|
|
103
|
+
## Validate incrementally
|
|
104
|
+
|
|
105
|
+
Do not generate the complete action and only inspect it at the end. After each candidate frame:
|
|
106
|
+
|
|
107
|
+
1. verify the image is an isolated source canvas rather than a shared pose sheet;
|
|
108
|
+
2. verify exact canvas dimensions and alpha transparency;
|
|
109
|
+
3. measure the real alpha bounding box from pixels;
|
|
110
|
+
4. compare apparent size against the anchor within the declared `bbox_drift_tolerance_px`;
|
|
111
|
+
5. verify pivot and footline tolerance;
|
|
112
|
+
6. verify the alpha bounding box remains inside `safe_rect` with the declared `bleed_margin_px` guard band;
|
|
113
|
+
7. reject unexpected opaque contamination;
|
|
114
|
+
8. only then accept the frame and continue.
|
|
115
|
+
|
|
116
|
+
Use:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
motionloom frame-set-preflight \
|
|
120
|
+
--input src/output/<scene>/<action>-frame-geometry.json \
|
|
121
|
+
--root src/output/<scene> --json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The preflight is intentionally stricter than a visual warning: shared source images, non-isolated frame rectangles, scale drift beyond the declared tolerance, guard-band violations, pivot/footline drift, hash mismatch or other deterministic frame-geometry failures block the sequence.
|
|
125
|
+
|
|
126
|
+
If one frame fails, regenerate or repair **that frame only** using the same generation lock. Do not silently rescale every previously accepted frame to match a bad frame, and do not weaken tolerances merely to make the set pass.
|
|
127
|
+
|
|
128
|
+
## Apparent-size consistency
|
|
129
|
+
|
|
130
|
+
Equal canvas dimensions are not enough. A 1920 × 1920 PNG can still contain a character that is 20% smaller than the previous frame. MotionLoom therefore treats measured alpha-bounds drift as a real source defect when it exceeds the declared tolerance.
|
|
131
|
+
|
|
132
|
+
Pose motion can legitimately change the bounding box, so choose a tolerance that allows expected limb extension while preventing whole-character zoom drift. For actions with large reaches, jumps or weapons, lock the body pivot/footline and define a safe rectangle large enough for those poses instead of changing the character scale between frames.
|
|
133
|
+
|
|
134
|
+
For pixel art, any intentional normalization step must preserve nearest-neighbor pixel density and the locked pivot/baseline. Large scale corrections should trigger regeneration rather than interpolation.
|
|
135
|
+
|
|
136
|
+
## Packing happens last
|
|
137
|
+
|
|
138
|
+
Only after every source frame passes preflight may the Agent build a sprite sheet or atlas. The packed result must then pass the atlas contract: no region overlap, no opaque pixels outside declared regions when required, explicit padding/extrusion, and no ambiguous neighboring-frame bleed.
|
|
139
|
+
|
|
140
|
+
Source-frame acceptance and atlas acceptance are separate gates. A clean atlas cannot repair inconsistent source-frame scale, and consistent source frames do not prove the packed atlas is clean.
|
|
141
|
+
|
|
142
|
+
## Agent decision rule
|
|
143
|
+
|
|
144
|
+
When the user asks for an animation such as idle, walk, run, attack, hurt, jump or any project-defined multi-frame action, the Agent should apply this contract automatically. Do not ask the user whether they want frame consistency checks or a generation lock; they are part of the default MotionLoom workflow. Only ask the user when a genuine artistic decision is required, such as choosing between materially different silhouettes or motion intent.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://motionloom.dev/schemas/action-separation-verifier-evidence.schema.json",
|
|
4
|
+
"title": "MotionLoom Action Separation Verifier Evidence",
|
|
5
|
+
"description": "Separate verifier artifact bound to one frame result. Hashes verify binding integrity, not the truthfulness of an asserted producer identity.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "contract", "sequence_id", "action_id", "frame_id", "frame_index", "image_sha256", "identity_lock_sha256", "generator_agent_id", "verifier_id", "result", "result_sha256", "provenance"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": {"const": "0.1"},
|
|
11
|
+
"contract": {"const": "action-separation-verifier-evidence"},
|
|
12
|
+
"sequence_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,95}$"},
|
|
13
|
+
"action_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
14
|
+
"frame_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
15
|
+
"frame_index": {"type": "integer", "minimum": 0},
|
|
16
|
+
"image_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
17
|
+
"identity_lock_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
18
|
+
"generator_agent_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
19
|
+
"verifier_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
20
|
+
"result": {
|
|
21
|
+
"type": "object", "additionalProperties": false,
|
|
22
|
+
"required": ["expected_action", "top_competitor", "margin", "threshold", "status", "method"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"expected_action": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
25
|
+
"top_competitor": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
26
|
+
"margin": {"type": "number", "minimum": 0},
|
|
27
|
+
"threshold": {"type": "number", "minimum": 0},
|
|
28
|
+
"status": {"enum": ["pass", "quarantined"]},
|
|
29
|
+
"method": {"type": "string", "minLength": 1}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"result_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
33
|
+
"provenance": {
|
|
34
|
+
"type": "object", "additionalProperties": false,
|
|
35
|
+
"required": ["kind", "producer_id", "run_id"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"kind": {"const": "separate_verifier_artifact"},
|
|
38
|
+
"producer_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
39
|
+
"run_id": {"type": "string", "minLength": 1}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://motionloom.dev/schemas/action-sequence-manifest.schema.json",
|
|
4
|
+
"title": "MotionLoom Action Sequence Manifest",
|
|
5
|
+
"description": "Manifest binding for isolated frame sequences and explicitly classified action-separation evidence.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "sequence_id", "asset_identity", "action_id", "identity_lock_sha256", "generator_agent_id", "forbidden_action_ids", "actions", "frames", "status", "approval"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": {"const": "0.2"},
|
|
11
|
+
"sequence_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,95}$"},
|
|
12
|
+
"asset_identity": {"type": "string", "minLength": 1},
|
|
13
|
+
"action_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
14
|
+
"identity_lock_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
15
|
+
"generator_agent_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
16
|
+
"forbidden_action_ids": {
|
|
17
|
+
"type": "array", "minItems": 1, "uniqueItems": true,
|
|
18
|
+
"items": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"}
|
|
19
|
+
},
|
|
20
|
+
"actions": {
|
|
21
|
+
"type": "array", "minItems": 2,
|
|
22
|
+
"items": {
|
|
23
|
+
"type": "object", "additionalProperties": false,
|
|
24
|
+
"required": ["action_id", "positive_cues", "negative_cues"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"action_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
27
|
+
"positive_cues": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
28
|
+
"negative_cues": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"frames": {
|
|
33
|
+
"type": "array", "minItems": 2,
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "object", "additionalProperties": false,
|
|
36
|
+
"required": ["frame_index", "frame_id", "image", "envelope"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"frame_index": {"type": "integer", "minimum": 0},
|
|
39
|
+
"frame_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$"},
|
|
40
|
+
"image": {"type": "string", "pattern": "^.+\\.png$"},
|
|
41
|
+
"envelope": {"type": "string", "pattern": "^.+\\.json$"}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"status": {"enum": ["review_required", "quarantined"]},
|
|
46
|
+
"approval": {"const": false}
|
|
47
|
+
}
|
|
48
|
+
}
|