prizmkit 1.1.85 → 1.1.87

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 (42) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +462 -4
  3. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  4. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -0
  5. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +18 -3
  6. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +18 -3
  7. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
  8. package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
  9. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +260 -0
  10. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -1
  11. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -1
  12. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +429 -0
  13. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +462 -4
  14. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +1 -1
  15. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +14 -0
  16. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +18 -3
  17. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +18 -3
  18. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +2 -2
  19. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +2 -2
  20. package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +186 -0
  21. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +5 -1
  22. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +5 -1
  23. package/bundled/skills/_metadata.json +1 -1
  24. package/bundled/skills/prizmkit-code-review/SKILL.md +7 -2
  25. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
  26. package/bundled/skills/prizmkit-test/SKILL.md +107 -13
  27. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
  28. package/bundled/skills/prizmkit-test/references/examples.md +82 -9
  29. package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +225 -0
  30. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +74 -14
  31. package/bundled/skills/prizmkit-test/references/test-report-template.md +74 -5
  32. package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +347 -0
  33. package/bundled/skills-windows/prizmkit-code-review/SKILL.md +7 -2
  34. package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
  35. package/bundled/skills-windows/prizmkit-test/SKILL.md +107 -13
  36. package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
  37. package/bundled/skills-windows/prizmkit-test/references/examples.md +82 -9
  38. package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +225 -0
  39. package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +74 -14
  40. package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +74 -5
  41. package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +347 -0
  42. package/package.json +1 -1
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate a prizmkit-test report's boundary coverage gate.
4
+
5
+ Usage:
6
+ python3 scripts/validate_boundary_report.py --report .prizmkit/test/.../test-report.md
7
+ python3 scripts/validate_boundary_report.py --report .prizmkit/test/.../test-report.md --openapi openapi.yaml
8
+
9
+ The script intentionally performs deterministic structural checks only. It cannot prove that every
10
+ business boundary is semantically correct, but it prevents reports from silently claiming completion
11
+ when interfaces are missing from the Boundary Matrix or remain happy-path-only / boundary-missing.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Dict, List, Set, Tuple
21
+
22
+ ALLOWED_FINAL_STATUSES = {
23
+ "boundary-covered",
24
+ "generated-needs-review",
25
+ "skipped-with-reason",
26
+ }
27
+ INCOMPLETE_MARKERS = {
28
+ "boundary-missing",
29
+ "happy-path-only",
30
+ "unresolved",
31
+ }
32
+ REQUIRED_SECTIONS = [
33
+ "## Boundary Matrix",
34
+ "## Boundary Completion Gate",
35
+ "## Final Status",
36
+ ]
37
+ REQUIRED_BOUNDARY_COLUMNS = [
38
+ "Module",
39
+ "Interface",
40
+ "Service Type",
41
+ "Happy Path",
42
+ "Request Validation",
43
+ "Auth / Permission / Ownership",
44
+ "Domain Invariants",
45
+ "Collection / Pagination",
46
+ "Date / Time",
47
+ "State Transitions",
48
+ "Dependency Failure",
49
+ "Response Contract",
50
+ "N/A Reasons",
51
+ "Final Status",
52
+ ]
53
+ BOUNDARY_CATEGORY_COLUMNS = [
54
+ "Happy Path",
55
+ "Request Validation",
56
+ "Auth / Permission / Ownership",
57
+ "Domain Invariants",
58
+ "Collection / Pagination",
59
+ "Date / Time",
60
+ "State Transitions",
61
+ "Dependency Failure",
62
+ "Response Contract",
63
+ ]
64
+
65
+
66
+ def read_text(path: Path) -> str:
67
+ try:
68
+ return path.read_text(encoding="utf-8")
69
+ except FileNotFoundError:
70
+ raise SystemExit(f"FAIL: file not found: {path}")
71
+
72
+
73
+ HTTP_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE"}
74
+
75
+
76
+ def extract_openapi_paths(openapi_text: str) -> List[str]:
77
+ """Extract OpenAPI path keys without requiring PyYAML.
78
+
79
+ Supports common YAML forms inside the `paths:` mapping:
80
+ - /users:
81
+ - "/users/{id}":
82
+ - '/users/{id}':
83
+ """
84
+ return [path for _method, path in extract_openapi_operations(openapi_text)]
85
+
86
+
87
+ def extract_openapi_operations(openapi_text: str) -> List[Tuple[str, str]]:
88
+ """Extract OpenAPI method+path operations without requiring PyYAML."""
89
+ operations: List[Tuple[str, str]] = []
90
+ in_paths = False
91
+ paths_indent = 0
92
+ current_path = ""
93
+ current_path_indent = 0
94
+ path_key_re = re.compile(
95
+ r"^\s*(?:(['\"])(/.*?)\1|(/[^:\s]+))\s*:\s*(?:#.*)?$"
96
+ )
97
+ method_re = re.compile(r"^\s*([A-Za-z]+)\s*:\s*(?:#.*)?$")
98
+
99
+ for raw_line in openapi_text.splitlines():
100
+ stripped = raw_line.strip()
101
+ if not stripped or stripped.startswith("#"):
102
+ continue
103
+
104
+ indent = len(raw_line) - len(raw_line.lstrip())
105
+ if not in_paths:
106
+ if re.match(r"^\s*paths\s*:\s*(?:#.*)?$", raw_line):
107
+ in_paths = True
108
+ paths_indent = indent
109
+ continue
110
+
111
+ if indent <= paths_indent:
112
+ break
113
+
114
+ path_match = path_key_re.match(raw_line)
115
+ if path_match:
116
+ current_path = path_match.group(2) or path_match.group(3) or ""
117
+ current_path_indent = indent
118
+ continue
119
+
120
+ method_match = method_re.match(raw_line)
121
+ if current_path and method_match and indent > current_path_indent:
122
+ method = method_match.group(1).upper()
123
+ if method in HTTP_METHODS:
124
+ operations.append((method, current_path))
125
+
126
+ return operations
127
+
128
+
129
+ def extract_section(text: str, heading: str) -> str:
130
+ start = text.find(heading)
131
+ if start == -1:
132
+ return ""
133
+ rest = text[start + len(heading) :]
134
+ match = re.search(r"\n## ", rest)
135
+ if not match:
136
+ return rest
137
+ return rest[: match.start()]
138
+
139
+
140
+ def split_markdown_row(line: str) -> List[str]:
141
+ return [cell.strip() for cell in line.strip().strip("|").split("|")]
142
+
143
+
144
+ def parse_boundary_matrix(text: str) -> Tuple[List[str], List[Dict[str, str]]]:
145
+ section = extract_section(text, "## Boundary Matrix")
146
+ if not section:
147
+ return [], []
148
+
149
+ lines = [line.strip() for line in section.splitlines() if line.strip().startswith("|")]
150
+ header_index = -1
151
+ for idx, line in enumerate(lines):
152
+ cells = split_markdown_row(line)
153
+ if "Interface" in cells and "Final Status" in cells:
154
+ header_index = idx
155
+ break
156
+ if header_index == -1:
157
+ return [], []
158
+
159
+ headers = split_markdown_row(lines[header_index])
160
+ rows: List[Dict[str, str]] = []
161
+ for line in lines[header_index + 1 :]:
162
+ cells = split_markdown_row(line)
163
+ if all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells):
164
+ continue
165
+ if len(cells) < len(headers):
166
+ cells.extend([""] * (len(headers) - len(cells)))
167
+ row = {header: cells[i] if i < len(cells) else "" for i, header in enumerate(headers)}
168
+ # Ignore example placeholder rows that were accidentally left in templates.
169
+ if row.get("Interface", "").strip(".` ") in {"...", ""}:
170
+ continue
171
+ rows.append(row)
172
+ return headers, rows
173
+
174
+
175
+ def has_inline_reason(value: str) -> bool:
176
+ normalized = value.strip().lower()
177
+ if not normalized:
178
+ return False
179
+ if "—" in value or " - " in value or ":" in value:
180
+ return True
181
+ # Accept phrases longer than a bare status marker.
182
+ bare = {"n/a", "na", "skipped", "skip"}
183
+ return normalized not in bare and (normalized.startswith("n/a ") or normalized.startswith("skipped "))
184
+
185
+
186
+ def extract_matrix_operations(interface_values: List[str]) -> Tuple[Set[Tuple[str, str]], Set[str]]:
187
+ """Extract method+path and path-only tokens from Boundary Matrix Interface cells."""
188
+ operations: Set[Tuple[str, str]] = set()
189
+ path_only: Set[str] = set()
190
+ token_re = re.compile(
191
+ r"(?:(\b(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b)\s+)?(/[^\s,`|)]+)",
192
+ flags=re.IGNORECASE,
193
+ )
194
+ for value in interface_values:
195
+ for match in token_re.finditer(value):
196
+ method = match.group(1).upper() if match.group(1) else ""
197
+ path = normalize_path(match.group(2))
198
+ if not path:
199
+ continue
200
+ if method:
201
+ operations.add((method, path))
202
+ else:
203
+ path_only.add(path)
204
+ return operations, path_only
205
+
206
+
207
+ def normalize_path(path: str) -> str:
208
+ normalized = path.strip().strip("'\"`")
209
+ if len(normalized) > 1:
210
+ normalized = normalized.rstrip("/")
211
+ return normalized
212
+
213
+
214
+ def validate_report(report_text: str, openapi_paths: List[str] | None,
215
+ openapi_operations: List[Tuple[str, str]] | None = None) -> List[str]:
216
+ errors: List[str] = []
217
+
218
+ for section in REQUIRED_SECTIONS:
219
+ if section not in report_text:
220
+ errors.append(f"missing required section: {section}")
221
+
222
+ headers, rows = parse_boundary_matrix(report_text)
223
+ if not rows:
224
+ errors.append("Boundary Matrix table is missing or empty")
225
+ return errors
226
+
227
+ for column in REQUIRED_BOUNDARY_COLUMNS:
228
+ if column not in headers:
229
+ errors.append(f"Boundary Matrix missing required column: {column}")
230
+
231
+ final_statuses = []
232
+ interface_values = []
233
+ for idx, row in enumerate(rows, start=1):
234
+ interface = row.get("Interface", "").strip()
235
+ interface_values.append(interface)
236
+
237
+ final_status = row.get("Final Status", "").strip().lower()
238
+ final_statuses.append(final_status)
239
+ if not final_status:
240
+ errors.append(f"row {idx} ({interface}) missing Final Status")
241
+ elif final_status not in ALLOWED_FINAL_STATUSES:
242
+ errors.append(
243
+ f"row {idx} ({interface}) has invalid/incomplete Final Status: {row.get('Final Status', '')}"
244
+ )
245
+
246
+ for column in BOUNDARY_CATEGORY_COLUMNS:
247
+ if column in headers and not row.get(column, "").strip():
248
+ errors.append(f"row {idx} ({interface}) missing boundary category value: {column}")
249
+
250
+ for header, value in row.items():
251
+ normalized = value.strip().lower()
252
+ if any(marker in normalized for marker in INCOMPLETE_MARKERS):
253
+ errors.append(f"row {idx} ({interface}) has incomplete marker in {header}: {value}")
254
+
255
+ n_a_reasons = row.get("N/A Reasons", "").strip()
256
+ for header, value in row.items():
257
+ if header in {"Module", "Interface", "Service Type", "Final Status", "N/A Reasons"}:
258
+ continue
259
+ normalized = value.strip().lower()
260
+ if normalized in {"n/a", "na", "skipped", "skip"} and not n_a_reasons:
261
+ errors.append(f"row {idx} ({interface}) marks {header} as {value} without N/A reason")
262
+ if ("skipped" in normalized or normalized.startswith("n/a")) and not (n_a_reasons or has_inline_reason(value)):
263
+ errors.append(f"row {idx} ({interface}) marks {header} as {value} without concrete reason")
264
+
265
+ if openapi_operations is not None:
266
+ matrix_operations, matrix_path_only = extract_matrix_operations(interface_values)
267
+ for method, path in openapi_operations:
268
+ normalized_path = normalize_path(path)
269
+ if (method, normalized_path) not in matrix_operations:
270
+ if normalized_path in matrix_path_only:
271
+ errors.append(
272
+ "OpenAPI operation missing from Boundary Matrix: "
273
+ f"{method} {path} (path-only row is ambiguous)"
274
+ )
275
+ else:
276
+ errors.append(
277
+ f"OpenAPI operation missing from Boundary Matrix: {method} {path}"
278
+ )
279
+ elif openapi_paths is not None:
280
+ _matrix_operations, matrix_path_only = extract_matrix_operations(interface_values)
281
+ missing_paths = [path for path in openapi_paths
282
+ if normalize_path(path) not in matrix_path_only]
283
+ for path in missing_paths:
284
+ errors.append(f"OpenAPI path missing from Boundary Matrix: {path}")
285
+
286
+ gate_section = extract_section(report_text, "## Boundary Completion Gate")
287
+ if gate_section:
288
+ for label in ["Interfaces total", "Boundary-covered", "Boundary-missing", "Happy-path-only", "Completion gate passed"]:
289
+ if label not in gate_section:
290
+ errors.append(f"Boundary Completion Gate missing metric: {label}")
291
+ for label in ["Boundary-missing", "Happy-path-only"]:
292
+ match = re.search(rf"{re.escape(label)}:\s*(\d+)", gate_section)
293
+ if match and int(match.group(1)) > 0:
294
+ errors.append(f"Boundary Completion Gate reports {label}: {match.group(1)}")
295
+ if re.search(r"Completion gate passed:\s*no", gate_section, flags=re.IGNORECASE):
296
+ errors.append("Boundary Completion Gate reports failure")
297
+
298
+ final_section = extract_section(report_text, "## Final Status")
299
+ if final_section:
300
+ if re.search(r"Ready for commit:\s*yes", final_section, flags=re.IGNORECASE):
301
+ if any(status != "boundary-covered" for status in final_statuses):
302
+ errors.append("Final Status says ready for commit but not every interface is boundary-covered")
303
+ review_match = re.search(r"Needs human review:\s*(.+)", final_section, flags=re.IGNORECASE)
304
+ if review_match and review_match.group(1).strip().lower() not in {"none", "no", "n/a", ""}:
305
+ errors.append("Final Status says ready for commit while human review is still needed")
306
+
307
+ return errors
308
+
309
+
310
+ def main() -> int:
311
+ parser = argparse.ArgumentParser(description="Validate prizmkit-test boundary coverage report")
312
+ parser.add_argument("--report", required=True, help="Path to test-report.md")
313
+ parser.add_argument("--openapi", help="Optional OpenAPI file whose paths must appear in Boundary Matrix")
314
+ args = parser.parse_args()
315
+
316
+ report_path = Path(args.report)
317
+ report_text = read_text(report_path)
318
+
319
+ openapi_paths = None
320
+ openapi_operations = None
321
+ if args.openapi:
322
+ openapi_path = Path(args.openapi)
323
+ if openapi_path.exists():
324
+ openapi_text = read_text(openapi_path)
325
+ openapi_operations = extract_openapi_operations(openapi_text)
326
+ openapi_paths = [path for _method, path in openapi_operations]
327
+ else:
328
+ print(f"WARN: OpenAPI file not found, skipping path coverage check: {openapi_path}")
329
+
330
+ errors = validate_report(report_text, openapi_paths, openapi_operations)
331
+ if errors:
332
+ print("Boundary report validation: FAIL")
333
+ for error in errors:
334
+ print(f"- {error}")
335
+ return 1
336
+
337
+ print("Boundary report validation: PASS")
338
+ print(f"Report: {report_path}")
339
+ if openapi_operations is not None:
340
+ print(f"OpenAPI operations checked: {len(openapi_operations)}")
341
+ elif openapi_paths is not None:
342
+ print(f"OpenAPI paths checked: {len(openapi_paths)}")
343
+ return 0
344
+
345
+
346
+ if __name__ == "__main__":
347
+ sys.exit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.85",
3
+ "version": "1.1.87",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {