prizmkit 1.1.136 → 1.1.138

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.
Files changed (47) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +0 -1
  4. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +3 -5
  5. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +24 -40
  6. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +3 -5
  7. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -5
  8. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +3 -5
  9. package/bundled/dev-pipeline/scripts/prompt_framework.py +0 -5
  10. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +3 -3
  11. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +24 -26
  12. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +5 -4
  13. package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
  14. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -2
  15. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
  16. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +2 -10
  17. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +2 -1
  18. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -2
  19. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +5 -5
  20. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +5 -5
  21. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +2 -12
  22. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +2 -10
  23. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
  25. package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +2 -2
  26. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +2 -10
  27. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +2 -1
  28. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +5 -4
  29. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +11 -17
  30. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +3 -1
  31. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +1 -6
  32. package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -5
  33. package/bundled/skills/_metadata.json +2 -2
  34. package/bundled/skills/prizmkit-test/SKILL.md +18 -22
  35. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +6 -78
  36. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +1 -2
  37. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +9 -56
  38. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +2 -2
  39. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +13 -13
  40. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +8 -16
  41. package/bundled/skills/prizmkit-test/references/examples.md +3 -5
  42. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +6 -8
  43. package/bundled/skills/prizmkit-test/references/test-report-template.md +4 -7
  44. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +6 -8
  45. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +70 -247
  46. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +251 -201
  47. package/package.json +1 -1
@@ -8,7 +8,6 @@ Usage:
8
8
  python3 build_test_evidence.py --project-root ROOT --evidence-dir DIR prepare-tests --request REQUEST
9
9
  python3 build_test_evidence.py --project-root ROOT --evidence-dir DIR execute --request REQUEST
10
10
  python3 build_test_evidence.py --project-root ROOT --evidence-dir DIR execute --replay-receipt RECEIPT
11
- python3 build_test_evidence.py --project-root ROOT --evidence-dir DIR differential --request REQUEST
12
11
  python3 build_test_evidence.py --project-root ROOT --evidence-dir DIR finalize --request REQUEST
13
12
  python3 build_test_evidence.py --project-root ROOT --evidence-dir DIR render-report
14
13
  python3 build_test_evidence.py --project-root ROOT --evidence-dir DIR resume --manifest MANIFEST --inventory INVENTORY
@@ -30,7 +29,6 @@ import shutil
30
29
  import signal
31
30
  import subprocess
32
31
  import sys
33
- import tempfile
34
32
  import uuid
35
33
  from pathlib import Path
36
34
  from typing import Any
@@ -46,6 +44,9 @@ LAYERS = (
46
44
  "affected-module-regression", "regression-ring",
47
45
  )
48
46
  BLOCKED_EXTERNAL_CLASSES = {"production", "unknown"}
47
+ REPAIR_SCOPES = {
48
+ "test-infrastructure", "production", "runtime", "schema", "dependency", "public-interface",
49
+ }
49
50
  INVENTORY_RESERVED_ROOTS = (
50
51
  ".prizmkit/state",
51
52
  ".prizmkit/test/evidence",
@@ -413,9 +414,14 @@ def validate_execution_request(request: Any, *, plan: dict[str, Any] | None = No
413
414
  ensure_string_list(probe, f"tool probe {name}", allow_empty=False)
414
415
  has_ids = "test_ids" in request
415
416
  has_paths = "test_paths" in request
416
- auto_bind = request.get("auto_bind") is True
417
- if not has_ids and not has_paths and not auto_bind:
417
+ has_auto_bind = "auto_bind" in request
418
+ if has_auto_bind and not isinstance(request["auto_bind"], bool):
419
+ raise RequestError("execution request auto_bind must be boolean")
420
+ binding_modes = int(has_ids) + int(has_paths) + int(request.get("auto_bind") is True)
421
+ if binding_modes == 0:
418
422
  raise RequestError("execution request must provide test_ids, test_paths, or auto_bind=true")
423
+ if binding_modes != 1 or (has_auto_bind and request["auto_bind"] is False):
424
+ raise RequestError("execution request binding modes test_ids, test_paths, and auto_bind=true are mutually exclusive")
419
425
  if has_ids:
420
426
  test_ids = ensure_string_list(request["test_ids"], "test_ids", allow_empty=False)
421
427
  if len(set(test_ids)) != len(test_ids):
@@ -424,8 +430,6 @@ def validate_execution_request(request: Any, *, plan: dict[str, Any] | None = No
424
430
  test_paths = ensure_string_list(request["test_paths"], "test_paths", allow_empty=False)
425
431
  if len(set(test_paths)) != len(test_paths):
426
432
  raise RequestError("execution request test_paths contains duplicates")
427
- if "auto_bind" in request and not isinstance(request["auto_bind"], bool):
428
- raise RequestError("execution request auto_bind must be boolean")
429
433
  if request["layer"] not in LAYERS and request["layer"] != "lightweight":
430
434
  raise RequestError(f"unknown execution layer: {request['layer']}")
431
435
  if plan is not None and has_ids:
@@ -470,7 +474,17 @@ def command_scope_test_ids(project_root: Path, request: dict[str, Any], tests: l
470
474
  "-C", "--config", "--config-file", "--project", "--package", "-p",
471
475
  "--root", "--rootdir", "--cwd", "--testPathPattern", "--testNamePattern",
472
476
  }
477
+ scope_affecting_options = {
478
+ "-C", "--config", "--config-file", "--project", "--package", "--root", "--rootdir",
479
+ "--cwd", "--testPathPattern", "--testNamePattern", "--grep", "-k", "--filter", "-t",
480
+ }
473
481
  for token in command[1:]:
482
+ option_name = token.split("=", 1)[0]
483
+ if option_name in scope_affecting_options:
484
+ raise RequestError(
485
+ "command scope cannot be inferred when the native command uses a test-selection "
486
+ f"option: {option_name}"
487
+ )
474
488
  if skip_option_value:
475
489
  skip_option_value = False
476
490
  continue
@@ -497,7 +511,7 @@ def command_scope_test_ids(project_root: Path, request: dict[str, Any], tests: l
497
511
  if package_wide:
498
512
  return sorted({item["id"] for item in tests})
499
513
  raise RequestError(
500
- "auto_bind cannot infer native test scope from command; provide test_paths or use a recognized package-wide test command"
514
+ "command scope cannot be inferred from the native command; use a recognized package-wide command or an explicit test path in the command"
501
515
  )
502
516
 
503
517
  matched_paths: set[str] = set()
@@ -517,7 +531,7 @@ def command_scope_test_ids(project_root: Path, request: dict[str, Any], tests: l
517
531
  matched_paths.add(test_path)
518
532
  if not matched_paths:
519
533
  raise RequestError(
520
- "auto_bind command scope does not cover any prepared native test; provide test_paths explicitly"
534
+ "native command scope does not cover any prepared test"
521
535
  )
522
536
  return [item["id"] for item in tests if item["project_path"] in matched_paths]
523
537
 
@@ -529,19 +543,26 @@ def resolve_execution_test_ids(project_root: Path, evidence_dir: Path, request:
529
543
  raise RequestError("builder-generated test plan is required before path-based or auto-bound execution")
530
544
  return request
531
545
  if (evidence_dir / "lifecycle.json").is_file() and not (evidence_dir / "test-preparation.json").is_file():
532
- raise RequestError("canonical lifecycle requires prepare-tests before execute or differential")
546
+ raise RequestError("canonical lifecycle requires prepare-tests before execute")
533
547
  tests = test_plan_entries(plan)
534
548
  by_id = {item["id"]: item for item in tests}
535
549
  by_path = {item["project_path"]: item for item in tests}
536
550
  if "test_ids" in request:
537
- resolved = list(request["test_ids"])
551
+ declared = list(request["test_ids"])
538
552
  elif "test_paths" in request:
539
553
  unknown_paths = sorted(set(request["test_paths"]) - set(by_path))
540
554
  if unknown_paths:
541
555
  raise RequestError(f"execution request references unknown test paths: {', '.join(unknown_paths)}")
542
- resolved = [by_path[path]["id"] for path in request["test_paths"]]
556
+ declared = [by_path[path]["id"] for path in request["test_paths"]]
543
557
  else:
544
- resolved = command_scope_test_ids(project_root, request, tests)
558
+ declared = None
559
+ inferred = command_scope_test_ids(project_root, request, tests)
560
+ if declared is not None and set(declared) != set(inferred):
561
+ raise RequestError(
562
+ "execution request binding does not exactly match native command scope "
563
+ f"(declared: {', '.join(sorted(declared))}; inferred: {', '.join(sorted(inferred))})"
564
+ )
565
+ resolved = inferred
545
566
  if not resolved:
546
567
  raise RequestError("execution request resolved no prepared tests")
547
568
  unknown = sorted(set(resolved) - set(by_id))
@@ -553,6 +574,8 @@ def resolve_execution_test_ids(project_root: Path, evidence_dir: Path, request:
553
574
  )
554
575
  if invalid_layer:
555
576
  raise RequestError(f"execution layer is not planned for test IDs: {', '.join(invalid_layer)}")
577
+ request.pop("test_paths", None)
578
+ request.pop("auto_bind", None)
556
579
  request["test_ids"] = resolved
557
580
  return request
558
581
 
@@ -821,9 +844,6 @@ def append_receipt(evidence_dir: Path, receipt: dict[str, Any]) -> None:
821
844
  existing: Any = load_json(path) if path.exists() else []
822
845
  if not isinstance(existing, list):
823
846
  raise RequestError("executions.json must remain an array")
824
- previous = canonical_sha256(existing[-1]) if existing else None
825
- if receipt["previous_receipt_sha256"] != previous:
826
- raise RequestError("receipt chain mismatch")
827
847
  existing.append(receipt)
828
848
  write_json_atomic(path, existing)
829
849
 
@@ -847,19 +867,20 @@ def execute_request(
847
867
  evidence_dir: Path,
848
868
  request_path: Path,
849
869
  *,
850
- execution_root: Path | None = None,
851
870
  replay_of: str | None = None,
852
- isolation: dict[str, Any] | None = None,
853
871
  selected_execution: bool = True,
854
872
  ) -> dict[str, Any]:
855
873
  request_value = load_json(request_path)
856
874
  original_test_ids = list(request_value["test_ids"]) if isinstance(request_value, dict) and isinstance(request_value.get("test_ids"), list) else None
875
+ original_binding_fields = {
876
+ key for key in ("test_ids", "test_paths", "auto_bind")
877
+ if isinstance(request_value, dict) and key in request_value
878
+ }
857
879
  request = validate_execution_request(request_value, plan=plan_for_execution(evidence_dir))
858
880
  request = resolve_execution_test_ids(project_root, evidence_dir, request)
859
- if original_test_ids != request["test_ids"]:
881
+ if original_binding_fields != {"test_ids"} or original_test_ids != request["test_ids"]:
860
882
  write_json_atomic(request_path, request)
861
- root = execution_root or project_root
862
- cwd = confined(root, request["cwd"] or ".", must_exist=True, directory=True)
883
+ cwd = confined(project_root, request["cwd"] or ".", must_exist=True, directory=True)
863
884
  complete_environment = {"PATH": os.environ.get("PATH", os.defpath)}
864
885
  if os.name == "nt" and os.environ.get("SYSTEMROOT"):
865
886
  complete_environment["SYSTEMROOT"] = os.environ["SYSTEMROOT"]
@@ -900,7 +921,6 @@ def execute_request(
900
921
  existing = load_json(evidence_dir / "executions.json") if (evidence_dir / "executions.json").exists() else []
901
922
  if not isinstance(existing, list):
902
923
  raise RequestError("executions.json must remain an array")
903
- previous = canonical_sha256(existing[-1]) if existing else None
904
924
  same_request_attempts = sum(1 for item in existing if isinstance(item, dict) and item.get("request_sha256") == file_sha256(request_path))
905
925
  receipt = {
906
926
  "receipt_format": "prizmkit-runner-generated-v1",
@@ -927,213 +947,13 @@ def execute_request(
927
947
  "reliable": probes_reliable and not timed_out and not execution_error,
928
948
  "started_at": started,
929
949
  "finished_at": finished,
930
- "previous_receipt_sha256": previous,
931
950
  "replay_of": replay_of,
932
- "isolation": isolation,
951
+ "isolation": {"kind": "project-direct"},
933
952
  }
934
953
  append_receipt(evidence_dir, receipt)
935
954
  return receipt
936
955
 
937
956
 
938
- def copy_project_tree(project_root: Path, destination: Path, evidence_dir: Path) -> None:
939
- def ignore(directory: str, names: list[str]) -> set[str]:
940
- directory_relative = Path(directory).resolve().relative_to(project_root.resolve())
941
- ignored: set[str] = set()
942
- for name in names:
943
- relative = directory_relative / name
944
- if any(part in _IGNORED_DIR_NAMES for part in relative.parts):
945
- ignored.add(name)
946
- continue
947
- if any(relative == Path(prefix) or Path(prefix) in relative.parents for prefix in INVENTORY_RESERVED_ROOTS):
948
- ignored.add(name)
949
- elif (project_root / relative).resolve() == evidence_dir.resolve() or evidence_dir.resolve() in (project_root / relative).resolve().parents:
950
- ignored.add(name)
951
- return ignored
952
-
953
- shutil.copytree(project_root, destination, ignore=ignore)
954
-
955
-
956
- def _iso_tree_sha256(root: Path) -> str:
957
- """Hash an isolated temp tree (not the live project)."""
958
- entries: list[dict[str, str]] = []
959
- for candidate in sorted(root.rglob("*")):
960
- if not candidate.is_file():
961
- continue
962
- entries.append({"path": candidate.relative_to(root).as_posix(), "sha256": file_sha256(candidate)})
963
- return canonical_sha256(entries)
964
-
965
-
966
- def git_archive_baseline(project_root: Path, baseline_commit: str, destination: Path) -> None:
967
- result = subprocess.run(
968
- ["git", "-C", str(project_root), "archive", "--format=tar", baseline_commit],
969
- capture_output=True, check=False,
970
- )
971
- if result.returncode != 0:
972
- raise RequestError(f"cannot archive baseline commit {baseline_commit}: {result.stderr.decode(errors='replace')}")
973
- destination.mkdir(parents=True)
974
- extracted = subprocess.run(["tar", "-xf", "-", "-C", str(destination)], input=result.stdout, capture_output=True, check=False)
975
- if extracted.returncode != 0:
976
- raise RequestError(f"cannot extract baseline archive: {extracted.stderr.decode(errors='replace')}")
977
-
978
-
979
- def materialize_request(evidence_dir: Path, name: str, request: dict[str, Any]) -> Path:
980
- path = evidence_dir / "requests" / name
981
- write_json_atomic(path, request)
982
- return path
983
-
984
-
985
- def validate_not_applicable(value: Any) -> dict[str, Any]:
986
- value = ensure_object(value, "differential N/A decision")
987
- required = {"reason", "rationale", "evidence", "considered_signals", "conflicts"}
988
- if set(value) != required:
989
- raise RequestError(f"differential N/A fields must be exactly {sorted(required)}")
990
- if value["reason"] not in {"new-behavior", "textual-contract"}:
991
- raise RequestError("differential N/A reason must be new-behavior or textual-contract")
992
- if not isinstance(value["rationale"], str) or len(value["rationale"].strip()) < 16:
993
- raise RequestError("differential N/A rationale is too short")
994
- ensure_string_list(value["evidence"], "differential N/A evidence", allow_empty=False)
995
- ensure_string_list(value["considered_signals"], "differential N/A considered_signals", allow_empty=False)
996
- if not isinstance(value["conflicts"], list):
997
- raise RequestError("differential N/A conflicts must be an array")
998
- if value["reason"] == "new-behavior" and "no stable pre-change observable" not in value["rationale"].lower():
999
- raise RequestError("new-behavior differential N/A must explain that no stable pre-change observable exists")
1000
- if value["reason"] == "textual-contract" and "textual" not in value["rationale"].lower():
1001
- raise RequestError("textual-contract differential N/A must explain why behavior is textual")
1002
- return value
1003
-
1004
-
1005
- def append_proof(evidence_dir: Path, proof: dict[str, Any]) -> None:
1006
- proof_path = evidence_dir / "differential-proof.json"
1007
- proof_record = load_json(proof_path) if proof_path.exists() else {"proofs": []}
1008
- if not isinstance(proof_record, dict) or not isinstance(proof_record.get("proofs"), list):
1009
- raise RequestError("differential-proof.json has invalid append target")
1010
- if any(item.get("behavior_id") == proof.get("behavior_id") for item in proof_record["proofs"] if isinstance(item, dict)):
1011
- raise RequestError(f"duplicate differential proof: {proof.get('behavior_id')}")
1012
- proof_record["proofs"].append(proof)
1013
- write_json_atomic(proof_path, proof_record)
1014
-
1015
-
1016
- def run_differential(project_root: Path, evidence_dir: Path, request_path: Path) -> dict[str, Any]:
1017
- request = ensure_object(load_json(request_path), "differential request")
1018
- required = {"request_version", "behavior_id", "method", "execution_request", "baseline_commit", "mutation_patch_path", "test_overlay_paths", "expected_failure_signals"}
1019
- na_required = {"request_version", "behavior_id", "method", "not_applicable"}
1020
- if set(request) == na_required:
1021
- if request["request_version"] != "1.0" or request["method"] != "not-applicable":
1022
- raise RequestError("invalid differential N/A request")
1023
- not_applicable = validate_not_applicable(request["not_applicable"])
1024
- proof = {
1025
- "behavior_id": request["behavior_id"], "classification": "NOT_APPLICABLE",
1026
- "method": None, "differential_request_path": evidence_relative(evidence_dir, request_path),
1027
- "differential_request_sha256": file_sha256(request_path), "baseline_commit": None,
1028
- "baseline_execution_id": None, "mutation_execution_id": None,
1029
- "current_execution_id": None, "failure_reason_matched": False, "cleanup_succeeded": True,
1030
- "isolation_tree_sha256": None, "mutation_apply_sha256": None, "mutation_restore_sha256": None,
1031
- "not_applicable": not_applicable,
1032
- }
1033
- append_proof(evidence_dir, proof)
1034
- return proof
1035
- if set(request) != required or request["request_version"] != "1.0":
1036
- raise RequestError(f"differential request fields must be exactly {sorted(required)} with version 1.0")
1037
- execution_request = resolve_execution_test_ids(
1038
- project_root,
1039
- evidence_dir,
1040
- validate_execution_request(
1041
- request["execution_request"], plan=plan_for_execution(evidence_dir),
1042
- ),
1043
- )
1044
- request["execution_request"] = execution_request
1045
- write_json_atomic(request_path, request)
1046
- expected_failure_signals = ensure_string_list(request["expected_failure_signals"], "expected_failure_signals", allow_empty=False)
1047
- overlay_paths = ensure_string_list(request["test_overlay_paths"], "test_overlay_paths")
1048
- for relative in overlay_paths:
1049
- confined(project_root, relative, must_exist=True)
1050
- current_request_path = materialize_request(evidence_dir, f"differential-{uuid.uuid4().hex}.execution.json", execution_request)
1051
- baseline_receipt = None
1052
- mutation_receipt = None
1053
- current_receipt = None
1054
- isolation_tree = None
1055
- mutation_apply_sha = None
1056
- mutation_restore_sha = None
1057
- cleanup_succeeded = False
1058
- try:
1059
- with tempfile.TemporaryDirectory(prefix="prizmkit-differential-") as temporary:
1060
- temporary_root = Path(temporary)
1061
- current_root = temporary_root / "current"
1062
- copy_project_tree(project_root, current_root, evidence_dir)
1063
- isolation_tree = _iso_tree_sha256(current_root)
1064
- if request["method"] == "baseline":
1065
- failing_root = temporary_root / "baseline"
1066
- git_archive_baseline(project_root, request["baseline_commit"], failing_root)
1067
- for relative in overlay_paths:
1068
- source = confined(project_root, relative, must_exist=True)
1069
- destination = confined(failing_root, relative)
1070
- destination.parent.mkdir(parents=True, exist_ok=True)
1071
- shutil.copy2(source, destination)
1072
- baseline_receipt = execute_request(
1073
- project_root, evidence_dir, current_request_path, execution_root=failing_root,
1074
- isolation={"kind": "baseline", "tree_sha256": _iso_tree_sha256(failing_root), "baseline_commit": request["baseline_commit"]},
1075
- selected_execution=False,
1076
- )
1077
- elif request["method"] == "controlled-mutation":
1078
- failing_root = temporary_root / "mutation"
1079
- shutil.copytree(current_root, failing_root)
1080
- patch_value = request["mutation_patch_path"]
1081
- if not isinstance(patch_value, str) or not patch_value:
1082
- raise RequestError("controlled mutation requires mutation_patch_path")
1083
- patch_path = request_file(evidence_dir, patch_value)
1084
- applied = subprocess.run(["git", "apply", "--whitespace=nowarn", str(patch_path)], cwd=failing_root, capture_output=True, check=False)
1085
- if applied.returncode != 0:
1086
- raise RequestError(f"controlled mutation could not be applied: {applied.stderr.decode(errors='replace')}")
1087
- mutation_apply_sha = _iso_tree_sha256(failing_root)
1088
- mutation_receipt = execute_request(
1089
- project_root, evidence_dir, current_request_path, execution_root=failing_root,
1090
- isolation={"kind": "controlled-mutation", "tree_sha256": _iso_tree_sha256(failing_root), "patch_sha256": file_sha256(patch_path)},
1091
- selected_execution=False,
1092
- )
1093
- mutation_restore_sha = isolation_tree
1094
- else:
1095
- raise RequestError("differential method must be baseline or controlled-mutation")
1096
- current_receipt = execute_request(
1097
- project_root, evidence_dir, current_request_path, execution_root=current_root,
1098
- isolation={"kind": "current", "tree_sha256": isolation_tree},
1099
- )
1100
- cleanup_succeeded = True
1101
- finally:
1102
- # tempfile.TemporaryDirectory handles cleanup automatically;
1103
- # cleanup_succeeded is set above because the context manager exited
1104
- # without exception, meaning isolation was preserved
1105
- pass
1106
- failing_receipt = baseline_receipt or mutation_receipt
1107
- failing_output = b""
1108
- if failing_receipt:
1109
- failing_output = (evidence_dir / failing_receipt["stdout_path"]).read_bytes() + (evidence_dir / failing_receipt["stderr_path"]).read_bytes()
1110
- failure_reason_matched = any(signal.encode() in failing_output for signal in expected_failure_signals)
1111
- proven = bool(
1112
- failing_receipt and current_receipt and failing_receipt["exit_code"] != 0
1113
- and current_receipt["exit_code"] == 0 and failure_reason_matched
1114
- and cleanup_succeeded
1115
- )
1116
- proof = {
1117
- "behavior_id": request["behavior_id"],
1118
- "classification": "PROVEN" if proven else "UNPROVEN",
1119
- "method": request["method"],
1120
- "differential_request_path": evidence_relative(evidence_dir, request_path),
1121
- "differential_request_sha256": file_sha256(request_path),
1122
- "baseline_commit": request["baseline_commit"],
1123
- "baseline_execution_id": baseline_receipt["execution_id"] if baseline_receipt else None,
1124
- "mutation_execution_id": mutation_receipt["execution_id"] if mutation_receipt else None,
1125
- "current_execution_id": current_receipt["execution_id"] if current_receipt else None,
1126
- "failure_reason_matched": failure_reason_matched,
1127
- "cleanup_succeeded": cleanup_succeeded,
1128
- "isolation_tree_sha256": isolation_tree,
1129
- "mutation_apply_sha256": mutation_apply_sha,
1130
- "mutation_restore_sha256": mutation_restore_sha,
1131
- "not_applicable": None,
1132
- }
1133
- append_proof(evidence_dir, proof)
1134
- return proof
1135
-
1136
-
1137
957
  def run_capture_change(
1138
958
  project_root: Path,
1139
959
  evidence_dir: Path,
@@ -1166,7 +986,6 @@ def run_init(project_root: Path, evidence_dir: Path, baseline: str | None, outpu
1166
986
  for directory in ("requests", "receipts", "raw", "generated-tests", "contracts", "runner"):
1167
987
  (evidence_dir / directory).mkdir(parents=True, exist_ok=True)
1168
988
  write_json_atomic(evidence_dir / "executions.json", [])
1169
- write_json_atomic(evidence_dir / "differential-proof.json", {"proofs": []})
1170
989
  write_json_atomic(evidence_dir / "lifecycle.json", {
1171
990
  "lifecycle_format": "prizmkit-canonical-lifecycle-v1",
1172
991
  "status": "initialized",
@@ -1217,8 +1036,6 @@ def run_render_report(evidence_dir: Path, output_name: str) -> Path:
1217
1036
  validation = ensure_object(load_json(validation_path), "validation") if validation_path.exists() else None
1218
1037
  lifecycle_path = evidence_dir / "lifecycle.json"
1219
1038
  lifecycle = ensure_object(load_json(lifecycle_path), "lifecycle") if lifecycle_path.exists() else {}
1220
- proofs_path = evidence_dir / "differential-proof.json"
1221
- proofs = load_json(proofs_path).get("proofs", []) if proofs_path.exists() else []
1222
1039
  executions = load_json(evidence_dir / "executions.json")
1223
1040
  artifact_dir = lifecycle.get("artifact_dir")
1224
1041
  lines = [
@@ -1236,7 +1053,6 @@ def run_render_report(evidence_dir: Path, output_name: str) -> Path:
1236
1053
  f"- Artifact Dir: {artifact_dir or 'not provided'}",
1237
1054
  "",
1238
1055
  f"Execution receipts: {len(executions) if isinstance(executions, list) else 0}",
1239
- f"Differential proofs: {len(proofs) if isinstance(proofs, list) else 0}",
1240
1056
  "The legacy test-report interface is not supported.",
1241
1057
  "",
1242
1058
  ]
@@ -1461,7 +1277,7 @@ def stage_output_owner(relative: str, change_class: str) -> str:
1461
1277
  return "INFRA_READY"
1462
1278
  if relative.startswith("generated-tests/"):
1463
1279
  return "TEST_BUILD"
1464
- if relative in {"executions.json", "differential-proof.json"} or relative.startswith("receipts/") or relative.startswith("raw/") or relative.startswith("runner/"):
1280
+ if relative == "executions.json" or relative.startswith("receipts/") or relative.startswith("raw/") or relative.startswith("runner/"):
1465
1281
  return "EXECUTE_PROVE"
1466
1282
  if relative in FINAL_RECORDS or relative == "lifecycle.json" or relative == "init.json" or relative == "finalize.json":
1467
1283
  return "EVIDENCE_PACKAGE"
@@ -1481,25 +1297,15 @@ def build_manifest(evidence_dir: Path, *, evidence_id: str, identity: dict[str,
1481
1297
  outputs_by_stage = {stage: [] for stage in STAGES}
1482
1298
  for entry in files:
1483
1299
  outputs_by_stage.setdefault(entry["produced_by"], []).append(entry["path"])
1484
- predecessor_files: list[dict[str, str]] = []
1485
1300
  stages = []
1486
1301
  for stage in STAGES:
1487
1302
  outputs = sorted(outputs_by_stage.get(stage, []))
1488
1303
  stages.append({
1489
1304
  "name": stage,
1490
1305
  "status": "complete",
1491
- "input_sha256": canonical_sha256({
1492
- "stage": stage,
1493
- "target_hashes": target_hashes,
1494
- "predecessor_outputs": sorted(predecessor_files, key=lambda item: item["path"]),
1495
- }),
1496
1306
  "outputs": outputs,
1497
1307
  "not_applicable": None,
1498
1308
  })
1499
- predecessor_files.extend(
1500
- {"path": output, "sha256": next(item["sha256"] for item in files if item["path"] == output)}
1501
- for output in outputs if output not in FINAL_RECORDS
1502
- )
1503
1309
  return {
1504
1310
  "protocol_version": "1.0",
1505
1311
  "evidence_id": evidence_id,
@@ -1536,13 +1342,15 @@ def run_finalize(project_root: Path, evidence_dir: Path, request: dict[str, Any]
1536
1342
  raise RequestError(f"finalize request contains unsupported fields: {sorted(set(request) - allowed)}")
1537
1343
  if request.get("request_version", "1.0") != "1.0":
1538
1344
  raise RequestError("unsupported finalize request version")
1345
+ if request.get("repair_scope") is not None and request.get("repair_scope") not in REPAIR_SCOPES:
1346
+ raise RequestError("repair_scope must identify an evidence-backed repair domain")
1539
1347
  lifecycle = load_lifecycle(evidence_dir, required=True)
1540
1348
  if lifecycle.get("finalized") is True:
1541
1349
  raise RequestError("evidence identity is finalized; re-init a new package before changing scope or source")
1542
1350
  capture_path = evidence_dir / "change-capture.json"
1543
1351
  if not capture_path.exists():
1544
1352
  run_capture_change(project_root, evidence_dir, request.get("baseline_commit"), "change-capture.json")
1545
- capture = ensure_object(load_json(capture_path), "change capture")
1353
+ capture = load_change_capture(project_root, evidence_dir, require_fresh=True)
1546
1354
  baseline = capture.get("baseline_commit")
1547
1355
  patch_path = evidence_dir / "source-change.patch"
1548
1356
  if not isinstance(baseline, str) or not patch_path.is_file():
@@ -1592,7 +1400,7 @@ def run_finalize(project_root: Path, evidence_dir: Path, request: dict[str, Any]
1592
1400
  if change_class == "behavior":
1593
1401
  required_records.update({
1594
1402
  "behavior-risk-matrix.json", "test-plan.json", "test-preparation.json",
1595
- "infrastructure-changes.json", "differential-proof.json",
1403
+ "infrastructure-changes.json",
1596
1404
  })
1597
1405
  else:
1598
1406
  required_records.add("lightweight-verification.json")
@@ -1604,10 +1412,31 @@ def run_finalize(project_root: Path, evidence_dir: Path, request: dict[str, Any]
1604
1412
  blockers.append(
1605
1413
  "required evidence records are missing: " + ", ".join(missing_records)
1606
1414
  )
1607
- selected_failures = [item for item in executions if isinstance(item, dict) and item.get("selected_execution") is True and item.get("reliable") is True and item.get("exit_code") != 0]
1415
+ selected_unreliable = [
1416
+ item for item in executions
1417
+ if isinstance(item, dict)
1418
+ and item.get("selected_execution") is True
1419
+ and item.get("reliable") is not True
1420
+ ]
1421
+ for execution in selected_unreliable:
1422
+ blockers.append(
1423
+ "selected execution is unreliable: "
1424
+ + str(execution.get("execution_id", "unknown-execution"))
1425
+ )
1426
+ selected_failures = [
1427
+ item for item in executions
1428
+ if isinstance(item, dict)
1429
+ and item.get("selected_execution") is True
1430
+ and item.get("reliable") is True
1431
+ and item.get("exit_code") != 0
1432
+ ]
1608
1433
  if selected_failures and not request.get("repair_scope"):
1609
1434
  blockers.append("reliable failure exists but repair_scope is not evidence-backed")
1610
- verdict = "TEST_FAIL" if selected_failures and request.get("repair_scope") else ("TEST_BLOCKED" if blockers else "TEST_PASS")
1435
+ verdict = (
1436
+ "TEST_BLOCKED" if blockers
1437
+ else "TEST_FAIL" if selected_failures and request.get("repair_scope")
1438
+ else "TEST_PASS"
1439
+ )
1611
1440
  verdict_record: dict[str, Any] = {
1612
1441
  "verdict": verdict,
1613
1442
  "testing_domain_only": True,
@@ -1775,8 +1604,6 @@ def main() -> int:
1775
1604
  group = execute.add_mutually_exclusive_group(required=True)
1776
1605
  group.add_argument("--request")
1777
1606
  group.add_argument("--replay-receipt")
1778
- differential = subparsers.add_parser("differential")
1779
- differential.add_argument("--request", required=True)
1780
1607
  finalize = subparsers.add_parser("finalize")
1781
1608
  finalize.add_argument("--request", required=True, help="semantic finalization request")
1782
1609
  finalize.add_argument("--output", default="finalize.json")
@@ -1825,10 +1652,6 @@ def main() -> int:
1825
1652
  raise RequestError("recorded request hash no longer matches replay source")
1826
1653
  receipt = execute_request(project_root, evidence_dir, original_request, replay_of=prior.get("execution_id"))
1827
1654
  print(json.dumps(receipt, ensure_ascii=False))
1828
- elif args.subcommand == "differential":
1829
- require_mutable_lifecycle(evidence_dir)
1830
- proof = run_differential(project_root, evidence_dir, request_file(evidence_dir, args.request))
1831
- print(json.dumps(proof, ensure_ascii=False))
1832
1655
  elif args.subcommand == "render-report":
1833
1656
  load_lifecycle(evidence_dir)
1834
1657
  output = run_render_report(evidence_dir, args.output)