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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +41 -121
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -6
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +1 -2
- package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +150 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +21 -804
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +4 -6
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +4 -6
- package/bundled/dev-pipeline/scripts/prompt_framework.py +56 -55
- package/bundled/dev-pipeline/scripts/update-bug-status.py +1 -2
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +14 -96
- package/bundled/dev-pipeline/scripts/update-feature-status.py +1 -2
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +1 -2
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +7 -18
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -6
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +5 -5
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +13 -10
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +1 -1
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +14 -9
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +14 -26
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +3 -3
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +13 -10
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +287 -384
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +119 -684
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -40
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +20 -23
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +29 -32
- package/bundled/skills/_metadata.json +6 -6
- package/bundled/skills/prizmkit/SKILL.md +8 -13
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +61 -80
- package/bundled/skills/prizmkit-code-review/SKILL.md +3 -3
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +61 -80
- package/bundled/skills/prizmkit-committer/SKILL.md +3 -3
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +61 -80
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +61 -80
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +61 -80
- package/bundled/skills/prizmkit-retrospective/SKILL.md +2 -2
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +61 -80
- package/bundled/skills/prizmkit-test/SKILL.md +217 -187
- package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +119 -0
- package/bundled/skills/prizmkit-test/references/independent-test-review.md +269 -0
- package/bundled/skills/prizmkit-test/references/test-coverage-model.md +193 -0
- package/bundled/skills/prizmkit-test/references/test-report-template.md +164 -79
- package/bundled/skills/prizmkit-workflow/SKILL.md +15 -23
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +61 -80
- package/package.json +1 -1
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +0 -476
- package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +0 -116
- package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +0 -111
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +0 -66
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +0 -104
- package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +0 -65
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +0 -208
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +0 -80
- package/bundled/skills/prizmkit-test/references/examples.md +0 -100
- package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +0 -228
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +0 -118
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +0 -110
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +0 -181
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +0 -1851
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +0 -1793
package/bundled/VERSION.json
CHANGED
|
@@ -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", "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
515
|
-
error_message = "completed checkpoint lacks
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
)
|