@mc-and-his-agents/loom-installer 0.1.12 → 0.1.13
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 +93 -4
- package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +93 -4
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
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": "b07d54d4963a674da0ff2397f4ad244cb3028584",
|
|
6
6
|
"source_ref": "main",
|
|
7
|
-
"built_at": "2026-04-
|
|
7
|
+
"built_at": "2026-04-25T12:04:56+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": 260856,
|
|
632
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
633
633
|
},
|
|
634
634
|
{
|
|
635
635
|
"path": "plugin/loom/skills/shared/scripts/loom_flow.py",
|
|
636
|
-
"bytes":
|
|
637
|
-
"sha256": "
|
|
636
|
+
"bytes": 262117,
|
|
637
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
1217
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
1218
1218
|
},
|
|
1219
1219
|
{
|
|
1220
1220
|
"path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
|
|
1221
|
-
"bytes":
|
|
1222
|
-
"sha256": "
|
|
1221
|
+
"bytes": 262117,
|
|
1222
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
1832
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
1833
1833
|
},
|
|
1834
1834
|
{
|
|
1835
1835
|
"path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
|
|
1836
|
-
"bytes":
|
|
1837
|
-
"sha256": "
|
|
1836
|
+
"bytes": 262117,
|
|
1837
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
2447
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
2448
2448
|
},
|
|
2449
2449
|
{
|
|
2450
2450
|
"path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
|
|
2451
|
-
"bytes":
|
|
2452
|
-
"sha256": "
|
|
2451
|
+
"bytes": 262117,
|
|
2452
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
3067
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
3068
3068
|
},
|
|
3069
3069
|
{
|
|
3070
3070
|
"path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
|
|
3071
|
-
"bytes":
|
|
3072
|
-
"sha256": "
|
|
3071
|
+
"bytes": 262117,
|
|
3072
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
3682
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
3683
3683
|
},
|
|
3684
3684
|
{
|
|
3685
3685
|
"path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
3686
|
-
"bytes":
|
|
3687
|
-
"sha256": "
|
|
3686
|
+
"bytes": 262117,
|
|
3687
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
4297
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
4298
4298
|
},
|
|
4299
4299
|
{
|
|
4300
4300
|
"path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
|
|
4301
|
-
"bytes":
|
|
4302
|
-
"sha256": "
|
|
4301
|
+
"bytes": 262117,
|
|
4302
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
4912
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
4913
4913
|
},
|
|
4914
4914
|
{
|
|
4915
4915
|
"path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
|
|
4916
|
-
"bytes":
|
|
4917
|
-
"sha256": "
|
|
4916
|
+
"bytes": 262117,
|
|
4917
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
5527
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
5528
5528
|
},
|
|
5529
5529
|
{
|
|
5530
5530
|
"path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
5531
|
-
"bytes":
|
|
5532
|
-
"sha256": "
|
|
5531
|
+
"bytes": 262117,
|
|
5532
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
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": 260856,
|
|
6142
|
+
"sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
|
|
6143
6143
|
},
|
|
6144
6144
|
{
|
|
6145
6145
|
"path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
6146
|
-
"bytes":
|
|
6147
|
-
"sha256": "
|
|
6146
|
+
"bytes": 262117,
|
|
6147
|
+
"sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
|
|
6148
6148
|
},
|
|
6149
6149
|
{
|
|
6150
6150
|
"path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -94,6 +94,7 @@ CORE_DOCS = (
|
|
|
94
94
|
"docs/evidence/landing-map.md",
|
|
95
95
|
"docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
|
|
96
96
|
"docs/evidence/validations/validation-loom-core-runtime-parity.md",
|
|
97
|
+
"docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
|
|
97
98
|
"docs/evidence/validations/validation-syvert-strong-governance-parity.md",
|
|
98
99
|
"docs/adoption/rationale.md",
|
|
99
100
|
"docs/adoption/routing-and-checkpoints.md",
|
|
@@ -969,10 +970,17 @@ def require_shadow_parity_payload(
|
|
|
969
970
|
return
|
|
970
971
|
if payload.get("command") != "shadow-parity":
|
|
971
972
|
failures.append(Failure(category, f"{context} must report `command: shadow-parity`"))
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
973
|
+
mode = payload.get("mode", "validation-only")
|
|
974
|
+
if mode not in {"validation-only", "blocking"}:
|
|
975
|
+
failures.append(Failure(category, f"{context} mode must be `validation-only` or `blocking`"))
|
|
976
|
+
if payload.get("blocking") != (mode == "blocking"):
|
|
977
|
+
failures.append(Failure(category, f"{context} blocking flag must match mode"))
|
|
978
|
+
allowed_results = {"pass", "block"} if mode == "blocking" else {"pass", "warn"}
|
|
979
|
+
if payload.get("result") not in allowed_results:
|
|
980
|
+
failures.append(Failure(category, f"{context} result must stay within the stable mode-specific contract"))
|
|
981
|
+
expected_fallbacks = {"manual-reconciliation"} if payload.get("result") == "block" else {None}
|
|
982
|
+
if payload.get("fallback_to") not in expected_fallbacks:
|
|
983
|
+
failures.append(Failure(category, f"{context} fallback_to must match the shadow parity enforcement mode"))
|
|
976
984
|
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
977
985
|
failures.append(Failure(category, f"{context} must include non-empty `summary`"))
|
|
978
986
|
if not isinstance(payload.get("missing_inputs"), list):
|
|
@@ -1008,6 +1016,14 @@ def require_shadow_parity_payload(
|
|
|
1008
1016
|
failures.append(Failure(category, f"{context} reports[{index}] must declare a known surface"))
|
|
1009
1017
|
if report.get("result") not in {"match", "mismatch", "unreadable"}:
|
|
1010
1018
|
failures.append(Failure(category, f"{context} reports[{index}] result must stay within the stable contract"))
|
|
1019
|
+
if report.get("classification") not in {None, "drift", "gate_failure"}:
|
|
1020
|
+
failures.append(Failure(category, f"{context} reports[{index}] classification must stay within the stable contract"))
|
|
1021
|
+
if not isinstance(report.get("blocking"), bool):
|
|
1022
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include boolean `blocking`"))
|
|
1023
|
+
if not isinstance(report.get("recommended_action"), str) or not report.get("recommended_action"):
|
|
1024
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `recommended_action`"))
|
|
1025
|
+
if mode == "blocking" and report.get("result") != "match" and report.get("blocking") is not True:
|
|
1026
|
+
failures.append(Failure(category, f"{context} reports[{index}] must block non-matching reports in blocking mode"))
|
|
1011
1027
|
if not isinstance(report.get("summary"), str) or not report.get("summary"):
|
|
1012
1028
|
failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `summary`"))
|
|
1013
1029
|
if not isinstance(report.get("missing_inputs"), list):
|
|
@@ -5301,6 +5317,23 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5301
5317
|
if parity_payload.get("result") != "pass":
|
|
5302
5318
|
failures.append(Failure("repo-interop", "`shadow-parity` must pass when all declared surfaces match"))
|
|
5303
5319
|
|
|
5320
|
+
blocking_match_payload, error = load_command_json(
|
|
5321
|
+
root,
|
|
5322
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(present_target), "--blocking"],
|
|
5323
|
+
)
|
|
5324
|
+
if error:
|
|
5325
|
+
failures.append(Failure("repo-interop", f"`shadow-parity --blocking` match sample failed: {error}"))
|
|
5326
|
+
else:
|
|
5327
|
+
require_shadow_parity_payload(
|
|
5328
|
+
failures,
|
|
5329
|
+
category="repo-interop",
|
|
5330
|
+
context="`shadow-parity --blocking` match sample",
|
|
5331
|
+
payload=blocking_match_payload,
|
|
5332
|
+
expected_reports=4,
|
|
5333
|
+
)
|
|
5334
|
+
if blocking_match_payload.get("result") != "pass":
|
|
5335
|
+
failures.append(Failure("repo-interop", "`shadow-parity --blocking` must pass when all declared surfaces match"))
|
|
5336
|
+
|
|
5304
5337
|
mismatch_target = base / "mismatch"
|
|
5305
5338
|
shutil.copytree(present_target, mismatch_target)
|
|
5306
5339
|
write_json(mismatch_target / ".loom/shadow/review-repo.json", {"decision": "block"})
|
|
@@ -5322,6 +5355,62 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5322
5355
|
if not isinstance(reports, list) or not reports or reports[0].get("result") != "mismatch":
|
|
5323
5356
|
failures.append(Failure("repo-interop", "`shadow-parity` mismatch sample must report `mismatch`"))
|
|
5324
5357
|
|
|
5358
|
+
blocking_mismatch_payload, error = load_command_json(
|
|
5359
|
+
root,
|
|
5360
|
+
[
|
|
5361
|
+
"python3",
|
|
5362
|
+
"tools/loom_flow.py",
|
|
5363
|
+
"shadow-parity",
|
|
5364
|
+
"--target",
|
|
5365
|
+
str(mismatch_target),
|
|
5366
|
+
"--surface",
|
|
5367
|
+
"review",
|
|
5368
|
+
"--mode",
|
|
5369
|
+
"blocking",
|
|
5370
|
+
],
|
|
5371
|
+
)
|
|
5372
|
+
if error:
|
|
5373
|
+
failures.append(Failure("repo-interop", f"`shadow-parity --mode blocking` mismatch sample failed: {error}"))
|
|
5374
|
+
else:
|
|
5375
|
+
require_shadow_parity_payload(
|
|
5376
|
+
failures,
|
|
5377
|
+
category="repo-interop",
|
|
5378
|
+
context="`shadow-parity --mode blocking` mismatch sample",
|
|
5379
|
+
payload=blocking_mismatch_payload,
|
|
5380
|
+
expected_reports=1,
|
|
5381
|
+
)
|
|
5382
|
+
if blocking_mismatch_payload.get("result") != "block":
|
|
5383
|
+
failures.append(Failure("repo-interop", "`shadow-parity --mode blocking` must block mismatches"))
|
|
5384
|
+
|
|
5385
|
+
unreadable_target = base / "unreadable"
|
|
5386
|
+
shutil.copytree(present_target, unreadable_target)
|
|
5387
|
+
(unreadable_target / ".loom/shadow/closeout-repo.json").unlink()
|
|
5388
|
+
blocking_unreadable_payload, error = load_command_json(
|
|
5389
|
+
root,
|
|
5390
|
+
[
|
|
5391
|
+
"python3",
|
|
5392
|
+
"tools/loom_flow.py",
|
|
5393
|
+
"shadow-parity",
|
|
5394
|
+
"--target",
|
|
5395
|
+
str(unreadable_target),
|
|
5396
|
+
"--surface",
|
|
5397
|
+
"closeout",
|
|
5398
|
+
"--blocking",
|
|
5399
|
+
],
|
|
5400
|
+
)
|
|
5401
|
+
if error:
|
|
5402
|
+
failures.append(Failure("repo-interop", f"`shadow-parity --blocking` unreadable sample failed: {error}"))
|
|
5403
|
+
else:
|
|
5404
|
+
require_shadow_parity_payload(
|
|
5405
|
+
failures,
|
|
5406
|
+
category="repo-interop",
|
|
5407
|
+
context="`shadow-parity --blocking` unreadable sample",
|
|
5408
|
+
payload=blocking_unreadable_payload,
|
|
5409
|
+
expected_reports=1,
|
|
5410
|
+
)
|
|
5411
|
+
if blocking_unreadable_payload.get("result") != "block":
|
|
5412
|
+
failures.append(Failure("repo-interop", "`shadow-parity --blocking` must block unreadable surfaces"))
|
|
5413
|
+
|
|
5325
5414
|
return failures
|
|
5326
5415
|
|
|
5327
5416
|
|
|
@@ -276,6 +276,17 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
276
276
|
default=".loom/bootstrap/init-result.json",
|
|
277
277
|
help="Init-result path relative to the target root",
|
|
278
278
|
)
|
|
279
|
+
shadow.add_argument(
|
|
280
|
+
"--mode",
|
|
281
|
+
choices=("validation-only", "blocking"),
|
|
282
|
+
default="validation-only",
|
|
283
|
+
help="Shadow parity enforcement mode; defaults to validation-only.",
|
|
284
|
+
)
|
|
285
|
+
shadow.add_argument(
|
|
286
|
+
"--blocking",
|
|
287
|
+
action="store_true",
|
|
288
|
+
help="Shortcut for --mode blocking. This is explicit opt-in and never the default.",
|
|
289
|
+
)
|
|
279
290
|
|
|
280
291
|
runtime_parity = subparsers.add_parser(
|
|
281
292
|
"runtime-parity",
|
|
@@ -543,8 +554,11 @@ def shadow_parity_report(
|
|
|
543
554
|
empty_report = {
|
|
544
555
|
"surface": surface,
|
|
545
556
|
"result": "unreadable",
|
|
557
|
+
"classification": "gate_failure",
|
|
558
|
+
"blocking": False,
|
|
546
559
|
"summary": "shadow parity could not be evaluated for this surface.",
|
|
547
560
|
"missing_inputs": [],
|
|
561
|
+
"recommended_action": "restore the declared Loom and repo-native shadow parity locators before treating this surface as authoritative.",
|
|
548
562
|
"host_adapters": [],
|
|
549
563
|
"repo_native_carriers": [],
|
|
550
564
|
"loom_surface": {
|
|
@@ -633,8 +647,11 @@ def shadow_parity_report(
|
|
|
633
647
|
return {
|
|
634
648
|
"surface": surface,
|
|
635
649
|
"result": "match",
|
|
650
|
+
"classification": None,
|
|
651
|
+
"blocking": False,
|
|
636
652
|
"summary": "Loom and repo-native surfaces report the same normalized result.",
|
|
637
653
|
"missing_inputs": [],
|
|
654
|
+
"recommended_action": "no shadow parity action required.",
|
|
638
655
|
"host_adapters": relevant_host_adapters,
|
|
639
656
|
"repo_native_carriers": relevant_repo_native_carriers,
|
|
640
657
|
"loom_surface": loom_surface,
|
|
@@ -643,8 +660,11 @@ def shadow_parity_report(
|
|
|
643
660
|
return {
|
|
644
661
|
"surface": surface,
|
|
645
662
|
"result": "mismatch",
|
|
663
|
+
"classification": "drift",
|
|
664
|
+
"blocking": False,
|
|
646
665
|
"summary": "Loom and repo-native surfaces disagree on the normalized result.",
|
|
647
666
|
"missing_inputs": [],
|
|
667
|
+
"recommended_action": "resolve the parity mismatch or explicitly choose the authoritative surface outside repo interop before enabling blocking consumption.",
|
|
648
668
|
"host_adapters": relevant_host_adapters,
|
|
649
669
|
"repo_native_carriers": relevant_repo_native_carriers,
|
|
650
670
|
"loom_surface": loom_surface,
|
|
@@ -6122,6 +6142,7 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
|
|
|
6122
6142
|
governance_surface = build_governance_surface(target_root)
|
|
6123
6143
|
repo_interop = governance_surface.get("repo_interop")
|
|
6124
6144
|
requested_surfaces = SHADOW_PARITY_SURFACES if args.surface == "all" else (args.surface,)
|
|
6145
|
+
mode = "blocking" if args.blocking else args.mode
|
|
6125
6146
|
reports = [
|
|
6126
6147
|
shadow_parity_report(
|
|
6127
6148
|
repo_interop,
|
|
@@ -6131,9 +6152,18 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
|
|
|
6131
6152
|
for surface in requested_surfaces
|
|
6132
6153
|
]
|
|
6133
6154
|
|
|
6134
|
-
|
|
6155
|
+
all_match = bool(reports) and all(report["result"] == "match" for report in reports)
|
|
6156
|
+
blocking_reports = [report for report in reports if report.get("result") != "match"]
|
|
6157
|
+
if mode == "blocking":
|
|
6158
|
+
result = "pass" if all_match else "block"
|
|
6159
|
+
for report in blocking_reports:
|
|
6160
|
+
report["blocking"] = True
|
|
6161
|
+
else:
|
|
6162
|
+
result = "pass" if all_match else "warn"
|
|
6135
6163
|
if result == "pass":
|
|
6136
6164
|
summary = "shadow parity matches across all requested surfaces."
|
|
6165
|
+
elif mode == "blocking":
|
|
6166
|
+
summary = "shadow parity blocking mode found mismatch or unreadable surfaces."
|
|
6137
6167
|
else:
|
|
6138
6168
|
summaries = {report["result"] for report in reports}
|
|
6139
6169
|
if "mismatch" in summaries:
|
|
@@ -6150,10 +6180,12 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
|
|
|
6150
6180
|
return emit(
|
|
6151
6181
|
{
|
|
6152
6182
|
"command": "shadow-parity",
|
|
6183
|
+
"mode": mode,
|
|
6184
|
+
"blocking": mode == "blocking",
|
|
6153
6185
|
"result": result,
|
|
6154
6186
|
"summary": summary,
|
|
6155
6187
|
"missing_inputs": missing_inputs,
|
|
6156
|
-
"fallback_to": None,
|
|
6188
|
+
"fallback_to": "manual-reconciliation" if result == "block" else None,
|
|
6157
6189
|
"runtime_state": runtime_state,
|
|
6158
6190
|
"governance_surface": governance_surface,
|
|
6159
6191
|
"reports": reports,
|
|
@@ -94,6 +94,7 @@ CORE_DOCS = (
|
|
|
94
94
|
"docs/evidence/landing-map.md",
|
|
95
95
|
"docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
|
|
96
96
|
"docs/evidence/validations/validation-loom-core-runtime-parity.md",
|
|
97
|
+
"docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
|
|
97
98
|
"docs/evidence/validations/validation-syvert-strong-governance-parity.md",
|
|
98
99
|
"docs/adoption/rationale.md",
|
|
99
100
|
"docs/adoption/routing-and-checkpoints.md",
|
|
@@ -969,10 +970,17 @@ def require_shadow_parity_payload(
|
|
|
969
970
|
return
|
|
970
971
|
if payload.get("command") != "shadow-parity":
|
|
971
972
|
failures.append(Failure(category, f"{context} must report `command: shadow-parity`"))
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
973
|
+
mode = payload.get("mode", "validation-only")
|
|
974
|
+
if mode not in {"validation-only", "blocking"}:
|
|
975
|
+
failures.append(Failure(category, f"{context} mode must be `validation-only` or `blocking`"))
|
|
976
|
+
if payload.get("blocking") != (mode == "blocking"):
|
|
977
|
+
failures.append(Failure(category, f"{context} blocking flag must match mode"))
|
|
978
|
+
allowed_results = {"pass", "block"} if mode == "blocking" else {"pass", "warn"}
|
|
979
|
+
if payload.get("result") not in allowed_results:
|
|
980
|
+
failures.append(Failure(category, f"{context} result must stay within the stable mode-specific contract"))
|
|
981
|
+
expected_fallbacks = {"manual-reconciliation"} if payload.get("result") == "block" else {None}
|
|
982
|
+
if payload.get("fallback_to") not in expected_fallbacks:
|
|
983
|
+
failures.append(Failure(category, f"{context} fallback_to must match the shadow parity enforcement mode"))
|
|
976
984
|
if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
|
|
977
985
|
failures.append(Failure(category, f"{context} must include non-empty `summary`"))
|
|
978
986
|
if not isinstance(payload.get("missing_inputs"), list):
|
|
@@ -1008,6 +1016,14 @@ def require_shadow_parity_payload(
|
|
|
1008
1016
|
failures.append(Failure(category, f"{context} reports[{index}] must declare a known surface"))
|
|
1009
1017
|
if report.get("result") not in {"match", "mismatch", "unreadable"}:
|
|
1010
1018
|
failures.append(Failure(category, f"{context} reports[{index}] result must stay within the stable contract"))
|
|
1019
|
+
if report.get("classification") not in {None, "drift", "gate_failure"}:
|
|
1020
|
+
failures.append(Failure(category, f"{context} reports[{index}] classification must stay within the stable contract"))
|
|
1021
|
+
if not isinstance(report.get("blocking"), bool):
|
|
1022
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include boolean `blocking`"))
|
|
1023
|
+
if not isinstance(report.get("recommended_action"), str) or not report.get("recommended_action"):
|
|
1024
|
+
failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `recommended_action`"))
|
|
1025
|
+
if mode == "blocking" and report.get("result") != "match" and report.get("blocking") is not True:
|
|
1026
|
+
failures.append(Failure(category, f"{context} reports[{index}] must block non-matching reports in blocking mode"))
|
|
1011
1027
|
if not isinstance(report.get("summary"), str) or not report.get("summary"):
|
|
1012
1028
|
failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `summary`"))
|
|
1013
1029
|
if not isinstance(report.get("missing_inputs"), list):
|
|
@@ -5301,6 +5317,23 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5301
5317
|
if parity_payload.get("result") != "pass":
|
|
5302
5318
|
failures.append(Failure("repo-interop", "`shadow-parity` must pass when all declared surfaces match"))
|
|
5303
5319
|
|
|
5320
|
+
blocking_match_payload, error = load_command_json(
|
|
5321
|
+
root,
|
|
5322
|
+
["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(present_target), "--blocking"],
|
|
5323
|
+
)
|
|
5324
|
+
if error:
|
|
5325
|
+
failures.append(Failure("repo-interop", f"`shadow-parity --blocking` match sample failed: {error}"))
|
|
5326
|
+
else:
|
|
5327
|
+
require_shadow_parity_payload(
|
|
5328
|
+
failures,
|
|
5329
|
+
category="repo-interop",
|
|
5330
|
+
context="`shadow-parity --blocking` match sample",
|
|
5331
|
+
payload=blocking_match_payload,
|
|
5332
|
+
expected_reports=4,
|
|
5333
|
+
)
|
|
5334
|
+
if blocking_match_payload.get("result") != "pass":
|
|
5335
|
+
failures.append(Failure("repo-interop", "`shadow-parity --blocking` must pass when all declared surfaces match"))
|
|
5336
|
+
|
|
5304
5337
|
mismatch_target = base / "mismatch"
|
|
5305
5338
|
shutil.copytree(present_target, mismatch_target)
|
|
5306
5339
|
write_json(mismatch_target / ".loom/shadow/review-repo.json", {"decision": "block"})
|
|
@@ -5322,6 +5355,62 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
|
|
|
5322
5355
|
if not isinstance(reports, list) or not reports or reports[0].get("result") != "mismatch":
|
|
5323
5356
|
failures.append(Failure("repo-interop", "`shadow-parity` mismatch sample must report `mismatch`"))
|
|
5324
5357
|
|
|
5358
|
+
blocking_mismatch_payload, error = load_command_json(
|
|
5359
|
+
root,
|
|
5360
|
+
[
|
|
5361
|
+
"python3",
|
|
5362
|
+
"tools/loom_flow.py",
|
|
5363
|
+
"shadow-parity",
|
|
5364
|
+
"--target",
|
|
5365
|
+
str(mismatch_target),
|
|
5366
|
+
"--surface",
|
|
5367
|
+
"review",
|
|
5368
|
+
"--mode",
|
|
5369
|
+
"blocking",
|
|
5370
|
+
],
|
|
5371
|
+
)
|
|
5372
|
+
if error:
|
|
5373
|
+
failures.append(Failure("repo-interop", f"`shadow-parity --mode blocking` mismatch sample failed: {error}"))
|
|
5374
|
+
else:
|
|
5375
|
+
require_shadow_parity_payload(
|
|
5376
|
+
failures,
|
|
5377
|
+
category="repo-interop",
|
|
5378
|
+
context="`shadow-parity --mode blocking` mismatch sample",
|
|
5379
|
+
payload=blocking_mismatch_payload,
|
|
5380
|
+
expected_reports=1,
|
|
5381
|
+
)
|
|
5382
|
+
if blocking_mismatch_payload.get("result") != "block":
|
|
5383
|
+
failures.append(Failure("repo-interop", "`shadow-parity --mode blocking` must block mismatches"))
|
|
5384
|
+
|
|
5385
|
+
unreadable_target = base / "unreadable"
|
|
5386
|
+
shutil.copytree(present_target, unreadable_target)
|
|
5387
|
+
(unreadable_target / ".loom/shadow/closeout-repo.json").unlink()
|
|
5388
|
+
blocking_unreadable_payload, error = load_command_json(
|
|
5389
|
+
root,
|
|
5390
|
+
[
|
|
5391
|
+
"python3",
|
|
5392
|
+
"tools/loom_flow.py",
|
|
5393
|
+
"shadow-parity",
|
|
5394
|
+
"--target",
|
|
5395
|
+
str(unreadable_target),
|
|
5396
|
+
"--surface",
|
|
5397
|
+
"closeout",
|
|
5398
|
+
"--blocking",
|
|
5399
|
+
],
|
|
5400
|
+
)
|
|
5401
|
+
if error:
|
|
5402
|
+
failures.append(Failure("repo-interop", f"`shadow-parity --blocking` unreadable sample failed: {error}"))
|
|
5403
|
+
else:
|
|
5404
|
+
require_shadow_parity_payload(
|
|
5405
|
+
failures,
|
|
5406
|
+
category="repo-interop",
|
|
5407
|
+
context="`shadow-parity --blocking` unreadable sample",
|
|
5408
|
+
payload=blocking_unreadable_payload,
|
|
5409
|
+
expected_reports=1,
|
|
5410
|
+
)
|
|
5411
|
+
if blocking_unreadable_payload.get("result") != "block":
|
|
5412
|
+
failures.append(Failure("repo-interop", "`shadow-parity --blocking` must block unreadable surfaces"))
|
|
5413
|
+
|
|
5325
5414
|
return failures
|
|
5326
5415
|
|
|
5327
5416
|
|
|
@@ -276,6 +276,17 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
276
276
|
default=".loom/bootstrap/init-result.json",
|
|
277
277
|
help="Init-result path relative to the target root",
|
|
278
278
|
)
|
|
279
|
+
shadow.add_argument(
|
|
280
|
+
"--mode",
|
|
281
|
+
choices=("validation-only", "blocking"),
|
|
282
|
+
default="validation-only",
|
|
283
|
+
help="Shadow parity enforcement mode; defaults to validation-only.",
|
|
284
|
+
)
|
|
285
|
+
shadow.add_argument(
|
|
286
|
+
"--blocking",
|
|
287
|
+
action="store_true",
|
|
288
|
+
help="Shortcut for --mode blocking. This is explicit opt-in and never the default.",
|
|
289
|
+
)
|
|
279
290
|
|
|
280
291
|
runtime_parity = subparsers.add_parser(
|
|
281
292
|
"runtime-parity",
|
|
@@ -543,8 +554,11 @@ def shadow_parity_report(
|
|
|
543
554
|
empty_report = {
|
|
544
555
|
"surface": surface,
|
|
545
556
|
"result": "unreadable",
|
|
557
|
+
"classification": "gate_failure",
|
|
558
|
+
"blocking": False,
|
|
546
559
|
"summary": "shadow parity could not be evaluated for this surface.",
|
|
547
560
|
"missing_inputs": [],
|
|
561
|
+
"recommended_action": "restore the declared Loom and repo-native shadow parity locators before treating this surface as authoritative.",
|
|
548
562
|
"host_adapters": [],
|
|
549
563
|
"repo_native_carriers": [],
|
|
550
564
|
"loom_surface": {
|
|
@@ -633,8 +647,11 @@ def shadow_parity_report(
|
|
|
633
647
|
return {
|
|
634
648
|
"surface": surface,
|
|
635
649
|
"result": "match",
|
|
650
|
+
"classification": None,
|
|
651
|
+
"blocking": False,
|
|
636
652
|
"summary": "Loom and repo-native surfaces report the same normalized result.",
|
|
637
653
|
"missing_inputs": [],
|
|
654
|
+
"recommended_action": "no shadow parity action required.",
|
|
638
655
|
"host_adapters": relevant_host_adapters,
|
|
639
656
|
"repo_native_carriers": relevant_repo_native_carriers,
|
|
640
657
|
"loom_surface": loom_surface,
|
|
@@ -643,8 +660,11 @@ def shadow_parity_report(
|
|
|
643
660
|
return {
|
|
644
661
|
"surface": surface,
|
|
645
662
|
"result": "mismatch",
|
|
663
|
+
"classification": "drift",
|
|
664
|
+
"blocking": False,
|
|
646
665
|
"summary": "Loom and repo-native surfaces disagree on the normalized result.",
|
|
647
666
|
"missing_inputs": [],
|
|
667
|
+
"recommended_action": "resolve the parity mismatch or explicitly choose the authoritative surface outside repo interop before enabling blocking consumption.",
|
|
648
668
|
"host_adapters": relevant_host_adapters,
|
|
649
669
|
"repo_native_carriers": relevant_repo_native_carriers,
|
|
650
670
|
"loom_surface": loom_surface,
|
|
@@ -6122,6 +6142,7 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
|
|
|
6122
6142
|
governance_surface = build_governance_surface(target_root)
|
|
6123
6143
|
repo_interop = governance_surface.get("repo_interop")
|
|
6124
6144
|
requested_surfaces = SHADOW_PARITY_SURFACES if args.surface == "all" else (args.surface,)
|
|
6145
|
+
mode = "blocking" if args.blocking else args.mode
|
|
6125
6146
|
reports = [
|
|
6126
6147
|
shadow_parity_report(
|
|
6127
6148
|
repo_interop,
|
|
@@ -6131,9 +6152,18 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
|
|
|
6131
6152
|
for surface in requested_surfaces
|
|
6132
6153
|
]
|
|
6133
6154
|
|
|
6134
|
-
|
|
6155
|
+
all_match = bool(reports) and all(report["result"] == "match" for report in reports)
|
|
6156
|
+
blocking_reports = [report for report in reports if report.get("result") != "match"]
|
|
6157
|
+
if mode == "blocking":
|
|
6158
|
+
result = "pass" if all_match else "block"
|
|
6159
|
+
for report in blocking_reports:
|
|
6160
|
+
report["blocking"] = True
|
|
6161
|
+
else:
|
|
6162
|
+
result = "pass" if all_match else "warn"
|
|
6135
6163
|
if result == "pass":
|
|
6136
6164
|
summary = "shadow parity matches across all requested surfaces."
|
|
6165
|
+
elif mode == "blocking":
|
|
6166
|
+
summary = "shadow parity blocking mode found mismatch or unreadable surfaces."
|
|
6137
6167
|
else:
|
|
6138
6168
|
summaries = {report["result"] for report in reports}
|
|
6139
6169
|
if "mismatch" in summaries:
|
|
@@ -6150,10 +6180,12 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
|
|
|
6150
6180
|
return emit(
|
|
6151
6181
|
{
|
|
6152
6182
|
"command": "shadow-parity",
|
|
6183
|
+
"mode": mode,
|
|
6184
|
+
"blocking": mode == "blocking",
|
|
6153
6185
|
"result": result,
|
|
6154
6186
|
"summary": summary,
|
|
6155
6187
|
"missing_inputs": missing_inputs,
|
|
6156
|
-
"fallback_to": None,
|
|
6188
|
+
"fallback_to": "manual-reconciliation" if result == "block" else None,
|
|
6157
6189
|
"runtime_state": runtime_state,
|
|
6158
6190
|
"governance_surface": governance_surface,
|
|
6159
6191
|
"reports": reports,
|