prizmkit 1.1.87 → 1.1.89

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.87",
3
- "bundledAt": "2026-07-01T13:22:59.563Z",
4
- "bundledFrom": "3ecfd63"
2
+ "frameworkVersion": "1.1.89",
3
+ "bundledAt": "2026-07-01T15:42:06.738Z",
4
+ "bundledFrom": "4e9108f"
5
5
  }
@@ -483,19 +483,6 @@ run_one() {
483
483
  local bugs_count
484
484
  bugs_count=$(echo "$init_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('bugs_count', 0))" 2>/dev/null || echo "0")
485
485
  log_success "Bugfix pipeline initialized with $bugs_count bugs"
486
-
487
- # Ensure state directory is gitignored
488
- local _gitignore_path
489
- _gitignore_path="$PROJECT_ROOT/.gitignore"
490
- local _state_rel
491
- _state_rel=$(python3 -c "import os; print(os.path.relpath('$STATE_DIR', '$PROJECT_ROOT'))" 2>/dev/null || echo ".prizmkit/state/bugfix")
492
- if [[ -f "$_gitignore_path" ]]; then
493
- if ! grep -qF "$_state_rel" "$_gitignore_path" 2>/dev/null; then
494
- printf '\n# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" >> "$_gitignore_path"
495
- fi
496
- else
497
- printf '# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" > "$_gitignore_path"
498
- fi
499
486
  fi
500
487
 
501
488
  # Verify bug exists
@@ -843,19 +830,6 @@ main() {
843
830
  local bugs_count
844
831
  bugs_count=$(echo "$init_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('bugs_count', 0))" 2>/dev/null || echo "0")
845
832
  log_success "Bugfix pipeline initialized with $bugs_count bugs"
846
-
847
- # Ensure state directory is gitignored
848
- local _gitignore_path
849
- _gitignore_path="$PROJECT_ROOT/.gitignore"
850
- local _state_rel
851
- _state_rel=$(python3 -c "import os; print(os.path.relpath('$STATE_DIR', '$PROJECT_ROOT'))" 2>/dev/null || echo ".prizmkit/state/bugfix")
852
- if [[ -f "$_gitignore_path" ]]; then
853
- if ! grep -qF "$_state_rel" "$_gitignore_path" 2>/dev/null; then
854
- printf '\n# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" >> "$_gitignore_path"
855
- fi
856
- else
857
- printf '# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" > "$_gitignore_path"
858
- fi
859
833
  else
860
834
  log_info "Resuming existing bugfix pipeline..."
861
835
  fi
@@ -1084,21 +1084,6 @@ main() {
1084
1084
  local features_count
1085
1085
  features_count=$(echo "$init_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('features_count', 0))" 2>/dev/null || echo "0")
1086
1086
  log_success "Pipeline initialized with $features_count features"
1087
-
1088
- # Ensure state directory is gitignored (prevents branch-switch state loss)
1089
- local _gitignore_path
1090
- _gitignore_path="$PROJECT_ROOT/.gitignore"
1091
- local _state_rel
1092
- _state_rel=$(python3 -c "import os; print(os.path.relpath('$STATE_DIR', '$PROJECT_ROOT'))" 2>/dev/null || echo ".prizmkit/state/features")
1093
- if [[ -f "$_gitignore_path" ]]; then
1094
- if ! grep -qF "$_state_rel" "$_gitignore_path" 2>/dev/null; then
1095
- printf '\n# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" >> "$_gitignore_path"
1096
- log_info "Added $_state_rel/ to .gitignore"
1097
- fi
1098
- else
1099
- printf '# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" > "$_gitignore_path"
1100
- log_info "Created .gitignore with $_state_rel/"
1101
- fi
1102
1087
  else
1103
1088
  log_info "Resuming existing pipeline..."
1104
1089
  fi
@@ -510,19 +510,6 @@ run_one() {
510
510
  local refactors_count
511
511
  refactors_count=$(echo "$init_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refactors_count', 0))" 2>/dev/null || echo "0")
512
512
  log_success "Refactor pipeline initialized with $refactors_count refactors"
513
-
514
- # Ensure state directory is gitignored
515
- local _gitignore_path
516
- _gitignore_path="$PROJECT_ROOT/.gitignore"
517
- local _state_rel
518
- _state_rel=$(python3 -c "import os; print(os.path.relpath('$STATE_DIR', '$PROJECT_ROOT'))" 2>/dev/null || echo ".prizmkit/state/refactor")
519
- if [[ -f "$_gitignore_path" ]]; then
520
- if ! grep -qF "$_state_rel" "$_gitignore_path" 2>/dev/null; then
521
- printf '\n# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" >> "$_gitignore_path"
522
- fi
523
- else
524
- printf '# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" > "$_gitignore_path"
525
- fi
526
513
  fi
527
514
 
528
515
  # Verify refactor exists
@@ -873,19 +860,6 @@ main() {
873
860
  local refactors_count
874
861
  refactors_count=$(echo "$init_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refactors_count', 0))" 2>/dev/null || echo "0")
875
862
  log_success "Refactor pipeline initialized with $refactors_count refactors"
876
-
877
- # Ensure state directory is gitignored
878
- local _gitignore_path
879
- _gitignore_path="$PROJECT_ROOT/.gitignore"
880
- local _state_rel
881
- _state_rel=$(python3 -c "import os; print(os.path.relpath('$STATE_DIR', '$PROJECT_ROOT'))" 2>/dev/null || echo ".prizmkit/state/refactor")
882
- if [[ -f "$_gitignore_path" ]]; then
883
- if ! grep -qF "$_state_rel" "$_gitignore_path" 2>/dev/null; then
884
- printf '\n# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" >> "$_gitignore_path"
885
- fi
886
- else
887
- printf '# Pipeline runtime state (auto-added by dev-pipeline)\n%s/\n' "$_state_rel" > "$_gitignore_path"
888
- fi
889
863
  else
890
864
  log_info "Resuming existing refactor pipeline..."
891
865
  fi
@@ -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())