prizmkit 1.1.140 → 1.1.142

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 (83) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +1 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +165 -125
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +13 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -6
  6. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +5 -3
  7. package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +160 -0
  8. package/bundled/dev-pipeline/scripts/check-session-status.py +11 -1
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +23 -806
  10. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +6 -8
  11. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
  12. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +6 -8
  13. package/bundled/dev-pipeline/scripts/prompt_framework.py +68 -56
  14. package/bundled/dev-pipeline/scripts/update-bug-status.py +21 -2
  15. package/bundled/dev-pipeline/scripts/update-checkpoint.py +52 -131
  16. package/bundled/dev-pipeline/scripts/update-feature-status.py +21 -2
  17. package/bundled/dev-pipeline/scripts/update-refactor-status.py +21 -2
  18. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +3 -3
  19. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +8 -19
  20. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -6
  21. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +5 -5
  22. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +1 -1
  23. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +1 -1
  25. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +1 -1
  26. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +13 -10
  27. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +1 -1
  28. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +18 -9
  29. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
  31. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  32. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +2 -2
  33. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
  34. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +1 -1
  35. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +1 -1
  36. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +14 -26
  37. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +2 -2
  38. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +3 -3
  39. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
  40. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +13 -10
  41. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  42. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  43. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +494 -413
  44. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +144 -684
  45. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +20 -41
  46. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +24 -24
  47. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +176 -36
  48. package/bundled/skills/_metadata.json +6 -6
  49. package/bundled/skills/prizmkit/SKILL.md +13 -18
  50. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +73 -80
  51. package/bundled/skills/prizmkit-code-review/SKILL.md +7 -7
  52. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +73 -80
  53. package/bundled/skills/prizmkit-committer/SKILL.md +8 -7
  54. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +73 -80
  55. package/bundled/skills/prizmkit-implement/SKILL.md +3 -2
  56. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +73 -80
  57. package/bundled/skills/prizmkit-plan/SKILL.md +2 -1
  58. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +73 -80
  59. package/bundled/skills/prizmkit-retrospective/SKILL.md +8 -6
  60. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +73 -80
  61. package/bundled/skills/prizmkit-test/SKILL.md +217 -189
  62. package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +119 -0
  63. package/bundled/skills/prizmkit-test/references/independent-test-review.md +269 -0
  64. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +193 -0
  65. package/bundled/skills/prizmkit-test/references/test-report-template.md +164 -79
  66. package/bundled/skills/prizmkit-workflow/SKILL.md +28 -36
  67. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +73 -80
  68. package/package.json +1 -1
  69. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +0 -477
  70. package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +0 -116
  71. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +0 -111
  72. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +0 -66
  73. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +0 -104
  74. package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +0 -65
  75. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +0 -208
  76. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +0 -80
  77. package/bundled/skills/prizmkit-test/references/examples.md +0 -100
  78. package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +0 -228
  79. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +0 -118
  80. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +0 -107
  81. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +0 -181
  82. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +0 -1930
  83. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +0 -1847
@@ -4,7 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
 
7
- from prizmkit_runtime.checkpoint_state import load_checkpoint_state, read_checkpoint_cursor, validate_checkpoint_data
7
+ from prizmkit_runtime.checkpoint_state import (
8
+ VALID_CHECKPOINT_STATUSES,
9
+ VALID_STAGE_RESULTS,
10
+ expected_status_for_stage_result,
11
+ load_checkpoint_state,
12
+ read_checkpoint_cursor,
13
+ validate_checkpoint_data,
14
+ )
8
15
  from update_checkpoint import update_checkpoint
9
16
 
10
17
 
@@ -24,10 +31,74 @@ def _step(step_id, skill="skill", name="Step", status="pending", depends_on=None
24
31
 
25
32
 
26
33
  def _write(path, data):
34
+ path.parent.mkdir(parents=True, exist_ok=True)
27
35
  path.write_text(json.dumps(data), encoding="utf-8")
28
36
  return path
29
37
 
30
38
 
39
+ def _write_test_artifacts(root, slug="feature", *, result="TEST_PASS"):
40
+ artifact_dir = root / ".prizmkit" / "specs" / slug
41
+ artifact_dir.mkdir(parents=True, exist_ok=True)
42
+ (artifact_dir / "test-report.md").write_text(
43
+ f"# Test Report\n\n## Final Result\n\n- Result: {result}\n",
44
+ encoding="utf-8",
45
+ )
46
+ projection = {
47
+ "schema_version": 1,
48
+ "result": result,
49
+ "report": "test-report.md",
50
+ "main_review_rounds": 1,
51
+ "independent_review": {
52
+ "status": "downgraded",
53
+ "responses": 0,
54
+ "downgrade_reason": "read-only continuation capability unavailable",
55
+ "final_state_rechecked": False,
56
+ },
57
+ "repair_rounds": 0,
58
+ "production_changed": False,
59
+ "review_required": False,
60
+ "review_scope": None,
61
+ "unresolved_items": [],
62
+ }
63
+ (artifact_dir / "test-result.json").write_text(
64
+ json.dumps(projection), encoding="utf-8"
65
+ )
66
+ return artifact_dir
67
+
68
+
69
+ def _family_checkpoint(root=None, *, semantic=None, terminal=True):
70
+ skills = [
71
+ "prizmkit-implement",
72
+ "prizmkit-code-review",
73
+ "prizmkit-test",
74
+ "prizmkit-retrospective",
75
+ "prizmkit-committer",
76
+ "completion-summary",
77
+ ]
78
+ steps = []
79
+ previous = None
80
+ for index, skill in enumerate(skills, start=1):
81
+ step = _step(f"S{index:02d}", skill, skill, "completed", previous)
82
+ if skill == "prizmkit-code-review":
83
+ step["stage_result"] = "REVIEW_PASS"
84
+ if skill == "prizmkit-test":
85
+ step["stage_result"] = "TEST_PASS"
86
+ step["required_artifacts"] = [
87
+ ".prizmkit/specs/feature/test-report.md",
88
+ ".prizmkit/specs/feature/test-result.json",
89
+ ]
90
+ steps.append(step)
91
+ previous = step["id"]
92
+ if root is not None:
93
+ _write_test_artifacts(root)
94
+ return {
95
+ "version": 1,
96
+ "workflow_type": "feature-pipeline",
97
+ "steps": steps,
98
+ "feature_state": semantic or ({"terminal_status": "WORKFLOW_COMPLETED"} if terminal else {}),
99
+ }
100
+
101
+
31
102
  def test_valid_checkpoint_current_step_uses_first_non_terminal(tmp_path):
32
103
  path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
33
104
  _step("S01", "plan", "Plan", "completed"),
@@ -44,36 +115,17 @@ def test_valid_checkpoint_current_step_uses_first_non_terminal(tmp_path):
44
115
  assert read_checkpoint_cursor(path) == 2
45
116
 
46
117
 
47
- def test_all_completed_checkpoint_has_complete_cursor(tmp_path):
48
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
49
- _step("S01", "plan", "Plan", "completed"),
50
- _step("S02", "commit", "Commit", "skipped", "S01"),
51
- ]))
52
-
53
- state = load_checkpoint_state(path)
54
-
55
- assert state.valid is True
56
- assert state.all_completed is True
57
- assert state.current_step is None
58
- assert state.current_step_name == ""
59
- assert state.cursor == 2
60
- assert state.cursor_signature == "complete"
61
-
62
-
63
118
  def test_missing_and_malformed_checkpoints_are_invalid(tmp_path):
64
119
  missing = load_checkpoint_state(tmp_path / "missing.json")
65
120
  assert missing.valid is False
66
121
  assert missing.exists is False
67
122
  assert missing.error_code == "missing"
68
- assert missing.current_step_name == ""
69
123
 
70
124
  malformed_path = tmp_path / "bad.json"
71
125
  malformed_path.write_text("{not-json", encoding="utf-8")
72
126
  malformed = load_checkpoint_state(malformed_path)
73
127
  assert malformed.valid is False
74
- assert malformed.exists is True
75
128
  assert malformed.error_code == "malformed_json"
76
- assert malformed.current_step_name == ""
77
129
 
78
130
 
79
131
  def test_structural_validation_error_codes(tmp_path):
@@ -83,7 +135,6 @@ def test_structural_validation_error_codes(tmp_path):
83
135
  ("duplicate_step_id", _checkpoint([_step("S01"), _step("S01")])),
84
136
  ("invalid_status", _checkpoint([_step("S01", status="done")])),
85
137
  ("invalid_depends_on", _checkpoint([_step("S01", depends_on="S99")])),
86
- ("invalid_depends_on", _checkpoint([_step("S01", depends_on="S02"), _step("S02")])),
87
138
  ("malformed_step_id", _checkpoint([_step("one")])),
88
139
  ("invalid_step", {"steps": ["not-object"]}),
89
140
  ]
@@ -92,183 +143,230 @@ def test_structural_validation_error_codes(tmp_path):
92
143
  state = load_checkpoint_state(path)
93
144
  assert state.valid is False
94
145
  assert state.error_code == expected_code
95
- assert state.current_step_name == ""
96
146
  assert read_checkpoint_cursor(path) is None
97
147
 
98
148
 
99
- def test_failed_checkpoint_step_is_current_cursor(tmp_path):
100
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
101
- _step("S01", "plan", "Plan", "completed"),
102
- _step("S02", "review", "Review", "failed", "S01"),
103
- ]))
149
+ def test_domain_results_map_to_lifecycle_statuses():
150
+ expected = {
151
+ "PLAN_READY": "completed",
152
+ "PLAN_BLOCKED": "failed",
153
+ "IMPLEMENTED": "completed",
154
+ "IMPLEMENT_REPAIR": "failed",
155
+ "IMPLEMENT_BLOCKED": "failed",
156
+ "REVIEW_PASS": "completed",
157
+ "REVIEW_NEEDS_FIXES": "failed",
158
+ "TEST_PASS": "completed",
159
+ "TEST_NEEDS_FIXES": "failed",
160
+ "TEST_BLOCKED": "failed",
161
+ "RETRO_COMPLETE": "completed",
162
+ "RETRO_BLOCKED": "failed",
163
+ "COMMIT_PENDING": "in_progress",
164
+ "COMMITTED": "completed",
165
+ "COMMIT_BLOCKED": "failed",
166
+ }
104
167
 
105
- state = load_checkpoint_state(path)
168
+ assert VALID_CHECKPOINT_STATUSES == {
169
+ "pending", "in_progress", "completed", "failed", "skipped",
170
+ }
171
+ for result, status in expected.items():
172
+ assert expected_status_for_stage_result(result) == status
173
+
174
+
175
+ def test_structural_validation_rejects_domain_result_as_status(tmp_path):
176
+ for domain_status in [
177
+ "PLAN_READY", "IMPLEMENTED", "REVIEW_PASS", "REVIEW_NEEDS_FIXES",
178
+ "TEST_PASS", "TEST_NEEDS_FIXES", "TEST_BLOCKED", "RETRO_COMPLETE",
179
+ "COMMIT_PENDING", "COMMITTED",
180
+ ]:
181
+ state = validate_checkpoint_data(
182
+ _checkpoint([_step("S01", status=domain_status)]),
183
+ path=tmp_path / f"{domain_status}.json",
184
+ )
185
+ assert state.valid is False
186
+ assert state.error_code == "invalid_status"
106
187
 
107
- assert state.valid is True
108
- assert state.current_step_name == "Review"
109
- assert state.cursor == 2
110
188
 
189
+ def test_structural_validation_rejects_status_result_mismatch(tmp_path):
190
+ step = _step("S01", "prizmkit-code-review", "Review", "failed")
191
+ step["stage_result"] = "REVIEW_PASS"
111
192
 
112
- def test_duplicate_skills_are_reported_but_exact_ids_remain_valid(tmp_path):
113
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
114
- _step("S01", "prizmkit-committer", "Commit First", "pending"),
115
- _step("S02", "prizmkit-committer", "Commit Second", "pending", "S01"),
116
- ]))
193
+ state = validate_checkpoint_data(
194
+ _checkpoint([step]),
195
+ path=tmp_path / "mismatch.json",
196
+ )
117
197
 
118
- state = load_checkpoint_state(path)
198
+ assert state.valid is False
199
+ assert state.error_code == "stage_result_status_mismatch"
119
200
 
120
- assert state.valid is True
121
- assert state.duplicate_skills == {"prizmkit-committer": ("S01", "S02")}
122
201
 
202
+ def test_structural_validation_rejects_stage_result_owner_mismatch(tmp_path):
203
+ for skill in ("prizmkit-test", "custom-stage"):
204
+ step = _step("S01", skill, "Step", "completed")
205
+ step["stage_result"] = "REVIEW_PASS"
123
206
 
124
- def test_update_checkpoint_rejects_ambiguous_skill_without_mutation(tmp_path):
125
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
126
- _step("S01", "prizmkit-committer", "Commit First", "pending"),
127
- _step("S02", "prizmkit-committer", "Commit Second", "pending", "S01"),
128
- ]))
129
- before = path.read_text(encoding="utf-8")
207
+ state = validate_checkpoint_data(
208
+ _checkpoint([step]),
209
+ path=tmp_path / f"{skill}-owner-mismatch.json",
210
+ )
130
211
 
131
- result = update_checkpoint(str(path), "prizmkit-committer", "completed")
212
+ assert state.valid is False
213
+ assert state.error_code == "stage_result_owner_mismatch"
132
214
 
133
- assert result["ok"] is False
134
- assert "matches multiple checkpoint steps" in result["error"]
135
- assert "Use an explicit step ID" in result["error"]
136
- assert path.read_text(encoding="utf-8") == before
137
215
 
216
+ def test_semantic_metadata_rejects_legacy_or_mismatched_status(tmp_path):
217
+ invalid = [
218
+ ({"status": "TEST_PASS", "stage_result": "TEST_PASS"}, "invalid_semantic_status"),
219
+ ({"status": "failed", "stage_result": "TEST_PASS"}, "semantic_stage_result_status_mismatch"),
220
+ ({"status": "completed", "stage_result": "UNKNOWN_RESULT"}, "invalid_semantic_stage_result"),
221
+ ({"stage": "test", "status": "completed", "stage_result": "REVIEW_PASS"}, "semantic_stage_result_owner_mismatch"),
222
+ ]
138
223
 
139
- def test_update_checkpoint_exact_id_updates_one_duplicate_skill_step(tmp_path):
140
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
141
- _step("S01", "prizmkit-committer", "Commit First", "completed"),
142
- _step("S02", "prizmkit-committer", "Commit Second", "pending", "S01"),
143
- ]))
224
+ for index, (semantic, error_code) in enumerate(invalid):
225
+ state = validate_checkpoint_data(
226
+ _checkpoint([_step("S01")]) | {"feature_state": semantic},
227
+ path=tmp_path / f"semantic-status-{index}.json",
228
+ )
144
229
 
145
- result = update_checkpoint(str(path), "S02", "completed")
146
- data = json.loads(path.read_text(encoding="utf-8"))
230
+ assert state.valid is True
231
+ assert state.semantic.error_code == error_code
147
232
 
148
- assert result["ok"] is True
149
- assert result["step_id"] == "S02"
150
- assert data["steps"][0]["status"] == "completed"
151
- assert data["steps"][1]["status"] == "completed"
152
233
 
234
+ def test_update_checkpoint_reenters_failed_stage_without_stale_result(tmp_path):
235
+ payload = _family_checkpoint(
236
+ tmp_path,
237
+ semantic={
238
+ "stage": "code-review",
239
+ "status": "failed",
240
+ "stage_result": "REVIEW_NEEDS_FIXES",
241
+ "repair_scope": "production",
242
+ "repair_round": 1,
243
+ },
244
+ terminal=False,
245
+ )
246
+ payload["steps"][1]["status"] = "failed"
247
+ payload["steps"][1]["stage_result"] = "REVIEW_NEEDS_FIXES"
248
+ for step in payload["steps"][2:]:
249
+ step["status"] = "pending"
250
+ step.pop("stage_result", None)
251
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
153
252
 
154
- def test_update_checkpoint_invalid_checkpoint_fails_without_mutation(tmp_path):
155
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
156
- _step("S01", "plan", "Plan", "done"),
157
- ]))
158
- before = path.read_text(encoding="utf-8")
253
+ result = update_checkpoint(
254
+ str(checkpoint),
255
+ "prizmkit-code-review",
256
+ "in_progress",
257
+ )
258
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
159
259
 
160
- result = update_checkpoint(str(path), "S01", "completed")
260
+ assert result["ok"] is True
261
+ assert result["semantic"]["current_stage"] == "prizmkit-code-review"
262
+ assert result["semantic"]["next_stage"] == "prizmkit-code-review"
263
+ assert persisted["steps"][1]["status"] == "in_progress"
264
+ assert "stage_result" not in persisted["steps"][1]
265
+ assert persisted["feature_state"]["stage"] == "code-review"
266
+ assert persisted["feature_state"]["status"] == "in_progress"
267
+ assert "stage_result" not in persisted["feature_state"]
161
268
 
162
- assert result["ok"] is False
163
- assert "invalid_status" in result["error"]
164
- assert path.read_text(encoding="utf-8") == before
165
269
 
270
+ def test_checkpoint_updater_supports_every_owned_stage_result(tmp_path):
271
+ cases = [
272
+ ("prizmkit-plan", "PLAN_READY", "completed"),
273
+ ("prizmkit-implement", "IMPLEMENTED", "completed"),
274
+ ("prizmkit-code-review", "REVIEW_PASS", "completed"),
275
+ ("prizmkit-test", "TEST_BLOCKED", "failed"),
276
+ ("prizmkit-retrospective", "RETRO_COMPLETE", "completed"),
277
+ ("prizmkit-committer", "COMMIT_PENDING", "in_progress"),
278
+ ("prizmkit-committer", "COMMITTED", "completed"),
279
+ ]
166
280
 
281
+ assert {stage_result for _skill, stage_result, _status in cases} <= VALID_STAGE_RESULTS
282
+ for index, (skill, stage_result, status) in enumerate(cases):
283
+ step = _step("S01", skill, skill, "pending")
284
+ checkpoint = _write(
285
+ tmp_path / f"owned-result-{index}.json",
286
+ _checkpoint([step]),
287
+ )
288
+
289
+ result = update_checkpoint(
290
+ str(checkpoint),
291
+ skill,
292
+ status,
293
+ semantic_update={"stage_result": stage_result},
294
+ )
295
+
296
+ assert result["ok"] is True
297
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
298
+ assert persisted["steps"][0]["status"] == status
299
+ assert persisted["steps"][0]["stage_result"] == stage_result
300
+ assert persisted["feature_state"]["stage"] == {
301
+ "prizmkit-plan": "plan",
302
+ "prizmkit-implement": "implement",
303
+ "prizmkit-code-review": "code-review",
304
+ "prizmkit-test": "test",
305
+ "prizmkit-retrospective": "retrospective",
306
+ "prizmkit-committer": "committer",
307
+ }[skill]
308
+ assert persisted["feature_state"]["status"] == status
309
+ assert persisted["feature_state"]["stage_result"] == stage_result
310
+
311
+
312
+ def test_duplicate_skills_require_exact_step_id(tmp_path):
313
+ path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
314
+ _step("S01", "prizmkit-committer", "First", "completed"),
315
+ _step("S02", "prizmkit-committer", "Second", "pending", "S01"),
316
+ ]))
167
317
 
168
- def _family_checkpoint(workflow_type, skills, *, semantic=None):
169
- steps = []
170
- previous = None
171
- for index, skill in enumerate(skills, start=1):
172
- step = _step(f"S{index:02d}", skill, skill, "completed", previous)
173
- if skill == "prizmkit-code-review":
174
- step["stage_result"] = "REVIEW_PASS"
175
- if skill == "prizmkit-test":
176
- step["stage_result"] = "TEST_PASS"
177
- steps.append(step)
178
- previous = step["id"]
179
- payload = {"version": 1, "workflow_type": workflow_type, "steps": steps}
180
- state_key = {
181
- "feature-pipeline": "feature_state",
182
- "bugfix-pipeline": "bugfix_state",
183
- "refactor-pipeline": "refactor_state",
184
- }[workflow_type]
185
- payload[state_key] = semantic or {}
186
- return payload
187
-
188
-
189
- def _write_test_evidence(root, verdict="TEST_PASS", validation_result="passed", evidence_id="evidence-1"):
190
- evidence = root / ".prizmkit" / "test" / "evidence" / evidence_id
191
- evidence.mkdir(parents=True)
192
- manifest = evidence / "manifest.json"
193
- verdict_path = evidence / "verdict.json"
194
- validation = evidence / "validation.json"
195
- manifest.write_text(json.dumps({"evidence_id": evidence_id, "final_verdict": verdict}), encoding="utf-8")
196
- verdict_path.write_text(json.dumps({"verdict": verdict}), encoding="utf-8")
197
- validation.write_text(json.dumps({"verdict": verdict, "result": validation_result}), encoding="utf-8")
198
- return {
199
- "manifest": str(manifest.relative_to(root)),
200
- "verdict": str(verdict_path.relative_to(root)),
201
- "validation": str(validation.relative_to(root)),
202
- }
318
+ rejected = update_checkpoint(str(path), "prizmkit-committer", "completed")
319
+ accepted = update_checkpoint(str(path), "S02", "completed")
203
320
 
321
+ assert rejected["ok"] is False
322
+ assert "matches multiple checkpoint steps" in rejected["error"]
323
+ assert accepted["ok"] is True
204
324
 
205
- def test_semantic_completion_requires_family_sequence_verdicts_and_structured_evidence(tmp_path, monkeypatch):
206
- monkeypatch.setattr(
207
- "prizmkit_runtime.checkpoint_state._resolve_evidence_validator",
208
- lambda _root: tmp_path / "validator.py",
209
- )
210
- monkeypatch.setattr(
211
- "prizmkit_runtime.checkpoint_state._evidence_validator_passes",
212
- lambda *_args: True,
213
- )
214
- paths = _write_test_evidence(tmp_path)
215
- skills = [
216
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
217
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
218
- ]
219
- payload = _family_checkpoint(
220
- "feature-pipeline",
221
- skills,
222
- semantic={
223
- "repair_round": 0,
224
- "authoritative_evidence_paths": paths,
225
- "terminal_status": "WORKFLOW_COMPLETED",
226
- },
325
+
326
+ def test_semantic_completion_requires_family_sequence_and_domain_results(tmp_path):
327
+ path = _write(
328
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
329
+ _family_checkpoint(tmp_path),
227
330
  )
228
- payload["steps"][2]["authoritative_evidence_paths"] = paths
229
- checkpoint = tmp_path / ".prizmkit" / "specs" / "feature" / "workflow-checkpoint.json"
230
- checkpoint.parent.mkdir(parents=True)
231
- _write(checkpoint, payload)
232
331
 
233
- state = load_checkpoint_state(checkpoint, project_root=tmp_path)
332
+ state = load_checkpoint_state(path, project_root=tmp_path)
234
333
 
235
334
  assert state.valid is True
236
335
  assert state.all_completed is True
237
336
  assert state.semantic_complete is True
238
- assert state.semantic.evidence_valid is True
337
+ assert state.semantic.test_result_valid is True
239
338
  assert state.semantic.terminal_status == "WORKFLOW_COMPLETED"
240
339
 
241
340
 
242
- def test_completed_steps_without_stage_verdicts_are_not_semantic_completion(tmp_path):
243
- skills = [
244
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
245
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
246
- ]
247
- payload = _family_checkpoint("feature-pipeline", skills)
341
+ def test_completion_does_not_require_retired_evidence_paths(tmp_path):
342
+ payload = _family_checkpoint(tmp_path)
343
+ assert "authoritative_evidence_paths" not in payload["steps"][2]
344
+ assert "authoritative_evidence_paths" not in payload["feature_state"]
345
+
346
+ state = validate_checkpoint_data(payload, project_root=tmp_path)
347
+
348
+ assert state.semantic_complete is True
349
+ assert state.semantic.test_result_valid is True
350
+
351
+
352
+ def test_completed_steps_without_review_or_test_results_do_not_complete(tmp_path):
353
+ payload = _family_checkpoint(tmp_path)
248
354
  payload["steps"][1].pop("stage_result")
249
355
  payload["steps"][2].pop("stage_result")
250
- payload["feature_state"]["terminal_status"] = "WORKFLOW_COMPLETED"
251
356
 
252
357
  state = validate_checkpoint_data(payload, project_root=tmp_path)
253
358
 
254
- assert state.valid is True
255
- assert state.all_completed is True
256
359
  assert state.semantic_complete is False
257
360
  assert state.semantic.error_code == "incomplete_semantic_evidence"
258
361
 
259
362
 
260
363
  def test_family_sequence_mismatch_cannot_complete(tmp_path):
261
- paths = _write_test_evidence(tmp_path)
262
- skills = [
263
- "prizmkit-implement", "prizmkit-test", "prizmkit-code-review",
264
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
265
- ]
266
- payload = _family_checkpoint(
267
- "feature-pipeline",
268
- skills,
269
- semantic={"authoritative_evidence_paths": paths, "terminal_status": "WORKFLOW_COMPLETED"},
270
- )
271
- payload["steps"][1]["authoritative_evidence_paths"] = paths
364
+ payload = _family_checkpoint(tmp_path)
365
+ payload["steps"][1], payload["steps"][2] = payload["steps"][2], payload["steps"][1]
366
+ payload["steps"][1]["id"] = "S02"
367
+ payload["steps"][2]["id"] = "S03"
368
+ payload["steps"][1]["depends_on"] = "S01"
369
+ payload["steps"][2]["depends_on"] = "S02"
272
370
 
273
371
  state = validate_checkpoint_data(payload, project_root=tmp_path)
274
372
 
@@ -277,157 +375,139 @@ def test_family_sequence_mismatch_cannot_complete(tmp_path):
277
375
  assert state.semantic.error_code == "invalid_stage_sequence"
278
376
 
279
377
 
280
- def test_stale_or_mismatched_test_evidence_cannot_complete(tmp_path):
281
- paths = _write_test_evidence(tmp_path, verdict="TEST_FAIL")
282
- skills = [
283
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
284
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
285
- ]
286
- payload = _family_checkpoint(
287
- "feature-pipeline",
288
- skills,
289
- semantic={"authoritative_evidence_paths": paths, "terminal_status": "WORKFLOW_COMPLETED"},
378
+ def test_test_needs_fixes_is_valid_domain_result_and_blocks_l4(tmp_path):
379
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
380
+ payload["steps"][2]["status"] = "in_progress"
381
+ payload["steps"][2].pop("stage_result")
382
+ for step in payload["steps"][3:]:
383
+ step["status"] = "pending"
384
+ step.pop("stage_result", None)
385
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
386
+
387
+ result = update_checkpoint(
388
+ str(checkpoint),
389
+ "prizmkit-test",
390
+ "failed",
391
+ semantic_update={"stage_result": "TEST_NEEDS_FIXES"},
290
392
  )
291
- payload["steps"][2]["authoritative_evidence_paths"] = paths
393
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
292
394
 
293
- state = validate_checkpoint_data(payload, project_root=tmp_path)
395
+ assert result["ok"] is True
396
+ assert persisted["steps"][2]["stage_result"] == "TEST_NEEDS_FIXES"
397
+ assert persisted["feature_state"]["stage_result"] == "TEST_NEEDS_FIXES"
398
+ assert persisted["feature_state"]["blocked_reason"] == "test_needs_fixes"
399
+ assert persisted["feature_state"]["terminal_status"] == "WORKFLOW_BLOCKED"
400
+ assert "authoritative_evidence_paths" not in persisted["steps"][2]
294
401
 
295
- assert state.semantic_complete is False
296
- assert state.semantic.evidence_valid is False
297
- assert state.semantic.error_code == "incomplete_semantic_evidence"
298
402
 
403
+ def test_test_blocked_preserves_l1_result_without_evidence_discovery(tmp_path):
404
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
405
+ payload["steps"][2]["status"] = "in_progress"
406
+ payload["steps"][2].pop("stage_result")
407
+ for step in payload["steps"][3:]:
408
+ step["status"] = "pending"
409
+ step.pop("stage_result", None)
410
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
299
411
 
300
- def test_blocked_terminal_is_semantic_and_requires_reason(tmp_path):
301
- payload = _family_checkpoint(
302
- "feature-pipeline",
303
- [
304
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
305
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
306
- ],
307
- semantic={
308
- "repair_scope": "production",
309
- "repair_round": 3,
310
- "blocked_reason": "repair_round_limit",
311
- "terminal_status": "WORKFLOW_BLOCKED",
312
- },
412
+ result = update_checkpoint(
413
+ str(checkpoint),
414
+ "prizmkit-test",
415
+ "failed",
416
+ semantic_update={"stage_result": "TEST_BLOCKED", "blocked_reason": "safe environment unavailable"},
313
417
  )
314
- payload["steps"][2]["stage_result"] = "TEST_FAIL"
315
-
316
- state = validate_checkpoint_data(payload, project_root=tmp_path)
317
-
318
- assert state.semantic_blocked is True
319
- assert state.semantic_complete is False
320
- assert state.semantic.blocked_reason == "repair_round_limit"
321
- assert state.cursor_signature == "blocked:repair_round_limit"
418
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
322
419
 
420
+ assert result["ok"] is True
421
+ assert persisted["steps"][2]["stage_result"] == "TEST_BLOCKED"
422
+ assert persisted["feature_state"]["stage_result"] == "TEST_BLOCKED"
423
+ assert persisted["feature_state"]["blocked_reason"] == "test_blocked"
424
+ assert "authoritative_evidence_paths" not in persisted["feature_state"]
323
425
 
324
- def test_step_progress_must_be_a_terminal_prefix(tmp_path):
325
- payload = _checkpoint([
326
- _step("S01", "plan", "Plan", "pending"),
327
- _step("S02", "implement", "Implement", "completed", "S01"),
328
- ])
329
426
 
330
- state = validate_checkpoint_data(payload)
427
+ def test_update_checkpoint_completes_test_pass_without_validator(tmp_path):
428
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
429
+ payload["steps"][2]["status"] = "in_progress"
430
+ payload["steps"][2].pop("stage_result")
431
+ for step in payload["steps"][3:]:
432
+ step["status"] = "pending"
433
+ step.pop("stage_result", None)
434
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
331
435
 
332
- assert state.valid is False
333
- assert state.error_code == "invalid_step_progress"
436
+ result = update_checkpoint(
437
+ str(checkpoint),
438
+ "prizmkit-test",
439
+ "completed",
440
+ semantic_update={"stage_result": "TEST_PASS"},
441
+ )
334
442
 
443
+ assert result["ok"] is True
444
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
445
+ assert persisted["steps"][2]["status"] == "completed"
446
+ assert persisted["steps"][2]["stage_result"] == "TEST_PASS"
335
447
 
336
448
 
337
- def test_update_checkpoint_rejects_out_of_order_transition(tmp_path):
338
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
339
- _step("S01", "plan", "Plan", "pending"),
340
- _step("S02", "implement", "Implement", "pending", "S01"),
341
- ]))
449
+ def test_update_checkpoint_rejects_test_completion_for_non_pass(tmp_path):
450
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
451
+ payload["steps"][2]["status"] = "in_progress"
452
+ payload["steps"][2].pop("stage_result")
453
+ for step in payload["steps"][3:]:
454
+ step["status"] = "pending"
455
+ step.pop("stage_result", None)
456
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
342
457
 
343
- result = update_checkpoint(str(path), "S02", "in_progress")
458
+ result = update_checkpoint(
459
+ str(checkpoint),
460
+ "prizmkit-test",
461
+ "completed",
462
+ semantic_update={"stage_result": "TEST_NEEDS_FIXES"},
463
+ )
344
464
 
345
465
  assert result["ok"] is False
346
- assert "before all predecessors" in result["error"]
466
+ assert "requires TEST_PASS" in result["error"]
347
467
 
348
468
 
349
- def test_update_checkpoint_blocks_unknown_repair_scope_without_mutating_retry_budget(tmp_path):
350
- payload = _family_checkpoint(
351
- "feature-pipeline",
352
- [
353
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
354
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
355
- ],
356
- )
357
- payload["steps"][1]["status"] = "in_progress"
358
- for step in payload["steps"][2:]:
469
+ def test_update_checkpoint_rejects_test_before_review_pass(tmp_path):
470
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
471
+ payload["steps"][1]["status"] = "skipped"
472
+ payload["steps"][1].pop("stage_result", None)
473
+ payload["steps"][2]["status"] = "pending"
474
+ payload["steps"][2].pop("stage_result", None)
475
+ for step in payload["steps"][3:]:
359
476
  step["status"] = "pending"
360
477
  step.pop("stage_result", None)
361
- path = _write(tmp_path / "workflow-checkpoint.json", payload)
478
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
362
479
 
363
- result = update_checkpoint(
364
- str(path),
365
- "prizmkit-code-review",
366
- "failed",
367
- semantic_update={"stage_result": "REVIEW_NEEDS_FIXES", "repair_round": 0},
368
- )
369
- data = json.loads(path.read_text(encoding="utf-8"))
480
+ result = update_checkpoint(str(checkpoint), "prizmkit-test", "in_progress")
370
481
 
371
- assert result["ok"] is True
372
- assert data["feature_state"]["terminal_status"] == "WORKFLOW_BLOCKED"
373
- assert data["feature_state"]["blocked_reason"] == "unsafe_repair_scope"
374
- assert "retry_count" not in data["feature_state"]
482
+ assert result["ok"] is False
483
+ assert "REVIEW_PASS" in result["error"]
375
484
 
376
485
 
377
- def test_update_checkpoint_enforces_three_outer_repair_rounds(tmp_path):
378
- payload = _family_checkpoint(
379
- "feature-pipeline",
380
- [
381
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
382
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
383
- ],
384
- semantic={"repair_scope": "production", "repair_round": 3},
385
- )
386
- payload["steps"][1]["status"] = "in_progress"
387
- for step in payload["steps"][2:]:
486
+ def test_update_checkpoint_rejects_finalization_before_test_pass(tmp_path):
487
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
488
+ payload["steps"][2]["status"] = "skipped"
489
+ payload["steps"][2].pop("stage_result", None)
490
+ payload["steps"][3]["status"] = "pending"
491
+ for step in payload["steps"][4:]:
388
492
  step["status"] = "pending"
389
493
  step.pop("stage_result", None)
390
- path = _write(tmp_path / "workflow-checkpoint.json", payload)
391
-
392
- result = update_checkpoint(
393
- str(path),
394
- "prizmkit-code-review",
395
- "failed",
396
- semantic_update={"stage_result": "REVIEW_NEEDS_FIXES", "repair_scope": "production"},
397
- )
398
- data = json.loads(path.read_text(encoding="utf-8"))
494
+ checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
399
495
 
400
- assert result["ok"] is True
401
- assert data["feature_state"]["repair_round"] == 3
402
- assert data["feature_state"]["terminal_status"] == "WORKFLOW_BLOCKED"
403
- assert data["feature_state"]["blocked_reason"] == "repair_round_limit"
496
+ result = update_checkpoint(str(checkpoint), "prizmkit-retrospective", "in_progress")
404
497
 
498
+ assert result["ok"] is False
499
+ assert "TEST_PASS" in result["error"]
405
500
 
406
501
 
407
- def test_declared_final_artifact_must_exist_for_semantic_completion(tmp_path, monkeypatch):
408
- monkeypatch.setattr(
409
- "prizmkit_runtime.checkpoint_state._resolve_evidence_validator",
410
- lambda _root: tmp_path / "validator.py",
411
- )
412
- monkeypatch.setattr(
413
- "prizmkit_runtime.checkpoint_state._evidence_validator_passes",
414
- lambda *_args: True,
415
- )
416
- paths = _write_test_evidence(tmp_path)
417
- skills = [
418
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
419
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
502
+ def test_declared_final_artifact_must_exist_for_completion(tmp_path):
503
+ payload = _family_checkpoint(tmp_path)
504
+ payload["steps"][-1]["required_artifacts"] = [
505
+ ".prizmkit/specs/feature/completion-summary.json"
420
506
  ]
421
- payload = _family_checkpoint(
422
- "feature-pipeline",
423
- skills,
424
- semantic={"authoritative_evidence_paths": paths, "terminal_status": "WORKFLOW_COMPLETED"},
507
+ checkpoint = _write(
508
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
509
+ payload,
425
510
  )
426
- payload["steps"][2]["authoritative_evidence_paths"] = paths
427
- payload["steps"][-1]["required_artifacts"] = [".prizmkit/specs/feature/completion-summary.json"]
428
- checkpoint = tmp_path / ".prizmkit" / "specs" / "feature" / "workflow-checkpoint.json"
429
- checkpoint.parent.mkdir(parents=True)
430
- _write(checkpoint, payload)
431
511
 
432
512
  missing = load_checkpoint_state(checkpoint, project_root=tmp_path)
433
513
  (checkpoint.parent / "completion-summary.json").write_text("{}", encoding="utf-8")
@@ -439,185 +519,186 @@ def test_declared_final_artifact_must_exist_for_semantic_completion(tmp_path, mo
439
519
  assert present.semantic.artifacts_valid is True
440
520
 
441
521
 
442
-
443
- def test_blocked_test_handoff_discovers_finalized_evidence_paths(tmp_path):
444
- artifact = tmp_path / ".prizmkit" / "specs" / "feature"
445
- artifact.mkdir(parents=True)
446
- evidence = tmp_path / ".prizmkit" / "test" / "evidence" / "evidence-1"
447
- evidence.mkdir(parents=True)
448
- (evidence / "manifest.json").write_text(
449
- json.dumps({"evidence_id": "evidence-1", "final_verdict": "TEST_BLOCKED"}),
450
- encoding="utf-8",
522
+ def test_completion_rejects_invalid_test_result_projection(tmp_path):
523
+ payload = _family_checkpoint(tmp_path)
524
+ checkpoint = _write(
525
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
526
+ payload,
451
527
  )
452
- (evidence / "verdict.json").write_text(
453
- json.dumps({"verdict": "TEST_BLOCKED"}),
454
- encoding="utf-8",
528
+ result_path = checkpoint.parent / "test-result.json"
529
+ result = json.loads(result_path.read_text(encoding="utf-8"))
530
+ result["main_review_rounds"] = 0
531
+ result_path.write_text(json.dumps(result), encoding="utf-8")
532
+
533
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
534
+
535
+ assert state.semantic_complete is False
536
+ assert state.semantic.artifacts_valid is False
537
+
538
+
539
+ def test_completion_rejects_inconsistent_independent_review_metadata(tmp_path):
540
+ payload = _family_checkpoint(tmp_path)
541
+ checkpoint = _write(
542
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
543
+ payload,
455
544
  )
456
- (evidence / "validation.json").write_text(
457
- json.dumps({"result": "passed", "verdict": "TEST_BLOCKED"}),
458
- encoding="utf-8",
545
+ result_path = checkpoint.parent / "test-result.json"
546
+ result = json.loads(result_path.read_text(encoding="utf-8"))
547
+ result["independent_review"] = {
548
+ "status": "completed",
549
+ "responses": 0,
550
+ "downgrade_reason": None,
551
+ "final_state_rechecked": True,
552
+ }
553
+ result_path.write_text(json.dumps(result), encoding="utf-8")
554
+
555
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
556
+
557
+ assert state.semantic_complete is False
558
+ assert state.semantic.artifacts_valid is False
559
+
560
+
561
+ def test_completion_accepts_legacy_terminal_result_report(tmp_path):
562
+ payload = _family_checkpoint(tmp_path)
563
+ checkpoint = _write(
564
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
565
+ payload,
459
566
  )
460
- (evidence / "lifecycle.json").write_text(
461
- json.dumps({"status": "sealed", "finalized": True}),
567
+ (checkpoint.parent / "test-report.md").write_text(
568
+ "# Test Report\n\n## Terminal Result\n\n- Result: TEST_PASS\n\n"
569
+ "## Final Decision\n\nTEST_PASS\n",
462
570
  encoding="utf-8",
463
571
  )
464
- report = evidence / "test-report.md"
465
- report.write_text("blocked", encoding="utf-8")
466
- (artifact / "test-report-path.txt").write_text(str(report), encoding="utf-8")
467
572
 
468
- payload = _family_checkpoint(
469
- "feature-pipeline",
470
- [
471
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
472
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
473
- ],
474
- semantic={"artifact_dir": ".prizmkit/specs/feature"},
475
- )
476
- payload["feature_state"]["artifact_dir"] = ".prizmkit/specs/feature"
477
- payload["steps"][1]["stage_result"] = "REVIEW_PASS"
478
- payload["steps"][2]["status"] = "in_progress"
479
- payload["steps"][2].pop("stage_result")
480
- for step in payload["steps"][3:]:
481
- step["status"] = "pending"
482
- step.pop("stage_result", None)
483
- checkpoint = _write(tmp_path / ".prizmkit" / "specs" / "feature" / "workflow-checkpoint.json", payload)
573
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
484
574
 
485
- result = update_checkpoint(
486
- str(checkpoint),
487
- "prizmkit-test",
488
- "failed",
489
- semantic_update={
490
- "stage_result": "TEST_BLOCKED",
491
- "blocked_reason": "test_blocked",
492
- "terminal_status": "WORKFLOW_BLOCKED",
493
- },
575
+ assert state.semantic_complete is True
576
+ assert state.semantic.artifacts_valid is True
577
+
578
+
579
+ def test_completion_rejects_conflicting_supported_report_results(tmp_path):
580
+ payload = _family_checkpoint(tmp_path)
581
+ checkpoint = _write(
582
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
583
+ payload,
584
+ )
585
+ (checkpoint.parent / "test-report.md").write_text(
586
+ "# Test Report\n\n## Terminal Result\n\n- Result: TEST_PASS\n\n"
587
+ "## Final Decision\n\nTEST_BLOCKED\n",
588
+ encoding="utf-8",
494
589
  )
495
590
 
496
- assert result["ok"] is True
497
- paths = json.loads(checkpoint.read_text(encoding="utf-8"))["steps"][2]["authoritative_evidence_paths"]
498
- assert paths["manifest"] == ".prizmkit/test/evidence/evidence-1/manifest.json"
499
- assert paths["verdict"] == ".prizmkit/test/evidence/evidence-1/verdict.json"
500
- assert paths["validation"] == ".prizmkit/test/evidence/evidence-1/validation.json"
591
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
501
592
 
593
+ assert state.semantic_complete is False
594
+ assert state.semantic.artifacts_valid is False
502
595
 
503
596
 
504
- def test_blocked_test_handoff_without_finalized_evidence_preserves_test_result(tmp_path):
505
- artifact = tmp_path / ".prizmkit" / "specs" / "feature"
506
- artifact.mkdir(parents=True)
507
- payload = _family_checkpoint(
508
- "feature-pipeline",
509
- [
510
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
511
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
512
- ],
513
- semantic={"artifact_dir": ".prizmkit/specs/feature"},
597
+ def test_completion_rejects_mismatched_test_report_result(tmp_path):
598
+ payload = _family_checkpoint(tmp_path)
599
+ checkpoint = _write(
600
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
601
+ payload,
514
602
  )
515
- payload["feature_state"]["artifact_dir"] = ".prizmkit/specs/feature"
516
- payload["steps"][1]["stage_result"] = "REVIEW_PASS"
517
- payload["steps"][2]["status"] = "in_progress"
518
- payload["steps"][2].pop("stage_result")
519
- for step in payload["steps"][3:]:
520
- step["status"] = "pending"
521
- step.pop("stage_result", None)
522
- checkpoint = _write(artifact / "workflow-checkpoint.json", payload)
523
-
524
- result = update_checkpoint(
525
- str(checkpoint),
526
- "prizmkit-test",
527
- "failed",
528
- semantic_update={
529
- "stage_result": "TEST_BLOCKED",
530
- "blocked_reason": "test_blocked",
531
- "terminal_status": "WORKFLOW_BLOCKED",
532
- },
603
+ (checkpoint.parent / "test-report.md").write_text(
604
+ "# Test Report\n\n## Final Result\n\n- Result: TEST_BLOCKED\n",
605
+ encoding="utf-8",
533
606
  )
534
607
 
535
- persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
536
- assert result["ok"] is True
537
- assert result["semantic"]["stage_result"] == "TEST_BLOCKED"
538
- assert persisted["steps"][2]["stage_result"] == "TEST_BLOCKED"
539
- assert persisted["feature_state"]["stage_result"] == "TEST_BLOCKED"
540
- assert "authoritative_evidence_paths" not in persisted["steps"][2]
608
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
541
609
 
610
+ assert state.semantic_complete is False
611
+ assert state.semantic.artifacts_valid is False
542
612
 
543
- def test_update_checkpoint_rejects_test_pass_when_validator_replay_fails(tmp_path, monkeypatch):
544
- paths = _write_test_evidence(tmp_path)
545
- payload = _family_checkpoint(
546
- "feature-pipeline",
547
- [
548
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
549
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
550
- ],
551
- )
552
- payload["steps"][2]["status"] = "in_progress"
553
- for step in payload["steps"][3:]:
554
- step["status"] = "pending"
555
- step.pop("stage_result", None)
556
- checkpoint = tmp_path / "workflow-checkpoint.json"
557
- _write(checkpoint, payload)
558
- monkeypatch.setattr(
559
- "prizmkit_runtime.checkpoint_state._resolve_evidence_validator",
560
- lambda _root: tmp_path / "validator.py",
613
+
614
+ def test_completion_rejects_report_with_result_only_outside_final_section(tmp_path):
615
+ payload = _family_checkpoint(tmp_path)
616
+ checkpoint = _write(
617
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
618
+ payload,
561
619
  )
562
- monkeypatch.setattr(
563
- "prizmkit_runtime.checkpoint_state._evidence_validator_passes",
564
- lambda *_args: False,
620
+ (checkpoint.parent / "test-report.md").write_text(
621
+ "# Test Report\n\n## Final Result\n\n- Result: TEST_BLOCKED\n\n"
622
+ "## Remaining Risks and Unresolved Items\n\n- Expected eventual result: TEST_PASS\n",
623
+ encoding="utf-8",
565
624
  )
566
625
 
567
- result = update_checkpoint(
568
- str(checkpoint),
569
- "prizmkit-test",
570
- "completed",
571
- semantic_update={
572
- "stage_result": "TEST_PASS",
573
- "authoritative_evidence_paths": paths,
574
- },
626
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
627
+
628
+ assert state.semantic_complete is False
629
+ assert state.semantic.artifacts_valid is False
630
+
631
+
632
+ def test_completion_rejects_test_artifacts_outside_project(tmp_path):
633
+ outside = tmp_path.parent / f"{tmp_path.name}-outside"
634
+ outside.mkdir()
635
+ _write_test_artifacts(outside)
636
+ payload = _family_checkpoint(tmp_path)
637
+ test_step = payload["steps"][2]
638
+ test_step["required_artifacts"] = [
639
+ f"../{outside.name}/.prizmkit/specs/feature/test-report.md",
640
+ f"../{outside.name}/.prizmkit/specs/feature/test-result.json",
641
+ ]
642
+ checkpoint = _write(
643
+ tmp_path / ".prizmkit/specs/feature/workflow-checkpoint.json",
644
+ payload,
575
645
  )
576
646
 
577
- assert result["ok"] is False
578
- assert "validator-attested" in result["error"]
647
+ state = load_checkpoint_state(checkpoint, project_root=tmp_path)
579
648
 
649
+ assert state.semantic_complete is False
650
+ assert state.semantic.artifacts_valid is False
580
651
 
581
652
 
582
- def test_update_checkpoint_rejects_test_before_review_pass(tmp_path):
653
+ def test_review_needs_fixes_preserves_implementation_repair_route(tmp_path):
583
654
  payload = _family_checkpoint(
584
- "feature-pipeline",
585
- [
586
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
587
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
588
- ],
655
+ tmp_path,
656
+ semantic={"repair_scope": "production", "repair_round": 0},
657
+ terminal=False,
589
658
  )
590
- payload["steps"][1]["status"] = "skipped"
591
- payload["steps"][1].pop("stage_result", None)
592
- payload["steps"][2]["status"] = "pending"
593
- for step in payload["steps"][3:]:
659
+ payload["steps"][1]["status"] = "in_progress"
660
+ payload["steps"][1].pop("stage_result")
661
+ for step in payload["steps"][2:]:
594
662
  step["status"] = "pending"
595
663
  step.pop("stage_result", None)
596
664
  checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
597
665
 
598
- result = update_checkpoint(str(checkpoint), "prizmkit-test", "in_progress")
666
+ result = update_checkpoint(
667
+ str(checkpoint),
668
+ "prizmkit-code-review",
669
+ "failed",
670
+ semantic_update={"stage_result": "REVIEW_NEEDS_FIXES", "repair_scope": "production"},
671
+ )
672
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
599
673
 
600
- assert result["ok"] is False
601
- assert "REVIEW_PASS" in result["error"]
674
+ assert result["ok"] is True
675
+ assert result["semantic"]["next_stage"] == "prizmkit-implement"
676
+ assert persisted["feature_state"]["repair_round"] == 1
677
+ assert persisted["feature_state"]["current_stage"] == "prizmkit-code-review"
678
+ assert persisted["feature_state"]["next_stage"] == "prizmkit-implement"
602
679
 
603
680
 
604
- def test_update_checkpoint_rejects_finalization_before_test_pass(tmp_path):
681
+ def test_review_outer_repair_cap_remains_runtime_owned(tmp_path):
605
682
  payload = _family_checkpoint(
606
- "feature-pipeline",
607
- [
608
- "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
609
- "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
610
- ],
683
+ tmp_path, semantic={"repair_scope": "production", "repair_round": 3},
684
+ terminal=False,
611
685
  )
612
- payload["steps"][2]["status"] = "skipped"
613
- payload["steps"][2].pop("stage_result", None)
614
- payload["steps"][3]["status"] = "pending"
615
- for step in payload["steps"][4:]:
686
+ payload["steps"][1]["status"] = "in_progress"
687
+ payload["steps"][1].pop("stage_result")
688
+ for step in payload["steps"][2:]:
616
689
  step["status"] = "pending"
617
690
  step.pop("stage_result", None)
618
691
  checkpoint = _write(tmp_path / "workflow-checkpoint.json", payload)
619
692
 
620
- result = update_checkpoint(str(checkpoint), "prizmkit-retrospective", "in_progress")
693
+ result = update_checkpoint(
694
+ str(checkpoint),
695
+ "prizmkit-code-review",
696
+ "failed",
697
+ semantic_update={"stage_result": "REVIEW_NEEDS_FIXES", "repair_scope": "production"},
698
+ )
699
+ persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
621
700
 
622
- assert result["ok"] is False
623
- assert "TEST_PASS" in result["error"]
701
+ assert result["ok"] is True
702
+ assert persisted["feature_state"]["repair_round"] == 3
703
+ assert persisted["feature_state"]["blocked_reason"] == "repair_round_limit"
704
+ assert persisted["feature_state"]["terminal_status"] == "WORKFLOW_BLOCKED"