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,13 +1,12 @@
1
1
  ### Scoped Feature Test Gate — PrizmKit Test
2
2
 
3
- **Goal**: Generate and verify tests for this feature's changed scope before code review.
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
4
 
5
5
  Create a start marker immediately before invoking the skill so the gate can reject stale reports from older sessions:
6
6
 
7
7
  ```bash
8
- mkdir -p .prizmkit/specs/{{FEATURE_SLUG}}/
9
- test_gate_marker=".prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started"
10
- touch "$test_gate_marker"
8
+ python3 "$PIPELINE_DIR/scripts/prizmkit-test-gate.py" start \
9
+ --feature-slug "{{FEATURE_SLUG}}"
11
10
  ```
12
11
 
13
12
  Run `/prizmkit-test` with the feature artifact directory:
@@ -24,237 +23,17 @@ Rules:
24
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.
25
24
 
26
25
  **Gate Check — Test Report**:
27
- After `/prizmkit-test` returns, locate the newest test report created after the start marker and validate that it belongs to this feature scope, has a PASS verdict, and has passed the boundary completion/validation gates before writing the checkpoint pointer:
28
- ```bash
29
- feature_artifact_dir=".prizmkit/specs/{{FEATURE_SLUG}}/"
30
- latest_test_report=$(python3 - "$test_gate_marker" <<'PY'
31
- import os
32
- import sys
33
-
34
- marker = sys.argv[1]
35
- try:
36
- marker_mtime = os.path.getmtime(marker)
37
- except OSError:
38
- sys.exit(0)
39
-
40
- matches = []
41
- for root, _dirs, files in os.walk(".prizmkit/test"):
42
- if "test-report.md" not in files:
43
- continue
44
- path = os.path.join(root, "test-report.md")
45
- try:
46
- report_mtime = os.path.getmtime(path)
47
- except OSError:
48
- continue
49
- if report_mtime >= marker_mtime:
50
- matches.append((report_mtime, path))
51
-
52
- if matches:
53
- print(sorted(matches)[-1][1])
54
- PY
55
- )
56
- if [ -n "$latest_test_report" ]; then
57
- report_mode=$(awk '
58
- /^## Scope[[:space:]]*$/ { in_scope=1; next }
59
- /^## / && in_scope { exit }
60
- in_scope && /^- Mode:/ { sub(/^- Mode:[[:space:]]*/, ""); print; exit }
61
- ' "$latest_test_report")
62
- report_artifact_dir=$(awk '
63
- /^## Scope[[:space:]]*$/ { in_scope=1; next }
64
- /^## / && in_scope { exit }
65
- in_scope && /^- Artifact Dir:/ { sub(/^- Artifact Dir:[[:space:]]*/, ""); print; exit }
66
- ' "$latest_test_report")
67
- report_verdict=$(awk '
68
- /^## Verdict[[:space:]]*$/ { in_verdict=1; next }
69
- in_verdict && NF { gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit }
70
- ' "$latest_test_report")
71
- boundary_gate_passed=$(awk '
72
- /^## Boundary Completion Gate[[:space:]]*$/ { in_gate=1; next }
73
- /^## / && in_gate { exit }
74
- in_gate && /^-?[[:space:]]*Completion gate passed:[[:space:]]*yes[[:space:]]*$/ { print "yes"; exit }
75
- ' "$latest_test_report")
76
- boundary_missing=$(awk '
77
- /^## Boundary Completion Gate[[:space:]]*$/ { in_gate=1; next }
78
- /^## / && in_gate { exit }
79
- in_gate && /^-?[[:space:]]*Boundary-missing:/ { sub(/^.*:[[:space:]]*/, ""); print; exit }
80
- ' "$latest_test_report")
81
- happy_path_only=$(awk '
82
- /^## Boundary Completion Gate[[:space:]]*$/ { in_gate=1; next }
83
- /^## / && in_gate { exit }
84
- in_gate && /^-?[[:space:]]*Happy-path-only:/ { sub(/^.*:[[:space:]]*/, ""); print; exit }
85
- ' "$latest_test_report")
86
- boundary_columns_ok=$(awk '
87
- /^## Boundary Matrix[[:space:]]*$/ { in_matrix=1; next }
88
- /^## / && in_matrix { exit }
89
- in_matrix && /^\|/ {
90
- if ($0 ~ /\|[[:space:]]*Module[[:space:]]*\|/ &&
91
- $0 ~ /\|[[:space:]]*Interface[[:space:]]*\|/ &&
92
- $0 ~ /\|[[:space:]]*Service Type[[:space:]]*\|/ &&
93
- $0 ~ /\|[[:space:]]*Happy Path[[:space:]]*\|/ &&
94
- $0 ~ /\|[[:space:]]*Request Validation[[:space:]]*\|/ &&
95
- $0 ~ /\|[[:space:]]*Auth \/ Permission \/ Ownership[[:space:]]*\|/ &&
96
- $0 ~ /\|[[:space:]]*Domain Invariants[[:space:]]*\|/ &&
97
- $0 ~ /\|[[:space:]]*Collection \/ Pagination[[:space:]]*\|/ &&
98
- $0 ~ /\|[[:space:]]*Date \/ Time[[:space:]]*\|/ &&
99
- $0 ~ /\|[[:space:]]*State Transitions[[:space:]]*\|/ &&
100
- $0 ~ /\|[[:space:]]*Dependency Failure[[:space:]]*\|/ &&
101
- $0 ~ /\|[[:space:]]*Response Contract[[:space:]]*\|/ &&
102
- $0 ~ /\|[[:space:]]*N\/A Reasons[[:space:]]*\|/ &&
103
- $0 ~ /\|[[:space:]]*Final Status[[:space:]]*\|/) { print "yes"; exit }
104
- }
105
- ' "$latest_test_report")
106
- boundary_incomplete=$(awk '
107
- /^## Boundary Matrix[[:space:]]*$/ { in_matrix=1; next }
108
- /^## / && in_matrix { exit }
109
- in_matrix && /boundary-missing|happy-path-only|unresolved/ { print "yes"; exit }
110
- ' "$latest_test_report")
111
- boundary_validation_result=$(awk '
112
- /^## Boundary Validation[[:space:]]*$/ { in_validation=1; next }
113
- /^## / && in_validation { exit }
114
- in_validation && /^-?[[:space:]]*Validator result:/ { sub(/^.*:[[:space:]]*/, ""); print; exit }
115
- ' "$latest_test_report" | tr '[:upper:]' '[:lower:]')
116
- openapi_file=""
117
- for openapi_candidate in \
118
- openapi.yaml openapi.yml swagger.yaml swagger.yml \
119
- docs/openapi.yaml docs/openapi.yml docs/swagger.yaml docs/swagger.yml \
120
- api/openapi.yaml api/openapi.yml api/swagger.yaml api/swagger.yml
121
- do
122
- if [ -f "$openapi_candidate" ]; then
123
- openapi_file="$openapi_candidate"
124
- break
125
- fi
126
- done
127
- boundary_validator_passed=0
128
- for boundary_validator in \
129
- .claude/command-assets/prizmkit-test/scripts/validate_boundary_report.py \
130
- .codebuddy/skills/prizmkit-test/scripts/validate_boundary_report.py \
131
- .agents/skills/prizmkit-test/scripts/validate_boundary_report.py
132
- do
133
- if [ -f "$boundary_validator" ]; then
134
- boundary_validator_args=(--report "$latest_test_report")
135
- if [ -n "$openapi_file" ]; then
136
- boundary_validator_args+=(--openapi "$openapi_file")
137
- fi
138
- if python3 "$boundary_validator" "${boundary_validator_args[@]}" >/dev/null 2>&1; then
139
- boundary_validator_passed=1
140
- break
141
- fi
142
- fi
143
- done
144
-
145
- scope_fresh_ok=$(python3 - "$latest_test_report" "$test_gate_marker" <<'PY'
146
- import os
147
- import sys
148
-
149
- report_path, marker_path = sys.argv[1], sys.argv[2]
150
- try:
151
- report_mtime = os.path.getmtime(report_path)
152
- marker_mtime = os.path.getmtime(marker_path)
153
- except OSError:
154
- print("0")
155
- sys.exit(0)
156
- if report_mtime < marker_mtime:
157
- print("0")
158
- sys.exit(0)
159
- try:
160
- with open(report_path, "r", encoding="utf-8") as report_file:
161
- report_text = report_file.read()
162
- except OSError:
163
- print("0")
164
- sys.exit(0)
165
-
166
- def scope_list(name):
167
- in_scope = False
168
- in_list = False
169
- marker = f"- {name}:"
170
- values = []
171
- for raw_line in report_text.splitlines():
172
- line = raw_line.strip()
173
- if line == "## Scope":
174
- in_scope = True
175
- continue
176
- if in_scope and line.startswith("## "):
177
- break
178
- if not in_scope:
179
- continue
180
- if line == marker:
181
- in_list = True
182
- continue
183
- if in_list:
184
- if raw_line.startswith(" -") or raw_line.startswith("\t-"):
185
- value = line[2:].strip().strip("`")
186
- if value and value not in {"{...}", "..."}:
187
- values.append(value)
188
- continue
189
- if line.startswith("- "):
190
- break
191
- return values
192
-
193
- source_files = scope_list("In-Scope Source Files")
194
- test_files = scope_list("In-Scope Test Files")
195
- if not source_files or not test_files:
196
- print("0")
197
- sys.exit(0)
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`:
198
27
 
199
- project_root = os.path.realpath(os.getcwd())
200
- ignored = {"none", "n/a", "not applicable", "(none)", "{...}", "..."}
201
- for listed_file in source_files + test_files:
202
- normalized = listed_file.strip().strip("`")
203
- if not normalized or normalized.lower() in ignored:
204
- print("0")
205
- sys.exit(0)
206
- candidate = normalized if os.path.isabs(normalized) else os.path.join(os.getcwd(), normalized)
207
- candidate_real = os.path.realpath(candidate)
208
- try:
209
- if os.path.commonpath([project_root, candidate_real]) != project_root:
210
- print("0")
211
- sys.exit(0)
212
- except ValueError:
213
- print("0")
214
- sys.exit(0)
215
- if not os.path.isfile(candidate_real):
216
- print("0")
217
- sys.exit(0)
218
- if os.path.getmtime(candidate_real) > report_mtime:
219
- print("0")
220
- sys.exit(0)
221
- print("1")
222
- PY
223
- )
224
-
225
- artifact_ok=0
226
- case "$report_artifact_dir" in
227
- "$feature_artifact_dir"|"${feature_artifact_dir%/}") artifact_ok=1 ;;
228
- esac
229
-
230
- boundary_ok=0
231
- if [ "$boundary_columns_ok" = "yes" ] && [ -z "$boundary_incomplete" ] && [ "$boundary_gate_passed" = "yes" ] && [ "$boundary_missing" = "0" ] && [ "$happy_path_only" = "0" ] && [ "$boundary_validator_passed" = "1" ] && [ "$scope_fresh_ok" = "1" ]; then
232
- case "$boundary_validation_result" in
233
- passed) boundary_ok=1 ;;
234
- esac
235
- fi
236
-
237
- if [ "$report_mode" = "this-change" ] && [ "$artifact_ok" = "1" ] && [ "$report_verdict" = "PASS" ] && [ "$boundary_ok" = "1" ]; then
238
- printf "%s\n" "$latest_test_report" > .prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt
239
- echo "GATE:PASS $latest_test_report"
240
- else
241
- echo "GATE:FAIL verdict=$report_verdict mode=$report_mode artifact_dir=$report_artifact_dir boundary_gate=$boundary_gate_passed boundary_missing=$boundary_missing happy_path_only=$happy_path_only boundary_validation=$boundary_validation_result validator_passed=$boundary_validator_passed scope_fresh=$scope_fresh_ok openapi=$openapi_file report=$latest_test_report"
242
- fi
243
- else
244
- echo "GATE:MISSING"
245
- fi
28
+ ```bash
29
+ python3 "$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}}"
246
33
  ```
247
34
 
248
35
  Gate outcomes:
249
- - `GATE:PASS` → append the report path and a 3-5 bullet summary to `context-snapshot.md` under `## PrizmKit Test Gate`, then update the checkpoint.
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.
250
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`.
251
- - `GATE:FAIL` with `BLOCKED`, wrong mode, wrong artifact dir, or unreadable report → write `.prizmkit/specs/{{FEATURE_SLUG}}/failure-log.md` with the scoped test failure and stop for recovery.
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.
252
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.
253
-
254
- Only after `GATE:PASS`, run the update script to set step `prizmkit-test` to `"completed"`:
255
- ```bash
256
- python3 $PIPELINE_DIR/scripts/update-checkpoint.py \
257
- --checkpoint-path {{CHECKPOINT_PATH}} \
258
- --step prizmkit-test \
259
- --status completed
260
- ```
@@ -24,4 +24,5 @@ def _load_hyphenated_module(module_name, filename):
24
24
  # Register hyphenated script files so tests can import them with underscores.
25
25
  _load_hyphenated_module("generate_bootstrap_prompt", "generate-bootstrap-prompt.py")
26
26
  _load_hyphenated_module("generate_bugfix_prompt", "generate-bugfix-prompt.py")
27
+ _load_hyphenated_module("prizmkit_test_gate", "prizmkit-test-gate.py")
27
28
  _load_hyphenated_module("update_feature_status", "update-feature-status.py")
@@ -315,9 +315,14 @@ class TestScopedFeatureTestGate:
315
315
  assert names.index("phase-implement") < names.index("phase-prizmkit-test") < names.index("phase-review")
316
316
 
317
317
  rendered = "\n".join(content for _, content in sections)
318
+ assert "prizmkit-test-gate.py" in rendered
319
+ assert "prizmkit-test-gate.py\" start" in rendered
320
+ assert "prizmkit-test-gate.py\" check" in rendered
318
321
  assert "/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/" in rendered
319
322
  assert ".prizmkit/bugfix/" in rendered # explicitly forbidden in the gate wording
320
323
  assert ".prizmkit/refactor/" in rendered # explicitly forbidden in the gate wording
324
+ assert "latest_test_report=$(python3" not in rendered
325
+ assert "boundary_columns_ok=$(awk" not in rendered
321
326
 
322
327
  def test_lite_sections_include_prizmkit_test_before_commit(self):
323
328
  sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
@@ -0,0 +1,107 @@
1
+ """Tests for prizmkit-test-gate.py."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from prizmkit_test_gate import check_gate, start_gate
9
+
10
+
11
+ def scoped_report_text():
12
+ return "\n".join([
13
+ "# Test Report",
14
+ "",
15
+ "## Scope",
16
+ "- Mode: this-change",
17
+ "- Artifact Dir: .prizmkit/specs/123-scope-test/",
18
+ "- In-Scope Source Files:",
19
+ " - src/feature.js",
20
+ "- In-Scope Test Files:",
21
+ " - tests/feature.test.js",
22
+ "",
23
+ "## Boundary Matrix",
24
+ "| Module | Interface | Service Type | Happy Path | Request Validation | Auth / Permission / Ownership | Domain Invariants | Collection / Pagination | Date / Time | State Transitions | Dependency Failure | Response Contract | N/A Reasons | Final Status |",
25
+ "|--------|-----------|--------------|------------|--------------------|-------------------------------|-------------------|-------------------------|-------------|-------------------|-------------------|-------------------|-------------|--------------|",
26
+ "| feature | POST /feature | API | covered | covered | covered | covered | covered | covered | covered | covered | covered | none | boundary-covered |",
27
+ "",
28
+ "## Boundary Completion Gate",
29
+ "- Interfaces total: 1",
30
+ "- Boundary-covered: 1",
31
+ "- Boundary-missing: 0",
32
+ "- Happy-path-only: 0",
33
+ "- Completion gate passed: yes",
34
+ "",
35
+ "## Boundary Validation",
36
+ "- Validator command: `python3 validate_boundary_report.py`",
37
+ "- Validator result: passed",
38
+ "- Validator output: Boundary report validation: PASS",
39
+ "",
40
+ "## Verdict",
41
+ "PASS",
42
+ "",
43
+ ])
44
+
45
+
46
+ def prepare_project(tmp_path):
47
+ (tmp_path / "src").mkdir()
48
+ (tmp_path / "tests").mkdir()
49
+ (tmp_path / "src" / "feature.js").write_text("export const feature = true;\n", encoding="utf-8")
50
+ (tmp_path / "tests" / "feature.test.js").write_text("test('feature', () => {});\n", encoding="utf-8")
51
+
52
+ validator = tmp_path / ".claude" / "command-assets" / "prizmkit-test" / "scripts" / "validate_boundary_report.py"
53
+ validator.parent.mkdir(parents=True, exist_ok=True)
54
+ validator.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
55
+
56
+ checkpoint = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "workflow-checkpoint.json"
57
+ checkpoint.parent.mkdir(parents=True, exist_ok=True)
58
+ checkpoint.write_text(
59
+ json.dumps({"steps": [{"id": "S01", "skill": "prizmkit-test", "status": "pending"}]}),
60
+ encoding="utf-8",
61
+ )
62
+ return checkpoint
63
+
64
+
65
+ def write_report(tmp_path):
66
+ report = tmp_path / ".prizmkit" / "test" / "run" / "test-report.md"
67
+ report.parent.mkdir(parents=True, exist_ok=True)
68
+ time.sleep(0.01)
69
+ report.write_text(scoped_report_text(), encoding="utf-8")
70
+ return report
71
+
72
+
73
+ def test_gate_pass_writes_pointer_and_updates_checkpoint(tmp_path, monkeypatch):
74
+ monkeypatch.chdir(tmp_path)
75
+ checkpoint = prepare_project(tmp_path)
76
+ start = start_gate("123-scope-test")
77
+ report = write_report(tmp_path)
78
+
79
+ result = check_gate(
80
+ "123-scope-test",
81
+ ".prizmkit/specs/123-scope-test/",
82
+ str(checkpoint),
83
+ )
84
+
85
+ assert start["gate"] == "GATE:START"
86
+ assert result["status"] == "PASS"
87
+ assert result["report"] == os.path.join(".prizmkit", "test", "run", "test-report.md")
88
+ pointer = tmp_path / ".prizmkit" / "specs" / "123-scope-test" / "test-report-path.txt"
89
+ assert pointer.read_text(encoding="utf-8").strip() == str(report.relative_to(tmp_path))
90
+ checkpoint_data = json.loads(checkpoint.read_text(encoding="utf-8"))
91
+ assert checkpoint_data["steps"][0]["status"] == "completed"
92
+
93
+
94
+ def test_gate_fails_when_in_scope_file_is_newer_than_report(tmp_path, monkeypatch):
95
+ monkeypatch.chdir(tmp_path)
96
+ prepare_project(tmp_path)
97
+ start_gate("123-scope-test")
98
+ report = write_report(tmp_path)
99
+ now = time.time()
100
+ os.utime(report, (now, now))
101
+ os.utime(tmp_path / "src" / "feature.js", (now + 10, now + 10))
102
+
103
+ result = check_gate("123-scope-test", ".prizmkit/specs/123-scope-test/", "")
104
+
105
+ assert result["status"] == "FAIL"
106
+ assert result["reason"] == "scope_not_fresh"
107
+ assert result["scope_fresh"] == "0"