prizmkit 1.1.86 → 1.1.88
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/scripts/generate-bootstrap-prompt.py +462 -4
- package/bundled/dev-pipeline/scripts/prizmkit-test-gate.py +446 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +18 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +18 -3
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +39 -0
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -1
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +434 -0
- package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +107 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +462 -4
- package/bundled/dev-pipeline-windows/scripts/prizmkit-test-gate.py +446 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +14 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +18 -3
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +18 -3
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +2 -2
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +2 -2
- package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +39 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +5 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +5 -1
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +7 -2
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
- package/bundled/skills/prizmkit-test/SKILL.md +106 -14
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
- package/bundled/skills/prizmkit-test/references/examples.md +82 -9
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +68 -12
- package/bundled/skills/prizmkit-test/references/test-report-template.md +73 -9
- package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +347 -0
- package/bundled/skills-windows/prizmkit-code-review/SKILL.md +7 -2
- package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
- package/bundled/skills-windows/prizmkit-test/SKILL.md +106 -14
- package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
- package/bundled/skills-windows/prizmkit-test/references/examples.md +82 -9
- package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +4 -4
- package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +68 -12
- package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +73 -9
- package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +347 -0
- package/package.json +1 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scoped PrizmKit test gate for feature pipeline prompts.
|
|
3
|
+
|
|
4
|
+
The feature bootstrap prompt calls this script instead of embedding the full
|
|
5
|
+
Bash/PowerShell report validation logic inline. It intentionally prints compact
|
|
6
|
+
`GATE:*` lines so the AI session can decide whether to continue, retry, or stop.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
REQUIRED_BOUNDARY_COLUMNS = [
|
|
16
|
+
"Module",
|
|
17
|
+
"Interface",
|
|
18
|
+
"Service Type",
|
|
19
|
+
"Happy Path",
|
|
20
|
+
"Request Validation",
|
|
21
|
+
"Auth / Permission / Ownership",
|
|
22
|
+
"Domain Invariants",
|
|
23
|
+
"Collection / Pagination",
|
|
24
|
+
"Date / Time",
|
|
25
|
+
"State Transitions",
|
|
26
|
+
"Dependency Failure",
|
|
27
|
+
"Response Contract",
|
|
28
|
+
"N/A Reasons",
|
|
29
|
+
"Final Status",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
BOUNDARY_VALIDATOR_CANDIDATES = [
|
|
33
|
+
".claude/command-assets/prizmkit-test/scripts/validate_boundary_report.py",
|
|
34
|
+
".codebuddy/skills/prizmkit-test/scripts/validate_boundary_report.py",
|
|
35
|
+
".agents/skills/prizmkit-test/scripts/validate_boundary_report.py",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
OPENAPI_CANDIDATES = [
|
|
39
|
+
"openapi.yaml",
|
|
40
|
+
"openapi.yml",
|
|
41
|
+
"swagger.yaml",
|
|
42
|
+
"swagger.yml",
|
|
43
|
+
"docs/openapi.yaml",
|
|
44
|
+
"docs/openapi.yml",
|
|
45
|
+
"docs/swagger.yaml",
|
|
46
|
+
"docs/swagger.yml",
|
|
47
|
+
"api/openapi.yaml",
|
|
48
|
+
"api/openapi.yml",
|
|
49
|
+
"api/swagger.yaml",
|
|
50
|
+
"api/swagger.yml",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
IGNORED_SCOPE_VALUES = {"none", "n/a", "not applicable", "(none)", "{...}", "..."}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def default_artifact_dir(feature_slug):
|
|
57
|
+
return ".prizmkit/specs/{}/".format(feature_slug)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def marker_path(feature_slug):
|
|
61
|
+
return os.path.join(default_artifact_dir(feature_slug), ".prizmkit-test-started")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def pointer_path(feature_slug):
|
|
65
|
+
return os.path.join(default_artifact_dir(feature_slug), "test-report-path.txt")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalize_artifact_dir(path):
|
|
69
|
+
return str(path or "").strip().replace("\\", "/").rstrip("/")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read_text(path):
|
|
73
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
74
|
+
return fh.read()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def start_gate(feature_slug):
|
|
78
|
+
marker = marker_path(feature_slug)
|
|
79
|
+
os.makedirs(os.path.dirname(marker), exist_ok=True)
|
|
80
|
+
Path(marker).touch()
|
|
81
|
+
return {"gate": "GATE:START", "marker": marker}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def newest_report_after_marker(marker):
|
|
85
|
+
if not os.path.isfile(marker):
|
|
86
|
+
return ""
|
|
87
|
+
try:
|
|
88
|
+
marker_mtime = os.path.getmtime(marker)
|
|
89
|
+
except OSError:
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
matches = []
|
|
93
|
+
for root, _dirs, files in os.walk(os.path.join(".prizmkit", "test")):
|
|
94
|
+
if "test-report.md" not in files:
|
|
95
|
+
continue
|
|
96
|
+
path = os.path.join(root, "test-report.md")
|
|
97
|
+
try:
|
|
98
|
+
report_mtime = os.path.getmtime(path)
|
|
99
|
+
except OSError:
|
|
100
|
+
continue
|
|
101
|
+
if report_mtime >= marker_mtime:
|
|
102
|
+
matches.append((report_mtime, path))
|
|
103
|
+
|
|
104
|
+
if not matches:
|
|
105
|
+
return ""
|
|
106
|
+
return sorted(matches)[-1][1]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def extract_section(report_text, heading):
|
|
110
|
+
in_section = False
|
|
111
|
+
lines = []
|
|
112
|
+
for raw_line in report_text.splitlines():
|
|
113
|
+
line = raw_line.strip()
|
|
114
|
+
if line == heading:
|
|
115
|
+
in_section = True
|
|
116
|
+
continue
|
|
117
|
+
if in_section and line.startswith("## "):
|
|
118
|
+
break
|
|
119
|
+
if in_section:
|
|
120
|
+
lines.append(raw_line)
|
|
121
|
+
return lines
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def extract_scope_field(report_text, field_name):
|
|
125
|
+
prefix = "- {}:".format(field_name)
|
|
126
|
+
for raw_line in extract_section(report_text, "## Scope"):
|
|
127
|
+
line = raw_line.strip()
|
|
128
|
+
if line.startswith(prefix):
|
|
129
|
+
return line[len(prefix):].strip()
|
|
130
|
+
return ""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def extract_verdict(report_text):
|
|
134
|
+
for raw_line in extract_section(report_text, "## Verdict"):
|
|
135
|
+
line = raw_line.strip()
|
|
136
|
+
if line:
|
|
137
|
+
return line
|
|
138
|
+
return ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def extract_key_value(section_lines, field_name):
|
|
142
|
+
prefix = "{}:".format(field_name).lower()
|
|
143
|
+
for raw_line in section_lines:
|
|
144
|
+
line = raw_line.strip()
|
|
145
|
+
if line.startswith("-"):
|
|
146
|
+
line = line[1:].strip()
|
|
147
|
+
if line.lower().startswith(prefix):
|
|
148
|
+
return line[len(prefix):].strip()
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_markdown_cells(line):
|
|
153
|
+
stripped = line.strip()
|
|
154
|
+
if not stripped.startswith("|"):
|
|
155
|
+
return []
|
|
156
|
+
return [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def boundary_columns_ok(matrix_lines):
|
|
160
|
+
for line in matrix_lines:
|
|
161
|
+
cells = parse_markdown_cells(line)
|
|
162
|
+
if cells and all(column in cells for column in REQUIRED_BOUNDARY_COLUMNS):
|
|
163
|
+
return True
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def boundary_incomplete(matrix_lines):
|
|
168
|
+
text = "\n".join(matrix_lines).lower()
|
|
169
|
+
return any(token in text for token in ["boundary-missing", "happy-path-only", "unresolved"])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def discover_openapi_file():
|
|
173
|
+
for candidate in OPENAPI_CANDIDATES:
|
|
174
|
+
if os.path.isfile(candidate):
|
|
175
|
+
return candidate
|
|
176
|
+
return ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def boundary_validator_passed(report_path, openapi_file):
|
|
180
|
+
for validator in BOUNDARY_VALIDATOR_CANDIDATES:
|
|
181
|
+
if not os.path.isfile(validator):
|
|
182
|
+
continue
|
|
183
|
+
args = [sys.executable, validator, "--report", report_path]
|
|
184
|
+
if openapi_file:
|
|
185
|
+
args.extend(["--openapi", openapi_file])
|
|
186
|
+
result = subprocess.run(
|
|
187
|
+
args,
|
|
188
|
+
stdout=subprocess.DEVNULL,
|
|
189
|
+
stderr=subprocess.DEVNULL,
|
|
190
|
+
check=False,
|
|
191
|
+
)
|
|
192
|
+
if result.returncode == 0:
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def scope_list(report_text, name):
|
|
198
|
+
values = []
|
|
199
|
+
in_list = False
|
|
200
|
+
marker = "- {}:".format(name)
|
|
201
|
+
for raw_line in extract_section(report_text, "## Scope"):
|
|
202
|
+
line = raw_line.strip()
|
|
203
|
+
if line == marker:
|
|
204
|
+
in_list = True
|
|
205
|
+
continue
|
|
206
|
+
if not in_list:
|
|
207
|
+
continue
|
|
208
|
+
if raw_line.startswith(" -") or raw_line.startswith("\t-"):
|
|
209
|
+
value = line[2:].strip().strip("`")
|
|
210
|
+
if value and value not in {"{...}", "..."}:
|
|
211
|
+
values.append(value)
|
|
212
|
+
continue
|
|
213
|
+
if line.startswith("- "):
|
|
214
|
+
break
|
|
215
|
+
return values
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def path_is_inside_project(project_root, candidate):
|
|
219
|
+
try:
|
|
220
|
+
return os.path.commonpath([project_root, candidate]) == project_root
|
|
221
|
+
except ValueError:
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def scope_fresh_ok(report_path, marker, report_text):
|
|
226
|
+
try:
|
|
227
|
+
report_mtime = os.path.getmtime(report_path)
|
|
228
|
+
marker_mtime = os.path.getmtime(marker)
|
|
229
|
+
except OSError:
|
|
230
|
+
return False
|
|
231
|
+
if report_mtime < marker_mtime:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
source_files = scope_list(report_text, "In-Scope Source Files")
|
|
235
|
+
test_files = scope_list(report_text, "In-Scope Test Files")
|
|
236
|
+
if not source_files or not test_files:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
project_root = os.path.realpath(os.getcwd())
|
|
240
|
+
for listed_file in source_files + test_files:
|
|
241
|
+
normalized = listed_file.strip().strip("`")
|
|
242
|
+
if not normalized or normalized.lower() in IGNORED_SCOPE_VALUES:
|
|
243
|
+
return False
|
|
244
|
+
candidate = normalized if os.path.isabs(normalized) else os.path.join(os.getcwd(), normalized)
|
|
245
|
+
candidate_real = os.path.realpath(candidate)
|
|
246
|
+
if not path_is_inside_project(project_root, candidate_real):
|
|
247
|
+
return False
|
|
248
|
+
if not os.path.isfile(candidate_real):
|
|
249
|
+
return False
|
|
250
|
+
if os.path.getmtime(candidate_real) > report_mtime:
|
|
251
|
+
return False
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def write_report_pointer(feature_slug, report_path):
|
|
256
|
+
path = pointer_path(feature_slug)
|
|
257
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
258
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
259
|
+
fh.write(report_path + "\n")
|
|
260
|
+
return path
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def update_checkpoint(checkpoint_path):
|
|
264
|
+
script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "update-checkpoint.py")
|
|
265
|
+
if not checkpoint_path:
|
|
266
|
+
return True, ""
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
[
|
|
269
|
+
sys.executable,
|
|
270
|
+
script_path,
|
|
271
|
+
"--checkpoint-path",
|
|
272
|
+
checkpoint_path,
|
|
273
|
+
"--step",
|
|
274
|
+
"prizmkit-test",
|
|
275
|
+
"--status",
|
|
276
|
+
"completed",
|
|
277
|
+
],
|
|
278
|
+
stdout=subprocess.PIPE,
|
|
279
|
+
stderr=subprocess.STDOUT,
|
|
280
|
+
text=True,
|
|
281
|
+
check=False,
|
|
282
|
+
)
|
|
283
|
+
if result.returncode == 0:
|
|
284
|
+
return True, ""
|
|
285
|
+
output = " ".join((result.stdout or "").split())
|
|
286
|
+
return False, output[:500]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def validate_report(report_path, marker, feature_slug, expected_artifact_dir):
|
|
290
|
+
try:
|
|
291
|
+
report_text = read_text(report_path)
|
|
292
|
+
except OSError as exc:
|
|
293
|
+
return {
|
|
294
|
+
"status": "FAIL",
|
|
295
|
+
"reason": "unreadable_report",
|
|
296
|
+
"error": str(exc),
|
|
297
|
+
"report": report_path,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
report_mode = extract_scope_field(report_text, "Mode")
|
|
301
|
+
report_artifact_dir = extract_scope_field(report_text, "Artifact Dir")
|
|
302
|
+
report_verdict = extract_verdict(report_text)
|
|
303
|
+
|
|
304
|
+
boundary_gate = extract_section(report_text, "## Boundary Completion Gate")
|
|
305
|
+
boundary_matrix = extract_section(report_text, "## Boundary Matrix")
|
|
306
|
+
boundary_validation = extract_section(report_text, "## Boundary Validation")
|
|
307
|
+
|
|
308
|
+
gate_passed = extract_key_value(boundary_gate, "Completion gate passed")
|
|
309
|
+
boundary_missing = extract_key_value(boundary_gate, "Boundary-missing")
|
|
310
|
+
happy_path_only = extract_key_value(boundary_gate, "Happy-path-only")
|
|
311
|
+
validation_result = extract_key_value(boundary_validation, "Validator result").lower()
|
|
312
|
+
|
|
313
|
+
columns_ok = boundary_columns_ok(boundary_matrix)
|
|
314
|
+
incomplete = boundary_incomplete(boundary_matrix)
|
|
315
|
+
openapi_file = discover_openapi_file()
|
|
316
|
+
validator_ok = boundary_validator_passed(report_path, openapi_file)
|
|
317
|
+
fresh_ok = scope_fresh_ok(report_path, marker, report_text)
|
|
318
|
+
|
|
319
|
+
artifact_ok = normalize_artifact_dir(report_artifact_dir) == normalize_artifact_dir(expected_artifact_dir)
|
|
320
|
+
boundary_ok = (
|
|
321
|
+
columns_ok
|
|
322
|
+
and not incomplete
|
|
323
|
+
and gate_passed == "yes"
|
|
324
|
+
and boundary_missing == "0"
|
|
325
|
+
and happy_path_only == "0"
|
|
326
|
+
and validation_result == "passed"
|
|
327
|
+
and validator_ok
|
|
328
|
+
and fresh_ok
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
result = {
|
|
332
|
+
"status": "PASS" if report_mode == "this-change" and artifact_ok and report_verdict == "PASS" and boundary_ok else "FAIL",
|
|
333
|
+
"verdict": report_verdict,
|
|
334
|
+
"mode": report_mode,
|
|
335
|
+
"artifact_dir": report_artifact_dir,
|
|
336
|
+
"boundary_gate": gate_passed,
|
|
337
|
+
"boundary_missing": boundary_missing,
|
|
338
|
+
"happy_path_only": happy_path_only,
|
|
339
|
+
"boundary_validation": validation_result,
|
|
340
|
+
"validator_passed": "1" if validator_ok else "0",
|
|
341
|
+
"scope_fresh": "1" if fresh_ok else "0",
|
|
342
|
+
"openapi": openapi_file,
|
|
343
|
+
"report": report_path,
|
|
344
|
+
"feature_slug": feature_slug,
|
|
345
|
+
}
|
|
346
|
+
if not columns_ok:
|
|
347
|
+
result["reason"] = "boundary_columns_missing"
|
|
348
|
+
elif incomplete:
|
|
349
|
+
result["reason"] = "boundary_incomplete"
|
|
350
|
+
elif not fresh_ok:
|
|
351
|
+
result["reason"] = "scope_not_fresh"
|
|
352
|
+
elif not artifact_ok:
|
|
353
|
+
result["reason"] = "wrong_artifact_dir"
|
|
354
|
+
elif report_mode != "this-change":
|
|
355
|
+
result["reason"] = "wrong_mode"
|
|
356
|
+
elif report_verdict != "PASS":
|
|
357
|
+
result["reason"] = "verdict_not_pass"
|
|
358
|
+
elif not validator_ok:
|
|
359
|
+
result["reason"] = "validator_failed"
|
|
360
|
+
elif not boundary_ok:
|
|
361
|
+
result["reason"] = "boundary_gate_failed"
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def check_gate(feature_slug, artifact_dir=None, checkpoint_path=None):
|
|
366
|
+
expected_artifact_dir = artifact_dir or default_artifact_dir(feature_slug)
|
|
367
|
+
marker = marker_path(feature_slug)
|
|
368
|
+
report_path = newest_report_after_marker(marker)
|
|
369
|
+
if not report_path:
|
|
370
|
+
return {"status": "MISSING", "marker": marker}
|
|
371
|
+
|
|
372
|
+
result = validate_report(report_path, marker, feature_slug, expected_artifact_dir)
|
|
373
|
+
if result["status"] != "PASS":
|
|
374
|
+
return result
|
|
375
|
+
|
|
376
|
+
write_report_pointer(feature_slug, report_path)
|
|
377
|
+
checkpoint_ok, checkpoint_error = update_checkpoint(checkpoint_path)
|
|
378
|
+
if not checkpoint_ok:
|
|
379
|
+
result["status"] = "FAIL"
|
|
380
|
+
result["reason"] = "checkpoint_update_failed"
|
|
381
|
+
result["checkpoint_error"] = checkpoint_error
|
|
382
|
+
return result
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def format_result(result):
|
|
386
|
+
status = result.get("status") or result.get("gate", "")
|
|
387
|
+
if status == "GATE:START":
|
|
388
|
+
return "GATE:START marker={}".format(result.get("marker", ""))
|
|
389
|
+
if status == "MISSING":
|
|
390
|
+
return "GATE:MISSING marker={}".format(result.get("marker", ""))
|
|
391
|
+
if status == "PASS":
|
|
392
|
+
return "GATE:PASS {}".format(result.get("report", ""))
|
|
393
|
+
|
|
394
|
+
ordered = [
|
|
395
|
+
"reason",
|
|
396
|
+
"verdict",
|
|
397
|
+
"mode",
|
|
398
|
+
"artifact_dir",
|
|
399
|
+
"boundary_gate",
|
|
400
|
+
"boundary_missing",
|
|
401
|
+
"happy_path_only",
|
|
402
|
+
"boundary_validation",
|
|
403
|
+
"validator_passed",
|
|
404
|
+
"scope_fresh",
|
|
405
|
+
"openapi",
|
|
406
|
+
"report",
|
|
407
|
+
"checkpoint_error",
|
|
408
|
+
"error",
|
|
409
|
+
]
|
|
410
|
+
parts = []
|
|
411
|
+
for key in ordered:
|
|
412
|
+
value = result.get(key)
|
|
413
|
+
if value is not None and value != "":
|
|
414
|
+
parts.append("{}={}".format(key, value))
|
|
415
|
+
return "GATE:FAIL {}".format(" ".join(parts)).rstrip()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def build_parser():
|
|
419
|
+
parser = argparse.ArgumentParser(
|
|
420
|
+
description="Run the scoped PrizmKit feature test gate.",
|
|
421
|
+
)
|
|
422
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
423
|
+
|
|
424
|
+
start = subparsers.add_parser("start", help="Create the test gate marker")
|
|
425
|
+
start.add_argument("--feature-slug", required=True)
|
|
426
|
+
|
|
427
|
+
check = subparsers.add_parser("check", help="Validate the latest scoped test report")
|
|
428
|
+
check.add_argument("--feature-slug", required=True)
|
|
429
|
+
check.add_argument("--artifact-dir", default="")
|
|
430
|
+
check.add_argument("--checkpoint-path", default="")
|
|
431
|
+
return parser
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def main(argv=None):
|
|
435
|
+
parser = build_parser()
|
|
436
|
+
args = parser.parse_args(argv)
|
|
437
|
+
if args.command == "start":
|
|
438
|
+
result = start_gate(args.feature_slug)
|
|
439
|
+
else:
|
|
440
|
+
result = check_gate(args.feature_slug, args.artifact_dir, args.checkpoint_path)
|
|
441
|
+
print(format_result(result))
|
|
442
|
+
return 0
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
if __name__ == "__main__":
|
|
446
|
+
sys.exit(main())
|
|
@@ -36,7 +36,7 @@ Infer what needs to be done from the feature context above and follow the standa
|
|
|
36
36
|
|
|
37
37
|
3. **Implement**: Run `/prizmkit-implement` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to execute the plan using TDD (write tests first, then implement).
|
|
38
38
|
|
|
39
|
-
4. **Test**: Run
|
|
39
|
+
4. **Test**: Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to generate and verify tests only for this feature's changed scope. Do not use bugfix/refactor artifact directories for this gate.
|
|
40
40
|
|
|
41
41
|
5. **Review**: Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to review and auto-fix changes against the spec (internal review-fix loop, max 3 rounds).
|
|
42
42
|
|
|
@@ -197,6 +197,20 @@ When tests fail, use convergence recovery — keep fixing while progress is bein
|
|
|
197
197
|
**Key rule**: If failures decrease (even by 1), plateau counter resets. Do NOT block commit for unresolved failures — document and defer to next session.
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
### Scoped Feature Test Gate — PrizmKit Test
|
|
201
|
+
|
|
202
|
+
Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
|
|
203
|
+
|
|
204
|
+
Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
|
|
205
|
+
|
|
206
|
+
Gate requirements:
|
|
207
|
+
- Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
|
|
208
|
+
- Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
|
|
209
|
+
- Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
|
|
210
|
+
- Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
|
|
211
|
+
- Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
|
|
212
|
+
- If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
|
|
213
|
+
|
|
200
214
|
{{IF_BROWSER_INTERACTION}}
|
|
201
215
|
### Phase 3.5: Browser Verification — MANDATORY
|
|
202
216
|
|
|
@@ -276,6 +276,20 @@ if (Select-String -Path ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md" -
|
|
|
276
276
|
```
|
|
277
277
|
If GATE:MISSING — send message to Dev (re-spawn if needed): "Write the '## Implementation Log' section to context-snapshot.md before I can proceed to review. Include: files changed/created, key decisions, deviations from plan, notable discoveries."
|
|
278
278
|
|
|
279
|
+
### Scoped Feature Test Gate — PrizmKit Test
|
|
280
|
+
|
|
281
|
+
Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
|
|
282
|
+
|
|
283
|
+
Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
|
|
284
|
+
|
|
285
|
+
Gate requirements:
|
|
286
|
+
- Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
|
|
287
|
+
- Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
|
|
288
|
+
- Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
|
|
289
|
+
- Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
|
|
290
|
+
- Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
|
|
291
|
+
- If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
|
|
292
|
+
|
|
279
293
|
### Phase 5: Review + Test — Reviewer Subagent
|
|
280
294
|
|
|
281
295
|
Spawn Reviewer subagent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
|
|
@@ -286,9 +300,10 @@ Prompt:
|
|
|
286
300
|
> "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
|
|
287
301
|
> 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
|
|
288
302
|
> 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
|
|
289
|
-
> 3.
|
|
290
|
-
> 4. Run
|
|
291
|
-
> 5.
|
|
303
|
+
> 3. Read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, then read the referenced `/prizmkit-test` report. Review generated/updated tests, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the report verdict.
|
|
304
|
+
> 4. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer → filter → Dev fix, max 3 rounds) and writes review-report.md.
|
|
305
|
+
> 5. Run the full test suite using `{{TEST_CMD}}`. When running: `& { {{TEST_CMD}} } 2>&1 | Tee-Object (Join-Path $env:TEMP "review-test-out.txt") | Select-Object -Last 20`, then use Select-String on the file for details — do NOT re-run the suite multiple times.
|
|
306
|
+
> 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
|
|
292
307
|
> Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
|
|
293
308
|
|
|
294
309
|
Wait for Reviewer to return.
|
|
@@ -337,6 +337,20 @@ Wait for Dev to return. **If Dev times out before all tasks are `[x]`**:
|
|
|
337
337
|
|
|
338
338
|
All tasks `[x]`, tests pass.
|
|
339
339
|
|
|
340
|
+
### Scoped Feature Test Gate — PrizmKit Test
|
|
341
|
+
|
|
342
|
+
Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
|
|
343
|
+
|
|
344
|
+
Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
|
|
345
|
+
|
|
346
|
+
Gate requirements:
|
|
347
|
+
- Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
|
|
348
|
+
- Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
|
|
349
|
+
- Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
|
|
350
|
+
- Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
|
|
351
|
+
- Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
|
|
352
|
+
- If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
|
|
353
|
+
|
|
340
354
|
### Phase 5: Review + Test — Reviewer Agent
|
|
341
355
|
|
|
342
356
|
Spawn Reviewer agent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
|
|
@@ -347,9 +361,10 @@ Prompt:
|
|
|
347
361
|
> "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
|
|
348
362
|
> 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
|
|
349
363
|
> 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
|
|
350
|
-
> 3.
|
|
351
|
-
> 4. Run
|
|
352
|
-
> 5.
|
|
364
|
+
> 3. Read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, then read the referenced `/prizmkit-test` report. Review generated/updated tests, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the report verdict.
|
|
365
|
+
> 4. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer → filter → Dev fix, max 3 rounds) and writes review-report.md.
|
|
366
|
+
> 5. Run the full test suite using `{{TEST_CMD}}`. When running tests: `& { {{TEST_CMD}} } 2>&1 | Tee-Object (Join-Path $env:TEMP "review-test-out.txt") | Select-Object -Last 20`, then Select-String `(Join-Path $env:TEMP "review-test-out.txt")` for details — do NOT re-run the suite multiple times.
|
|
367
|
+
> 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
|
|
353
368
|
> Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
|
|
354
369
|
|
|
355
370
|
Wait for Reviewer to return.
|
|
@@ -70,13 +70,13 @@ Invoke-PrizmPython {{PIPELINE_DIR}}\scripts\update-checkpoint.py `
|
|
|
70
70
|
--status completed
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
### TERMINAL STOP —
|
|
73
|
+
### TERMINAL STOP — Commit Completed
|
|
74
74
|
|
|
75
75
|
After all of the following are true, the session is **TERMINAL**:
|
|
76
76
|
|
|
77
77
|
- `/prizmkit-committer` has completed
|
|
78
78
|
- `git status --short` is clean (except ignored/local-only pipeline artifacts)
|
|
79
79
|
- `.prizmkit/specs/{{FEATURE_SLUG}}/completion-summary.json` is written
|
|
80
|
-
- the
|
|
80
|
+
- the `prizmkit-committer` checkpoint is marked `completed`
|
|
81
81
|
|
|
82
82
|
At that point, stop immediately. Do **not** process delayed teammate/reviewer findings, do **not** edit files, do **not** amend the commit, and do **not** start any new tool calls. Any findings that arrive after S09 is completed are follow-up work for a later session, not part of this feature commit.
|
|
@@ -63,13 +63,13 @@ Invoke-PrizmPython {{PIPELINE_DIR}}\scripts\update-checkpoint.py `
|
|
|
63
63
|
--status completed
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
### TERMINAL STOP —
|
|
66
|
+
### TERMINAL STOP — Commit Completed
|
|
67
67
|
|
|
68
68
|
After all of the following are true, the session is **TERMINAL**:
|
|
69
69
|
|
|
70
70
|
- `/prizmkit-committer` has completed
|
|
71
71
|
- `git status --short` is clean (except ignored/local-only pipeline artifacts)
|
|
72
72
|
- `.prizmkit/specs/{{FEATURE_SLUG}}/completion-summary.json` is written
|
|
73
|
-
- the
|
|
73
|
+
- the `prizmkit-committer` checkpoint is marked `completed`
|
|
74
74
|
|
|
75
75
|
At that point, stop immediately. Do **not** process delayed teammate/reviewer findings, do **not** edit files, do **not** amend the commit, and do **not** start any new tool calls. Any findings that arrive after S09 is completed are follow-up work for a later session, not part of this feature commit.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
### Scoped Feature Test Gate — PrizmKit Test
|
|
2
|
+
|
|
3
|
+
**Goal**: Generate and verify tests for this feature's changed scope before code review without embedding the full gate implementation in this prompt.
|
|
4
|
+
|
|
5
|
+
Create a start marker immediately before invoking the skill so the gate can reject stale reports from older sessions:
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
Invoke-PrizmPython {{PIPELINE_DIR}}\scripts\prizmkit-test-gate.py start `
|
|
9
|
+
--feature-slug "{{FEATURE_SLUG}}"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run `/prizmkit-test` with the feature artifact directory:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Rules:
|
|
19
|
+
- This gate is feature-pipeline only. Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories here.
|
|
20
|
+
- Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
|
|
21
|
+
- Record unrelated historical coverage gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
|
|
22
|
+
- If generated tests fail because implementation is wrong, fix implementation within this feature scope and rerun the affected tests.
|
|
23
|
+
- If failures are baseline or unrelated to this feature, document them in the report and continue only when no in-scope failure remains unexplained.
|
|
24
|
+
|
|
25
|
+
**Gate Check — Test Report**:
|
|
26
|
+
After `/prizmkit-test` returns, run the gate script to locate the newest current-run report, validate scope/freshness/boundary coverage, write `test-report-path.txt`, and mark the `prizmkit-test` checkpoint completed only on `GATE:PASS`:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
Invoke-PrizmPython {{PIPELINE_DIR}}\scripts\prizmkit-test-gate.py check `
|
|
30
|
+
--feature-slug "{{FEATURE_SLUG}}" `
|
|
31
|
+
--artifact-dir ".prizmkit/specs/{{FEATURE_SLUG}}/" `
|
|
32
|
+
--checkpoint-path "{{CHECKPOINT_PATH}}"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Gate outcomes:
|
|
36
|
+
- `GATE:PASS` → append the report path and a 3-5 bullet summary to `context-snapshot.md` under `## PrizmKit Test Gate`, then proceed to the next checkpoint step. The gate script already wrote `test-report-path.txt` and marked `prizmkit-test` completed.
|
|
37
|
+
- `GATE:FAIL` with `NEEDS_FIXES` → read the report's `In-Scope Failures`, fix the feature implementation or generated tests within this feature scope, rerun `/prizmkit-test`, and repeat the gate check. Do not proceed to code review while the verdict is not `PASS`.
|
|
38
|
+
- `GATE:FAIL` with `BLOCKED`, wrong mode, wrong artifact dir, unreadable report, stale scope, boundary failure, validator failure, or checkpoint update failure → write `.prizmkit/specs/{{FEATURE_SLUG}}/failure-log.md` with the scoped test failure and stop for recovery.
|
|
39
|
+
- `GATE:MISSING` → perform one bounded status check: inspect `/prizmkit-test` output and `.prizmkit/test/` for a report directory. If no current-run report exists, write `failure-log.md` and stop for recovery.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
### Review
|
|
1
|
+
### Review — Code Review
|
|
2
|
+
|
|
3
|
+
Precondition: the `prizmkit-test` checkpoint is completed and `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` points to the scoped test report.
|
|
4
|
+
|
|
5
|
+
Before invoking review, read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, read the referenced `/prizmkit-test` report, and confirm the summary in `context-snapshot.md` under `## PrizmKit Test Gate` matches it. The code-review skill must receive this context through `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` and the Reviewer must assess generated/updated test quality, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the scoped report verdict.
|
|
2
6
|
|
|
3
7
|
Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`.
|
|
4
8
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
### Review
|
|
1
|
+
### Review — Code Review
|
|
2
|
+
|
|
3
|
+
Precondition: the `prizmkit-test` checkpoint is completed and `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` points to the scoped test report.
|
|
4
|
+
|
|
5
|
+
Before invoking review, read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, read the referenced `/prizmkit-test` report, and confirm the summary in `context-snapshot.md` under `## PrizmKit Test Gate` matches it. The code-review skill must receive this context through `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` and the Reviewer must assess generated/updated test quality, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the scoped report verdict.
|
|
2
6
|
|
|
3
7
|
Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`.
|
|
4
8
|
|