@mc-and-his-agents/loom-installer 0.1.23 → 0.1.24
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/package.json +1 -1
- package/payload/manifest.json +42 -42
- package/payload/plugin/loom/skills/shared/scripts/loom_check.py +133 -9
- package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +127 -15
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +133 -9
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +127 -15
package/package.json
CHANGED
package/payload/manifest.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schema_version": "loom-installer-payload/v1",
|
|
3
3
|
"loom_version": "0.4.0",
|
|
4
4
|
"source_repository": "https://github.com/MC-and-his-Agents/Loom",
|
|
5
|
-
"source_commit": "
|
|
5
|
+
"source_commit": "9ade7b1664d19d46002d8122e8c3eabba7cf6bf4",
|
|
6
6
|
"source_ref": "main",
|
|
7
|
-
"built_at": "2026-04-26T23:
|
|
7
|
+
"built_at": "2026-04-26T23:38:43+08:00",
|
|
8
8
|
"runtime": {
|
|
9
9
|
"python_minimum": "3.10",
|
|
10
10
|
"python_recommended": "3.11+"
|
|
@@ -628,13 +628,13 @@
|
|
|
628
628
|
},
|
|
629
629
|
{
|
|
630
630
|
"path": "plugin/loom/skills/shared/scripts/loom_check.py",
|
|
631
|
-
"bytes":
|
|
632
|
-
"sha256": "
|
|
631
|
+
"bytes": 285470,
|
|
632
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
633
633
|
},
|
|
634
634
|
{
|
|
635
635
|
"path": "plugin/loom/skills/shared/scripts/loom_flow.py",
|
|
636
|
-
"bytes":
|
|
637
|
-
"sha256": "
|
|
636
|
+
"bytes": 296133,
|
|
637
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
638
638
|
},
|
|
639
639
|
{
|
|
640
640
|
"path": "plugin/loom/skills/shared/scripts/loom_init.py",
|
|
@@ -1213,13 +1213,13 @@
|
|
|
1213
1213
|
},
|
|
1214
1214
|
{
|
|
1215
1215
|
"path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py",
|
|
1216
|
-
"bytes":
|
|
1217
|
-
"sha256": "
|
|
1216
|
+
"bytes": 285470,
|
|
1217
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
1218
1218
|
},
|
|
1219
1219
|
{
|
|
1220
1220
|
"path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
|
|
1221
|
-
"bytes":
|
|
1222
|
-
"sha256": "
|
|
1221
|
+
"bytes": 296133,
|
|
1222
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
1223
1223
|
},
|
|
1224
1224
|
{
|
|
1225
1225
|
"path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -1828,13 +1828,13 @@
|
|
|
1828
1828
|
},
|
|
1829
1829
|
{
|
|
1830
1830
|
"path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py",
|
|
1831
|
-
"bytes":
|
|
1832
|
-
"sha256": "
|
|
1831
|
+
"bytes": 285470,
|
|
1832
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
1833
1833
|
},
|
|
1834
1834
|
{
|
|
1835
1835
|
"path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
|
|
1836
|
-
"bytes":
|
|
1837
|
-
"sha256": "
|
|
1836
|
+
"bytes": 296133,
|
|
1837
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
1838
1838
|
},
|
|
1839
1839
|
{
|
|
1840
1840
|
"path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -2443,13 +2443,13 @@
|
|
|
2443
2443
|
},
|
|
2444
2444
|
{
|
|
2445
2445
|
"path": "skills/loom-init/.loom-runtime/shared/scripts/loom_check.py",
|
|
2446
|
-
"bytes":
|
|
2447
|
-
"sha256": "
|
|
2446
|
+
"bytes": 285470,
|
|
2447
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
2448
2448
|
},
|
|
2449
2449
|
{
|
|
2450
2450
|
"path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
|
|
2451
|
-
"bytes":
|
|
2452
|
-
"sha256": "
|
|
2451
|
+
"bytes": 296133,
|
|
2452
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
2453
2453
|
},
|
|
2454
2454
|
{
|
|
2455
2455
|
"path": "skills/loom-init/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -3063,13 +3063,13 @@
|
|
|
3063
3063
|
},
|
|
3064
3064
|
{
|
|
3065
3065
|
"path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py",
|
|
3066
|
-
"bytes":
|
|
3067
|
-
"sha256": "
|
|
3066
|
+
"bytes": 285470,
|
|
3067
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
3068
3068
|
},
|
|
3069
3069
|
{
|
|
3070
3070
|
"path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
|
|
3071
|
-
"bytes":
|
|
3072
|
-
"sha256": "
|
|
3071
|
+
"bytes": 296133,
|
|
3072
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
3073
3073
|
},
|
|
3074
3074
|
{
|
|
3075
3075
|
"path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -3678,13 +3678,13 @@
|
|
|
3678
3678
|
},
|
|
3679
3679
|
{
|
|
3680
3680
|
"path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py",
|
|
3681
|
-
"bytes":
|
|
3682
|
-
"sha256": "
|
|
3681
|
+
"bytes": 285470,
|
|
3682
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
3683
3683
|
},
|
|
3684
3684
|
{
|
|
3685
3685
|
"path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
3686
|
-
"bytes":
|
|
3687
|
-
"sha256": "
|
|
3686
|
+
"bytes": 296133,
|
|
3687
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
3688
3688
|
},
|
|
3689
3689
|
{
|
|
3690
3690
|
"path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -4293,13 +4293,13 @@
|
|
|
4293
4293
|
},
|
|
4294
4294
|
{
|
|
4295
4295
|
"path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py",
|
|
4296
|
-
"bytes":
|
|
4297
|
-
"sha256": "
|
|
4296
|
+
"bytes": 285470,
|
|
4297
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
4298
4298
|
},
|
|
4299
4299
|
{
|
|
4300
4300
|
"path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
|
|
4301
|
-
"bytes":
|
|
4302
|
-
"sha256": "
|
|
4301
|
+
"bytes": 296133,
|
|
4302
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
4303
4303
|
},
|
|
4304
4304
|
{
|
|
4305
4305
|
"path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -4908,13 +4908,13 @@
|
|
|
4908
4908
|
},
|
|
4909
4909
|
{
|
|
4910
4910
|
"path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py",
|
|
4911
|
-
"bytes":
|
|
4912
|
-
"sha256": "
|
|
4911
|
+
"bytes": 285470,
|
|
4912
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
4913
4913
|
},
|
|
4914
4914
|
{
|
|
4915
4915
|
"path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
|
|
4916
|
-
"bytes":
|
|
4917
|
-
"sha256": "
|
|
4916
|
+
"bytes": 296133,
|
|
4917
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
4918
4918
|
},
|
|
4919
4919
|
{
|
|
4920
4920
|
"path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -5523,13 +5523,13 @@
|
|
|
5523
5523
|
},
|
|
5524
5524
|
{
|
|
5525
5525
|
"path": "skills/loom-review/.loom-runtime/shared/scripts/loom_check.py",
|
|
5526
|
-
"bytes":
|
|
5527
|
-
"sha256": "
|
|
5526
|
+
"bytes": 285470,
|
|
5527
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
5528
5528
|
},
|
|
5529
5529
|
{
|
|
5530
5530
|
"path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
5531
|
-
"bytes":
|
|
5532
|
-
"sha256": "
|
|
5531
|
+
"bytes": 296133,
|
|
5532
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
5533
5533
|
},
|
|
5534
5534
|
{
|
|
5535
5535
|
"path": "skills/loom-review/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -6138,13 +6138,13 @@
|
|
|
6138
6138
|
},
|
|
6139
6139
|
{
|
|
6140
6140
|
"path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py",
|
|
6141
|
-
"bytes":
|
|
6142
|
-
"sha256": "
|
|
6141
|
+
"bytes": 285470,
|
|
6142
|
+
"sha256": "e834612a8da8cd22cc13679b61529bc0074d0399004f4e4f45540d6b8f314c9a"
|
|
6143
6143
|
},
|
|
6144
6144
|
{
|
|
6145
6145
|
"path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
6146
|
-
"bytes":
|
|
6147
|
-
"sha256": "
|
|
6146
|
+
"bytes": 296133,
|
|
6147
|
+
"sha256": "e8bc02d131b1007889b7f9b394b5b2d605c8bf790ba1baccc37da1e6aecbf46c"
|
|
6148
6148
|
},
|
|
6149
6149
|
{
|
|
6150
6150
|
"path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
|
+
import hashlib
|
|
7
8
|
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
@@ -1071,6 +1072,14 @@ def require_shadow_parity_payload(
|
|
|
1071
1072
|
locator = surface_payload.get("locator")
|
|
1072
1073
|
if not isinstance(locator, str) or not locator:
|
|
1073
1074
|
failures.append(Failure(category, f"{context} reports[{index}] `{surface_key}.locator` must be non-empty"))
|
|
1075
|
+
if surface_payload.get("status") == "readable":
|
|
1076
|
+
source_files = surface_payload.get("source_files")
|
|
1077
|
+
source_sha256 = surface_payload.get("source_sha256")
|
|
1078
|
+
if not isinstance(source_files, list) or not source_files:
|
|
1079
|
+
failures.append(Failure(category, f"{context} reports[{index}] `{surface_key}.source_files` must be non-empty for readable evidence"))
|
|
1080
|
+
source_files = []
|
|
1081
|
+
if not isinstance(source_sha256, dict) or set(source_sha256) != set(source_files):
|
|
1082
|
+
failures.append(Failure(category, f"{context} reports[{index}] `{surface_key}.source_sha256` must match source_files exactly"))
|
|
1074
1083
|
|
|
1075
1084
|
|
|
1076
1085
|
def require_host_lifecycle_payload(
|
|
@@ -5472,6 +5481,29 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5472
5481
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
5473
5482
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
5474
5483
|
|
|
5484
|
+
def sha256_file(path: Path) -> str:
|
|
5485
|
+
digest = hashlib.sha256()
|
|
5486
|
+
with path.open("rb") as handle:
|
|
5487
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
5488
|
+
digest.update(chunk)
|
|
5489
|
+
return digest.hexdigest()
|
|
5490
|
+
|
|
5491
|
+
def write_shadow_evidence(target: Path, evidence: str, value_key: str, value: str, source: str) -> None:
|
|
5492
|
+
source_path = target / source
|
|
5493
|
+
source_path.parent.mkdir(parents=True, exist_ok=True)
|
|
5494
|
+
if not source_path.exists():
|
|
5495
|
+
write_json(source_path, {"value": value})
|
|
5496
|
+
write_json(
|
|
5497
|
+
target / evidence,
|
|
5498
|
+
{
|
|
5499
|
+
value_key: value,
|
|
5500
|
+
"source_files": [source],
|
|
5501
|
+
"source_sha256": {
|
|
5502
|
+
source: sha256_file(source_path),
|
|
5503
|
+
},
|
|
5504
|
+
},
|
|
5505
|
+
)
|
|
5506
|
+
|
|
5475
5507
|
def install_interop(
|
|
5476
5508
|
target: Path,
|
|
5477
5509
|
*,
|
|
@@ -5484,17 +5516,21 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5484
5516
|
(target / ".loom" / "shadow").mkdir(parents=True, exist_ok=True)
|
|
5485
5517
|
(target / "native" / "status").mkdir(parents=True, exist_ok=True)
|
|
5486
5518
|
for relative, payload in {
|
|
5487
|
-
".loom/shadow/admission-loom.json": {"result": "pass"},
|
|
5488
|
-
".loom/shadow/admission-repo.json": {"result": "pass"},
|
|
5489
|
-
".loom/shadow/review-loom.json": {"decision": "allow"},
|
|
5490
|
-
".loom/shadow/review-repo.json": {"decision": "allow"},
|
|
5491
|
-
".loom/shadow/merge-ready-loom.json": {"status": "pass"},
|
|
5492
|
-
".loom/shadow/merge-ready-repo.json": {"status": "pass"},
|
|
5493
|
-
".loom/shadow/closeout-loom.json": {"status": "done"},
|
|
5494
|
-
".loom/shadow/closeout-repo.json": {"status": "done"},
|
|
5495
5519
|
"host/guardian-review.json": {"verdict": "allow"},
|
|
5520
|
+
"native/status/admission.json": {"result": "pass"},
|
|
5521
|
+
"native/status/review.json": {"decision": "allow"},
|
|
5522
|
+
"native/status/merge-ready.json": {"status": "pass"},
|
|
5523
|
+
"native/status/closeout.json": {"status": "done"},
|
|
5496
5524
|
}.items():
|
|
5497
5525
|
write_json(target / relative, payload)
|
|
5526
|
+
write_shadow_evidence(target, ".loom/shadow/admission-loom.json", "result", "pass", ".loom/status/current.md")
|
|
5527
|
+
write_shadow_evidence(target, ".loom/shadow/admission-repo.json", "result", "pass", "native/status/admission.json")
|
|
5528
|
+
write_shadow_evidence(target, ".loom/shadow/review-loom.json", "decision", "allow", "host/guardian-review.json")
|
|
5529
|
+
write_shadow_evidence(target, ".loom/shadow/review-repo.json", "decision", "allow", "native/status/review.json")
|
|
5530
|
+
write_shadow_evidence(target, ".loom/shadow/merge-ready-loom.json", "status", "pass", "host/guardian-review.json")
|
|
5531
|
+
write_shadow_evidence(target, ".loom/shadow/merge-ready-repo.json", "status", "pass", "native/status/merge-ready.json")
|
|
5532
|
+
write_shadow_evidence(target, ".loom/shadow/closeout-loom.json", "status", "done", ".loom/status/current.md")
|
|
5533
|
+
write_shadow_evidence(target, ".loom/shadow/closeout-repo.json", "status", "done", "native/status/closeout.json")
|
|
5498
5534
|
if interop is not None:
|
|
5499
5535
|
write_json(companion_dir / "interop.json", interop)
|
|
5500
5536
|
|
|
@@ -5641,7 +5677,8 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5641
5677
|
|
|
5642
5678
|
mismatch_target = base / "mismatch"
|
|
5643
5679
|
shutil.copytree(present_target, mismatch_target)
|
|
5644
|
-
write_json(mismatch_target / "
|
|
5680
|
+
write_json(mismatch_target / "native/status/review.json", {"decision": "block"})
|
|
5681
|
+
write_shadow_evidence(mismatch_target, ".loom/shadow/review-repo.json", "decision", "block", "native/status/review.json")
|
|
5645
5682
|
mismatch_payload, error = load_command_json(
|
|
5646
5683
|
root,
|
|
5647
5684
|
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(mismatch_target), "--surface", "review"],
|
|
@@ -5716,6 +5753,93 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5716
5753
|
if blocking_unreadable_payload.get("result") != "block":
|
|
5717
5754
|
failures.append(Failure("repo-interop", "`shadow-parity --blocking` must block unreadable surfaces"))
|
|
5718
5755
|
|
|
5756
|
+
missing_hash_target = base / "missing-hash"
|
|
5757
|
+
shutil.copytree(present_target, missing_hash_target)
|
|
5758
|
+
write_json(
|
|
5759
|
+
missing_hash_target / ".loom/shadow/review-repo.json",
|
|
5760
|
+
{
|
|
5761
|
+
"decision": "allow",
|
|
5762
|
+
"source_files": ["native/status/review.json"],
|
|
5763
|
+
},
|
|
5764
|
+
)
|
|
5765
|
+
missing_hash_payload, error = load_command_json(
|
|
5766
|
+
root,
|
|
5767
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(missing_hash_target), "--surface", "review"],
|
|
5768
|
+
)
|
|
5769
|
+
if error:
|
|
5770
|
+
failures.append(Failure("repo-interop", f"`shadow-parity` missing hash sample failed: {error}"))
|
|
5771
|
+
else:
|
|
5772
|
+
require_shadow_parity_payload(
|
|
5773
|
+
failures,
|
|
5774
|
+
category="repo-interop",
|
|
5775
|
+
context="`shadow-parity` missing hash sample",
|
|
5776
|
+
payload=missing_hash_payload,
|
|
5777
|
+
expected_reports=1,
|
|
5778
|
+
)
|
|
5779
|
+
reports = missing_hash_payload.get("reports")
|
|
5780
|
+
if not isinstance(reports, list) or not reports or reports[0].get("result") != "unreadable":
|
|
5781
|
+
failures.append(Failure("repo-interop", "`shadow-parity` missing hash sample must report `unreadable`"))
|
|
5782
|
+
|
|
5783
|
+
partial_hash_target = base / "partial-hash"
|
|
5784
|
+
shutil.copytree(present_target, partial_hash_target)
|
|
5785
|
+
write_json(
|
|
5786
|
+
partial_hash_target / ".loom/shadow/review-repo.json",
|
|
5787
|
+
{
|
|
5788
|
+
"decision": "allow",
|
|
5789
|
+
"source_files": ["native/status/review.json", "host/guardian-review.json"],
|
|
5790
|
+
"source_sha256": {
|
|
5791
|
+
"native/status/review.json": sha256_file(partial_hash_target / "native/status/review.json"),
|
|
5792
|
+
},
|
|
5793
|
+
},
|
|
5794
|
+
)
|
|
5795
|
+
partial_hash_payload, error = load_command_json(
|
|
5796
|
+
root,
|
|
5797
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(partial_hash_target), "--surface", "review"],
|
|
5798
|
+
)
|
|
5799
|
+
if error:
|
|
5800
|
+
failures.append(Failure("repo-interop", f"`shadow-parity` partial hash sample failed: {error}"))
|
|
5801
|
+
else:
|
|
5802
|
+
reports = partial_hash_payload.get("reports")
|
|
5803
|
+
if not isinstance(reports, list) or not reports or reports[0].get("result") != "unreadable":
|
|
5804
|
+
failures.append(Failure("repo-interop", "`shadow-parity` partial hash sample must report `unreadable`"))
|
|
5805
|
+
|
|
5806
|
+
hash_drift_target = base / "hash-drift"
|
|
5807
|
+
shutil.copytree(present_target, hash_drift_target)
|
|
5808
|
+
write_json(hash_drift_target / "native/status/review.json", {"decision": "changed-after-evidence"})
|
|
5809
|
+
hash_drift_payload, error = load_command_json(
|
|
5810
|
+
root,
|
|
5811
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(hash_drift_target), "--surface", "review"],
|
|
5812
|
+
)
|
|
5813
|
+
if error:
|
|
5814
|
+
failures.append(Failure("repo-interop", f"`shadow-parity` hash drift sample failed: {error}"))
|
|
5815
|
+
else:
|
|
5816
|
+
reports = hash_drift_payload.get("reports")
|
|
5817
|
+
if not isinstance(reports, list) or not reports or reports[0].get("result") != "unreadable":
|
|
5818
|
+
failures.append(Failure("repo-interop", "`shadow-parity` hash drift sample must report `unreadable`"))
|
|
5819
|
+
|
|
5820
|
+
rogue_target = base / "rogue"
|
|
5821
|
+
shutil.copytree(present_target, rogue_target)
|
|
5822
|
+
write_json(
|
|
5823
|
+
rogue_target / ".loom/shadow/rogue.json",
|
|
5824
|
+
{
|
|
5825
|
+
"result": "pass",
|
|
5826
|
+
"source_files": ["native/status/review.json"],
|
|
5827
|
+
"source_sha256": {
|
|
5828
|
+
"native/status/review.json": sha256_file(rogue_target / "native/status/review.json"),
|
|
5829
|
+
},
|
|
5830
|
+
},
|
|
5831
|
+
)
|
|
5832
|
+
rogue_payload, error = load_command_json(
|
|
5833
|
+
root,
|
|
5834
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(rogue_target), "--surface", "review"],
|
|
5835
|
+
)
|
|
5836
|
+
if error:
|
|
5837
|
+
failures.append(Failure("repo-interop", f"`shadow-parity` rogue evidence sample failed: {error}"))
|
|
5838
|
+
else:
|
|
5839
|
+
reports = rogue_payload.get("reports")
|
|
5840
|
+
if not isinstance(reports, list) or not reports or reports[0].get("result") != "unreadable":
|
|
5841
|
+
failures.append(Failure("repo-interop", "`shadow-parity` rogue evidence sample must report `unreadable`"))
|
|
5842
|
+
|
|
5719
5843
|
return failures
|
|
5720
5844
|
|
|
5721
5845
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
@@ -531,15 +532,110 @@ def load_repo_interop_contract(repo_interop: object, *, target_root: Path) -> tu
|
|
|
531
532
|
return payload, []
|
|
532
533
|
|
|
533
534
|
|
|
534
|
-
def
|
|
535
|
+
def sha256_file(path: Path) -> str:
|
|
536
|
+
digest = hashlib.sha256()
|
|
537
|
+
with path.open("rb") as handle:
|
|
538
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
539
|
+
digest.update(chunk)
|
|
540
|
+
return digest.hexdigest()
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def repo_relative_path(target_root: Path, relative: str) -> Path | None:
|
|
544
|
+
candidate = (target_root / relative).resolve()
|
|
545
|
+
try:
|
|
546
|
+
candidate.relative_to(target_root.resolve())
|
|
547
|
+
except ValueError:
|
|
548
|
+
return None
|
|
549
|
+
return candidate
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def validate_shadow_sources(payload: dict[str, Any], *, path: Path, target_root: Path) -> tuple[dict[str, Any], list[str]]:
|
|
553
|
+
source_files = payload.get("source_files")
|
|
554
|
+
source_sha256 = payload.get("source_sha256")
|
|
555
|
+
errors: list[str] = []
|
|
556
|
+
if not isinstance(source_files, list) or not source_files:
|
|
557
|
+
errors.append(f"shadow evidence `{path}` must declare non-empty `source_files`")
|
|
558
|
+
source_files = []
|
|
559
|
+
if not isinstance(source_sha256, dict) or not source_sha256:
|
|
560
|
+
errors.append(f"shadow evidence `{path}` must declare non-empty `source_sha256`")
|
|
561
|
+
source_sha256 = {}
|
|
562
|
+
|
|
563
|
+
normalized_sources: list[str] = []
|
|
564
|
+
for index, source in enumerate(source_files, start=1):
|
|
565
|
+
if not isinstance(source, str) or not source.strip():
|
|
566
|
+
errors.append(f"shadow evidence `{path}` source_files[{index}] must be a non-empty relative path")
|
|
567
|
+
continue
|
|
568
|
+
source = source.strip()
|
|
569
|
+
if Path(source).is_absolute() or ".." in Path(source).parts:
|
|
570
|
+
errors.append(f"shadow evidence `{path}` source `{source}` must stay inside the repository")
|
|
571
|
+
continue
|
|
572
|
+
source_path = repo_relative_path(target_root, source)
|
|
573
|
+
if source_path is None:
|
|
574
|
+
errors.append(f"shadow evidence `{path}` source `{source}` must stay inside the repository")
|
|
575
|
+
continue
|
|
576
|
+
if not source_path.exists() or source_path.is_dir():
|
|
577
|
+
errors.append(f"shadow evidence `{path}` source `{source}` must be an existing file")
|
|
578
|
+
continue
|
|
579
|
+
normalized_sources.append(source)
|
|
580
|
+
|
|
581
|
+
source_keys = set(normalized_sources)
|
|
582
|
+
hash_keys = {key for key in source_sha256.keys() if isinstance(key, str)}
|
|
583
|
+
if source_keys != hash_keys:
|
|
584
|
+
errors.append(f"shadow evidence `{path}` source_files and source_sha256 keys must match exactly")
|
|
585
|
+
for source in sorted(source_keys & hash_keys):
|
|
586
|
+
expected = source_sha256.get(source)
|
|
587
|
+
if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-fA-F]{64}", expected):
|
|
588
|
+
errors.append(f"shadow evidence `{path}` source `{source}` must declare a 64-character sha256")
|
|
589
|
+
continue
|
|
590
|
+
actual = sha256_file(target_root / source)
|
|
591
|
+
if actual.lower() != expected.lower():
|
|
592
|
+
errors.append(f"shadow evidence `{path}` source `{source}` sha256 drifted")
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
"source_files": normalized_sources,
|
|
596
|
+
"source_sha256": {source: source_sha256.get(source) for source in normalized_sources if isinstance(source_sha256.get(source), str)},
|
|
597
|
+
}, errors
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def declared_shadow_locators(interop_payload: dict[str, Any]) -> set[str]:
|
|
601
|
+
shadow_surfaces = interop_payload.get("shadow_surfaces")
|
|
602
|
+
declared: set[str] = set()
|
|
603
|
+
if not isinstance(shadow_surfaces, dict):
|
|
604
|
+
return declared
|
|
605
|
+
for entry in shadow_surfaces.values():
|
|
606
|
+
if not isinstance(entry, dict):
|
|
607
|
+
continue
|
|
608
|
+
for key in ("loom_locator", "repo_locator"):
|
|
609
|
+
value = entry.get(key)
|
|
610
|
+
if isinstance(value, str) and value.strip():
|
|
611
|
+
declared.add(value.strip())
|
|
612
|
+
return declared
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def undeclared_shadow_evidence_errors(target_root: Path, interop_payload: dict[str, Any]) -> list[str]:
|
|
616
|
+
shadow_root = target_root / ".loom/shadow"
|
|
617
|
+
if not shadow_root.exists():
|
|
618
|
+
return []
|
|
619
|
+
declared = declared_shadow_locators(interop_payload)
|
|
620
|
+
errors: list[str] = []
|
|
621
|
+
for path in sorted(shadow_root.glob("*.json")):
|
|
622
|
+
relative = path.relative_to(target_root).as_posix()
|
|
623
|
+
if relative == ".loom/shadow/shadow-parity.json":
|
|
624
|
+
continue
|
|
625
|
+
if relative not in declared:
|
|
626
|
+
errors.append(f"shadow evidence `{relative}` is not declared in repo interop shadow_surfaces")
|
|
627
|
+
return errors
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def normalized_shadow_value(path: Path, *, target_root: Path) -> tuple[dict[str, Any], str | None]:
|
|
535
631
|
try:
|
|
536
632
|
if path.is_dir():
|
|
537
|
-
return None, f"shadow parity locator points to a directory: {path}"
|
|
633
|
+
return {"normalized_value": None}, f"shadow parity locator points to a directory: {path}"
|
|
538
634
|
raw_text = path.read_text(encoding="utf-8")
|
|
539
635
|
except OSError as exc:
|
|
540
|
-
return None, f"cannot read shadow parity locator `{path}`: {exc.strerror or exc}"
|
|
636
|
+
return {"normalized_value": None}, f"cannot read shadow parity locator `{path}`: {exc.strerror or exc}"
|
|
541
637
|
if not raw_text.strip():
|
|
542
|
-
return None, f"shadow parity locator is empty: {path}"
|
|
638
|
+
return {"normalized_value": None}, f"shadow parity locator is empty: {path}"
|
|
543
639
|
|
|
544
640
|
try:
|
|
545
641
|
payload = json.loads(raw_text)
|
|
@@ -547,21 +643,22 @@ def normalized_shadow_value(path: Path) -> tuple[str | None, str | None]:
|
|
|
547
643
|
payload = None
|
|
548
644
|
|
|
549
645
|
if isinstance(payload, dict):
|
|
646
|
+
source_evidence, source_errors = validate_shadow_sources(payload, path=path, target_root=target_root)
|
|
550
647
|
for key in ("parity_value", "result", "decision", "status", "verdict", "value"):
|
|
551
648
|
value = payload.get(key)
|
|
552
649
|
if isinstance(value, (str, int, float, bool)) and str(value).strip():
|
|
553
|
-
return str(value).strip().lower(), None
|
|
554
|
-
return json.dumps(payload, ensure_ascii=False, sort_keys=True), None
|
|
650
|
+
return {**source_evidence, "normalized_value": str(value).strip().lower()}, "; ".join(source_errors) if source_errors else None
|
|
651
|
+
return {**source_evidence, "normalized_value": json.dumps(payload, ensure_ascii=False, sort_keys=True)}, "; ".join(source_errors) if source_errors else None
|
|
555
652
|
if isinstance(payload, list):
|
|
556
|
-
return json.dumps(payload, ensure_ascii=False, sort_keys=True),
|
|
653
|
+
return {"normalized_value": json.dumps(payload, ensure_ascii=False, sort_keys=True)}, f"shadow evidence `{path}` must be a JSON object with source_files/source_sha256"
|
|
557
654
|
if isinstance(payload, (str, int, float, bool)) and str(payload).strip():
|
|
558
|
-
return str(payload).strip().lower(),
|
|
655
|
+
return {"normalized_value": str(payload).strip().lower()}, f"shadow evidence `{path}` must be a JSON object with source_files/source_sha256"
|
|
559
656
|
|
|
560
657
|
for line in raw_text.splitlines():
|
|
561
658
|
stripped = line.strip()
|
|
562
659
|
if stripped and not stripped.startswith("#"):
|
|
563
|
-
return stripped.lower(),
|
|
564
|
-
return None, f"shadow parity locator does not expose a comparable value: {path}"
|
|
660
|
+
return {"normalized_value": stripped.lower()}, f"shadow evidence `{path}` must be a JSON object with source_files/source_sha256"
|
|
661
|
+
return {"normalized_value": None}, f"shadow parity locator does not expose a comparable value: {path}"
|
|
565
662
|
|
|
566
663
|
|
|
567
664
|
def shadow_parity_report(
|
|
@@ -632,27 +729,42 @@ def shadow_parity_report(
|
|
|
632
729
|
loom_path = target_root / str(loom_locator)
|
|
633
730
|
repo_path = target_root / str(repo_locator)
|
|
634
731
|
|
|
635
|
-
|
|
636
|
-
|
|
732
|
+
loom_surface = {
|
|
733
|
+
"status": "missing",
|
|
734
|
+
"locator": str(loom_locator),
|
|
735
|
+
"normalized_value": None,
|
|
736
|
+
}
|
|
737
|
+
repo_surface = {
|
|
738
|
+
"status": "missing",
|
|
739
|
+
"locator": str(repo_locator),
|
|
740
|
+
"normalized_value": None,
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
global_errors = undeclared_shadow_evidence_errors(target_root, interop_payload)
|
|
744
|
+
loom_evidence, loom_error = normalized_shadow_value(loom_path, target_root=target_root)
|
|
745
|
+
repo_evidence, repo_error = normalized_shadow_value(repo_path, target_root=target_root)
|
|
746
|
+
loom_value = loom_evidence.get("normalized_value")
|
|
747
|
+
repo_value = repo_evidence.get("normalized_value")
|
|
637
748
|
|
|
638
749
|
missing_inputs: list[str] = []
|
|
750
|
+
missing_inputs.extend(global_errors)
|
|
639
751
|
if loom_error:
|
|
640
752
|
missing_inputs.append(loom_error)
|
|
641
753
|
if repo_error:
|
|
642
754
|
missing_inputs.append(repo_error)
|
|
643
755
|
|
|
644
756
|
loom_surface = {
|
|
757
|
+
**loom_evidence,
|
|
645
758
|
"status": "readable" if loom_error is None else "missing",
|
|
646
759
|
"locator": str(loom_locator),
|
|
647
|
-
"normalized_value": loom_value,
|
|
648
760
|
}
|
|
649
761
|
repo_surface = {
|
|
762
|
+
**repo_evidence,
|
|
650
763
|
"status": "readable" if repo_error is None else "missing",
|
|
651
764
|
"locator": str(repo_locator),
|
|
652
|
-
"normalized_value": repo_value,
|
|
653
765
|
}
|
|
654
766
|
|
|
655
|
-
if loom_error or repo_error or loom_value is None or repo_value is None:
|
|
767
|
+
if global_errors or loom_error or repo_error or loom_value is None or repo_value is None:
|
|
656
768
|
return {
|
|
657
769
|
**empty_report,
|
|
658
770
|
"summary": "shadow parity is unreadable because one or both declared surfaces cannot be normalized.",
|