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
@@ -14,6 +14,7 @@ import json
14
14
  import math
15
15
  import os
16
16
  import re
17
+ import subprocess
17
18
  import sys
18
19
  import tempfile
19
20
  from pathlib import Path
@@ -46,12 +47,22 @@ COMMON_RECORDS = {
46
47
  }
47
48
  BEHAVIOR_RECORDS = {
48
49
  "behavior-risk-matrix.json", "test-plan.json",
49
- "infrastructure-changes.json", "differential-proof.json",
50
+ "infrastructure-changes.json",
50
51
  }
51
52
  LIGHTWEIGHT_RECORDS = {"lightweight-verification.json"}
52
53
  SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
53
54
  UUID_RE = re.compile(r"^[a-f0-9-]{36}$")
54
55
  BLOCKED_EXTERNAL_CLASSES = {"production", "unknown"}
56
+ INVENTORY_RESERVED_ROOTS = (
57
+ ".prizmkit/state",
58
+ ".prizmkit/test/evidence",
59
+ ".prizmkit/" + "dev-" + "pipeline",
60
+ ".claude/worktrees",
61
+ ".agents/worktrees",
62
+ ".codebuddy/worktrees",
63
+ ".codex/worktrees",
64
+ )
65
+ _IGNORED_DIR_NAMES = {".git", "__pycache__", ".pytest_cache", ".mypy_cache"}
55
66
  RISK_CONFLICT_PATTERNS = {
56
67
  "permission": re.compile(r"auth|tenant|role|permission|access|acl|rbac", re.I),
57
68
  "concurrency": re.compile(r"lock|shared[ _-]?state|worker|queue|concurr|parallel|race", re.I),
@@ -161,6 +172,152 @@ def path_below_or_equal(path: str, root: str) -> bool:
161
172
  return normalized_path == normalized_root or normalized_path.startswith(f"{normalized_root}/")
162
173
 
163
174
 
175
+ def normalize_project_relative(value: str) -> str:
176
+ normalized = Path(value).as_posix()
177
+ if normalized == "." or normalized.startswith("../") or "/../" in normalized:
178
+ raise EvidenceError(f"Git returned a non-project-relative path: {value}")
179
+ return normalized
180
+
181
+
182
+ def is_reserved_inventory_path(relative: str) -> bool:
183
+ path = Path(relative)
184
+ if any(part in _IGNORED_DIR_NAMES for part in path.parts):
185
+ return True
186
+ return any(path == Path(prefix) or Path(prefix) in path.parents for prefix in INVENTORY_RESERVED_ROOTS)
187
+
188
+
189
+ def project_git(project_root: Path, *args: str) -> subprocess.CompletedProcess[bytes]:
190
+ result = subprocess.run(
191
+ ["git", "-C", str(project_root), *args],
192
+ capture_output=True,
193
+ check=False,
194
+ )
195
+ if result.returncode != 0:
196
+ raise EvidenceError(f"git {' '.join(args)} failed: {result.stderr.decode(errors='replace')}")
197
+ return result
198
+
199
+
200
+ def git_status_entries(project_root: Path) -> list[dict[str, Any]]:
201
+ raw = project_git(
202
+ project_root, "status", "--porcelain=v1", "-z", "--untracked-files=all",
203
+ ).stdout
204
+ entries: list[dict[str, Any]] = []
205
+ tokens = raw.split(b"\0")
206
+ index = 0
207
+ while index < len(tokens):
208
+ token = tokens[index]
209
+ index += 1
210
+ if not token:
211
+ continue
212
+ status = token[:2].decode(errors="replace")
213
+ relative = normalize_project_relative(token[3:].decode(errors="replace"))
214
+ old_path = None
215
+ if "R" in status or "C" in status:
216
+ if index >= len(tokens):
217
+ raise EvidenceError("git status returned an incomplete rename/copy record")
218
+ old_path = tokens[index].decode(errors="replace")
219
+ index += 1
220
+ if not is_reserved_inventory_path(relative):
221
+ entries.append({"path": relative, "status": status, "old_path": old_path})
222
+ if old_path is not None:
223
+ old_relative = normalize_project_relative(old_path)
224
+ if not is_reserved_inventory_path(old_relative):
225
+ entries.append({"path": old_relative, "status": status, "old_path": old_relative, "role": "source"})
226
+ return sorted(entries, key=lambda item: (item["path"], item.get("old_path") or ""))
227
+
228
+
229
+ def git_diff_entries(project_root: Path, baseline: str) -> list[dict[str, Any]]:
230
+ raw = project_git(
231
+ project_root, "diff", "--name-status", "-z", "--find-renames", "--find-copies",
232
+ baseline, "--", ".",
233
+ ":!.prizmkit/state/**", ":!.prizmkit/test/evidence/**", ":!.prizmkit/" + "dev-" + "pipeline/**",
234
+ ":!.claude/worktrees/**", ":!.agents/worktrees/**", ":!.codebuddy/worktrees/**", ":!.codex/worktrees/**",
235
+ ).stdout
236
+ tokens = raw.split(b"\0")
237
+ entries: list[dict[str, Any]] = []
238
+ index = 0
239
+ while index < len(tokens):
240
+ status_token = tokens[index].decode(errors="replace")
241
+ index += 1
242
+ if not status_token:
243
+ continue
244
+ if index >= len(tokens):
245
+ raise EvidenceError("git diff returned an incomplete changed-file record")
246
+ first = normalize_project_relative(tokens[index].decode(errors="replace"))
247
+ index += 1
248
+ status = status_token[0]
249
+ old_path = None
250
+ if status in {"R", "C"}:
251
+ if index >= len(tokens):
252
+ raise EvidenceError("git diff returned an incomplete rename/copy record")
253
+ old_path = first
254
+ first = normalize_project_relative(tokens[index].decode(errors="replace"))
255
+ index += 1
256
+ if not is_reserved_inventory_path(first):
257
+ entries.append({"path": first, "status": status, "old_path": old_path})
258
+ if old_path and not is_reserved_inventory_path(old_path):
259
+ entries.append({"path": old_path, "status": status, "old_path": old_path, "role": "source"})
260
+ return entries
261
+
262
+
263
+ def merge_changed_file_entries(
264
+ baseline_entries: list[dict[str, Any]],
265
+ working_entries: list[dict[str, Any]],
266
+ ) -> list[dict[str, Any]]:
267
+ merged: dict[tuple[str, str | None, str | None], dict[str, Any]] = {}
268
+ for entry in baseline_entries + working_entries:
269
+ key = (entry["path"], entry.get("old_path"), entry.get("role"))
270
+ existing = merged.get(key)
271
+ if existing is None:
272
+ merged[key] = dict(entry)
273
+ continue
274
+ statuses = {existing.get("status", "").strip(), entry.get("status", "").strip()}
275
+ if "D" in statuses:
276
+ existing["status"] = "D"
277
+ elif "R" in statuses:
278
+ existing["status"] = "R"
279
+ elif "C" in statuses:
280
+ existing["status"] = "C"
281
+ elif statuses:
282
+ existing["status"] = "M"
283
+ return sorted(merged.values(), key=lambda item: (item["path"], item.get("old_path") or "", item.get("role") or ""))
284
+
285
+
286
+ def canonical_change_patch(project_root: Path, baseline: str) -> tuple[bytes, list[dict[str, Any]]]:
287
+ status_entries = merge_changed_file_entries(
288
+ git_diff_entries(project_root, baseline),
289
+ git_status_entries(project_root),
290
+ )
291
+ tracked = project_git(
292
+ project_root, "diff", "--binary", "--full-index", "--find-renames", "--find-copies",
293
+ baseline, "--", ".",
294
+ ":!.prizmkit/state/**", ":!.prizmkit/test/evidence/**", ":!.prizmkit/" + "dev-" + "pipeline/**",
295
+ ":!.claude/worktrees/**", ":!.agents/worktrees/**", ":!.codebuddy/worktrees/**", ":!.codex/worktrees/**",
296
+ ).stdout
297
+ chunks = [tracked]
298
+ untracked = [
299
+ item["path"] for item in status_entries
300
+ if item["status"].strip() == "??" and item.get("old_path") is None
301
+ ]
302
+ for relative in sorted(untracked):
303
+ candidate = (project_root / relative).resolve()
304
+ try:
305
+ candidate.relative_to(project_root.resolve())
306
+ except ValueError as exc:
307
+ raise EvidenceError(f"untracked path escapes project root: {relative}") from exc
308
+ if candidate.is_file():
309
+ result = subprocess.run(
310
+ ["git", "diff", "--no-index", "--binary", "/dev/null", "--", relative],
311
+ cwd=project_root,
312
+ capture_output=True,
313
+ check=False,
314
+ )
315
+ if result.returncode not in (0, 1):
316
+ raise EvidenceError(f"cannot capture untracked file: {relative}")
317
+ chunks.append(result.stdout)
318
+ return b"".join(chunk for chunk in chunks if chunk), status_entries
319
+
320
+
164
321
  def resolve_ref(schema: dict[str, Any], root_schema: dict[str, Any]) -> dict[str, Any]:
165
322
  reference = schema.get("$ref")
166
323
  if not reference:
@@ -225,6 +382,11 @@ def validate_schema(
225
382
  if not branch_errors:
226
383
  matching += 1
227
384
  require(matching == 1, f"{location}: value must match exactly one schema branch", errors)
385
+ prohibited = schema.get("not")
386
+ if isinstance(prohibited, dict):
387
+ prohibited_errors: list[str] = []
388
+ validate_schema(value, prohibited, root_schema, location, prohibited_errors)
389
+ require(bool(prohibited_errors), f"{location}: value matches a prohibited schema branch", errors)
228
390
 
229
391
  for child in schema.get("allOf", []):
230
392
  if isinstance(child, dict):
@@ -348,31 +510,6 @@ def require_manifest_entry(
348
510
  return path
349
511
 
350
512
 
351
- def validate_differential_na(
352
- value: Any,
353
- location: str,
354
- errors: list[str],
355
- ) -> None:
356
- """Validate the differential-only N/A shape without weakening risk-cell N/A."""
357
- if not isinstance(value, dict):
358
- errors.append(f"{location}: differential N/A must be an object")
359
- return
360
- required = {"reason", "rationale", "evidence", "considered_signals", "conflicts"}
361
- require(set(value) == required, f"{location}: differential N/A fields are incomplete", errors)
362
- require(value.get("reason") in {"new-behavior", "textual-contract"}, f"{location}: invalid differential N/A reason", errors)
363
- rationale = value.get("rationale")
364
- require(isinstance(rationale, str) and len(rationale.strip()) >= 16, f"{location}: differential N/A rationale is too short", errors)
365
- if value.get("reason") == "new-behavior" and isinstance(rationale, str):
366
- require("no stable pre-change observable" in rationale.lower(), f"{location}: new behavior N/A lacks baseline rationale", errors)
367
- if value.get("reason") == "textual-contract" and isinstance(rationale, str):
368
- require("textual" in rationale.lower(), f"{location}: textual contract N/A lacks textual rationale", errors)
369
- evidence = value.get("evidence")
370
- require(isinstance(evidence, list) and bool(evidence) and all(isinstance(item, str) and item.strip() for item in evidence), f"{location}: differential N/A needs evidence strings", errors)
371
- signals = value.get("considered_signals")
372
- require(isinstance(signals, list) and bool(signals) and all(isinstance(item, str) and item.strip() for item in signals), f"{location}: differential N/A needs considered signals", errors)
373
- require(isinstance(value.get("conflicts"), list), f"{location}: differential N/A conflicts must be an array", errors)
374
-
375
-
376
513
  def structured_na(
377
514
  value: Any,
378
515
  records_schema: dict[str, Any],
@@ -445,14 +582,6 @@ def validate_external_targets(value: Any, location: str, errors: list[str]) -> N
445
582
  require(isinstance(deny_evidence, list) and not deny_evidence, f"{target_location}: external target has deny evidence", errors)
446
583
 
447
584
 
448
- def stage_input_sha(stage_name: str, target_hashes: dict[str, str], predecessor_files: list[dict[str, str]]) -> str:
449
- return canonical_sha256({
450
- "stage": stage_name,
451
- "target_hashes": target_hashes,
452
- "predecessor_outputs": sorted(predecessor_files, key=lambda item: item["path"]),
453
- })
454
-
455
-
456
585
  def validate_manifest(
457
586
  root: Path,
458
587
  manifest: dict[str, Any],
@@ -507,7 +636,6 @@ def validate_manifest(
507
636
  return entry_map
508
637
  names = [stage.get("name") for stage in stages if isinstance(stage, dict)]
509
638
  require(names == STAGES, "stages are missing or out of order", errors)
510
- predecessor_files: list[dict[str, str]] = []
511
639
  for stage in stages:
512
640
  if not isinstance(stage, dict):
513
641
  continue
@@ -525,13 +653,6 @@ def validate_manifest(
525
653
  require(entry is not None, f"stage output is not a hashed evidence file: {name}/{output}", errors)
526
654
  if entry is not None:
527
655
  require(entry.get("produced_by") == name, f"stage output producer mismatch: {name}/{output}", errors)
528
- if isinstance(name, str) and isinstance(manifest.get("target_hashes"), dict):
529
- expected_input = stage_input_sha(name, manifest["target_hashes"], predecessor_files)
530
- require(stage.get("input_sha256") == expected_input, f"stage input dependency hash mismatch: {name}", errors)
531
- predecessor_files.extend(
532
- {"path": output, "sha256": entry_map[output]["sha256"]}
533
- for output in outputs if isinstance(outputs, list) and output in entry_map and output not in FINAL_RECORDS
534
- )
535
656
  return entry_map
536
657
 
537
658
 
@@ -750,9 +871,8 @@ def validate_requests_and_receipts(
750
871
  entry_map: dict[str, dict[str, Any]],
751
872
  records_schema: dict[str, Any],
752
873
  errors: list[str],
753
- ) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]], list[dict[str, Any]]]:
874
+ ) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]:
754
875
  execution_requests: dict[str, dict[str, Any]] = {}
755
- differential_requests: dict[str, dict[str, Any]] = {}
756
876
  requests_root = root / "requests"
757
877
  if requests_root.exists():
758
878
  for path in sorted(requests_root.rglob("*.json")):
@@ -768,19 +888,6 @@ def validate_requests_and_receipts(
768
888
  continue
769
889
  if set(request) == {"request_version", "layers", "tests"}:
770
890
  validate_definition(request, "testPreparationRequest", records_schema, relative, errors)
771
- elif request.get("method") == "not-applicable":
772
- validate_definition(request, "differentialRequest", records_schema, relative, errors)
773
- not_applicable = request.get("not_applicable")
774
- if isinstance(not_applicable, dict):
775
- validate_differential_na(not_applicable, f"{relative}.not_applicable", errors)
776
- differential_requests[relative] = request
777
- elif "execution_request" in request:
778
- validate_definition(request, "differentialRequest", records_schema, relative, errors)
779
- nested = request.get("execution_request")
780
- if isinstance(nested, dict):
781
- validate_definition(nested, "executionRequest", records_schema, f"{relative}.execution_request", errors)
782
- validate_external_targets(nested.get("external_targets"), f"{relative}.execution_request.external_targets", errors)
783
- differential_requests[relative] = request
784
891
  elif "command" in request and "layer" in request:
785
892
  validate_definition(request, "executionRequest", records_schema, relative, errors)
786
893
  validate_execution_request_semantics(request, relative, errors)
@@ -814,15 +921,14 @@ def validate_requests_and_receipts(
814
921
  executions = load_json(executions_path)
815
922
  except EvidenceError as exc:
816
923
  errors.append(str(exc))
817
- return execution_requests, differential_requests, []
924
+ return execution_requests, []
818
925
  validate_definition(executions, "executionLog", records_schema, "executions", errors)
819
926
  if not isinstance(executions, list):
820
- return execution_requests, differential_requests, []
927
+ return execution_requests, []
821
928
 
822
929
  receipt_by_id: dict[str, dict[str, Any]] = {}
823
930
  seen_receipt_paths: set[str] = set()
824
931
  attempts_by_request: dict[str, int] = {}
825
- previous_receipt: dict[str, Any] | None = None
826
932
  for index, receipt in enumerate(executions):
827
933
  location = f"executions[{index}]"
828
934
  if not isinstance(receipt, dict):
@@ -874,6 +980,12 @@ def validate_requests_and_receipts(
874
980
  require("exit_code" in probe or "error" in probe, f"{location}: tool probe has no result: {probe_name}", errors)
875
981
 
876
982
  validate_execution_request_semantics(receipt, location, errors, receipt=True)
983
+ isolation = receipt.get("isolation")
984
+ require(
985
+ isinstance(isolation, dict) and isolation.get("kind") == "project-direct",
986
+ f"{location}: receipt did not execute in the current project workspace",
987
+ errors,
988
+ )
877
989
  validate_external_targets(receipt.get("external_targets"), f"{location}.external_targets", errors)
878
990
  for raw_key, hash_key in (("stdout_path", "stdout_sha256"), ("stderr_path", "stderr_sha256")):
879
991
  relative = receipt.get(raw_key)
@@ -894,8 +1006,6 @@ def validate_requests_and_receipts(
894
1006
  errors.append(str(exc))
895
1007
  seen_receipt_paths.add(recorded_receipt_path)
896
1008
 
897
- expected_previous = canonical_sha256(previous_receipt) if previous_receipt is not None else None
898
- require(receipt.get("previous_receipt_sha256") == expected_previous, f"{location}: receipt chain mismatch", errors)
899
1009
  request_hash = receipt.get("request_sha256")
900
1010
  if isinstance(request_hash, str):
901
1011
  attempts_by_request[request_hash] = attempts_by_request.get(request_hash, 0) + 1
@@ -906,14 +1016,13 @@ def validate_requests_and_receipts(
906
1016
  require(previous is not None, f"{location}: replay source is missing", errors)
907
1017
  if previous is not None:
908
1018
  require(previous.get("request_sha256") == request_hash, f"{location}: replay request differs from source", errors)
909
- previous_receipt = receipt
910
1019
 
911
1020
  receipt_files = {
912
1021
  path.relative_to(root).as_posix()
913
1022
  for path in (root / "receipts").rglob("*.json")
914
1023
  } if (root / "receipts").is_dir() else set()
915
1024
  require(receipt_files == seen_receipt_paths, "receipt directory does not exactly match executions.json", errors)
916
- return execution_requests, differential_requests, executions
1025
+ return execution_requests, executions
917
1026
 
918
1027
 
919
1028
  def validate_execution_request_semantics(
@@ -926,17 +1035,36 @@ def validate_execution_request_semantics(
926
1035
  require(isinstance(command, list) and bool(command) and all(isinstance(item, str) and item for item in command), f"{location}: command must be a non-empty string argv", errors)
927
1036
  environment = value.get("environment")
928
1037
  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)
929
- test_ids = value.get("test_ids")
930
- test_paths = value.get("test_paths")
1038
+ has_ids = "test_ids" in value
1039
+ has_paths = "test_paths" in value
1040
+ has_auto_bind = "auto_bind" in value
931
1041
  auto_bind = value.get("auto_bind") is True
1042
+ binding_modes = int(has_ids) + int(has_paths) + int(auto_bind)
932
1043
  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",
1044
+ binding_modes == 1 and not (has_auto_bind and not auto_bind),
1045
+ f"{location}: binding modes test_ids, test_paths, and auto_bind=true must be mutually exclusive",
937
1046
  errors,
938
1047
  )
1048
+ if has_ids:
1049
+ test_ids = value.get("test_ids")
1050
+ require(
1051
+ isinstance(test_ids, list) and bool(test_ids)
1052
+ and all(isinstance(item, str) and item for item in test_ids)
1053
+ and len(set(test_ids)) == len(test_ids),
1054
+ f"{location}: test_ids must be a non-empty unique string array",
1055
+ errors,
1056
+ )
1057
+ if has_paths:
1058
+ test_paths = value.get("test_paths")
1059
+ require(
1060
+ isinstance(test_paths, list) and bool(test_paths)
1061
+ and all(isinstance(item, str) and item for item in test_paths)
1062
+ and len(set(test_paths)) == len(test_paths),
1063
+ f"{location}: test_paths must be a non-empty unique string array",
1064
+ errors,
1065
+ )
939
1066
  if receipt:
1067
+ require(has_ids and not has_paths and not has_auto_bind, f"{location}: receipt binding must use canonical test_ids only", errors)
940
1068
  require(type(value.get("selected_execution")) is bool, f"{location}: selected_execution must be boolean", errors)
941
1069
  require(type(value.get("reliable")) is bool, f"{location}: reliable must be boolean", errors)
942
1070
  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)
@@ -984,6 +1112,25 @@ def validate_canonical_capture(
984
1112
  scope_changed = set(scope.get("changed_files", [])) if isinstance(scope.get("changed_files"), list) else set()
985
1113
  require(captured_paths == inventory_changed, "canonical capture and inventory changed_files do not match", errors)
986
1114
  require(captured_paths == scope_changed, "canonical capture and scope changed_files do not match", errors)
1115
+ baseline = capture.get("baseline_commit")
1116
+ if not isinstance(baseline, str) or not baseline:
1117
+ errors.append("canonical capture baseline_commit is missing")
1118
+ return
1119
+ try:
1120
+ live_patch, live_entries = canonical_change_patch(project_root, baseline)
1121
+ except EvidenceError as exc:
1122
+ errors.append(f"cannot verify live canonical change: {exc}")
1123
+ return
1124
+ require(
1125
+ hashlib.sha256(live_patch).hexdigest() == capture.get("patch_sha256"),
1126
+ "canonical change capture is stale: live patch hash differs",
1127
+ errors,
1128
+ )
1129
+ require(
1130
+ live_entries == capture.get("changed_files"),
1131
+ "canonical change capture is stale: live changed-file entries differ",
1132
+ errors,
1133
+ )
987
1134
 
988
1135
 
989
1136
  def validate_test_preparation(
@@ -1122,7 +1269,14 @@ def validate_plan_and_scope_bindings(
1122
1269
  for layer in test_layers if isinstance(test_layers, list) else []:
1123
1270
  require(layer in LAYER_ORDER, f"planned test uses unknown layer: {test_id}/{layer}", errors)
1124
1271
  matching_receipts = selected_by_test.get(test_id, [])
1125
- require(bool(matching_receipts), f"planned test has no selected receipt: {test_id}", errors)
1272
+ if manifest.get("final_verdict") == "TEST_PASS":
1273
+ passing_receipts = [
1274
+ receipt for receipt in matching_receipts
1275
+ if receipt.get("reliable") is True and receipt.get("exit_code") == 0
1276
+ ]
1277
+ require(bool(passing_receipts), f"planned test has no selected reliable zero-exit receipt: {test_id}", errors)
1278
+ else:
1279
+ require(bool(matching_receipts), f"planned test has no selected receipt: {test_id}", errors)
1126
1280
  for receipt in matching_receipts:
1127
1281
  require(receipt.get("layer") in test_layers, f"planned test receipt uses an unplanned layer: {test_id}", errors)
1128
1282
 
@@ -1184,6 +1338,12 @@ def validate_matrix_risks(
1184
1338
  )
1185
1339
  if verdict == "TEST_FAIL":
1186
1340
  require(reliably_failed, "TEST_FAIL lacks a reliable reproduced failing execution", errors)
1341
+ elif verdict == "TEST_PASS":
1342
+ require(
1343
+ not reliably_failed,
1344
+ "TEST_PASS contains a reliable selected failing execution",
1345
+ errors,
1346
+ )
1187
1347
 
1188
1348
  for behavior in matrix.get("behaviors", []):
1189
1349
  if not isinstance(behavior, dict):
@@ -1225,123 +1385,6 @@ def validate_matrix_risks(
1225
1385
  errors.append(f"TEST_PASS has unresolved behavior risk: {behavior_id}/{risk_name}")
1226
1386
 
1227
1387
 
1228
- def require_sha_or_null(value: Any, location: str, errors: list[str]) -> None:
1229
- require(value is None or is_sha256(value), f"{location} must be a sha256 or null", errors)
1230
-
1231
-
1232
- def validate_differential_proofs(
1233
- root: Path,
1234
- project_root: Path,
1235
- manifest: dict[str, Any],
1236
- matrix: dict[str, Any],
1237
- proof: dict[str, Any],
1238
- differential_requests: dict[str, dict[str, Any]],
1239
- executions: list[dict[str, Any]],
1240
- entry_map: dict[str, dict[str, Any]],
1241
- records_schema: dict[str, Any],
1242
- errors: list[str],
1243
- ) -> None:
1244
- verdict = manifest.get("final_verdict")
1245
- receipts = {item.get("execution_id"): item for item in executions if isinstance(item.get("execution_id"), str)}
1246
- behavior_ids = {item.get("id") for item in matrix.get("behaviors", []) if isinstance(item, dict)}
1247
- proof_by_behavior: dict[str, dict[str, Any]] = {}
1248
- proofs = proof.get("proofs")
1249
- if not isinstance(proofs, list):
1250
- return
1251
- for item in proofs:
1252
- if not isinstance(item, dict):
1253
- continue
1254
- behavior_id = item.get("behavior_id")
1255
- require(behavior_id in behavior_ids, f"differential proof maps unknown behavior: {behavior_id}", errors)
1256
- require(behavior_id not in proof_by_behavior, f"duplicate differential proof: {behavior_id}", errors)
1257
- if isinstance(behavior_id, str):
1258
- proof_by_behavior[behavior_id] = item
1259
- classification = item.get("classification")
1260
- if classification == "NOT_APPLICABLE":
1261
- validate_differential_na(item.get("not_applicable"), f"differential proof {behavior_id} not_applicable", errors)
1262
- for key in (
1263
- "method", "differential_request_path", "differential_request_sha256", "baseline_commit",
1264
- "baseline_execution_id", "mutation_execution_id", "current_execution_id",
1265
- "isolation_tree_sha256", "mutation_apply_sha256", "mutation_restore_sha256",
1266
- ):
1267
- require(item.get(key) is None, f"N/A differential proof must clear {key}: {behavior_id}", errors)
1268
- require(item.get("failure_reason_matched") is False, f"N/A differential proof cannot claim a failure match: {behavior_id}", errors)
1269
- continue
1270
-
1271
- request_relative = item.get("differential_request_path")
1272
- request = differential_requests.get(request_relative) if isinstance(request_relative, str) else None
1273
- require(request is not None, f"differential proof has no matching request: {behavior_id}", errors)
1274
- request_path = require_manifest_entry(request_relative, entry_map, root, errors, "differential request") if isinstance(request_relative, str) else None
1275
- if request_path is not None:
1276
- require(file_sha256(request_path) == item.get("differential_request_sha256"), f"differential request hash mismatch: {behavior_id}", errors)
1277
- if not isinstance(request, dict):
1278
- continue
1279
- require(request.get("behavior_id") == behavior_id, f"differential request behavior mismatch: {behavior_id}", errors)
1280
- method = item.get("method")
1281
- require(method == request.get("method") and method in {"baseline", "controlled-mutation"}, f"differential method mismatch: {behavior_id}", errors)
1282
- require(item.get("baseline_commit") == request.get("baseline_commit") == manifest.get("baseline_commit"), f"differential baseline commit mismatch: {behavior_id}", errors)
1283
- for key in (
1284
- "differential_request_sha256", "isolation_tree_sha256", "mutation_apply_sha256", "mutation_restore_sha256",
1285
- ):
1286
- require_sha_or_null(item.get(key), f"differential proof {behavior_id}.{key}", errors)
1287
- require(item.get("cleanup_succeeded") is True, f"differential cleanup failed: {behavior_id}", errors)
1288
-
1289
- current = receipts.get(item.get("current_execution_id"))
1290
- require(current is not None, f"differential current receipt is missing: {behavior_id}", errors)
1291
- failing_id = item.get("baseline_execution_id") if method == "baseline" else item.get("mutation_execution_id")
1292
- failing = receipts.get(failing_id)
1293
- require(failing is not None, f"differential failing receipt is missing: {behavior_id}", errors)
1294
- if current is not None and failing is not None:
1295
- require(current.get("request_sha256") == failing.get("request_sha256"), f"differential sides use different execution requests: {behavior_id}", errors)
1296
- nested = request.get("execution_request")
1297
- current_request_path = current.get("request_path")
1298
- current_request = load_json(root / current_request_path) if isinstance(current_request_path, str) and safe_path(root, current_request_path, errors, "differential receipt request") else None
1299
- require(current_request == nested, f"differential current receipt request does not match request: {behavior_id}", errors)
1300
- require(current.get("selected_execution") is True and current.get("reliable") is True and current.get("exit_code") == 0, f"differential current side is not a reliable success: {behavior_id}", errors)
1301
- require(failing.get("selected_execution") is False and failing.get("reliable") is True and failing.get("exit_code") != 0, f"differential failing side is not a reliable failure: {behavior_id}", errors)
1302
- current_isolation = current.get("isolation")
1303
- failing_isolation = failing.get("isolation")
1304
- require(isinstance(current_isolation, dict) and current_isolation.get("kind") == "current", f"differential current receipt lacks current isolation: {behavior_id}", errors)
1305
- require(isinstance(failing_isolation, dict) and failing_isolation.get("kind") == method, f"differential failing receipt isolation mismatch: {behavior_id}", errors)
1306
- if isinstance(current_isolation, dict):
1307
- require(current_isolation.get("tree_sha256") == item.get("isolation_tree_sha256"), f"differential current isolation hash mismatch: {behavior_id}", errors)
1308
- if method == "baseline" and isinstance(failing_isolation, dict):
1309
- require(failing_isolation.get("baseline_commit") == request.get("baseline_commit"), f"baseline receipt commit mismatch: {behavior_id}", errors)
1310
- if method == "controlled-mutation" and isinstance(failing_isolation, dict):
1311
- patch_relative = request.get("mutation_patch_path")
1312
- patch_path = require_manifest_entry(patch_relative, entry_map, root, errors, "controlled mutation patch") if isinstance(patch_relative, str) else None
1313
- require(patch_path is not None, f"controlled mutation patch is missing: {behavior_id}", errors)
1314
- if patch_path is not None:
1315
- patch_hash = file_sha256(patch_path)
1316
- require(failing_isolation.get("patch_sha256") == patch_hash, f"controlled mutation receipt patch hash mismatch: {behavior_id}", errors)
1317
- require(item.get("mutation_apply_sha256") == failing_isolation.get("tree_sha256"), f"controlled mutation apply hash mismatch: {behavior_id}", errors)
1318
- require(item.get("mutation_restore_sha256") == item.get("isolation_tree_sha256"), f"controlled mutation restore hash mismatch: {behavior_id}", errors)
1319
- if method == "baseline":
1320
- require(item.get("mutation_execution_id") is None, f"baseline proof has mutation receipt: {behavior_id}", errors)
1321
- require(request.get("mutation_patch_path") is None, f"baseline request has mutation patch: {behavior_id}", errors)
1322
- require(item.get("mutation_apply_sha256") is None and item.get("mutation_restore_sha256") is None, f"baseline proof has mutation hashes: {behavior_id}", errors)
1323
- elif method == "controlled-mutation":
1324
- require(item.get("baseline_execution_id") is None, f"mutation proof has baseline receipt: {behavior_id}", errors)
1325
- require(isinstance(request.get("mutation_patch_path"), str) and bool(request.get("mutation_patch_path")), f"mutation request lacks patch: {behavior_id}", errors)
1326
- if classification == "PROVEN":
1327
- require(item.get("failure_reason_matched") is True, f"differential failure reason was not matched: {behavior_id}", errors)
1328
- if failing is not None:
1329
- output = b""
1330
- for key in ("stdout_path", "stderr_path"):
1331
- relative = failing.get(key)
1332
- path = safe_path(root, relative, errors, "differential raw output") if isinstance(relative, str) else None
1333
- if path is not None and path.is_file():
1334
- output += path.read_bytes()
1335
- signals = request.get("expected_failure_signals")
1336
- require(isinstance(signals, list) and any(isinstance(signal, str) and signal.encode() in output for signal in signals), f"differential expected failure signal is absent: {behavior_id}", errors)
1337
- elif classification == "UNPROVEN" and verdict == "TEST_PASS":
1338
- errors.append(f"TEST_PASS has unproven necessary behavior: {behavior_id}")
1339
-
1340
- if verdict == "TEST_PASS":
1341
- for behavior_id in behavior_ids:
1342
- require(behavior_id in proof_by_behavior, f"behavior lacks differential proof: {behavior_id}", errors)
1343
-
1344
-
1345
1388
  def validate_infrastructure(
1346
1389
  manifest: dict[str, Any],
1347
1390
  infrastructure: dict[str, Any],
@@ -1409,6 +1452,23 @@ def validate_verdict_and_attestation(
1409
1452
  require(verdict_record.get("real_environment_validated") is False, "code evidence must not claim real-environment validation", errors)
1410
1453
  require(verdict_record.get("repairs_business_defects") is False, "test skill must not repair business defects", errors)
1411
1454
  if verdict == "TEST_PASS":
1455
+ selected = [
1456
+ item for item in executions
1457
+ if isinstance(item, dict) and item.get("selected_execution") is True
1458
+ ]
1459
+ require(bool(selected), "TEST_PASS has no selected executions", errors)
1460
+ for item in selected:
1461
+ execution_id = item.get("execution_id", "unknown-execution")
1462
+ require(
1463
+ item.get("reliable") is True,
1464
+ f"TEST_PASS contains a selected unreliable execution: {execution_id}",
1465
+ errors,
1466
+ )
1467
+ require(
1468
+ item.get("exit_code") == 0,
1469
+ f"TEST_PASS contains a selected failing execution: {execution_id}",
1470
+ errors,
1471
+ )
1412
1472
  require(verdict_record.get("code_evidence_replayable") is True, "TEST_PASS must declare replayable code evidence", errors)
1413
1473
  require(not verdict_record.get("blockers"), "TEST_PASS contains blockers", errors)
1414
1474
  require(not verdict_record.get("reproduced_failures"), "TEST_PASS contains reproduced failures", errors)
@@ -1539,20 +1599,18 @@ def validate_evidence(
1539
1599
  scope, inventory, inventory_by_category, project_root, root,
1540
1600
  get_captured_deleted_paths(root), errors,
1541
1601
  )
1542
- execution_requests, differential_requests, executions = validate_requests_and_receipts(root, entry_map, records_schema, errors)
1602
+ execution_requests, executions = validate_requests_and_receipts(root, entry_map, records_schema, errors)
1543
1603
 
1544
1604
  if change_class == "behavior":
1545
1605
  try:
1546
1606
  matrix = load_json(root / "behavior-risk-matrix.json")
1547
1607
  infrastructure = load_json(root / "infrastructure-changes.json")
1548
- proof = load_json(root / "differential-proof.json")
1549
1608
  except EvidenceError as exc:
1550
1609
  errors.append(str(exc))
1551
- matrix = infrastructure = proof = None
1552
- if all(isinstance(item, dict) for item in (matrix, infrastructure, proof, plan)):
1610
+ matrix = infrastructure = None
1611
+ if all(isinstance(item, dict) for item in (matrix, infrastructure, plan)):
1553
1612
  validate_schema(matrix, matrix_schema, matrix_schema, "behavior-risk-matrix", errors)
1554
1613
  validate_definition(infrastructure, "infrastructureChanges", records_schema, "infrastructure-changes", errors)
1555
- validate_definition(proof, "differentialProof", records_schema, "differential-proof", errors)
1556
1614
  stages = {item.get("name"): item for item in manifest.get("stages", []) if isinstance(item, dict)}
1557
1615
  for stage_name in CORE_BEHAVIOR_STAGES:
1558
1616
  require(stages.get(stage_name, {}).get("status") == "complete", f"behavior protocol stage cannot be N/A: {stage_name}", errors)
@@ -1561,10 +1619,6 @@ def validate_evidence(
1561
1619
  executions, records_schema, errors,
1562
1620
  )
1563
1621
  validate_matrix_risks(manifest, matrix, test_map, executions, records_schema, errors)
1564
- validate_differential_proofs(
1565
- root, project_root, manifest, matrix, proof, differential_requests, executions,
1566
- entry_map, records_schema, errors,
1567
- )
1568
1622
  validate_infrastructure(manifest, infrastructure, executions, entry_map, root, errors)
1569
1623
  else:
1570
1624
  errors.append("behavior evidence records must be JSON objects")
@@ -1612,10 +1666,7 @@ def render_report_text(
1612
1666
  ) -> str:
1613
1667
  lifecycle_path = root / "lifecycle.json"
1614
1668
  lifecycle = load_json(lifecycle_path) if lifecycle_path.exists() else {}
1615
- proof_path = root / "differential-proof.json"
1616
- proof_record = load_json(proof_path) if proof_path.exists() else {}
1617
1669
  executions_record = load_json(root / "executions.json")
1618
- proofs = proof_record.get("proofs", []) if isinstance(proof_record, dict) else []
1619
1670
  executions = executions_record if isinstance(executions_record, list) else []
1620
1671
  artifact_dir = lifecycle.get("artifact_dir") if isinstance(lifecycle, dict) else None
1621
1672
  lines = [
@@ -1633,7 +1684,6 @@ def render_report_text(
1633
1684
  f"- Artifact Dir: {artifact_dir or 'not provided'}",
1634
1685
  "",
1635
1686
  f"Execution receipts: {len(executions)}",
1636
- f"Differential proofs: {len(proofs) if isinstance(proofs, list) else 0}",
1637
1687
  "The legacy test-report interface is not supported.",
1638
1688
  "",
1639
1689
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.136",
3
+ "version": "1.1.138",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {