motionloom 2.0.0 → 2.2.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/skills/motionloom/SKILL.md +14 -0
- package/.claude/skills/motionloom.md +5 -0
- package/.codex/skills/motionloom.md +11 -0
- package/AGENTS.md +17 -0
- package/CHANGELOG.md +68 -0
- package/CODE_OF_CONDUCT.md +19 -0
- package/CONTRIBUTING.md +65 -0
- package/README.md +193 -134
- package/ROADMAP.md +36 -0
- package/SECURITY.md +28 -0
- package/SKILL.md +57 -9
- package/SUPPORT.md +23 -0
- package/agent-card.json +42 -6
- package/agent-surfaces.json +79 -0
- package/bin/motionloom.mjs +33 -5
- package/docs/AGENT-INTEGRATION.md +47 -0
- package/docs/CHECKLIST.md +2 -1
- package/docs/STATUS.md +33 -0
- package/docs/audits/2.1.0-deep-stress-evaluation.md +97 -0
- package/docs/audits/ci-replay-remediation-2026-08-13.md +33 -0
- package/docs/audits/data/2.1.0-deep-stress-6900.json +329 -0
- package/docs/audits/data/deep-stress-latest.json +329 -0
- package/docs/audits/external-project-corpus-2026-08-13.md +26 -0
- package/docs/releases/2.1.0.md +23 -0
- package/docs/releases/2.2.0.md +35 -0
- package/docs/releases/npm-publish-from-workstation.md +6 -6
- package/examples/agent-consumer/README.md +18 -0
- package/examples/agent-consumer/fixture-manifest.json +82 -0
- package/package.json +69 -28
- package/references/agent-interoperability.md +29 -0
- package/references/intelligence-core.md +5 -1
- package/schemas/agent-surfaces.schema.json +78 -0
- package/schemas/project-memory.schema.json +180 -0
- package/schemas/remediation-history.schema.json +23 -0
- package/schemas/scene-manifest.schema.json +1 -0
- package/schemas/visual-truth.schema.json +80 -0
- package/scripts/analyze.py +56 -0
- package/scripts/capture-runtime-telemetry.py +119 -0
- package/scripts/devlab.py +126 -0
- package/scripts/discovery.py +257 -0
- package/scripts/docs-audit.py +112 -0
- package/scripts/eval-intelligence.py +23 -0
- package/scripts/eval-projects.py +156 -0
- package/scripts/intelligence.py +106 -6
- package/scripts/pr.py +151 -0
- package/scripts/prepack-clean.mjs +37 -0
- package/scripts/project-memory.py +483 -0
- package/scripts/project_memory_loader.py +31 -0
- package/scripts/quality-gate.py +43 -3
- package/scripts/release-verify.py +52 -0
- package/scripts/remediation-learning.py +326 -0
- package/scripts/render.py +65 -0
- package/scripts/report.py +60 -2
- package/scripts/review-hook.py +13 -2
- package/scripts/skill-doctor.py +12 -2
- package/scripts/to-dotlottie.mjs +26 -20
- package/scripts/visual-truth.py +310 -0
- package/src/core/analyzer.py +174 -25
- package/src/output/browser-review-smoke/manifest.json +1 -0
- package/src/output/browser-review-smoke/visual-truth.json +68 -0
- package/tests/evals/intelligence-cases.json +10 -0
- package/tests/evals/project-corpus.json +51 -0
- package/tests/scripts/run_tests.py +111 -1
- package/tests/scripts/test_project_memory.py +129 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"contract": "motionloom-visual-truth",
|
|
4
|
+
"generated_at": "2026-08-13T13:53:38.679496Z",
|
|
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": "467fea255a18efb8687e78a1a60662ad30d4fa6f77d9c3f3878196f3c5e670ad",
|
|
55
|
+
"runtime_evidence_path": "artifacts/browser-review-smoke-task/runtime-adapters/runtime-evidence.json",
|
|
56
|
+
"runtime_evidence_sha256": "2d51a37dbd1962c3f8539fed53fd5709448da55f2b00f2ceb37dfd8a55d0d716",
|
|
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
|
+
}
|
|
@@ -126,6 +126,16 @@
|
|
|
126
126
|
"id": "p2-attestation-unknown-signer",
|
|
127
127
|
"class": "negative",
|
|
128
128
|
"assertion": "A signature whose key id is absent from the configured trust policy is rejected."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "project-corpus-first-party-pass",
|
|
132
|
+
"class": "positive",
|
|
133
|
+
"assertion": "The analyzer recognizes the first-party MotionLoom project through the labeled corpus harness."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "project-corpus-insufficient-external-explicit",
|
|
137
|
+
"class": "negative",
|
|
138
|
+
"assertion": "Missing external project checkouts are reported as insufficient evidence instead of being treated as product-value proof."
|
|
129
139
|
}
|
|
130
140
|
]
|
|
131
141
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"corpus_id": "motionloom-project-corpus-v1",
|
|
4
|
+
"description": "Provenance-labeled analyzer corpus. External checkouts are opt-in and are never fetched by MotionLoom CI.",
|
|
5
|
+
"required_external_projects": 3,
|
|
6
|
+
"projects": [
|
|
7
|
+
{
|
|
8
|
+
"id": "motionloom-first-party",
|
|
9
|
+
"class": "first-party",
|
|
10
|
+
"external": false,
|
|
11
|
+
"scope": "repository",
|
|
12
|
+
"local_path": ".",
|
|
13
|
+
"source": "https://github.com/lenhonbp/MotionLoom",
|
|
14
|
+
"expected": {
|
|
15
|
+
"name": "motionloom"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "motion-one-external",
|
|
20
|
+
"class": "external",
|
|
21
|
+
"external": true,
|
|
22
|
+
"local_path": "external/framer-motion",
|
|
23
|
+
"source": "https://github.com/motiondivision/motion",
|
|
24
|
+
"expected": {
|
|
25
|
+
"name": "motion-one",
|
|
26
|
+
"framework": "motion"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "gsap-external",
|
|
31
|
+
"class": "external",
|
|
32
|
+
"external": true,
|
|
33
|
+
"local_path": "external/gsap",
|
|
34
|
+
"source": "https://github.com/greensock/GSAP",
|
|
35
|
+
"expected": {
|
|
36
|
+
"framework": "gsap"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "rive-external",
|
|
41
|
+
"class": "external",
|
|
42
|
+
"external": true,
|
|
43
|
+
"local_path": "external/rive",
|
|
44
|
+
"source": "https://github.com/rive-app/rive-react",
|
|
45
|
+
"expected": {
|
|
46
|
+
"name": "rive-react",
|
|
47
|
+
"framework": "rive"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -49,6 +49,45 @@ def test_analyzer_on_fixture():
|
|
|
49
49
|
check("analyzer extracts brand primary", ctx.get("brand", {}).get("primary") == "#2563EB")
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def test_analyzer_scan_budget_and_ignore_rules():
|
|
53
|
+
with tempfile.TemporaryDirectory() as td:
|
|
54
|
+
root = Path(td)
|
|
55
|
+
(root / "package.json").write_text(json.dumps({"name": "bounded-fixture", "dependencies": {"gsap": "^3.0.0"}}))
|
|
56
|
+
(root / "ignored-dir").mkdir()
|
|
57
|
+
(root / "ignored-dir" / "ignored.ts").write_text("duration: 999s")
|
|
58
|
+
(root / "src").mkdir()
|
|
59
|
+
for index in range(5):
|
|
60
|
+
(root / "src" / f"scene-{index}.tsx").write_text("const easing = 'ease-out';")
|
|
61
|
+
context = root / "context.json"
|
|
62
|
+
result = subprocess.run([
|
|
63
|
+
sys.executable, str(ROOT / "scripts/analyze.py"), str(root),
|
|
64
|
+
"--output", str(context), "--max-files", "2", "--max-bytes", "100000",
|
|
65
|
+
"--max-seconds", "10", "--ignore-dir", "ignored-dir",
|
|
66
|
+
], capture_output=True, text=True)
|
|
67
|
+
data = json.loads(context.read_text())
|
|
68
|
+
scan = data.get("scan", {})
|
|
69
|
+
check("bounded analyzer exits cleanly", result.returncode == 0, result.stderr)
|
|
70
|
+
check("bounded analyzer emits scan metadata", scan.get("files_scanned") == 2)
|
|
71
|
+
check("bounded analyzer exposes truncation", data.get("scan_truncated") is True and scan.get("scan_truncated") is True)
|
|
72
|
+
check("bounded analyzer records file limit", "max_files" in scan.get("truncation_reasons", []))
|
|
73
|
+
check("bounded analyzer records ignored directory", "ignored-dir" in scan.get("ignored_directories", []))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_project_corpus_harness_is_explicit_about_evidence():
|
|
77
|
+
result = subprocess.run([
|
|
78
|
+
sys.executable, str(ROOT / "scripts/eval-projects.py"),
|
|
79
|
+
"--workspace", str(ROOT), "--require-external", "0", "--allow-insufficient",
|
|
80
|
+
], capture_output=True, text=True)
|
|
81
|
+
report = json.loads(result.stdout)
|
|
82
|
+
check("project corpus first-party evaluation passes", result.returncode == 0 and report.get("status") == "pass")
|
|
83
|
+
strict = subprocess.run([
|
|
84
|
+
sys.executable, str(ROOT / "scripts/eval-projects.py"),
|
|
85
|
+
"--workspace", str(ROOT), "--allow-insufficient",
|
|
86
|
+
], capture_output=True, text=True)
|
|
87
|
+
strict_report = json.loads(strict.stdout)
|
|
88
|
+
check("project corpus missing external evidence is explicit", strict.returncode == 0 and strict_report.get("status") == "insufficient_evidence")
|
|
89
|
+
|
|
90
|
+
|
|
52
91
|
def test_spec_generate_and_validate():
|
|
53
92
|
with tempfile.TemporaryDirectory() as td:
|
|
54
93
|
ctx = Path(td) / "project-context.json"
|
|
@@ -678,7 +717,7 @@ def test_p1_semantic_continuity_fix_plan():
|
|
|
678
717
|
task.update({"task_id": task_id, "scene": scene, "scene_order": order, "project_name": "p1-continuity"})
|
|
679
718
|
(path / "task.json").write_text(json.dumps(task, indent=2) + "\n")
|
|
680
719
|
ir = json.loads((path / "motion-ir.json").read_text())
|
|
681
|
-
ir.update({"task_id": task_id, "scene": scene, "context_hash": task.get("context_hash")})
|
|
720
|
+
ir.update({"task_id": task_id, "scene": scene, "context_hash": task.get("context_hash") or ir.get("context_hash")})
|
|
682
721
|
(path / "motion-ir.json").write_text(json.dumps(ir, indent=2) + "\n")
|
|
683
722
|
|
|
684
723
|
continuity = subprocess.run([
|
|
@@ -801,6 +840,18 @@ def test_observability_contract():
|
|
|
801
840
|
], capture_output=True, text=True)
|
|
802
841
|
check("review rejects foreign task candidate", foreign_review.returncode != 0 and "task_id" in foreign_review.stderr)
|
|
803
842
|
|
|
843
|
+
|
|
844
|
+
def test_quality_workflow_rebuilds_replay_after_generated_artifacts():
|
|
845
|
+
workflow = (ROOT / ".github/workflows/quality.yml").read_text(encoding="utf-8")
|
|
846
|
+
capture = workflow.find("python3 scripts/intelligence.py replay capture")
|
|
847
|
+
render = workflow.find("- name: Render runtime snapshot frames for changed scenes")
|
|
848
|
+
gate = workflow.find("- name: Enforce context-bound quality gate")
|
|
849
|
+
check("quality workflow rebuilds replay bundle", capture >= 0)
|
|
850
|
+
check("replay rebuild follows runtime rendering", render >= 0 and render < capture)
|
|
851
|
+
check("replay rebuild precedes quality gate", gate >= 0 and capture < gate)
|
|
852
|
+
check("replay rebuild binds task directory and root", '--task-dir "$task_dir" --root .' in workflow)
|
|
853
|
+
check("replay rebuild writes canonical task bundle path", '--output "$task_dir/replay-bundle.json"' in workflow)
|
|
854
|
+
|
|
804
855
|
doctor = subprocess.run([sys.executable, str(ROOT / "scripts/skill-doctor.py"), "--json"], capture_output=True, text=True)
|
|
805
856
|
doctor_data = json.loads(doctor.stdout)
|
|
806
857
|
check("skill doctor passes package structure", doctor.returncode == 0 and doctor_data.get("status") == "pass")
|
|
@@ -815,10 +866,68 @@ def test_observability_contract():
|
|
|
815
866
|
attestation_tests.returncode == 0 and "attestation contract tests: PASS" in attestation_tests.stdout,
|
|
816
867
|
)
|
|
817
868
|
|
|
869
|
+
memory_tests = subprocess.run(
|
|
870
|
+
[sys.executable, str(ROOT / "tests/scripts/test_project_memory.py")],
|
|
871
|
+
capture_output=True,
|
|
872
|
+
text=True,
|
|
873
|
+
)
|
|
874
|
+
check(
|
|
875
|
+
"project memory recovery and cross-platform contract passes",
|
|
876
|
+
memory_tests.returncode == 0 and "project memory contract tests: PASS" in memory_tests.stdout,
|
|
877
|
+
)
|
|
878
|
+
discovery_tests = subprocess.run(
|
|
879
|
+
[sys.executable, str(ROOT / "tests/scripts/test_discovery.py")],
|
|
880
|
+
capture_output=True,
|
|
881
|
+
text=True,
|
|
882
|
+
)
|
|
883
|
+
check(
|
|
884
|
+
"Agent discovery and installation contract passes",
|
|
885
|
+
discovery_tests.returncode == 0 and "discovery contract tests: PASS" in discovery_tests.stdout,
|
|
886
|
+
)
|
|
887
|
+
consumer_tests = subprocess.run(
|
|
888
|
+
[sys.executable, str(ROOT / "tests/scripts/test_consumer_fixtures.py")],
|
|
889
|
+
capture_output=True,
|
|
890
|
+
text=True,
|
|
891
|
+
)
|
|
892
|
+
check(
|
|
893
|
+
"Agent consumer fixture contract passes",
|
|
894
|
+
consumer_tests.returncode == 0 and "consumer fixture tests: PASS" in consumer_tests.stdout,
|
|
895
|
+
)
|
|
896
|
+
matrix_tests = subprocess.run(
|
|
897
|
+
[sys.executable, str(ROOT / "tests/scripts/test_installation_matrix.py")],
|
|
898
|
+
capture_output=True,
|
|
899
|
+
text=True,
|
|
900
|
+
)
|
|
901
|
+
check(
|
|
902
|
+
"cross-platform installation matrix contract passes",
|
|
903
|
+
matrix_tests.returncode == 0 and "installation matrix tests: PASS" in matrix_tests.stdout,
|
|
904
|
+
)
|
|
905
|
+
visual_tests = subprocess.run(
|
|
906
|
+
[sys.executable, str(ROOT / "tests/scripts/test_visual_truth.py")],
|
|
907
|
+
capture_output=True,
|
|
908
|
+
text=True,
|
|
909
|
+
)
|
|
910
|
+
check(
|
|
911
|
+
"Visual Truth contract preserves provenance and review boundary",
|
|
912
|
+
visual_tests.returncode == 0 and "visual truth contract tests: PASS" in visual_tests.stdout,
|
|
913
|
+
)
|
|
914
|
+
remediation_tests = subprocess.run(
|
|
915
|
+
[sys.executable, str(ROOT / "tests/scripts/test_remediation_learning.py")],
|
|
916
|
+
capture_output=True,
|
|
917
|
+
text=True,
|
|
918
|
+
)
|
|
919
|
+
check(
|
|
920
|
+
"Remediation Learning preserves append-only history and first-pass metrics",
|
|
921
|
+
remediation_tests.returncode == 0 and "remediation learning tests: PASS" in remediation_tests.stdout,
|
|
922
|
+
remediation_tests.stdout.strip() or remediation_tests.stderr.strip(),
|
|
923
|
+
)
|
|
924
|
+
|
|
818
925
|
|
|
819
926
|
if __name__ == "__main__":
|
|
820
927
|
print("== MotionLoom engine tests ==")
|
|
821
928
|
test_analyzer_on_fixture()
|
|
929
|
+
test_analyzer_scan_budget_and_ignore_rules()
|
|
930
|
+
test_project_corpus_harness_is_explicit_about_evidence()
|
|
822
931
|
test_spec_generate_and_validate()
|
|
823
932
|
test_rig_build_and_pose()
|
|
824
933
|
test_lottie_scaffold_valid()
|
|
@@ -836,6 +945,7 @@ if __name__ == "__main__":
|
|
|
836
945
|
sys.path.insert(0, str(ROOT))
|
|
837
946
|
test_category_coverage()
|
|
838
947
|
test_observability_contract()
|
|
948
|
+
test_quality_workflow_rebuilds_replay_after_generated_artifacts()
|
|
839
949
|
print()
|
|
840
950
|
if FAILED:
|
|
841
951
|
print(f"{len(FAILED)} test(s) FAILED: {', '.join(FAILED)}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Project Memory recovery and cross-platform path contract tests."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
16
|
+
CLI = ROOT / "scripts" / "project-memory.py"
|
|
17
|
+
NODE_CLI = ROOT / "bin" / "motionloom.mjs"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_memory(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
21
|
+
return subprocess.run(
|
|
22
|
+
[sys.executable, str(CLI), *args, "--project-root", str(root)],
|
|
23
|
+
cwd=ROOT,
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
check=False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_memory_recovery_and_relocation() -> None:
|
|
31
|
+
with tempfile.TemporaryDirectory(prefix="motionloom memory ") as td:
|
|
32
|
+
root = Path(td) / "Dự án MotionLoom"
|
|
33
|
+
root.mkdir(parents=True)
|
|
34
|
+
(root / "package.json").write_text(json.dumps({"name": "memory-fixture"}), encoding="utf-8")
|
|
35
|
+
(root / "project-context.json").write_text(json.dumps({
|
|
36
|
+
"schema_version": "2.0",
|
|
37
|
+
"name": "memory-fixture",
|
|
38
|
+
"generated_at": "2026-08-13T00:00:00Z",
|
|
39
|
+
}), encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
created = run_memory(root, "init", "--context-path", "project-context.json", "--json")
|
|
42
|
+
assert created.returncode == 0, created.stderr
|
|
43
|
+
memory_path = root / ".motionloom" / "project-memory.json"
|
|
44
|
+
assert memory_path.is_file()
|
|
45
|
+
|
|
46
|
+
decision = run_memory(root, "record-decision", "--id", "ease-ui", "--status", "accepted", "--summary", "Use ease-out for UI entry", "--user-confirmed", "--json")
|
|
47
|
+
assert decision.returncode == 0, decision.stderr
|
|
48
|
+
|
|
49
|
+
rejected = run_memory(root, "record-decision", "--id", "linear-ui", "--status", "rejected", "--summary", "Do not use linear easing for UI", "--rationale", "Perceptually mechanical", "--json")
|
|
50
|
+
assert rejected.returncode == 0, rejected.stderr
|
|
51
|
+
|
|
52
|
+
blocked_outcome = run_memory(root, "record-outcome", "--id", "fix-1", "--issue-id", "issue-1", "--summary", "Unreviewed fix", "--result", "pass")
|
|
53
|
+
assert blocked_outcome.returncode == 2
|
|
54
|
+
assert "user-confirmed" in blocked_outcome.stderr
|
|
55
|
+
|
|
56
|
+
outcome = run_memory(root, "record-outcome", "--id", "fix-1", "--issue-id", "issue-1", "--summary", "Reduce hand-track duration", "--root-cause", "timing", "--resolution", "duration 420ms", "--result", "pass", "--correction-count", "1", "--rerun-scope", "scene:onboarding-wave", "--user-confirmed", "--json")
|
|
57
|
+
assert outcome.returncode == 0, outcome.stderr
|
|
58
|
+
|
|
59
|
+
recovered = run_memory(root, "recover", "--limit", "10")
|
|
60
|
+
assert recovered.returncode == 0, recovered.stderr
|
|
61
|
+
payload = json.loads(recovered.stdout)
|
|
62
|
+
assert payload["project"]["project_id"].startswith("local:")
|
|
63
|
+
assert payload["decisions"][-1]["id"] == "linear-ui"
|
|
64
|
+
assert payload["rejected_patterns"][-1]["id"] == "linear-ui"
|
|
65
|
+
assert payload["remediation"][-1]["user_confirmed"] is True
|
|
66
|
+
assert payload["instructions"][0].endswith("user approval.")
|
|
67
|
+
assert payload["instructions"][-1].endswith("artifacts.")
|
|
68
|
+
|
|
69
|
+
moved = Path(td) / "relocated" / "MotionLoom copy"
|
|
70
|
+
moved.parent.mkdir()
|
|
71
|
+
shutil.copytree(root, moved)
|
|
72
|
+
relocated = run_memory(moved, "recover", "--limit", "10")
|
|
73
|
+
assert relocated.returncode == 0, relocated.stderr
|
|
74
|
+
relocated_payload = json.loads(relocated.stdout)
|
|
75
|
+
assert relocated_payload["project"]["root_path"] == str(moved.resolve())
|
|
76
|
+
relocated_validation = run_memory(moved, "validate", "--json")
|
|
77
|
+
assert relocated_validation.returncode == 0, relocated_validation.stderr
|
|
78
|
+
|
|
79
|
+
# Only the volatile checkout path may be rebound without changing the
|
|
80
|
+
# durable project payload; meaningful direct edits remain integrity failures.
|
|
81
|
+
rebound = json.loads((moved / ".motionloom" / "project-memory.json").read_text(encoding="utf-8"))
|
|
82
|
+
rebound["project"]["root_path"] = str(moved.parent / "another-location")
|
|
83
|
+
(moved / ".motionloom" / "project-memory.json").write_text(json.dumps(rebound), encoding="utf-8")
|
|
84
|
+
path_only = run_memory(moved, "validate", "--json")
|
|
85
|
+
assert path_only.returncode == 0, path_only.stdout + path_only.stderr
|
|
86
|
+
|
|
87
|
+
context = json.loads((moved / "project-context.json").read_text(encoding="utf-8"))
|
|
88
|
+
context["changed_by_other_task"] = True
|
|
89
|
+
(moved / "project-context.json").write_text(json.dumps(context), encoding="utf-8")
|
|
90
|
+
stale = run_memory(moved, "refresh", "--json")
|
|
91
|
+
assert stale.returncode == 10, stale.stdout + stale.stderr
|
|
92
|
+
stale_payload = json.loads(stale.stdout)
|
|
93
|
+
assert stale_payload["freshness"]["status"] == "stale"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_cross_project_recovery_is_fail_closed() -> None:
|
|
97
|
+
with tempfile.TemporaryDirectory(prefix="motionloom-project-a ") as td:
|
|
98
|
+
source = Path(td) / "source"
|
|
99
|
+
foreign = Path(td) / "foreign"
|
|
100
|
+
source.mkdir(); foreign.mkdir()
|
|
101
|
+
for root, name in ((source, "project-a"), (foreign, "project-b")):
|
|
102
|
+
(root / "package.json").write_text(json.dumps({"name": name}), encoding="utf-8")
|
|
103
|
+
(root / "project-context.json").write_text(json.dumps({"schema_version": "2.0", "name": name}), encoding="utf-8")
|
|
104
|
+
assert run_memory(source, "init").returncode == 0
|
|
105
|
+
foreign_memory = foreign / ".motionloom"
|
|
106
|
+
foreign_memory.mkdir()
|
|
107
|
+
shutil.copy2(source / ".motionloom" / "project-memory.json", foreign_memory / "project-memory.json")
|
|
108
|
+
result = run_memory(foreign, "recover")
|
|
109
|
+
assert result.returncode == 11
|
|
110
|
+
assert "identity mismatch" in result.stderr
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_node_cli_routes_memory_without_shell() -> None:
|
|
114
|
+
with tempfile.TemporaryDirectory(prefix="motionloom node cli ") as td:
|
|
115
|
+
root = Path(td)
|
|
116
|
+
(root / "package.json").write_text(json.dumps({"name": "node-cli-fixture"}), encoding="utf-8")
|
|
117
|
+
(root / "project-context.json").write_text(json.dumps({"schema_version": "2.0", "name": "node-cli-fixture"}), encoding="utf-8")
|
|
118
|
+
result = subprocess.run(["node", str(NODE_CLI), "memory", "init", "--project-root", str(root), "--json"], cwd=ROOT, capture_output=True, text=True, check=False)
|
|
119
|
+
assert result.returncode == 0, result.stderr
|
|
120
|
+
recover = subprocess.run(["node", str(NODE_CLI), "memory", "recover", "--project-root", str(root)], cwd=ROOT, capture_output=True, text=True, check=False)
|
|
121
|
+
assert recover.returncode == 0, recover.stderr
|
|
122
|
+
assert json.loads(recover.stdout)["project"]["name"] == "node-cli-fixture"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
test_memory_recovery_and_relocation()
|
|
127
|
+
test_cross_project_recovery_is_fail_closed()
|
|
128
|
+
test_node_cli_routes_memory_without_shell()
|
|
129
|
+
print("project memory contract tests: PASS")
|