prizmkit 1.1.139 → 1.1.141

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 (74) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +41 -121
  3. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -6
  4. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +1 -2
  5. package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +150 -0
  6. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +21 -804
  7. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +4 -6
  8. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
  9. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +4 -6
  10. package/bundled/dev-pipeline/scripts/prompt_framework.py +56 -55
  11. package/bundled/dev-pipeline/scripts/update-bug-status.py +1 -2
  12. package/bundled/dev-pipeline/scripts/update-checkpoint.py +14 -96
  13. package/bundled/dev-pipeline/scripts/update-feature-status.py +1 -2
  14. package/bundled/dev-pipeline/scripts/update-refactor-status.py +1 -2
  15. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +3 -3
  16. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +7 -18
  17. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -6
  18. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +5 -5
  19. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +1 -1
  20. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  21. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +13 -10
  23. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +14 -9
  25. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +1 -1
  26. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  28. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +2 -2
  29. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +1 -1
  31. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +1 -1
  32. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +14 -26
  33. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +3 -3
  34. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +13 -10
  35. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  36. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +287 -384
  37. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +119 -684
  38. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -40
  39. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +20 -23
  40. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +29 -32
  41. package/bundled/skills/_metadata.json +6 -6
  42. package/bundled/skills/prizmkit/SKILL.md +8 -13
  43. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +61 -80
  44. package/bundled/skills/prizmkit-code-review/SKILL.md +3 -3
  45. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +61 -80
  46. package/bundled/skills/prizmkit-committer/SKILL.md +3 -3
  47. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +61 -80
  48. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +61 -80
  49. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +61 -80
  50. package/bundled/skills/prizmkit-retrospective/SKILL.md +2 -2
  51. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +61 -80
  52. package/bundled/skills/prizmkit-test/SKILL.md +217 -187
  53. package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +119 -0
  54. package/bundled/skills/prizmkit-test/references/independent-test-review.md +269 -0
  55. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +193 -0
  56. package/bundled/skills/prizmkit-test/references/test-report-template.md +164 -79
  57. package/bundled/skills/prizmkit-workflow/SKILL.md +15 -23
  58. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +61 -80
  59. package/package.json +1 -1
  60. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +0 -476
  61. package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +0 -116
  62. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +0 -111
  63. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +0 -66
  64. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +0 -104
  65. package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +0 -65
  66. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +0 -208
  67. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +0 -80
  68. package/bundled/skills/prizmkit-test/references/examples.md +0 -100
  69. package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +0 -228
  70. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +0 -118
  71. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +0 -110
  72. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +0 -181
  73. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +0 -1851
  74. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +0 -1793
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.139",
3
- "bundledAt": "2026-07-20T10:59:24.300Z",
4
- "bundledFrom": "a055a29"
2
+ "frameworkVersion": "1.1.141",
3
+ "bundledAt": "2026-07-21T00:43:15.672Z",
4
+ "bundledFrom": "df5581e"
5
5
  }
@@ -4,19 +4,19 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import re
7
- import subprocess
8
- import sys
9
7
  from dataclasses import dataclass, field
10
8
  from pathlib import Path
11
9
  from typing import Any, Mapping
12
10
 
11
+ from .test_result import valid_test_artifact_pair
12
+
13
13
  VALID_CHECKPOINT_STATUSES = {"pending", "in_progress", "completed", "skipped", "failed"}
14
14
  TERMINAL_STEP_STATUSES = {"completed", "skipped"}
15
15
  STEP_ID_PATTERN = re.compile(r"^S[0-9]+$")
16
16
  MAX_OUTER_REPAIR_ROUNDS = 3
17
17
 
18
18
  REVIEW_RESULTS = {"REVIEW_PASS", "REVIEW_NEEDS_FIXES"}
19
- TEST_RESULTS = {"TEST_PASS", "TEST_FAIL", "TEST_BLOCKED"}
19
+ TEST_RESULTS = {"TEST_PASS", "TEST_NEEDS_FIXES", "TEST_BLOCKED"}
20
20
  VALID_STAGE_RESULTS = REVIEW_RESULTS | TEST_RESULTS
21
21
  VALID_TERMINAL_STATUSES = {"WORKFLOW_COMPLETED", "WORKFLOW_BLOCKED"}
22
22
  VALID_REPAIR_SCOPES = {
@@ -59,11 +59,6 @@ FAMILY_REQUIRED_STAGES = {
59
59
  "completion-summary",
60
60
  ),
61
61
  }
62
- EVIDENCE_FILENAMES = {
63
- "manifest": "manifest.json",
64
- "verdict": "verdict.json",
65
- "validation": "validation.json",
66
- }
67
62
 
68
63
 
69
64
  @dataclass(frozen=True)
@@ -77,7 +72,6 @@ class CheckpointStep:
77
72
  depends_on: str | None = None
78
73
  required_artifacts: tuple[str, ...] = ()
79
74
  stage_result: str | None = None
80
- authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
81
75
  index: int = 0
82
76
 
83
77
  @property
@@ -101,11 +95,10 @@ class SemanticCheckpoint:
101
95
  stage_result: str | None = None
102
96
  repair_scope: str | None = None
103
97
  repair_round: int = 0
104
- authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
105
98
  blocked_reason: str | None = None
106
99
  terminal_status: str | None = None
107
100
  sequence_valid: bool = False
108
- evidence_valid: bool = False
101
+ test_result_valid: bool = False
109
102
  artifacts_valid: bool = False
110
103
  completion_ready: bool = False
111
104
  complete: bool = False
@@ -121,7 +114,6 @@ class SemanticCheckpoint:
121
114
  "stage_result": self.stage_result,
122
115
  "repair_scope": self.repair_scope,
123
116
  "repair_round": self.repair_round,
124
- "authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
125
117
  "blocked_reason": self.blocked_reason,
126
118
  "terminal_status": self.terminal_status,
127
119
  }
@@ -278,11 +270,6 @@ def validate_checkpoint_data(
278
270
  stage_result = raw_step.get("stage_result")
279
271
  if stage_result is not None and stage_result not in VALID_STAGE_RESULTS:
280
272
  return _invalid(checkpoint_path, exists, "invalid_stage_result", f"Step {step_id} has invalid stage_result", data)
281
- evidence = raw_step.get("authoritative_evidence_paths", {})
282
- if evidence is None:
283
- evidence = {}
284
- if not _valid_evidence_mapping(evidence):
285
- return _invalid(checkpoint_path, exists, "invalid_evidence_paths", f"Step {step_id} has invalid evidence paths", data)
286
273
 
287
274
  step = CheckpointStep(
288
275
  id=step_id,
@@ -292,7 +279,6 @@ def validate_checkpoint_data(
292
279
  depends_on=depends_on,
293
280
  required_artifacts=tuple(artifacts),
294
281
  stage_result=stage_result,
295
- authoritative_evidence_paths=dict(evidence),
296
282
  index=index,
297
283
  )
298
284
  steps.append(step)
@@ -465,29 +451,23 @@ def _derive_semantic_state(
465
451
  and review_step.status == "completed"
466
452
  and review_step.stage_result == "REVIEW_PASS"
467
453
  )
468
- evidence_paths = (
469
- dict(test_step.authoritative_evidence_paths)
470
- if test_step and test_step.authoritative_evidence_paths
471
- else dict(raw_state.get("authoritative_evidence_paths") or {})
472
- )
473
- evidence_valid = bool(
454
+ test_result_valid = bool(
474
455
  test_step
475
456
  and test_step.status == "completed"
476
457
  and test_step.stage_result == "TEST_PASS"
477
- and _validate_test_evidence(evidence_paths, checkpoint_path, project_root)
478
458
  )
479
459
  all_required_completed = bool(required) and all(
480
460
  (step := _single_step(steps, skill)) is not None and step.status == "completed"
481
461
  for skill in required
482
462
  )
483
463
  artifacts_valid = all(
484
- _required_artifacts_exist(step, checkpoint_path, project_root)
464
+ _required_step_artifacts_valid(step, checkpoint_path, project_root)
485
465
  for step in steps
486
466
  if step.status == "completed"
487
467
  )
488
468
  completion_ready = (
489
469
  sequence_valid and all_required_completed and review_valid
490
- and evidence_valid and artifacts_valid
470
+ and test_result_valid and artifacts_valid
491
471
  )
492
472
  blocked = terminal_status == "WORKFLOW_BLOCKED" and bool(blocked_reason)
493
473
  complete = completion_ready and terminal_status == "WORKFLOW_COMPLETED" and not blocked
@@ -511,8 +491,8 @@ def _derive_semantic_state(
511
491
  error_code = "incomplete_semantic_evidence"
512
492
  if not review_valid:
513
493
  error_message = "completed checkpoint lacks REVIEW_PASS on the review stage"
514
- elif not evidence_valid:
515
- error_message = "completed checkpoint lacks valid TEST_PASS structured evidence"
494
+ elif not test_result_valid:
495
+ error_message = "completed checkpoint lacks TEST_PASS on the test stage"
516
496
  else:
517
497
  error_message = "completed checkpoint lacks mandatory completed stages"
518
498
  elif terminal_status == "WORKFLOW_COMPLETED" and not completion_ready:
@@ -526,11 +506,10 @@ def _derive_semantic_state(
526
506
  stage_result=raw_state.get("stage_result"),
527
507
  repair_scope=repair_scope,
528
508
  repair_round=repair_round,
529
- authoritative_evidence_paths=evidence_paths,
530
509
  blocked_reason=blocked_reason,
531
510
  terminal_status=terminal_status,
532
511
  sequence_valid=sequence_valid,
533
- evidence_valid=evidence_valid,
512
+ test_result_valid=test_result_valid,
534
513
  artifacts_valid=artifacts_valid,
535
514
  completion_ready=completion_ready,
536
515
  complete=complete,
@@ -563,6 +542,30 @@ def _single_step(steps: tuple[CheckpointStep, ...], skill: str) -> CheckpointSte
563
542
 
564
543
 
565
544
 
545
+ def _required_step_artifacts_valid(
546
+ step: CheckpointStep,
547
+ checkpoint_path: Path | None,
548
+ project_root: Path | None,
549
+ ) -> bool:
550
+ """Validate declared artifacts, including semantic test terminal output."""
551
+ if step.skill == "prizmkit-test":
552
+ root = (project_root or _infer_project_root(checkpoint_path)).resolve()
553
+ reports = [item for item in step.required_artifacts if item.endswith("/test-report.md")]
554
+ results = [item for item in step.required_artifacts if item.endswith("/test-result.json")]
555
+ if len(reports) != 1 or len(results) != 1:
556
+ return False
557
+ report_path = (root / reports[0]).resolve()
558
+ result_path = (root / results[0]).resolve()
559
+ if not _is_within_root(report_path, root) or not _is_within_root(result_path, root):
560
+ return False
561
+ return valid_test_artifact_pair(
562
+ report_path,
563
+ result_path,
564
+ required_result="TEST_PASS",
565
+ )
566
+ return _required_artifacts_exist(step, checkpoint_path, project_root)
567
+
568
+
566
569
  def _required_artifacts_exist(
567
570
  step: CheckpointStep,
568
571
  checkpoint_path: Path | None,
@@ -574,7 +577,9 @@ def _required_artifacts_exist(
574
577
 
575
578
  root = (project_root or _infer_project_root(checkpoint_path)).resolve()
576
579
  for artifact in step.required_artifacts:
577
- candidate = root / artifact
580
+ candidate = (root / artifact).resolve()
581
+ if not _is_within_root(candidate, root):
582
+ return False
578
583
  if glob.has_magic(str(candidate)):
579
584
  if not glob.glob(str(candidate)):
580
585
  return False
@@ -582,98 +587,13 @@ def _required_artifacts_exist(
582
587
  return False
583
588
  return True
584
589
 
585
- def _valid_evidence_mapping(value: object) -> bool:
586
- if not isinstance(value, dict):
587
- return False
588
- return all(
589
- key in EVIDENCE_FILENAMES
590
- and (path is None or isinstance(path, str))
591
- for key, path in value.items()
592
- )
593
-
594
-
595
- def _validate_test_evidence(
596
- paths: Mapping[str, str | None],
597
- checkpoint_path: Path | None,
598
- project_root: Path | None,
599
- ) -> bool:
600
- if set(paths) != set(EVIDENCE_FILENAMES):
601
- return False
602
- root = (project_root or _infer_project_root(checkpoint_path)).resolve()
603
- resolved: dict[str, Path] = {}
604
- for key, filename in EVIDENCE_FILENAMES.items():
605
- raw_path = paths.get(key)
606
- if not isinstance(raw_path, str) or not raw_path.strip():
607
- return False
608
- candidate = Path(raw_path)
609
- if not candidate.is_absolute():
610
- candidate = root / candidate
611
- try:
612
- candidate = candidate.resolve()
613
- candidate.relative_to(root)
614
- except (OSError, ValueError):
615
- return False
616
- if candidate.name != filename or not candidate.is_file():
617
- return False
618
- resolved[key] = candidate
619
- parents = {path.parent for path in resolved.values()}
620
- if len(parents) != 1:
621
- return False
622
- try:
623
- manifest = json.loads(resolved["manifest"].read_text(encoding="utf-8"))
624
- verdict = json.loads(resolved["verdict"].read_text(encoding="utf-8"))
625
- validation = json.loads(resolved["validation"].read_text(encoding="utf-8"))
626
- except (OSError, UnicodeDecodeError, json.JSONDecodeError):
627
- return False
628
- if not all(isinstance(record, dict) for record in (manifest, verdict, validation)):
629
- return False
630
- evidence_id = manifest.get("evidence_id")
631
- if isinstance(evidence_id, str) and evidence_id and resolved["manifest"].parent.name != evidence_id:
632
- return False
633
- records_agree = (
634
- manifest.get("final_verdict") == "TEST_PASS"
635
- and verdict.get("verdict") == "TEST_PASS"
636
- and validation.get("verdict") == "TEST_PASS"
637
- and validation.get("result") == "passed"
638
- )
639
- if not records_agree:
640
- return False
641
- validator = _resolve_evidence_validator(root)
642
- if validator is None:
643
- return False
644
- return _evidence_validator_passes(root, resolved["manifest"].parent, validator)
645
-
646
590
 
647
- def _evidence_validator_passes(
648
- project_root: Path,
649
- evidence_dir: Path,
650
- validator: Path,
651
- ) -> bool:
591
+ def _is_within_root(path: Path, root: Path) -> bool:
652
592
  try:
653
- result = subprocess.run(
654
- [
655
- sys.executable, str(validator),
656
- "--evidence-dir", str(evidence_dir),
657
- "--project-root", str(project_root),
658
- ],
659
- stdout=subprocess.DEVNULL,
660
- stderr=subprocess.DEVNULL,
661
- timeout=120,
662
- check=False,
663
- )
664
- except (OSError, subprocess.SubprocessError):
593
+ path.relative_to(root)
594
+ except ValueError:
665
595
  return False
666
- return result.returncode == 0
667
-
668
-
669
- def _resolve_evidence_validator(project_root: Path) -> Path | None:
670
- candidates = (
671
- project_root / ".claude" / "command-assets" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
672
- project_root / ".agents" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
673
- project_root / ".codebuddy" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
674
- project_root / "core" / "skills" / "prizmkit-skill" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
675
- )
676
- return next((path for path in candidates if path.is_file()), None)
596
+ return True
677
597
 
678
598
 
679
599
  def _infer_project_root(checkpoint_path: Path | None) -> Path:
@@ -137,23 +137,18 @@ class SemanticCheckpointState:
137
137
  stage_result: str | None = None
138
138
  repair_scope: str | None = None
139
139
  repair_round: int = 0
140
- authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
141
140
  blocked_reason: str | None = None
142
141
  terminal_status: str | None = None
143
142
 
144
143
  @classmethod
145
144
  def from_mapping(cls, value: Mapping[str, object] | None) -> "SemanticCheckpointState":
146
145
  data = value or {}
147
- evidence = data.get("authoritative_evidence_paths")
148
146
  return cls(
149
147
  current_stage=_optional_text(data.get("current_stage")),
150
148
  next_stage=_optional_text(data.get("next_stage")),
151
149
  stage_result=_optional_text(data.get("stage_result")),
152
150
  repair_scope=_optional_text(data.get("repair_scope")),
153
151
  repair_round=max(0, _safe_int(data.get("repair_round"), 0)),
154
- authoritative_evidence_paths=(
155
- dict(evidence) if isinstance(evidence, Mapping) else {}
156
- ),
157
152
  blocked_reason=_optional_text(data.get("blocked_reason")),
158
153
  terminal_status=_optional_text(data.get("terminal_status")),
159
154
  )
@@ -165,7 +160,6 @@ class SemanticCheckpointState:
165
160
  "stage_result": self.stage_result,
166
161
  "repair_scope": self.repair_scope,
167
162
  "repair_round": self.repair_round,
168
- "authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
169
163
  "blocked_reason": self.blocked_reason,
170
164
  "terminal_status": self.terminal_status,
171
165
  }
@@ -650,8 +650,7 @@ def _process_item_locked(
650
650
  key: metadata.get(key)
651
651
  for key in (
652
652
  "current_stage", "next_stage", "stage_result",
653
- "repair_scope", "repair_round",
654
- "authoritative_evidence_paths", "blocked_reason",
653
+ "repair_scope", "repair_round", "blocked_reason",
655
654
  "terminal_status",
656
655
  )
657
656
  },
@@ -0,0 +1,150 @@
1
+ """Validation for prizmkit-test terminal result and report artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ TEST_RESULTS = {"TEST_PASS", "TEST_NEEDS_FIXES", "TEST_BLOCKED"}
11
+ INDEPENDENT_REVIEW_STATUSES = {"completed", "downgraded", "not_applicable"}
12
+ _RESULT_KEYS = {
13
+ "schema_version",
14
+ "result",
15
+ "report",
16
+ "main_review_rounds",
17
+ "independent_review",
18
+ "repair_rounds",
19
+ "production_changed",
20
+ "review_required",
21
+ "review_scope",
22
+ "unresolved_items",
23
+ }
24
+ _INDEPENDENT_REVIEW_KEYS = {
25
+ "status",
26
+ "responses",
27
+ "downgrade_reason",
28
+ "final_state_rechecked",
29
+ }
30
+
31
+
32
+ def load_test_result(path: str | Path) -> dict[str, Any] | None:
33
+ """Load one valid terminal projection, or return None."""
34
+ try:
35
+ value = json.loads(Path(path).read_text(encoding="utf-8"))
36
+ except (OSError, ValueError, TypeError):
37
+ return None
38
+ if not isinstance(value, dict):
39
+ return None
40
+ if set(value) != _RESULT_KEYS or value.get("schema_version") != 1:
41
+ return None
42
+ if value.get("result") not in TEST_RESULTS or value.get("report") != "test-report.md":
43
+ return None
44
+
45
+ rounds = value.get("main_review_rounds")
46
+ repairs = value.get("repair_rounds")
47
+ if not _integer_in_range(rounds, 0, 10) or not _integer_in_range(repairs, 0, 3):
48
+ return None
49
+
50
+ independent = value.get("independent_review")
51
+ if not isinstance(independent, dict) or set(independent) != _INDEPENDENT_REVIEW_KEYS:
52
+ return None
53
+ status = independent.get("status")
54
+ responses = independent.get("responses")
55
+ if status not in INDEPENDENT_REVIEW_STATUSES or not _integer_in_range(responses, 0, 5):
56
+ return None
57
+ if not isinstance(independent.get("final_state_rechecked"), bool):
58
+ return None
59
+ downgrade_reason = independent.get("downgrade_reason")
60
+ if status == "downgraded":
61
+ if not isinstance(downgrade_reason, str) or not downgrade_reason.strip():
62
+ return None
63
+ if responses != 0 or independent["final_state_rechecked"]:
64
+ return None
65
+ elif downgrade_reason is not None:
66
+ return None
67
+ if status == "completed":
68
+ if responses < 1 or not independent["final_state_rechecked"]:
69
+ return None
70
+ if status == "not_applicable":
71
+ if responses != 0 or independent["final_state_rechecked"]:
72
+ return None
73
+
74
+ if not isinstance(value.get("production_changed"), bool):
75
+ return None
76
+ if not isinstance(value.get("review_required"), bool):
77
+ return None
78
+ expected_scope = "delta" if value["review_required"] else None
79
+ if value.get("review_scope") != expected_scope:
80
+ return None
81
+ unresolved = value.get("unresolved_items")
82
+ if not isinstance(unresolved, list) or any(
83
+ not isinstance(item, str) or not item.strip() for item in unresolved
84
+ ):
85
+ return None
86
+
87
+ if value["result"] == "TEST_PASS":
88
+ if value["review_required"] or unresolved or rounds < 1:
89
+ return None
90
+ return value
91
+
92
+
93
+ def valid_test_result(path: str | Path, *, required_result: str | None = None) -> bool:
94
+ """Return whether the terminal projection is valid and has the required result."""
95
+ value = load_test_result(path)
96
+ return value is not None and (
97
+ required_result is None or value.get("result") == required_result
98
+ )
99
+
100
+
101
+ def valid_test_artifact_pair(
102
+ report_path: str | Path,
103
+ result_path: str | Path,
104
+ *,
105
+ required_result: str | None = None,
106
+ ) -> bool:
107
+ """Validate one sibling report/result pair and their terminal result."""
108
+ report = Path(report_path)
109
+ result = Path(result_path)
110
+ if not report.is_file() or not result.is_file():
111
+ return False
112
+ if report.resolve().parent != result.resolve().parent:
113
+ return False
114
+ projection = load_test_result(result)
115
+ if projection is None:
116
+ return False
117
+ if required_result is not None and projection.get("result") != required_result:
118
+ return False
119
+ try:
120
+ report_text = report.read_text(encoding="utf-8")
121
+ except OSError:
122
+ return False
123
+ final_result = _report_final_result(report_text)
124
+ return (
125
+ report_text.startswith("# Test Report")
126
+ and final_result == projection["result"]
127
+ )
128
+
129
+
130
+ def _report_final_result(report: str) -> str | None:
131
+ """Extract the result declared in the report's Final Result section."""
132
+ match = re.search(
133
+ r"(?ms)^## Final Result\s*$\n(?P<body>.*?)(?=^## |\Z)",
134
+ report,
135
+ )
136
+ if not match:
137
+ return None
138
+ result = re.search(
139
+ r"(?m)^- Result:\s*(TEST_PASS|TEST_NEEDS_FIXES|TEST_BLOCKED)\s*$",
140
+ match.group("body"),
141
+ )
142
+ return result.group(1) if result else None
143
+
144
+
145
+ def _integer_in_range(value: object, minimum: int, maximum: int) -> bool:
146
+ return (
147
+ isinstance(value, int)
148
+ and not isinstance(value, bool)
149
+ and minimum <= value <= maximum
150
+ )