prizmkit 1.1.129 → 1.1.131
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/.env.example +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +398 -6
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +61 -3
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +64 -4
- package/bundled/dev-pipeline/scripts/prompt_framework.py +156 -7
- package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
- package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +404 -24
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
- package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
- package/bundled/skills/_metadata.json +62 -25
- package/bundled/skills/app-planner/SKILL.md +4 -3
- package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
- package/bundled/skills/bug-planner/SKILL.md +12 -27
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
- package/bundled/skills/feature-planner/SKILL.md +27 -22
- package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
- package/bundled/skills/feature-workflow/SKILL.md +94 -121
- package/bundled/skills/prizmkit/SKILL.md +130 -94
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-init/SKILL.md +5 -5
- package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-test/SKILL.md +28 -6
- package/bundled/skills/prizmkit-test/references/examples.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/recovery-workflow/SKILL.md +62 -241
- package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
- package/bundled/skills/recovery-workflow/references/detection.md +28 -52
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
- package/bundled/skills/refactor-planner/SKILL.md +12 -57
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
- package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
- package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
- package/bundled/skills/refactor-workflow/SKILL.md +98 -139
- package/bundled/templates/project-memory-template.md +4 -4
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/prompts.js +10 -3
- package/src/scaffold.js +1 -1
- package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
- package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
package/bundled/VERSION.json
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
# Unset = auto-detect from estimated_complexity
|
|
31
31
|
|
|
32
32
|
# ─── Refactor Pipeline Only ──────────────────────────────────────────
|
|
33
|
-
# STRICT_BEHAVIOR_CHECK=1 #
|
|
33
|
+
# STRICT_BEHAVIOR_CHECK=1 # Enable strict behavior-preservation gates for refactor tasks (1=on, 0=off)
|
|
34
34
|
|
|
35
35
|
# ─── Branch Management ────────────────────────────────────────────────
|
|
36
36
|
# AUTO_PUSH=0 # Auto-push to remote after successful task (0=off, 1=on)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
"""Shared workflow-checkpoint.json reader and validator."""
|
|
1
|
+
"""Shared workflow-checkpoint.json reader and semantic validator."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
7
9
|
from dataclasses import dataclass, field
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from typing import Any, Mapping
|
|
@@ -11,6 +13,57 @@ from typing import Any, Mapping
|
|
|
11
13
|
VALID_CHECKPOINT_STATUSES = {"pending", "in_progress", "completed", "skipped", "failed"}
|
|
12
14
|
TERMINAL_STEP_STATUSES = {"completed", "skipped"}
|
|
13
15
|
STEP_ID_PATTERN = re.compile(r"^S[0-9]+$")
|
|
16
|
+
MAX_OUTER_REPAIR_ROUNDS = 3
|
|
17
|
+
|
|
18
|
+
REVIEW_RESULTS = {"REVIEW_PASS", "REVIEW_NEEDS_FIXES"}
|
|
19
|
+
TEST_RESULTS = {"TEST_PASS", "TEST_FAIL", "TEST_BLOCKED"}
|
|
20
|
+
VALID_STAGE_RESULTS = REVIEW_RESULTS | TEST_RESULTS
|
|
21
|
+
VALID_TERMINAL_STATUSES = {"WORKFLOW_COMPLETED", "WORKFLOW_BLOCKED"}
|
|
22
|
+
VALID_REPAIR_SCOPES = {
|
|
23
|
+
"production",
|
|
24
|
+
"runtime",
|
|
25
|
+
"schema",
|
|
26
|
+
"dependency",
|
|
27
|
+
"public-interface",
|
|
28
|
+
"test-infrastructure",
|
|
29
|
+
}
|
|
30
|
+
SEMANTIC_STATE_KEYS = {
|
|
31
|
+
"feature-pipeline": "feature_state",
|
|
32
|
+
"bugfix-pipeline": "bugfix_state",
|
|
33
|
+
"refactor-pipeline": "refactor_state",
|
|
34
|
+
}
|
|
35
|
+
FAMILY_REQUIRED_STAGES = {
|
|
36
|
+
"feature-pipeline": (
|
|
37
|
+
"prizmkit-implement",
|
|
38
|
+
"prizmkit-code-review",
|
|
39
|
+
"prizmkit-test",
|
|
40
|
+
"prizmkit-retrospective",
|
|
41
|
+
"prizmkit-committer",
|
|
42
|
+
"completion-summary",
|
|
43
|
+
),
|
|
44
|
+
"bugfix-pipeline": (
|
|
45
|
+
"prizmkit-implement",
|
|
46
|
+
"prizmkit-code-review",
|
|
47
|
+
"prizmkit-test",
|
|
48
|
+
"prizmkit-retrospective",
|
|
49
|
+
"prizmkit-committer",
|
|
50
|
+
"bug-report",
|
|
51
|
+
),
|
|
52
|
+
"refactor-pipeline": (
|
|
53
|
+
"prizmkit-implement",
|
|
54
|
+
"prizmkit-code-review",
|
|
55
|
+
"prizmkit-test",
|
|
56
|
+
"prizmkit-retrospective",
|
|
57
|
+
"prizmkit-committer",
|
|
58
|
+
"refactor-report",
|
|
59
|
+
"completion-summary",
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
EVIDENCE_FILENAMES = {
|
|
63
|
+
"manifest": "manifest.json",
|
|
64
|
+
"verdict": "verdict.json",
|
|
65
|
+
"validation": "validation.json",
|
|
66
|
+
}
|
|
14
67
|
|
|
15
68
|
|
|
16
69
|
@dataclass(frozen=True)
|
|
@@ -23,6 +76,8 @@ class CheckpointStep:
|
|
|
23
76
|
status: str
|
|
24
77
|
depends_on: str | None = None
|
|
25
78
|
required_artifacts: tuple[str, ...] = ()
|
|
79
|
+
stage_result: str | None = None
|
|
80
|
+
authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
|
|
26
81
|
index: int = 0
|
|
27
82
|
|
|
28
83
|
@property
|
|
@@ -36,6 +91,42 @@ class CheckpointStep:
|
|
|
36
91
|
return f"{self.id} ({self.skill})"
|
|
37
92
|
|
|
38
93
|
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class SemanticCheckpoint:
|
|
96
|
+
"""Provider-neutral L4 state shared by runners, status, and recovery."""
|
|
97
|
+
|
|
98
|
+
workflow_type: str = ""
|
|
99
|
+
current_stage: str | None = None
|
|
100
|
+
next_stage: str | None = None
|
|
101
|
+
stage_result: str | None = None
|
|
102
|
+
repair_scope: str | None = None
|
|
103
|
+
repair_round: int = 0
|
|
104
|
+
authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
|
|
105
|
+
blocked_reason: str | None = None
|
|
106
|
+
terminal_status: str | None = None
|
|
107
|
+
sequence_valid: bool = False
|
|
108
|
+
evidence_valid: bool = False
|
|
109
|
+
artifacts_valid: bool = False
|
|
110
|
+
completion_ready: bool = False
|
|
111
|
+
complete: bool = False
|
|
112
|
+
blocked: bool = False
|
|
113
|
+
error_code: str = ""
|
|
114
|
+
error_message: str = ""
|
|
115
|
+
|
|
116
|
+
def as_dict(self) -> dict[str, object]:
|
|
117
|
+
"""Return the additive runtime-state representation."""
|
|
118
|
+
return {
|
|
119
|
+
"current_stage": self.current_stage,
|
|
120
|
+
"next_stage": self.next_stage,
|
|
121
|
+
"stage_result": self.stage_result,
|
|
122
|
+
"repair_scope": self.repair_scope,
|
|
123
|
+
"repair_round": self.repair_round,
|
|
124
|
+
"authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
|
|
125
|
+
"blocked_reason": self.blocked_reason,
|
|
126
|
+
"terminal_status": self.terminal_status,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
39
130
|
@dataclass(frozen=True)
|
|
40
131
|
class CheckpointState:
|
|
41
132
|
"""Result of loading and validating one workflow checkpoint."""
|
|
@@ -50,6 +141,7 @@ class CheckpointState:
|
|
|
50
141
|
all_completed: bool = False
|
|
51
142
|
duplicate_skills: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
|
|
52
143
|
raw_data: Mapping[str, Any] | None = None
|
|
144
|
+
semantic: SemanticCheckpoint = field(default_factory=SemanticCheckpoint)
|
|
53
145
|
|
|
54
146
|
@property
|
|
55
147
|
def current_step_name(self) -> str:
|
|
@@ -69,17 +161,38 @@ class CheckpointState:
|
|
|
69
161
|
|
|
70
162
|
@property
|
|
71
163
|
def cursor_signature(self) -> str:
|
|
72
|
-
"""Return a stable progress fingerprint cursor string."""
|
|
164
|
+
"""Return a stable semantic progress fingerprint cursor string."""
|
|
73
165
|
if not self.valid:
|
|
74
166
|
return f"invalid:{self.error_code}" if self.exists else ""
|
|
75
|
-
if self.
|
|
76
|
-
return "complete"
|
|
167
|
+
if self.semantic.complete:
|
|
168
|
+
return "complete:WORKFLOW_COMPLETED"
|
|
169
|
+
if self.semantic.blocked:
|
|
170
|
+
return f"blocked:{self.semantic.blocked_reason or 'unspecified'}"
|
|
77
171
|
if self.current_step is None:
|
|
78
|
-
return ""
|
|
172
|
+
return "complete"
|
|
79
173
|
return f"{self.current_step.id}:{self.current_step.skill}:{self.current_step.status}"
|
|
80
174
|
|
|
175
|
+
@property
|
|
176
|
+
def semantic_complete(self) -> bool:
|
|
177
|
+
"""Return whether this checkpoint authoritatively proves task completion."""
|
|
178
|
+
return self.valid and self.semantic.complete
|
|
81
179
|
|
|
82
|
-
|
|
180
|
+
@property
|
|
181
|
+
def semantic_blocked(self) -> bool:
|
|
182
|
+
"""Return whether this checkpoint has a deterministic blocked terminal state."""
|
|
183
|
+
return self.valid and self.semantic.blocked
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def semantic_snapshot(self) -> dict[str, object]:
|
|
187
|
+
"""Return the normalized state persisted by runtime consumers."""
|
|
188
|
+
return self.semantic.as_dict()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def load_checkpoint_state(
|
|
192
|
+
path: str | Path | None,
|
|
193
|
+
*,
|
|
194
|
+
project_root: str | Path | None = None,
|
|
195
|
+
) -> CheckpointState:
|
|
83
196
|
"""Load and validate one workflow-checkpoint.json file."""
|
|
84
197
|
checkpoint_path = Path(path) if path is not None else None
|
|
85
198
|
if checkpoint_path is None or not checkpoint_path.is_file():
|
|
@@ -90,7 +203,12 @@ def load_checkpoint_state(path: str | Path | None) -> CheckpointState:
|
|
|
90
203
|
return _invalid(checkpoint_path, True, "malformed_json", f"Malformed checkpoint JSON: {exc}")
|
|
91
204
|
except OSError as exc:
|
|
92
205
|
return _invalid(checkpoint_path, True, "read_error", f"Cannot read checkpoint: {exc}")
|
|
93
|
-
return validate_checkpoint_data(
|
|
206
|
+
return validate_checkpoint_data(
|
|
207
|
+
data,
|
|
208
|
+
path=checkpoint_path,
|
|
209
|
+
exists=True,
|
|
210
|
+
project_root=project_root,
|
|
211
|
+
)
|
|
94
212
|
|
|
95
213
|
|
|
96
214
|
def validate_checkpoint_data(
|
|
@@ -98,8 +216,9 @@ def validate_checkpoint_data(
|
|
|
98
216
|
*,
|
|
99
217
|
path: str | Path | None = None,
|
|
100
218
|
exists: bool = True,
|
|
219
|
+
project_root: str | Path | None = None,
|
|
101
220
|
) -> CheckpointState:
|
|
102
|
-
"""Validate already-loaded checkpoint data."""
|
|
221
|
+
"""Validate already-loaded checkpoint data and derive semantic L4 state."""
|
|
103
222
|
checkpoint_path = Path(path) if path is not None else None
|
|
104
223
|
if not isinstance(data, dict):
|
|
105
224
|
return _invalid(checkpoint_path, exists, "invalid_root", "Checkpoint root must be an object")
|
|
@@ -156,6 +275,15 @@ def validate_checkpoint_data(
|
|
|
156
275
|
if not isinstance(artifacts, list) or any(not isinstance(item, str) for item in artifacts):
|
|
157
276
|
return _invalid(checkpoint_path, exists, "invalid_required_artifacts", f"Step {step_id} has invalid required_artifacts", data)
|
|
158
277
|
|
|
278
|
+
stage_result = raw_step.get("stage_result")
|
|
279
|
+
if stage_result is not None and stage_result not in VALID_STAGE_RESULTS:
|
|
280
|
+
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
|
+
|
|
159
287
|
step = CheckpointStep(
|
|
160
288
|
id=step_id,
|
|
161
289
|
skill=skill,
|
|
@@ -163,6 +291,8 @@ def validate_checkpoint_data(
|
|
|
163
291
|
status=str(status),
|
|
164
292
|
depends_on=depends_on,
|
|
165
293
|
required_artifacts=tuple(artifacts),
|
|
294
|
+
stage_result=stage_result,
|
|
295
|
+
authoritative_evidence_paths=dict(evidence),
|
|
166
296
|
index=index,
|
|
167
297
|
)
|
|
168
298
|
steps.append(step)
|
|
@@ -186,12 +316,23 @@ def validate_checkpoint_data(
|
|
|
186
316
|
data,
|
|
187
317
|
)
|
|
188
318
|
|
|
319
|
+
progress_error = _progress_error(steps)
|
|
320
|
+
if progress_error:
|
|
321
|
+
return _invalid(checkpoint_path, exists, "invalid_step_progress", progress_error, data)
|
|
322
|
+
|
|
189
323
|
duplicate_skills = {
|
|
190
324
|
skill: tuple(ids)
|
|
191
325
|
for skill, ids in skill_to_ids.items()
|
|
192
326
|
if len(ids) > 1
|
|
193
327
|
}
|
|
194
328
|
current_step = next((step for step in steps if step.status not in TERMINAL_STEP_STATUSES), None)
|
|
329
|
+
semantic = _derive_semantic_state(
|
|
330
|
+
data,
|
|
331
|
+
tuple(steps),
|
|
332
|
+
current_step,
|
|
333
|
+
checkpoint_path,
|
|
334
|
+
Path(project_root) if project_root is not None else None,
|
|
335
|
+
)
|
|
195
336
|
return CheckpointState(
|
|
196
337
|
path=checkpoint_path,
|
|
197
338
|
exists=exists,
|
|
@@ -201,6 +342,7 @@ def validate_checkpoint_data(
|
|
|
201
342
|
all_completed=current_step is None,
|
|
202
343
|
duplicate_skills=duplicate_skills,
|
|
203
344
|
raw_data=data,
|
|
345
|
+
semantic=semantic,
|
|
204
346
|
)
|
|
205
347
|
|
|
206
348
|
|
|
@@ -224,6 +366,7 @@ def checkpoint_cursor(checkpoint: Mapping[str, Any] | None) -> dict[str, object]
|
|
|
224
366
|
"failed": [],
|
|
225
367
|
"first_cursor": f"(invalid checkpoint: {state.error_code})",
|
|
226
368
|
"current_step_name": "",
|
|
369
|
+
"semantic": state.semantic_snapshot,
|
|
227
370
|
}
|
|
228
371
|
|
|
229
372
|
grouped: dict[str, list[str]] = {
|
|
@@ -250,6 +393,7 @@ def checkpoint_cursor(checkpoint: Mapping[str, Any] | None) -> dict[str, object]
|
|
|
250
393
|
"failed": grouped["failed"],
|
|
251
394
|
"first_cursor": first_cursor,
|
|
252
395
|
"current_step_name": state.current_step_name,
|
|
396
|
+
"semantic": state.semantic_snapshot,
|
|
253
397
|
}
|
|
254
398
|
|
|
255
399
|
|
|
@@ -266,6 +410,298 @@ def invalid_existing_checkpoint(path: str | Path | None) -> CheckpointState | No
|
|
|
266
410
|
return None
|
|
267
411
|
|
|
268
412
|
|
|
413
|
+
def semantic_state_key(workflow_type: str) -> str:
|
|
414
|
+
"""Return the family-owned L4 semantic metadata key."""
|
|
415
|
+
return SEMANTIC_STATE_KEYS.get(workflow_type, "l4_state")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _progress_error(steps: list[CheckpointStep]) -> str:
|
|
419
|
+
active_seen = False
|
|
420
|
+
for step in steps:
|
|
421
|
+
if step.status in TERMINAL_STEP_STATUSES:
|
|
422
|
+
if active_seen:
|
|
423
|
+
return f"Step {step.id} is terminal after an unfinished predecessor"
|
|
424
|
+
else:
|
|
425
|
+
active_seen = True
|
|
426
|
+
return ""
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _derive_semantic_state(
|
|
430
|
+
data: Mapping[str, Any],
|
|
431
|
+
steps: tuple[CheckpointStep, ...],
|
|
432
|
+
current_step: CheckpointStep | None,
|
|
433
|
+
checkpoint_path: Path | None,
|
|
434
|
+
project_root: Path | None,
|
|
435
|
+
) -> SemanticCheckpoint:
|
|
436
|
+
workflow_type = str(data.get("workflow_type") or "")
|
|
437
|
+
state_key = semantic_state_key(workflow_type)
|
|
438
|
+
raw_state = data.get(state_key, {})
|
|
439
|
+
if not isinstance(raw_state, dict):
|
|
440
|
+
return _semantic_error(workflow_type, current_step, "invalid_semantic_state", f"{state_key} must be an object")
|
|
441
|
+
|
|
442
|
+
required = FAMILY_REQUIRED_STAGES.get(workflow_type)
|
|
443
|
+
sequence_error = _required_sequence_error(steps, required) if required else "unsupported workflow family"
|
|
444
|
+
sequence_valid = not sequence_error
|
|
445
|
+
|
|
446
|
+
repair_round = raw_state.get("repair_round", 0)
|
|
447
|
+
if isinstance(repair_round, bool) or not isinstance(repair_round, int) or not 0 <= repair_round <= MAX_OUTER_REPAIR_ROUNDS:
|
|
448
|
+
return _semantic_error(workflow_type, current_step, "invalid_repair_round", "repair_round must be an integer from 0 through 3")
|
|
449
|
+
repair_scope = raw_state.get("repair_scope")
|
|
450
|
+
if repair_scope is not None and repair_scope not in VALID_REPAIR_SCOPES:
|
|
451
|
+
return _semantic_error(workflow_type, current_step, "invalid_repair_scope", "repair_scope is not a safe shared scope")
|
|
452
|
+
terminal_status = raw_state.get("terminal_status")
|
|
453
|
+
if terminal_status is not None and terminal_status not in VALID_TERMINAL_STATUSES:
|
|
454
|
+
return _semantic_error(workflow_type, current_step, "invalid_terminal_status", "terminal_status is not recognized")
|
|
455
|
+
blocked_reason = raw_state.get("blocked_reason")
|
|
456
|
+
if blocked_reason is not None and (not isinstance(blocked_reason, str) or not blocked_reason.strip()):
|
|
457
|
+
return _semantic_error(workflow_type, current_step, "invalid_blocked_reason", "blocked_reason must be a non-empty string")
|
|
458
|
+
if terminal_status == "WORKFLOW_BLOCKED" and not blocked_reason:
|
|
459
|
+
return _semantic_error(workflow_type, current_step, "missing_blocked_reason", "WORKFLOW_BLOCKED requires blocked_reason")
|
|
460
|
+
|
|
461
|
+
review_step = _single_step(steps, "prizmkit-code-review")
|
|
462
|
+
test_step = _single_step(steps, "prizmkit-test")
|
|
463
|
+
review_valid = bool(
|
|
464
|
+
review_step
|
|
465
|
+
and review_step.status == "completed"
|
|
466
|
+
and review_step.stage_result == "REVIEW_PASS"
|
|
467
|
+
)
|
|
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(
|
|
474
|
+
test_step
|
|
475
|
+
and test_step.status == "completed"
|
|
476
|
+
and test_step.stage_result == "TEST_PASS"
|
|
477
|
+
and _validate_test_evidence(evidence_paths, checkpoint_path, project_root)
|
|
478
|
+
)
|
|
479
|
+
all_required_completed = bool(required) and all(
|
|
480
|
+
(step := _single_step(steps, skill)) is not None and step.status == "completed"
|
|
481
|
+
for skill in required
|
|
482
|
+
)
|
|
483
|
+
artifacts_valid = all(
|
|
484
|
+
_required_artifacts_exist(step, checkpoint_path, project_root)
|
|
485
|
+
for step in steps
|
|
486
|
+
if step.status == "completed"
|
|
487
|
+
)
|
|
488
|
+
completion_ready = (
|
|
489
|
+
sequence_valid and all_required_completed and review_valid
|
|
490
|
+
and evidence_valid and artifacts_valid
|
|
491
|
+
)
|
|
492
|
+
blocked = terminal_status == "WORKFLOW_BLOCKED" and bool(blocked_reason)
|
|
493
|
+
complete = completion_ready and terminal_status == "WORKFLOW_COMPLETED" and not blocked
|
|
494
|
+
|
|
495
|
+
current_stage = current_step.skill if current_step is not None else None
|
|
496
|
+
next_stage = current_stage
|
|
497
|
+
if current_step is not None and current_step.status == "in_progress":
|
|
498
|
+
later = steps[current_step.index + 1:]
|
|
499
|
+
next_step = next((step for step in later if step.status not in TERMINAL_STEP_STATUSES), None)
|
|
500
|
+
next_stage = next_step.skill if next_step is not None else None
|
|
501
|
+
if terminal_status is not None:
|
|
502
|
+
current_stage = None
|
|
503
|
+
next_stage = None
|
|
504
|
+
|
|
505
|
+
error_code = ""
|
|
506
|
+
error_message = ""
|
|
507
|
+
if sequence_error:
|
|
508
|
+
error_code = "invalid_stage_sequence"
|
|
509
|
+
error_message = sequence_error
|
|
510
|
+
elif all(step.status in TERMINAL_STEP_STATUSES for step in steps) and not completion_ready and not blocked:
|
|
511
|
+
error_code = "incomplete_semantic_evidence"
|
|
512
|
+
if not review_valid:
|
|
513
|
+
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"
|
|
516
|
+
else:
|
|
517
|
+
error_message = "completed checkpoint lacks mandatory completed stages"
|
|
518
|
+
elif terminal_status == "WORKFLOW_COMPLETED" and not completion_ready:
|
|
519
|
+
error_code = "invalid_completed_terminal"
|
|
520
|
+
error_message = "WORKFLOW_COMPLETED does not have semantic completion evidence"
|
|
521
|
+
|
|
522
|
+
return SemanticCheckpoint(
|
|
523
|
+
workflow_type=workflow_type,
|
|
524
|
+
current_stage=current_stage,
|
|
525
|
+
next_stage=next_stage,
|
|
526
|
+
stage_result=raw_state.get("stage_result"),
|
|
527
|
+
repair_scope=repair_scope,
|
|
528
|
+
repair_round=repair_round,
|
|
529
|
+
authoritative_evidence_paths=evidence_paths,
|
|
530
|
+
blocked_reason=blocked_reason,
|
|
531
|
+
terminal_status=terminal_status,
|
|
532
|
+
sequence_valid=sequence_valid,
|
|
533
|
+
evidence_valid=evidence_valid,
|
|
534
|
+
artifacts_valid=artifacts_valid,
|
|
535
|
+
completion_ready=completion_ready,
|
|
536
|
+
complete=complete,
|
|
537
|
+
blocked=blocked,
|
|
538
|
+
error_code=error_code,
|
|
539
|
+
error_message=error_message,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _required_sequence_error(
|
|
544
|
+
steps: tuple[CheckpointStep, ...],
|
|
545
|
+
required: tuple[str, ...] | None,
|
|
546
|
+
) -> str:
|
|
547
|
+
if not required:
|
|
548
|
+
return ""
|
|
549
|
+
positions: list[int] = []
|
|
550
|
+
for skill in required:
|
|
551
|
+
matches = [step.index for step in steps if step.skill == skill]
|
|
552
|
+
if len(matches) != 1:
|
|
553
|
+
return f"Expected exactly one mandatory stage {skill}"
|
|
554
|
+
positions.append(matches[0])
|
|
555
|
+
if positions != sorted(positions):
|
|
556
|
+
return "Mandatory stages are not in the expected family order"
|
|
557
|
+
return ""
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _single_step(steps: tuple[CheckpointStep, ...], skill: str) -> CheckpointStep | None:
|
|
561
|
+
matches = [step for step in steps if step.skill == skill]
|
|
562
|
+
return matches[0] if len(matches) == 1 else None
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _required_artifacts_exist(
|
|
567
|
+
step: CheckpointStep,
|
|
568
|
+
checkpoint_path: Path | None,
|
|
569
|
+
project_root: Path | None,
|
|
570
|
+
) -> bool:
|
|
571
|
+
if not step.required_artifacts:
|
|
572
|
+
return True
|
|
573
|
+
import glob
|
|
574
|
+
|
|
575
|
+
root = (project_root or _infer_project_root(checkpoint_path)).resolve()
|
|
576
|
+
for artifact in step.required_artifacts:
|
|
577
|
+
candidate = root / artifact
|
|
578
|
+
if glob.has_magic(str(candidate)):
|
|
579
|
+
if not glob.glob(str(candidate)):
|
|
580
|
+
return False
|
|
581
|
+
elif not candidate.exists():
|
|
582
|
+
return False
|
|
583
|
+
return True
|
|
584
|
+
|
|
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
|
+
|
|
647
|
+
def _evidence_validator_passes(
|
|
648
|
+
project_root: Path,
|
|
649
|
+
evidence_dir: Path,
|
|
650
|
+
validator: Path,
|
|
651
|
+
) -> bool:
|
|
652
|
+
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):
|
|
665
|
+
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)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _infer_project_root(checkpoint_path: Path | None) -> Path:
|
|
680
|
+
if checkpoint_path is None:
|
|
681
|
+
return Path.cwd()
|
|
682
|
+
resolved = checkpoint_path.resolve()
|
|
683
|
+
for parent in (resolved.parent, *resolved.parents):
|
|
684
|
+
if (parent / ".prizmkit").is_dir():
|
|
685
|
+
return parent
|
|
686
|
+
return resolved.parent
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _semantic_error(
|
|
690
|
+
workflow_type: str,
|
|
691
|
+
current_step: CheckpointStep | None,
|
|
692
|
+
code: str,
|
|
693
|
+
message: str,
|
|
694
|
+
) -> SemanticCheckpoint:
|
|
695
|
+
stage = current_step.skill if current_step is not None else None
|
|
696
|
+
return SemanticCheckpoint(
|
|
697
|
+
workflow_type=workflow_type,
|
|
698
|
+
current_stage=stage,
|
|
699
|
+
next_stage=stage,
|
|
700
|
+
error_code=code,
|
|
701
|
+
error_message=message,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
|
|
269
705
|
def _invalid(
|
|
270
706
|
path: Path | None,
|
|
271
707
|
exists: bool,
|
|
@@ -280,4 +716,5 @@ def _invalid(
|
|
|
280
716
|
error_code=code,
|
|
281
717
|
error_message=message,
|
|
282
718
|
raw_data=raw_data,
|
|
719
|
+
semantic=SemanticCheckpoint(error_code=code, error_message=message),
|
|
283
720
|
)
|