prizmkit 1.1.137 → 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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +3 -5
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +24 -39
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +3 -5
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -5
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +3 -5
- package/bundled/dev-pipeline/scripts/prompt_framework.py +0 -5
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +24 -26
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +5 -4
- package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +2 -10
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +2 -1
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +5 -5
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +5 -5
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +2 -12
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +2 -10
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +2 -2
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +2 -10
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +2 -1
- package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +5 -4
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +10 -16
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +3 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +1 -6
- package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -5
- package/bundled/skills/_metadata.json +2 -2
- package/bundled/skills/prizmkit-test/SKILL.md +17 -20
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +5 -73
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +0 -2
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +2 -2
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +10 -10
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +8 -16
- package/bundled/skills/prizmkit-test/references/examples.md +3 -5
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +6 -8
- package/bundled/skills/prizmkit-test/references/test-report-template.md +4 -7
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +5 -7
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +70 -196
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +251 -167
- 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
|
-
|
|
417
|
-
if
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
556
|
+
declared = [by_path[path]["id"] for path in request["test_paths"]]
|
|
543
557
|
else:
|
|
544
|
-
|
|
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
|
|
|
@@ -844,19 +867,20 @@ def execute_request(
|
|
|
844
867
|
evidence_dir: Path,
|
|
845
868
|
request_path: Path,
|
|
846
869
|
*,
|
|
847
|
-
execution_root: Path | None = None,
|
|
848
870
|
replay_of: str | None = None,
|
|
849
|
-
isolation: dict[str, Any] | None = None,
|
|
850
871
|
selected_execution: bool = True,
|
|
851
872
|
) -> dict[str, Any]:
|
|
852
873
|
request_value = load_json(request_path)
|
|
853
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
|
+
}
|
|
854
879
|
request = validate_execution_request(request_value, plan=plan_for_execution(evidence_dir))
|
|
855
880
|
request = resolve_execution_test_ids(project_root, evidence_dir, request)
|
|
856
|
-
if original_test_ids != request["test_ids"]:
|
|
881
|
+
if original_binding_fields != {"test_ids"} or original_test_ids != request["test_ids"]:
|
|
857
882
|
write_json_atomic(request_path, request)
|
|
858
|
-
|
|
859
|
-
cwd = confined(root, request["cwd"] or ".", must_exist=True, directory=True)
|
|
883
|
+
cwd = confined(project_root, request["cwd"] or ".", must_exist=True, directory=True)
|
|
860
884
|
complete_environment = {"PATH": os.environ.get("PATH", os.defpath)}
|
|
861
885
|
if os.name == "nt" and os.environ.get("SYSTEMROOT"):
|
|
862
886
|
complete_environment["SYSTEMROOT"] = os.environ["SYSTEMROOT"]
|
|
@@ -924,175 +948,12 @@ def execute_request(
|
|
|
924
948
|
"started_at": started,
|
|
925
949
|
"finished_at": finished,
|
|
926
950
|
"replay_of": replay_of,
|
|
927
|
-
"isolation":
|
|
951
|
+
"isolation": {"kind": "project-direct"},
|
|
928
952
|
}
|
|
929
953
|
append_receipt(evidence_dir, receipt)
|
|
930
954
|
return receipt
|
|
931
955
|
|
|
932
956
|
|
|
933
|
-
def materialize_request(evidence_dir: Path, name: str, request: dict[str, Any]) -> Path:
|
|
934
|
-
path = evidence_dir / "requests" / name
|
|
935
|
-
write_json_atomic(path, request)
|
|
936
|
-
return path
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
def validate_not_applicable(value: Any) -> dict[str, Any]:
|
|
940
|
-
value = ensure_object(value, "differential N/A decision")
|
|
941
|
-
required = {"reason", "rationale", "evidence", "considered_signals", "conflicts"}
|
|
942
|
-
if set(value) != required:
|
|
943
|
-
raise RequestError(f"differential N/A fields must be exactly {sorted(required)}")
|
|
944
|
-
if value["reason"] not in {"new-behavior", "textual-contract"}:
|
|
945
|
-
raise RequestError("differential N/A reason must be new-behavior or textual-contract")
|
|
946
|
-
if not isinstance(value["rationale"], str) or len(value["rationale"].strip()) < 16:
|
|
947
|
-
raise RequestError("differential N/A rationale is too short")
|
|
948
|
-
ensure_string_list(value["evidence"], "differential N/A evidence", allow_empty=False)
|
|
949
|
-
ensure_string_list(value["considered_signals"], "differential N/A considered_signals", allow_empty=False)
|
|
950
|
-
if not isinstance(value["conflicts"], list):
|
|
951
|
-
raise RequestError("differential N/A conflicts must be an array")
|
|
952
|
-
if value["reason"] == "new-behavior" and "no stable pre-change observable" not in value["rationale"].lower():
|
|
953
|
-
raise RequestError("new-behavior differential N/A must explain that no stable pre-change observable exists")
|
|
954
|
-
if value["reason"] == "textual-contract" and "textual" not in value["rationale"].lower():
|
|
955
|
-
raise RequestError("textual-contract differential N/A must explain why behavior is textual")
|
|
956
|
-
return value
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
def append_proof(evidence_dir: Path, proof: dict[str, Any]) -> None:
|
|
960
|
-
proof_path = evidence_dir / "differential-proof.json"
|
|
961
|
-
proof_record = load_json(proof_path) if proof_path.exists() else {"proofs": []}
|
|
962
|
-
if not isinstance(proof_record, dict) or not isinstance(proof_record.get("proofs"), list):
|
|
963
|
-
raise RequestError("differential-proof.json has invalid append target")
|
|
964
|
-
if any(item.get("behavior_id") == proof.get("behavior_id") for item in proof_record["proofs"] if isinstance(item, dict)):
|
|
965
|
-
raise RequestError(f"duplicate differential proof: {proof.get('behavior_id')}")
|
|
966
|
-
proof_record["proofs"].append(proof)
|
|
967
|
-
write_json_atomic(proof_path, proof_record)
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
def run_differential(project_root: Path, evidence_dir: Path, request_path: Path) -> dict[str, Any]:
|
|
971
|
-
request = ensure_object(load_json(request_path), "differential request")
|
|
972
|
-
required = {"request_version", "behavior_id", "method", "execution_request", "baseline_commit", "mutation_patch_path", "test_overlay_paths", "expected_failure_signals"}
|
|
973
|
-
na_required = {"request_version", "behavior_id", "method", "not_applicable"}
|
|
974
|
-
if set(request) == na_required:
|
|
975
|
-
if request["request_version"] != "1.0" or request["method"] != "not-applicable":
|
|
976
|
-
raise RequestError("invalid differential N/A request")
|
|
977
|
-
not_applicable = validate_not_applicable(request["not_applicable"])
|
|
978
|
-
proof = {
|
|
979
|
-
"behavior_id": request["behavior_id"], "classification": "NOT_APPLICABLE",
|
|
980
|
-
"method": None, "differential_request_path": evidence_relative(evidence_dir, request_path),
|
|
981
|
-
"differential_request_sha256": file_sha256(request_path), "baseline_commit": None,
|
|
982
|
-
"baseline_execution_id": None, "mutation_execution_id": None,
|
|
983
|
-
"current_execution_id": None, "failure_reason_matched": False, "cleanup_succeeded": True,
|
|
984
|
-
"not_applicable": not_applicable,
|
|
985
|
-
}
|
|
986
|
-
append_proof(evidence_dir, proof)
|
|
987
|
-
return proof
|
|
988
|
-
if set(request) != required or request["request_version"] != "1.0":
|
|
989
|
-
raise RequestError(f"differential request fields must be exactly {sorted(required)} with version 1.0")
|
|
990
|
-
execution_request = resolve_execution_test_ids(
|
|
991
|
-
project_root,
|
|
992
|
-
evidence_dir,
|
|
993
|
-
validate_execution_request(
|
|
994
|
-
request["execution_request"], plan=plan_for_execution(evidence_dir),
|
|
995
|
-
),
|
|
996
|
-
)
|
|
997
|
-
request["execution_request"] = execution_request
|
|
998
|
-
write_json_atomic(request_path, request)
|
|
999
|
-
expected_failure_signals = ensure_string_list(request["expected_failure_signals"], "expected_failure_signals", allow_empty=False)
|
|
1000
|
-
overlay_paths = ensure_string_list(request["test_overlay_paths"], "test_overlay_paths")
|
|
1001
|
-
for relative in overlay_paths:
|
|
1002
|
-
confined(project_root, relative, must_exist=True)
|
|
1003
|
-
current_request_path = materialize_request(evidence_dir, f"differential-{uuid.uuid4().hex}.execution.json", execution_request)
|
|
1004
|
-
baseline_receipt = None
|
|
1005
|
-
mutation_receipt = None
|
|
1006
|
-
current_receipt = None
|
|
1007
|
-
cleanup_succeeded = False
|
|
1008
|
-
worktree_path = None
|
|
1009
|
-
try:
|
|
1010
|
-
if request["method"] == "baseline":
|
|
1011
|
-
worktree_path = tempfile.mkdtemp(prefix="prizmkit-baseline-")
|
|
1012
|
-
result = subprocess.run(
|
|
1013
|
-
["git", "-C", str(project_root), "worktree", "add", "--detach", str(worktree_path), request["baseline_commit"]],
|
|
1014
|
-
capture_output=True, check=False,
|
|
1015
|
-
)
|
|
1016
|
-
if result.returncode != 0:
|
|
1017
|
-
raise RequestError(f"cannot create baseline worktree: {result.stderr.decode(errors='replace')}")
|
|
1018
|
-
for relative in overlay_paths:
|
|
1019
|
-
source = confined(project_root, relative, must_exist=True)
|
|
1020
|
-
destination = Path(worktree_path) / relative
|
|
1021
|
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1022
|
-
shutil.copy2(source, destination)
|
|
1023
|
-
baseline_receipt = execute_request(
|
|
1024
|
-
project_root, evidence_dir, current_request_path, execution_root=Path(worktree_path),
|
|
1025
|
-
isolation={"kind": "baseline", "baseline_commit": request["baseline_commit"]},
|
|
1026
|
-
selected_execution=False,
|
|
1027
|
-
)
|
|
1028
|
-
elif request["method"] == "controlled-mutation":
|
|
1029
|
-
worktree_path = tempfile.mkdtemp(prefix="prizmkit-mutation-")
|
|
1030
|
-
result = subprocess.run(
|
|
1031
|
-
["git", "-C", str(project_root), "worktree", "add", "--detach", str(worktree_path), "HEAD"],
|
|
1032
|
-
capture_output=True, check=False,
|
|
1033
|
-
)
|
|
1034
|
-
if result.returncode != 0:
|
|
1035
|
-
raise RequestError(f"cannot create mutation worktree: {result.stderr.decode(errors='replace')}")
|
|
1036
|
-
patch_value = request["mutation_patch_path"]
|
|
1037
|
-
if not isinstance(patch_value, str) or not patch_value:
|
|
1038
|
-
raise RequestError("controlled mutation requires mutation_patch_path")
|
|
1039
|
-
patch_path = request_file(evidence_dir, patch_value)
|
|
1040
|
-
applied = subprocess.run(
|
|
1041
|
-
["git", "apply", "--whitespace=nowarn", str(patch_path)],
|
|
1042
|
-
cwd=worktree_path, capture_output=True, check=False,
|
|
1043
|
-
)
|
|
1044
|
-
if applied.returncode != 0:
|
|
1045
|
-
raise RequestError(f"controlled mutation could not be applied: {applied.stderr.decode(errors='replace')}")
|
|
1046
|
-
mutation_receipt = execute_request(
|
|
1047
|
-
project_root, evidence_dir, current_request_path, execution_root=Path(worktree_path),
|
|
1048
|
-
isolation={"kind": "controlled-mutation", "patch_sha256": file_sha256(patch_path)},
|
|
1049
|
-
selected_execution=False,
|
|
1050
|
-
)
|
|
1051
|
-
else:
|
|
1052
|
-
raise RequestError("differential method must be baseline or controlled-mutation")
|
|
1053
|
-
current_receipt = execute_request(
|
|
1054
|
-
project_root, evidence_dir, current_request_path, execution_root=project_root,
|
|
1055
|
-
isolation={"kind": "project-direct"},
|
|
1056
|
-
)
|
|
1057
|
-
cleanup_succeeded = True
|
|
1058
|
-
finally:
|
|
1059
|
-
if worktree_path and Path(worktree_path).exists():
|
|
1060
|
-
subprocess.run(
|
|
1061
|
-
["git", "-C", str(project_root), "worktree", "remove", "--force", str(worktree_path)],
|
|
1062
|
-
capture_output=True, check=False,
|
|
1063
|
-
)
|
|
1064
|
-
try:
|
|
1065
|
-
shutil.rmtree(worktree_path, ignore_errors=True)
|
|
1066
|
-
except OSError:
|
|
1067
|
-
pass
|
|
1068
|
-
failing_receipt = baseline_receipt or mutation_receipt
|
|
1069
|
-
failing_output = b""
|
|
1070
|
-
if failing_receipt:
|
|
1071
|
-
failing_output = (evidence_dir / failing_receipt["stdout_path"]).read_bytes() + (evidence_dir / failing_receipt["stderr_path"]).read_bytes()
|
|
1072
|
-
failure_reason_matched = any(signal.encode() in failing_output for signal in expected_failure_signals)
|
|
1073
|
-
proven = bool(
|
|
1074
|
-
failing_receipt and current_receipt and failing_receipt["exit_code"] != 0
|
|
1075
|
-
and current_receipt["exit_code"] == 0 and failure_reason_matched
|
|
1076
|
-
and cleanup_succeeded
|
|
1077
|
-
)
|
|
1078
|
-
proof = {
|
|
1079
|
-
"behavior_id": request["behavior_id"],
|
|
1080
|
-
"classification": "PROVEN" if proven else "UNPROVEN",
|
|
1081
|
-
"method": request["method"],
|
|
1082
|
-
"differential_request_path": evidence_relative(evidence_dir, request_path),
|
|
1083
|
-
"differential_request_sha256": file_sha256(request_path),
|
|
1084
|
-
"baseline_commit": request["baseline_commit"],
|
|
1085
|
-
"baseline_execution_id": baseline_receipt["execution_id"] if baseline_receipt else None,
|
|
1086
|
-
"mutation_execution_id": mutation_receipt["execution_id"] if mutation_receipt else None,
|
|
1087
|
-
"current_execution_id": current_receipt["execution_id"] if current_receipt else None,
|
|
1088
|
-
"failure_reason_matched": failure_reason_matched,
|
|
1089
|
-
"cleanup_succeeded": cleanup_succeeded,
|
|
1090
|
-
"not_applicable": None,
|
|
1091
|
-
}
|
|
1092
|
-
append_proof(evidence_dir, proof)
|
|
1093
|
-
return proof
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
957
|
def run_capture_change(
|
|
1097
958
|
project_root: Path,
|
|
1098
959
|
evidence_dir: Path,
|
|
@@ -1125,7 +986,6 @@ def run_init(project_root: Path, evidence_dir: Path, baseline: str | None, outpu
|
|
|
1125
986
|
for directory in ("requests", "receipts", "raw", "generated-tests", "contracts", "runner"):
|
|
1126
987
|
(evidence_dir / directory).mkdir(parents=True, exist_ok=True)
|
|
1127
988
|
write_json_atomic(evidence_dir / "executions.json", [])
|
|
1128
|
-
write_json_atomic(evidence_dir / "differential-proof.json", {"proofs": []})
|
|
1129
989
|
write_json_atomic(evidence_dir / "lifecycle.json", {
|
|
1130
990
|
"lifecycle_format": "prizmkit-canonical-lifecycle-v1",
|
|
1131
991
|
"status": "initialized",
|
|
@@ -1176,8 +1036,6 @@ def run_render_report(evidence_dir: Path, output_name: str) -> Path:
|
|
|
1176
1036
|
validation = ensure_object(load_json(validation_path), "validation") if validation_path.exists() else None
|
|
1177
1037
|
lifecycle_path = evidence_dir / "lifecycle.json"
|
|
1178
1038
|
lifecycle = ensure_object(load_json(lifecycle_path), "lifecycle") if lifecycle_path.exists() else {}
|
|
1179
|
-
proofs_path = evidence_dir / "differential-proof.json"
|
|
1180
|
-
proofs = load_json(proofs_path).get("proofs", []) if proofs_path.exists() else []
|
|
1181
1039
|
executions = load_json(evidence_dir / "executions.json")
|
|
1182
1040
|
artifact_dir = lifecycle.get("artifact_dir")
|
|
1183
1041
|
lines = [
|
|
@@ -1195,7 +1053,6 @@ def run_render_report(evidence_dir: Path, output_name: str) -> Path:
|
|
|
1195
1053
|
f"- Artifact Dir: {artifact_dir or 'not provided'}",
|
|
1196
1054
|
"",
|
|
1197
1055
|
f"Execution receipts: {len(executions) if isinstance(executions, list) else 0}",
|
|
1198
|
-
f"Differential proofs: {len(proofs) if isinstance(proofs, list) else 0}",
|
|
1199
1056
|
"The legacy test-report interface is not supported.",
|
|
1200
1057
|
"",
|
|
1201
1058
|
]
|
|
@@ -1420,7 +1277,7 @@ def stage_output_owner(relative: str, change_class: str) -> str:
|
|
|
1420
1277
|
return "INFRA_READY"
|
|
1421
1278
|
if relative.startswith("generated-tests/"):
|
|
1422
1279
|
return "TEST_BUILD"
|
|
1423
|
-
if relative
|
|
1280
|
+
if relative == "executions.json" or relative.startswith("receipts/") or relative.startswith("raw/") or relative.startswith("runner/"):
|
|
1424
1281
|
return "EXECUTE_PROVE"
|
|
1425
1282
|
if relative in FINAL_RECORDS or relative == "lifecycle.json" or relative == "init.json" or relative == "finalize.json":
|
|
1426
1283
|
return "EVIDENCE_PACKAGE"
|
|
@@ -1485,13 +1342,15 @@ def run_finalize(project_root: Path, evidence_dir: Path, request: dict[str, Any]
|
|
|
1485
1342
|
raise RequestError(f"finalize request contains unsupported fields: {sorted(set(request) - allowed)}")
|
|
1486
1343
|
if request.get("request_version", "1.0") != "1.0":
|
|
1487
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")
|
|
1488
1347
|
lifecycle = load_lifecycle(evidence_dir, required=True)
|
|
1489
1348
|
if lifecycle.get("finalized") is True:
|
|
1490
1349
|
raise RequestError("evidence identity is finalized; re-init a new package before changing scope or source")
|
|
1491
1350
|
capture_path = evidence_dir / "change-capture.json"
|
|
1492
1351
|
if not capture_path.exists():
|
|
1493
1352
|
run_capture_change(project_root, evidence_dir, request.get("baseline_commit"), "change-capture.json")
|
|
1494
|
-
capture =
|
|
1353
|
+
capture = load_change_capture(project_root, evidence_dir, require_fresh=True)
|
|
1495
1354
|
baseline = capture.get("baseline_commit")
|
|
1496
1355
|
patch_path = evidence_dir / "source-change.patch"
|
|
1497
1356
|
if not isinstance(baseline, str) or not patch_path.is_file():
|
|
@@ -1541,7 +1400,7 @@ def run_finalize(project_root: Path, evidence_dir: Path, request: dict[str, Any]
|
|
|
1541
1400
|
if change_class == "behavior":
|
|
1542
1401
|
required_records.update({
|
|
1543
1402
|
"behavior-risk-matrix.json", "test-plan.json", "test-preparation.json",
|
|
1544
|
-
"infrastructure-changes.json",
|
|
1403
|
+
"infrastructure-changes.json",
|
|
1545
1404
|
})
|
|
1546
1405
|
else:
|
|
1547
1406
|
required_records.add("lightweight-verification.json")
|
|
@@ -1553,10 +1412,31 @@ def run_finalize(project_root: Path, evidence_dir: Path, request: dict[str, Any]
|
|
|
1553
1412
|
blockers.append(
|
|
1554
1413
|
"required evidence records are missing: " + ", ".join(missing_records)
|
|
1555
1414
|
)
|
|
1556
|
-
|
|
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
|
+
]
|
|
1557
1433
|
if selected_failures and not request.get("repair_scope"):
|
|
1558
1434
|
blockers.append("reliable failure exists but repair_scope is not evidence-backed")
|
|
1559
|
-
verdict =
|
|
1435
|
+
verdict = (
|
|
1436
|
+
"TEST_BLOCKED" if blockers
|
|
1437
|
+
else "TEST_FAIL" if selected_failures and request.get("repair_scope")
|
|
1438
|
+
else "TEST_PASS"
|
|
1439
|
+
)
|
|
1560
1440
|
verdict_record: dict[str, Any] = {
|
|
1561
1441
|
"verdict": verdict,
|
|
1562
1442
|
"testing_domain_only": True,
|
|
@@ -1724,8 +1604,6 @@ def main() -> int:
|
|
|
1724
1604
|
group = execute.add_mutually_exclusive_group(required=True)
|
|
1725
1605
|
group.add_argument("--request")
|
|
1726
1606
|
group.add_argument("--replay-receipt")
|
|
1727
|
-
differential = subparsers.add_parser("differential")
|
|
1728
|
-
differential.add_argument("--request", required=True)
|
|
1729
1607
|
finalize = subparsers.add_parser("finalize")
|
|
1730
1608
|
finalize.add_argument("--request", required=True, help="semantic finalization request")
|
|
1731
1609
|
finalize.add_argument("--output", default="finalize.json")
|
|
@@ -1774,10 +1652,6 @@ def main() -> int:
|
|
|
1774
1652
|
raise RequestError("recorded request hash no longer matches replay source")
|
|
1775
1653
|
receipt = execute_request(project_root, evidence_dir, original_request, replay_of=prior.get("execution_id"))
|
|
1776
1654
|
print(json.dumps(receipt, ensure_ascii=False))
|
|
1777
|
-
elif args.subcommand == "differential":
|
|
1778
|
-
require_mutable_lifecycle(evidence_dir)
|
|
1779
|
-
proof = run_differential(project_root, evidence_dir, request_file(evidence_dir, args.request))
|
|
1780
|
-
print(json.dumps(proof, ensure_ascii=False))
|
|
1781
1655
|
elif args.subcommand == "render-report":
|
|
1782
1656
|
load_lifecycle(evidence_dir)
|
|
1783
1657
|
output = run_render_report(evidence_dir, args.output)
|