prizmkit 1.1.134 → 1.1.136
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/bundled/VERSION.json +3 -3
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-test/SKILL.md +26 -15
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +87 -10
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +2 -0
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +4 -5
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +3 -3
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +2 -2
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +456 -61
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +142 -47
- package/package.json +1 -1
|
@@ -45,23 +45,13 @@ COMMON_RECORDS = {
|
|
|
45
45
|
"source-change.patch", "test-report.md",
|
|
46
46
|
}
|
|
47
47
|
BEHAVIOR_RECORDS = {
|
|
48
|
-
"behavior-risk-matrix.json", "test-plan.json",
|
|
49
|
-
"differential-proof.json",
|
|
48
|
+
"behavior-risk-matrix.json", "test-plan.json",
|
|
49
|
+
"infrastructure-changes.json", "differential-proof.json",
|
|
50
50
|
}
|
|
51
51
|
LIGHTWEIGHT_RECORDS = {"lightweight-verification.json"}
|
|
52
52
|
SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
|
|
53
53
|
UUID_RE = re.compile(r"^[a-f0-9-]{36}$")
|
|
54
54
|
BLOCKED_EXTERNAL_CLASSES = {"production", "unknown"}
|
|
55
|
-
SNAPSHOT_VOLATILE_ROOTS = (
|
|
56
|
-
".prizmkit/state",
|
|
57
|
-
".prizmkit/test/evidence",
|
|
58
|
-
".prizmkit/" + "dev-" + "pipeline",
|
|
59
|
-
".claude/worktrees",
|
|
60
|
-
".agents/worktrees",
|
|
61
|
-
".codebuddy/worktrees",
|
|
62
|
-
".codex/worktrees",
|
|
63
|
-
)
|
|
64
|
-
SNAPSHOT_VOLATILE_NAMES = {".git", "__pycache__", ".pytest_cache", ".mypy_cache"}
|
|
65
55
|
RISK_CONFLICT_PATTERNS = {
|
|
66
56
|
"permission": re.compile(r"auth|tenant|role|permission|access|acl|rbac", re.I),
|
|
67
57
|
"concurrency": re.compile(r"lock|shared[ _-]?state|worker|queue|concurr|parallel|race", re.I),
|
|
@@ -776,7 +766,9 @@ def validate_requests_and_receipts(
|
|
|
776
766
|
if not isinstance(request, dict):
|
|
777
767
|
errors.append(f"request must be an object: {relative}")
|
|
778
768
|
continue
|
|
779
|
-
if request
|
|
769
|
+
if set(request) == {"request_version", "layers", "tests"}:
|
|
770
|
+
validate_definition(request, "testPreparationRequest", records_schema, relative, errors)
|
|
771
|
+
elif request.get("method") == "not-applicable":
|
|
780
772
|
validate_definition(request, "differentialRequest", records_schema, relative, errors)
|
|
781
773
|
not_applicable = request.get("not_applicable")
|
|
782
774
|
if isinstance(not_applicable, dict):
|
|
@@ -935,13 +927,98 @@ def validate_execution_request_semantics(
|
|
|
935
927
|
environment = value.get("environment")
|
|
936
928
|
require(isinstance(environment, dict) and all(isinstance(key, str) and isinstance(item, str) for key, item in environment.items()), f"{location}: environment must contain complete string values", errors)
|
|
937
929
|
test_ids = value.get("test_ids")
|
|
938
|
-
|
|
930
|
+
test_paths = value.get("test_paths")
|
|
931
|
+
auto_bind = value.get("auto_bind") is True
|
|
932
|
+
require(
|
|
933
|
+
(isinstance(test_ids, list) and all(isinstance(item, str) and item for item in test_ids))
|
|
934
|
+
or (isinstance(test_paths, list) and all(isinstance(item, str) and item for item in test_paths))
|
|
935
|
+
or auto_bind,
|
|
936
|
+
f"{location}: execution must provide test_ids, test_paths, or auto_bind",
|
|
937
|
+
errors,
|
|
938
|
+
)
|
|
939
939
|
if receipt:
|
|
940
940
|
require(type(value.get("selected_execution")) is bool, f"{location}: selected_execution must be boolean", errors)
|
|
941
941
|
require(type(value.get("reliable")) is bool, f"{location}: reliable must be boolean", errors)
|
|
942
942
|
require(type(value.get("exit_code")) is int and not isinstance(value.get("exit_code"), bool), f"{location}: exit_code must be an integer", errors)
|
|
943
943
|
|
|
944
944
|
|
|
945
|
+
def changed_test_statuses(root: Path) -> dict[str, str]:
|
|
946
|
+
capture_path = root / "change-capture.json"
|
|
947
|
+
if not capture_path.is_file():
|
|
948
|
+
return {}
|
|
949
|
+
try:
|
|
950
|
+
capture = load_json(capture_path)
|
|
951
|
+
except EvidenceError:
|
|
952
|
+
return {}
|
|
953
|
+
statuses: dict[str, str] = {}
|
|
954
|
+
for item in capture.get("changed_files", []) if isinstance(capture, dict) else []:
|
|
955
|
+
if not isinstance(item, dict) or not isinstance(item.get("path"), str) or item.get("role") == "source":
|
|
956
|
+
continue
|
|
957
|
+
status = str(item.get("status", "")).strip()
|
|
958
|
+
if status == "??" or status.startswith("A"):
|
|
959
|
+
statuses[item["path"]] = "added"
|
|
960
|
+
elif status.startswith("M") or status.endswith("M") or status.startswith(("R", "C")):
|
|
961
|
+
statuses[item["path"]] = "modified"
|
|
962
|
+
return statuses
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def validate_canonical_capture(
|
|
966
|
+
root: Path,
|
|
967
|
+
project_root: Path,
|
|
968
|
+
capture: dict[str, Any],
|
|
969
|
+
inventory: dict[str, Any],
|
|
970
|
+
scope: dict[str, Any],
|
|
971
|
+
errors: list[str],
|
|
972
|
+
) -> None:
|
|
973
|
+
patch_relative = capture.get("patch_path")
|
|
974
|
+
patch = safe_path(root, patch_relative, errors, "canonical change patch") if isinstance(patch_relative, str) else None
|
|
975
|
+
require(patch_relative == "source-change.patch", "canonical capture patch_path must be source-change.patch", errors)
|
|
976
|
+
if patch is not None:
|
|
977
|
+
require(file_sha256(patch) == capture.get("patch_sha256"), "canonical capture patch hash mismatch", errors)
|
|
978
|
+
captured_paths = {
|
|
979
|
+
item.get("path")
|
|
980
|
+
for item in capture.get("changed_files", [])
|
|
981
|
+
if isinstance(item, dict) and isinstance(item.get("path"), str)
|
|
982
|
+
}
|
|
983
|
+
inventory_changed = set(inventory.get("changed_files", [])) if isinstance(inventory.get("changed_files"), list) else set()
|
|
984
|
+
scope_changed = set(scope.get("changed_files", [])) if isinstance(scope.get("changed_files"), list) else set()
|
|
985
|
+
require(captured_paths == inventory_changed, "canonical capture and inventory changed_files do not match", errors)
|
|
986
|
+
require(captured_paths == scope_changed, "canonical capture and scope changed_files do not match", errors)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def validate_test_preparation(
|
|
990
|
+
root: Path,
|
|
991
|
+
project_root: Path,
|
|
992
|
+
preparation: dict[str, Any],
|
|
993
|
+
plan: dict[str, Any],
|
|
994
|
+
inventory_by_category: dict[str, dict[str, str]],
|
|
995
|
+
entry_map: dict[str, dict[str, Any]],
|
|
996
|
+
records_schema: dict[str, Any],
|
|
997
|
+
errors: list[str],
|
|
998
|
+
) -> None:
|
|
999
|
+
validate_definition(preparation, "testPreparation", records_schema, "test-preparation", errors)
|
|
1000
|
+
request_relative = preparation.get("request_path")
|
|
1001
|
+
request_path = require_manifest_entry(request_relative, entry_map, root, errors, "test preparation request") if isinstance(request_relative, str) else None
|
|
1002
|
+
if request_path is not None:
|
|
1003
|
+
require(file_sha256(request_path) == preparation.get("request_sha256"), "test preparation request hash mismatch", errors)
|
|
1004
|
+
prepared_tests = preparation.get("tests")
|
|
1005
|
+
require(prepared_tests == plan.get("tests"), "test preparation tests do not match test plan", errors)
|
|
1006
|
+
statuses = preparation.get("changed_test_statuses")
|
|
1007
|
+
require(isinstance(statuses, dict), "test preparation changed_test_statuses must be an object", errors)
|
|
1008
|
+
if not isinstance(statuses, dict):
|
|
1009
|
+
return
|
|
1010
|
+
for relative, status in statuses.items():
|
|
1011
|
+
require(relative in inventory_by_category.get("tests", {}), f"test preparation status is not in test inventory: {relative}", errors)
|
|
1012
|
+
require(status in {"added", "modified"}, f"test preparation has invalid changed test status: {relative}", errors)
|
|
1013
|
+
for test in prepared_tests if isinstance(prepared_tests, list) else []:
|
|
1014
|
+
if not isinstance(test, dict):
|
|
1015
|
+
continue
|
|
1016
|
+
relative = test.get("project_path")
|
|
1017
|
+
if isinstance(relative, str):
|
|
1018
|
+
expected = statuses.get(relative, "existing")
|
|
1019
|
+
require(test.get("change_status") == expected, f"test preparation status mismatch: {relative}", errors)
|
|
1020
|
+
|
|
1021
|
+
|
|
945
1022
|
def validate_plan_and_scope_bindings(
|
|
946
1023
|
root: Path,
|
|
947
1024
|
project_root: Path,
|
|
@@ -1003,6 +1080,8 @@ def validate_plan_and_scope_bindings(
|
|
|
1003
1080
|
for test_id in receipt.get("test_ids", []):
|
|
1004
1081
|
if isinstance(test_id, str):
|
|
1005
1082
|
selected_by_test.setdefault(test_id, []).append(receipt)
|
|
1083
|
+
changed_tests = changed_test_statuses(root)
|
|
1084
|
+
planned_test_paths: set[str] = set()
|
|
1006
1085
|
known_behavior_ids = {item.get("id") for item in matrix.get("behaviors", []) if isinstance(item, dict)}
|
|
1007
1086
|
for test in tests:
|
|
1008
1087
|
if not isinstance(test, dict):
|
|
@@ -1018,9 +1097,13 @@ def validate_plan_and_scope_bindings(
|
|
|
1018
1097
|
for behavior_id in behavior_ids if isinstance(behavior_ids, list) else []:
|
|
1019
1098
|
require(behavior_id in known_behavior_ids, f"planned test maps unknown behavior: {test_id}/{behavior_id}", errors)
|
|
1020
1099
|
project_relative = test.get("project_path")
|
|
1100
|
+
planned_test_paths.add(project_relative)
|
|
1021
1101
|
inventory_relative = test.get("inventory_path")
|
|
1022
1102
|
snapshot_relative = test.get("snapshot_path")
|
|
1103
|
+
inferred_status = changed_tests.get(project_relative, "existing")
|
|
1023
1104
|
change_status = test.get("change_status", "modified" if isinstance(snapshot_relative, str) else "existing")
|
|
1105
|
+
if changed_tests:
|
|
1106
|
+
require(change_status == inferred_status, f"planned test change_status does not match canonical capture: {test_id}", errors)
|
|
1024
1107
|
require(change_status in {"existing", "added", "modified"}, f"planned test has invalid change_status: {test_id}", errors)
|
|
1025
1108
|
project_path = safe_path(project_root, project_relative, errors, f"planned test {test_id} project_path")
|
|
1026
1109
|
require(project_path is not None and project_path.is_file(), f"planned test live path is missing: {test_id}", errors)
|
|
@@ -1043,6 +1126,19 @@ def validate_plan_and_scope_bindings(
|
|
|
1043
1126
|
for receipt in matching_receipts:
|
|
1044
1127
|
require(receipt.get("layer") in test_layers, f"planned test receipt uses an unplanned layer: {test_id}", errors)
|
|
1045
1128
|
|
|
1129
|
+
for relative, status in changed_tests.items():
|
|
1130
|
+
if relative in inventory_by_category.get("tests", {}) and relative not in planned_test_paths:
|
|
1131
|
+
errors.append(f"changed native test is not bound by test plan: {relative}")
|
|
1132
|
+
generated_dir = root / "generated-tests"
|
|
1133
|
+
referenced_snapshots = {
|
|
1134
|
+
test.get("snapshot_path") for test in tests if isinstance(test, dict) and isinstance(test.get("snapshot_path"), str)
|
|
1135
|
+
}
|
|
1136
|
+
if generated_dir.is_dir():
|
|
1137
|
+
actual_snapshots = {
|
|
1138
|
+
path.relative_to(root).as_posix() for path in generated_dir.rglob("*") if path.is_file()
|
|
1139
|
+
}
|
|
1140
|
+
for relative in sorted(actual_snapshots - referenced_snapshots):
|
|
1141
|
+
errors.append(f"generated test snapshot is not bound by test plan: {relative}")
|
|
1046
1142
|
for ring in scope.get("regression_ring", []):
|
|
1047
1143
|
if not isinstance(ring, dict):
|
|
1048
1144
|
continue
|
|
@@ -1129,29 +1225,6 @@ def validate_matrix_risks(
|
|
|
1129
1225
|
errors.append(f"TEST_PASS has unresolved behavior risk: {behavior_id}/{risk_name}")
|
|
1130
1226
|
|
|
1131
1227
|
|
|
1132
|
-
def is_volatile_snapshot_path(root: Path, candidate: Path, evidence_root: Path) -> bool:
|
|
1133
|
-
try:
|
|
1134
|
-
relative = candidate.relative_to(root)
|
|
1135
|
-
except ValueError:
|
|
1136
|
-
return True
|
|
1137
|
-
if any(part in SNAPSHOT_VOLATILE_NAMES for part in relative.parts):
|
|
1138
|
-
return True
|
|
1139
|
-
if any(relative == Path(prefix) or Path(prefix) in relative.parents for prefix in SNAPSHOT_VOLATILE_ROOTS):
|
|
1140
|
-
return True
|
|
1141
|
-
resolved = candidate.resolve()
|
|
1142
|
-
excluded = evidence_root.resolve()
|
|
1143
|
-
return resolved == excluded or excluded in resolved.parents
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
def tree_sha256(root: Path, evidence_root: Path) -> str:
|
|
1147
|
-
entries: list[dict[str, str]] = []
|
|
1148
|
-
for candidate in sorted(root.rglob("*")):
|
|
1149
|
-
if not candidate.is_file() or is_volatile_snapshot_path(root, candidate, evidence_root):
|
|
1150
|
-
continue
|
|
1151
|
-
entries.append({"path": candidate.relative_to(root).as_posix(), "sha256": file_sha256(candidate)})
|
|
1152
|
-
return canonical_sha256(entries)
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
1228
|
def require_sha_or_null(value: Any, location: str, errors: list[str]) -> None:
|
|
1156
1229
|
require(value is None or is_sha256(value), f"{location} must be a sha256 or null", errors)
|
|
1157
1230
|
|
|
@@ -1172,7 +1245,6 @@ def validate_differential_proofs(
|
|
|
1172
1245
|
receipts = {item.get("execution_id"): item for item in executions if isinstance(item.get("execution_id"), str)}
|
|
1173
1246
|
behavior_ids = {item.get("id") for item in matrix.get("behaviors", []) if isinstance(item, dict)}
|
|
1174
1247
|
proof_by_behavior: dict[str, dict[str, Any]] = {}
|
|
1175
|
-
live_tree_hash = tree_sha256(project_root, root)
|
|
1176
1248
|
proofs = proof.get("proofs")
|
|
1177
1249
|
if not isinstance(proofs, list):
|
|
1178
1250
|
return
|
|
@@ -1189,9 +1261,8 @@ def validate_differential_proofs(
|
|
|
1189
1261
|
validate_differential_na(item.get("not_applicable"), f"differential proof {behavior_id} not_applicable", errors)
|
|
1190
1262
|
for key in (
|
|
1191
1263
|
"method", "differential_request_path", "differential_request_sha256", "baseline_commit",
|
|
1192
|
-
"
|
|
1193
|
-
"
|
|
1194
|
-
"mutation_apply_sha256", "mutation_restore_sha256",
|
|
1264
|
+
"baseline_execution_id", "mutation_execution_id", "current_execution_id",
|
|
1265
|
+
"isolation_tree_sha256", "mutation_apply_sha256", "mutation_restore_sha256",
|
|
1195
1266
|
):
|
|
1196
1267
|
require(item.get(key) is None, f"N/A differential proof must clear {key}: {behavior_id}", errors)
|
|
1197
1268
|
require(item.get("failure_reason_matched") is False, f"N/A differential proof cannot claim a failure match: {behavior_id}", errors)
|
|
@@ -1209,14 +1280,10 @@ def validate_differential_proofs(
|
|
|
1209
1280
|
method = item.get("method")
|
|
1210
1281
|
require(method == request.get("method") and method in {"baseline", "controlled-mutation"}, f"differential method mismatch: {behavior_id}", errors)
|
|
1211
1282
|
require(item.get("baseline_commit") == request.get("baseline_commit") == manifest.get("baseline_commit"), f"differential baseline commit mismatch: {behavior_id}", errors)
|
|
1212
|
-
require(item.get("current_tree_sha256") == request.get("current_tree_sha256") == live_tree_hash, f"differential current tree hash mismatch: {behavior_id}", errors)
|
|
1213
1283
|
for key in (
|
|
1214
|
-
"differential_request_sha256", "
|
|
1215
|
-
"project_tree_after_sha256", "isolation_tree_sha256", "mutation_apply_sha256", "mutation_restore_sha256",
|
|
1284
|
+
"differential_request_sha256", "isolation_tree_sha256", "mutation_apply_sha256", "mutation_restore_sha256",
|
|
1216
1285
|
):
|
|
1217
1286
|
require_sha_or_null(item.get(key), f"differential proof {behavior_id}.{key}", errors)
|
|
1218
|
-
require(item.get("project_tree_before_sha256") == live_tree_hash, f"differential project tree before mismatch: {behavior_id}", errors)
|
|
1219
|
-
require(item.get("project_tree_after_sha256") == live_tree_hash, f"differential project tree after mismatch: {behavior_id}", errors)
|
|
1220
1287
|
require(item.get("cleanup_succeeded") is True, f"differential cleanup failed: {behavior_id}", errors)
|
|
1221
1288
|
|
|
1222
1289
|
current = receipts.get(item.get("current_execution_id"))
|
|
@@ -1237,7 +1304,7 @@ def validate_differential_proofs(
|
|
|
1237
1304
|
require(isinstance(current_isolation, dict) and current_isolation.get("kind") == "current", f"differential current receipt lacks current isolation: {behavior_id}", errors)
|
|
1238
1305
|
require(isinstance(failing_isolation, dict) and failing_isolation.get("kind") == method, f"differential failing receipt isolation mismatch: {behavior_id}", errors)
|
|
1239
1306
|
if isinstance(current_isolation, dict):
|
|
1240
|
-
require(current_isolation.get("tree_sha256") == item.get("isolation_tree_sha256")
|
|
1307
|
+
require(current_isolation.get("tree_sha256") == item.get("isolation_tree_sha256"), f"differential current isolation hash mismatch: {behavior_id}", errors)
|
|
1241
1308
|
if method == "baseline" and isinstance(failing_isolation, dict):
|
|
1242
1309
|
require(failing_isolation.get("baseline_commit") == request.get("baseline_commit"), f"baseline receipt commit mismatch: {behavior_id}", errors)
|
|
1243
1310
|
if method == "controlled-mutation" and isinstance(failing_isolation, dict):
|
|
@@ -1415,11 +1482,25 @@ def validate_evidence(
|
|
|
1415
1482
|
entry_map = validate_manifest(root, manifest, manifest_schema, records_schema, errors, ignored_hash_paths)
|
|
1416
1483
|
change_class = manifest.get("change_class")
|
|
1417
1484
|
final_verdict = manifest.get("final_verdict")
|
|
1485
|
+
canonical_lifecycle = (root / "lifecycle.json").is_file()
|
|
1418
1486
|
required = COMMON_RECORDS
|
|
1487
|
+
if canonical_lifecycle:
|
|
1488
|
+
required = required | {"change-capture.json"}
|
|
1419
1489
|
if final_verdict != "TEST_BLOCKED":
|
|
1420
1490
|
required = required | (BEHAVIOR_RECORDS if change_class == "behavior" else LIGHTWEIGHT_RECORDS)
|
|
1491
|
+
if canonical_lifecycle and change_class == "behavior":
|
|
1492
|
+
required = required | {"test-preparation.json"}
|
|
1421
1493
|
for relative in required:
|
|
1422
1494
|
require(relative in entry_map, f"required evidence record not hashed: {relative}", errors)
|
|
1495
|
+
if canonical_lifecycle:
|
|
1496
|
+
capture_path = root / "change-capture.json"
|
|
1497
|
+
if capture_path.is_file():
|
|
1498
|
+
try:
|
|
1499
|
+
capture = load_json(capture_path)
|
|
1500
|
+
validate_definition(capture, "changeCapture", records_schema, "change-capture", errors)
|
|
1501
|
+
validate_canonical_capture(root, project_root, capture, inventory, scope, errors)
|
|
1502
|
+
except EvidenceError as exc:
|
|
1503
|
+
errors.append(str(exc))
|
|
1423
1504
|
validate_definition(classification, "changeClassification", records_schema, "change-classification", errors)
|
|
1424
1505
|
validate_definition(scope, "scope", records_schema, "scope", errors)
|
|
1425
1506
|
validate_definition(inventory, "targetInventory", records_schema, "target-inventory", errors)
|
|
@@ -1427,18 +1508,32 @@ def validate_evidence(
|
|
|
1427
1508
|
require(classification.get("full_protocol_required") is (change_class == "behavior"), "change classification protocol decision is inconsistent", errors)
|
|
1428
1509
|
|
|
1429
1510
|
plan: dict[str, Any] | None = None
|
|
1511
|
+
preparation: dict[str, Any] | None = None
|
|
1430
1512
|
if change_class == "behavior":
|
|
1431
1513
|
try:
|
|
1432
1514
|
plan = load_json(root / "test-plan.json")
|
|
1515
|
+
if canonical_lifecycle:
|
|
1516
|
+
preparation = load_json(root / "test-preparation.json")
|
|
1433
1517
|
except EvidenceError as exc:
|
|
1434
1518
|
errors.append(str(exc))
|
|
1519
|
+
plan = preparation = None
|
|
1435
1520
|
if isinstance(plan, dict):
|
|
1436
1521
|
validate_definition(plan, "testPlan", records_schema, "test-plan", errors)
|
|
1437
1522
|
else:
|
|
1438
1523
|
errors.append("test-plan.json must be an object")
|
|
1439
1524
|
plan = None
|
|
1525
|
+
if canonical_lifecycle:
|
|
1526
|
+
if isinstance(preparation, dict):
|
|
1527
|
+
pass
|
|
1528
|
+
else:
|
|
1529
|
+
errors.append("test-preparation.json must be an object")
|
|
1440
1530
|
|
|
1441
1531
|
inventory_by_category, _ = validate_inventory(root, project_root, manifest, inventory, scope, plan, entry_map, errors)
|
|
1532
|
+
if canonical_lifecycle and change_class == "behavior" and isinstance(preparation, dict):
|
|
1533
|
+
validate_test_preparation(
|
|
1534
|
+
root, project_root, preparation, plan or {}, inventory_by_category,
|
|
1535
|
+
entry_map, records_schema, errors,
|
|
1536
|
+
)
|
|
1442
1537
|
require(manifest.get("evidence_id_inputs", {}).get("scope_sha256") == canonical_sha256(scope), "scope identity hash mismatch", errors)
|
|
1443
1538
|
validate_scope(
|
|
1444
1539
|
scope, inventory, inventory_by_category, project_root, root,
|