prizmkit 1.1.48 → 1.1.51

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 (33) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/scripts/update-checkpoint.py +173 -0
  3. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +39 -9
  4. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +7 -1
  5. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +7 -1
  6. package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +7 -1
  7. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +14 -1
  8. package/bundled/dev-pipeline/templates/sections/phase-commit.md +14 -1
  9. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +7 -1
  10. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +7 -1
  11. package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +7 -1
  12. package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +7 -1
  13. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -1
  14. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -1
  15. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +7 -1
  16. package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +7 -1
  17. package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +7 -1
  18. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +7 -1
  19. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +7 -1
  20. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +7 -1
  21. package/bundled/dev-pipeline/templates/sections/phase0-init.md +7 -1
  22. package/bundled/dev-pipeline/templates/sections/phase0-test-baseline.md +7 -1
  23. package/bundled/skills/_metadata.json +9 -1
  24. package/bundled/skills/bug-planner/SKILL.md +17 -5
  25. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +234 -64
  26. package/bundled/skills/feature-planner/SKILL.md +10 -4
  27. package/bundled/skills/feature-planner/scripts/validate-and-generate.py +82 -0
  28. package/bundled/skills/prizmkit-implement/SKILL.md +3 -0
  29. package/bundled/skills/prizmkit-test/SKILL.md +281 -0
  30. package/bundled/skills/refactor-planner/SKILL.md +11 -6
  31. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +72 -0
  32. package/package.json +1 -1
  33. package/src/scaffold.js +5 -0
@@ -1,20 +1,30 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Validate .prizmkit/plans/bug-fix-list.json against the PrizmKit bug-fix-list schema.
3
+ Validate and generate .prizmkit/plans/bug-fix-list.json files
4
+ for the dev-pipeline system.
5
+
6
+ Commands:
7
+ validate Validate an existing bug-fix-list.json
8
+ generate Validate a draft JSON and generate final bug-fix-list.json with defaults
4
9
 
5
10
  Usage:
6
- python3 validate-bug-list.py [.prizmkit/plans/bug-fix-list.json] [--feature-list .prizmkit/plans/feature-list.json]
11
+ python3 validate-bug-list.py validate .prizmkit/plans/bug-fix-list.json [--feature-list .prizmkit/plans/feature-list.json]
12
+ python3 validate-bug-list.py generate --input draft.json --output .prizmkit/plans/bug-fix-list.json
7
13
 
8
14
  Exit codes:
9
- 0 = valid
10
- 1 = validation errors found
11
- 2 = file not found or JSON parse error
15
+ 0 = valid / generated
16
+ 1 = validation errors found
17
+ 2 = file not found or JSON parse error
18
+
19
+ Python 3.6+ required. No external dependencies.
12
20
  """
13
21
 
22
+ import argparse
14
23
  import json
15
24
  import sys
16
25
  import os
17
26
  import re
27
+ from datetime import datetime, timezone
18
28
 
19
29
  VALID_SEVERITIES = {"critical", "high", "medium", "low"}
20
30
  VALID_SOURCE_TYPES = {"stack_trace", "user_report", "failed_test", "log_pattern", "monitoring_alert"}
@@ -24,129 +34,289 @@ BUG_ID_PATTERN = re.compile(r"^B-\d{3}$")
24
34
  SCHEMA_VERSION = "dev-pipeline-bug-fix-list-v1"
25
35
 
26
36
 
27
- def validate(bug_list_path, feature_list_path=None):
28
- errors = []
29
- warnings = []
37
+ def _err(msg):
38
+ """Print an error message to stderr."""
39
+ print("ERROR: {}".format(msg), file=sys.stderr)
40
+
41
+
42
+ def _warn(msg):
43
+ """Print a warning message to stderr."""
44
+ print("WARN: {}".format(msg), file=sys.stderr)
45
+
30
46
 
31
- # Load bug-fix-list.json
47
+ def _info(msg):
48
+ """Print an informational message to stderr."""
49
+ print("INFO: {}".format(msg), file=sys.stderr)
50
+
51
+
52
+ def _load_json(path):
53
+ """Load and return parsed JSON from a file. Returns (data, error_string)."""
54
+ if not os.path.isfile(path):
55
+ return None, "File not found: {}".format(path)
32
56
  try:
33
- with open(bug_list_path) as f:
57
+ with open(path, "r", encoding="utf-8") as f:
34
58
  data = json.load(f)
35
- except FileNotFoundError:
36
- print(f"ERROR: File not found: {bug_list_path}", file=sys.stderr)
37
- return 2
59
+ return data, None
38
60
  except json.JSONDecodeError as e:
39
- print(f"ERROR: Invalid JSON in {bug_list_path}: {e}", file=sys.stderr)
40
- return 2
61
+ return None, "Invalid JSON in {}: {}".format(path, e)
62
+ except Exception as e:
63
+ return None, "Cannot read {}: {}".format(path, e)
41
64
 
42
- # Load feature-list.json (optional, for cross-reference)
43
- feature_ids = set()
44
- if feature_list_path:
45
- try:
46
- with open(feature_list_path) as f:
47
- fl_data = json.load(f)
48
- feature_ids = {f.get("id") for f in fl_data.get("features", [])}
49
- except (FileNotFoundError, json.JSONDecodeError):
50
- warnings.append(f"Could not load feature-list.json at {feature_list_path}")
65
+
66
+ def _write_json(path, data):
67
+ """Write data as pretty-printed JSON to path. Creates parent dirs if needed."""
68
+ parent = os.path.dirname(os.path.abspath(path))
69
+ if parent and not os.path.isdir(parent):
70
+ os.makedirs(parent, exist_ok=True)
71
+ with open(path, "w", encoding="utf-8") as f:
72
+ json.dump(data, f, indent=2, ensure_ascii=False)
73
+ f.write("\n")
74
+
75
+
76
+ def validate(data, feature_ids=None):
77
+ """Validate a parsed bug-fix-list data structure.
78
+
79
+ Returns a dict with keys: valid, errors, warnings, stats.
80
+ """
81
+ errors = []
82
+ warnings = []
51
83
 
52
84
  # Top-level required fields
53
85
  if "$schema" not in data:
54
86
  errors.append("Missing required field: $schema")
55
87
  elif data["$schema"] != SCHEMA_VERSION:
56
- errors.append(f"Invalid $schema: expected '{SCHEMA_VERSION}', got '{data['$schema']}'")
88
+ errors.append("Invalid $schema: expected '{}', got '{}'".format(SCHEMA_VERSION, data["$schema"]))
57
89
 
58
90
  if not data.get("project_name"):
59
91
  errors.append("Missing or empty required field: project_name")
60
92
 
61
93
  bugs = data.get("bugs", [])
62
- if not bugs:
94
+ if not bugs or not isinstance(bugs, list):
63
95
  errors.append("Missing or empty required field: bugs")
96
+ return {
97
+ "valid": False,
98
+ "errors": errors,
99
+ "warnings": warnings,
100
+ "stats": {"total_bugs": 0, "severity_distribution": {}},
101
+ }
64
102
 
65
103
  # Per-bug validation
66
104
  seen_ids = set()
67
- seen_priorities = set()
105
+ severity_counts = {}
68
106
 
69
107
  for i, bug in enumerate(bugs):
70
- prefix = f"bugs[{i}]"
108
+ prefix = "bugs[{}]".format(i)
71
109
 
72
110
  # Required fields
73
111
  bug_id = bug.get("id", "")
74
112
  if not bug_id:
75
- errors.append(f"{prefix}: missing required field 'id'")
113
+ errors.append("{}: missing required field 'id'".format(prefix))
76
114
  elif not BUG_ID_PATTERN.match(bug_id):
77
- errors.append(f"{prefix}: id '{bug_id}' does not match pattern B-NNN")
115
+ errors.append("{}: id '{}' does not match pattern B-NNN".format(prefix, bug_id))
78
116
 
79
117
  if bug_id in seen_ids:
80
- errors.append(f"{prefix}: duplicate bug id '{bug_id}'")
118
+ errors.append("{}: duplicate bug id '{}'".format(prefix, bug_id))
81
119
  seen_ids.add(bug_id)
82
120
 
83
121
  if not bug.get("title"):
84
- errors.append(f"{prefix} ({bug_id}): missing required field 'title'")
122
+ errors.append("{} ({}): missing required field 'title'".format(prefix, bug_id))
85
123
 
86
124
  if not bug.get("description"):
87
- errors.append(f"{prefix} ({bug_id}): missing required field 'description'")
125
+ errors.append("{} ({}): missing required field 'description'".format(prefix, bug_id))
88
126
 
89
127
  severity = bug.get("severity", "")
90
128
  if severity not in VALID_SEVERITIES:
91
- errors.append(f"{prefix} ({bug_id}): invalid severity '{severity}' — must be one of {VALID_SEVERITIES}")
129
+ errors.append("{} ({}): invalid severity '{}' — must be one of {}".format(
130
+ prefix, bug_id, severity, sorted(VALID_SEVERITIES)))
131
+ else:
132
+ severity_counts[severity] = severity_counts.get(severity, 0) + 1
92
133
 
93
134
  # error_source
94
135
  error_source = bug.get("error_source", {})
95
136
  source_type = error_source.get("type", "") if isinstance(error_source, dict) else ""
96
137
  if source_type not in VALID_SOURCE_TYPES:
97
- errors.append(f"{prefix} ({bug_id}): invalid error_source.type '{source_type}' — must be one of {VALID_SOURCE_TYPES}")
138
+ errors.append("{} ({}): invalid error_source.type '{}' — must be one of {}".format(
139
+ prefix, bug_id, source_type, sorted(VALID_SOURCE_TYPES)))
98
140
 
99
141
  # verification_type
100
142
  vtype = bug.get("verification_type", "")
101
143
  if vtype not in VALID_VERIFICATION_TYPES:
102
- errors.append(f"{prefix} ({bug_id}): invalid verification_type '{vtype}' — must be one of {VALID_VERIFICATION_TYPES}")
144
+ errors.append("{} ({}): invalid verification_type '{}' — must be one of {}".format(
145
+ prefix, bug_id, vtype, sorted(VALID_VERIFICATION_TYPES)))
103
146
 
104
147
  # acceptance_criteria
105
148
  ac = bug.get("acceptance_criteria", [])
106
149
  if not ac or not isinstance(ac, list):
107
- errors.append(f"{prefix} ({bug_id}): missing or empty acceptance_criteria array")
150
+ errors.append("{} ({}): missing or empty acceptance_criteria array".format(prefix, bug_id))
108
151
 
109
152
  # status
110
153
  status = bug.get("status", "")
111
154
  if status not in VALID_STATUSES:
112
- errors.append(f"{prefix} ({bug_id}): invalid status '{status}' — must be one of {VALID_STATUSES}")
155
+ errors.append("{} ({}): invalid status '{}' — must be one of {}".format(
156
+ prefix, bug_id, status, sorted(VALID_STATUSES)))
113
157
 
114
158
  # Priority validation (optional field)
115
159
  priority = bug.get("priority")
116
160
  if priority is not None:
117
161
  if priority not in ("high", "medium", "low"):
118
- errors.append(f"{prefix} ({bug_id}): invalid priority '{priority}' — must be one of 'high', 'medium', 'low'")
162
+ errors.append("{} ({}): invalid priority '{}' — must be one of 'high', 'medium', 'low'".format(
163
+ prefix, bug_id, priority))
164
+
165
+ return {
166
+ "valid": len(errors) == 0,
167
+ "errors": errors,
168
+ "warnings": warnings,
169
+ "stats": {
170
+ "total_bugs": len(bugs),
171
+ "severity_distribution": severity_counts,
172
+ },
173
+ }
174
+
175
+
176
+ def cmd_validate(args):
177
+ """Handle the 'validate' command."""
178
+ bug_list = args.input
179
+ feature_list = args.feature_list
180
+
181
+ data, load_err = _load_json(bug_list)
182
+ if load_err:
183
+ _err(load_err)
184
+ return 2
185
+
186
+ # Load feature-list.json (optional, for cross-reference)
187
+ if feature_list:
188
+ fl_data, fl_err = _load_json(feature_list)
189
+ if not fl_data:
190
+ _warn("Could not load feature-list.json at {}: {}".format(feature_list, fl_err))
191
+
192
+ result = validate(data)
119
193
 
194
+ # Print results to stdout
195
+ print(json.dumps(result, indent=2, ensure_ascii=False))
120
196
 
121
- # Output results
122
- if errors:
123
- print(f"VALIDATION FAILED — {len(errors)} error(s), {len(warnings)} warning(s)\n")
124
- for e in errors:
125
- print(f" ERROR: {e}")
126
- for w in warnings:
127
- print(f" WARN: {w}")
197
+ if result["valid"]:
198
+ bug_count = result["stats"]["total_bugs"]
199
+ sev = result["stats"]["severity_distribution"]
200
+ sev_str = ", ".join("{}={}".format(k, v) for k, v in sorted(sev.items()))
201
+ _info("VALIDATION PASSED — {} bugs ({})".format(bug_count, sev_str))
202
+ return 0
203
+ else:
204
+ _err("VALIDATION FAILED — {} error(s), {} warning(s)".format(
205
+ len(result["errors"]), len(result["warnings"])))
206
+ for e in result["errors"]:
207
+ _err(" ERROR: {}".format(e))
208
+ for w in result["warnings"]:
209
+ _warn(" WARN: {}".format(w))
128
210
  return 1
211
+
212
+
213
+ def cmd_generate(args):
214
+ """Handle the 'generate' command.
215
+
216
+ Loads a draft JSON (produced by AI), fills in defaults, validates,
217
+ and writes the final bug-fix-list.json.
218
+ """
219
+ # Load draft (supports stdin via '-')
220
+ if args.input == "-":
221
+ try:
222
+ data = json.load(sys.stdin)
223
+ except json.JSONDecodeError as exc:
224
+ _err("Invalid JSON from stdin: {}".format(exc))
225
+ return 2
129
226
  else:
130
- bug_count = len(bugs)
131
- severity_counts = {}
132
- for b in bugs:
133
- s = b.get("severity", "unknown")
134
- severity_counts[s] = severity_counts.get(s, 0) + 1
135
- sev_str = ", ".join(f"{k}={v}" for k, v in sorted(severity_counts.items()))
136
- print(f"VALIDATION PASSED — {bug_count} bugs ({sev_str})")
137
- if warnings:
138
- for w in warnings:
139
- print(f" WARN: {w}")
227
+ data, load_err = _load_json(args.input)
228
+ if load_err:
229
+ _err(load_err)
230
+ return 2
231
+
232
+ # Fill in defaults
233
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
234
+ data.setdefault("$schema", SCHEMA_VERSION)
235
+ data.setdefault("created_at", now)
236
+ data.setdefault("created_by", "bug-planner")
237
+
238
+ # Set default status for bugs without one
239
+ for bug in data.get("bugs", []):
240
+ bug.setdefault("status", "pending")
241
+
242
+ # Validate
243
+ result = validate(data)
244
+
245
+ # Output validation result
246
+ print(json.dumps(result, indent=2, ensure_ascii=False))
247
+
248
+ if result["valid"]:
249
+ _write_json(args.output, data)
250
+ _info("Generated bug-fix-list written to {}".format(args.output))
140
251
  return 0
252
+ else:
253
+ _err("Validation failed with {} error(s)".format(len(result["errors"])))
254
+ for e in result["errors"]:
255
+ _err(" ERROR: {}".format(e))
256
+ return 1
141
257
 
142
258
 
143
- if __name__ == "__main__":
144
- bug_list = sys.argv[1] if len(sys.argv) > 1 else ".prizmkit/plans/bug-fix-list.json"
145
- feature_list = None
259
+ def main():
260
+ # Backward compatibility: if first arg is a file path (not a subcommand),
261
+ # treat it as 'validate' command
262
+ if len(sys.argv) > 1 and not sys.argv[1].startswith("-") and sys.argv[1] not in ("validate", "generate"):
263
+ sys.argv.insert(1, "validate")
264
+
265
+ parser = argparse.ArgumentParser(
266
+ description="Validate and generate .prizmkit/plans/bug-fix-list.json files.",
267
+ formatter_class=argparse.RawDescriptionHelpFormatter,
268
+ epilog=(
269
+ "Examples:\n"
270
+ " %(prog)s validate .prizmkit/plans/bug-fix-list.json\n"
271
+ " %(prog)s validate .prizmkit/plans/bug-fix-list.json --feature-list .prizmkit/plans/feature-list.json\n"
272
+ " %(prog)s generate --input draft.json --output .prizmkit/plans/bug-fix-list.json\n"
273
+ ),
274
+ )
275
+
276
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
277
+
278
+ # -- validate --
279
+ p_validate = subparsers.add_parser(
280
+ "validate",
281
+ help="Validate an existing bug-fix-list.json",
282
+ )
283
+ p_validate.add_argument(
284
+ "input", help="Path to bug-fix-list.json"
285
+ )
286
+ p_validate.add_argument(
287
+ "--feature-list", default=None, help="Path to feature-list.json for cross-reference"
288
+ )
146
289
 
147
- if "--feature-list" in sys.argv:
148
- idx = sys.argv.index("--feature-list")
149
- if idx + 1 < len(sys.argv):
150
- feature_list = sys.argv[idx + 1]
290
+ # -- generate --
291
+ p_generate = subparsers.add_parser(
292
+ "generate",
293
+ help="Validate a draft and generate final bug-fix-list.json with defaults",
294
+ )
295
+ p_generate.add_argument(
296
+ "--input", required=True, help="Path to draft JSON (or '-' for stdin)"
297
+ )
298
+ p_generate.add_argument(
299
+ "--output", required=True, help="Path to write final bug-fix-list.json"
300
+ )
301
+
302
+ args = parser.parse_args()
303
+
304
+ if not args.command:
305
+ parser.print_help(sys.stderr)
306
+ return 2
151
307
 
152
- sys.exit(validate(bug_list, feature_list))
308
+ dispatch = {
309
+ "validate": cmd_validate,
310
+ "generate": cmd_generate,
311
+ }
312
+
313
+ handler = dispatch.get(args.command)
314
+ if handler is None:
315
+ _err("Unknown command: {}".format(args.command))
316
+ return 2
317
+
318
+ return handler(args)
319
+
320
+
321
+ if __name__ == "__main__":
322
+ sys.exit(main())
@@ -263,9 +263,12 @@ For simple incremental planning, skip detailed Phase 2-3 analysis:
263
263
  **NEVER proceed without explicit user selection via `AskUserQuestion`. Do NOT render options as plain text — the user must be able to click/select.**
264
264
  3. Generate next sequential feature IDs
265
265
  4. Draft features (title + description + acceptance_criteria + dependencies)
266
- 5. Run validation script immediately
266
+ 5. Write draft to `.prizmkit/plans/feature-list.draft.json`, then call the generate script:
267
+ ```bash
268
+ python3 ${SKILL_DIR}/scripts/validate-and-generate.py generate --input .prizmkit/plans/feature-list.draft.json --output .prizmkit/plans/feature-list.json --mode incremental
269
+ ```
267
270
  6. If valid → summarize and recommend next step
268
- 7. If invalid → apply fixes, re-validate (max 2 attempts, then escalate to full workflow)
271
+ 7. If invalid → apply fixes to the draft, re-run generate (max 2 attempts, then escalate to full workflow)
269
272
 
270
273
  ## Browser Interaction Planning
271
274
 
@@ -309,10 +312,13 @@ Key requirements:
309
312
  - `high` → **standard** (orchestrator + dev + reviewer, 3 agents)
310
313
  - `critical` → **full** (full team + critic agents, 5 agents). Use for: architectural changes touching 10+ files, cross-module refactoring with API surface changes, features requiring multi-critic voting
311
314
 
312
- Run the validation script after generation:
315
+ **IMPORTANT: Do NOT hand-write the final JSON file.** Instead:
316
+ 1. Write a draft JSON to a temporary path (e.g., `.prizmkit/plans/feature-list.draft.json`)
317
+ 2. Call the generate script to validate and produce the final file:
313
318
  ```bash
314
- python3 ${SKILL_DIR}/scripts/validate-and-generate.py validate --input <output-path> --mode <new|incremental>
319
+ python3 ${SKILL_DIR}/scripts/validate-and-generate.py generate --input .prizmkit/plans/feature-list.draft.json --output .prizmkit/plans/feature-list.json --mode <new|incremental>
315
320
  ```
321
+ The script fills in defaults (`$schema`, `created_at`, `created_by`), validates all fields, and writes the final file only if validation passes. If validation fails, fix the draft and retry.
316
322
 
317
323
  ## Testing Defaults (Phase 8)
318
324
 
@@ -6,12 +6,14 @@ for the dev-pipeline system.
6
6
  Commands:
7
7
  validate Validate an existing .prizmkit/plans/feature-list.json
8
8
  template Generate a blank template .prizmkit/plans/feature-list.json
9
+ generate Validate a draft JSON and generate final feature-list.json with defaults
9
10
  summary Print a summary table of features from a .prizmkit/plans/feature-list.json
10
11
  grade Generate grading results from eval runs (for npm run skill:review)
11
12
 
12
13
  Usage:
13
14
  python3 validate-and-generate.py validate --input .prizmkit/plans/feature-list.json [--output validated.json] [--mode new|incremental]
14
15
  python3 validate-and-generate.py template --output .prizmkit/plans/feature-list.json
16
+ python3 validate-and-generate.py generate --input draft.json --output .prizmkit/plans/feature-list.json [--mode new|incremental]
15
17
  python3 validate-and-generate.py summary --input .prizmkit/plans/feature-list.json [--format markdown|json]
16
18
  python3 validate-and-generate.py grade --workspace /.codebuddy/skill-evals/feature-planner-workspace --iteration iteration-1
17
19
 
@@ -716,6 +718,66 @@ def cmd_template(args):
716
718
  return 0
717
719
 
718
720
 
721
+ def cmd_generate(args):
722
+ """Handle the 'generate' command.
723
+
724
+ Loads a draft JSON (produced by AI), fills in defaults, validates,
725
+ and writes the final feature-list.json.
726
+ """
727
+ if not args.input:
728
+ _err("--input is required for the generate command")
729
+ return 2
730
+ if not args.output:
731
+ _err("--output is required for the generate command")
732
+ return 2
733
+
734
+ # Load draft (supports stdin via '-')
735
+ if args.input == "-":
736
+ try:
737
+ data = json.load(sys.stdin)
738
+ except json.JSONDecodeError as exc:
739
+ _err("Invalid JSON from stdin: {}".format(exc))
740
+ return 2
741
+ else:
742
+ data, load_err = _load_json(args.input)
743
+ if load_err:
744
+ _err(load_err)
745
+ return 2
746
+
747
+ # Fill in defaults
748
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
749
+ data.setdefault("$schema", SCHEMA_VERSION)
750
+ data.setdefault("created_at", now)
751
+ data.setdefault("created_by", "feature-planner")
752
+
753
+ # Ensure project_name from app_name if needed
754
+ if "project_name" not in data and "app_name" in data:
755
+ data["project_name"] = data["app_name"]
756
+
757
+ # Set default status for features without one
758
+ for feature in data.get("features", []):
759
+ feature.setdefault("status", "pending")
760
+
761
+ # Validate
762
+ mode = getattr(args, "mode", "new") or "new"
763
+ result = validate_feature_list(data, planning_mode=mode)
764
+
765
+ # Output validation result
766
+ print(json.dumps(result, indent=2, ensure_ascii=False))
767
+
768
+ if result["valid"]:
769
+ _write_json(args.output, data)
770
+ _info("Generated feature-list written to {}".format(args.output))
771
+ return 0
772
+ else:
773
+ _err("Validation failed with {} error(s)".format(len(result["errors"])))
774
+ for e in result["errors"]:
775
+ _err(" " + e)
776
+ for w in result.get("warnings", []):
777
+ _warn(" " + w)
778
+ return 1
779
+
780
+
719
781
  def cmd_summary(args):
720
782
  """Handle the 'summary' command."""
721
783
  if not args.input:
@@ -857,6 +919,7 @@ def main():
857
919
  " %(prog)s validate --input .prizmkit/plans/feature-list.json --mode incremental\n"
858
920
  " %(prog)s validate --input .prizmkit/plans/feature-list.json --output validated.json\n"
859
921
  " %(prog)s template --output .prizmkit/plans/feature-list.json\n"
922
+ " %(prog)s generate --input draft.json --output .prizmkit/plans/feature-list.json\n"
860
923
  " %(prog)s summary --input .prizmkit/plans/feature-list.json\n"
861
924
  " %(prog)s summary --input .prizmkit/plans/feature-list.json --format json\n"
862
925
  ),
@@ -891,6 +954,24 @@ def main():
891
954
  "--output", required=True, help="Path to write template file"
892
955
  )
893
956
 
957
+ # -- generate --
958
+ p_generate = subparsers.add_parser(
959
+ "generate",
960
+ help="Validate a draft and generate final feature-list.json with defaults filled in",
961
+ )
962
+ p_generate.add_argument(
963
+ "--input", required=True, help="Path to draft JSON (or '-' for stdin)"
964
+ )
965
+ p_generate.add_argument(
966
+ "--output", required=True, help="Path to write final feature-list.json"
967
+ )
968
+ p_generate.add_argument(
969
+ "--mode",
970
+ choices=["new", "incremental"],
971
+ default="new",
972
+ help="Validation mode (default: new)",
973
+ )
974
+
894
975
  # -- summary --
895
976
  p_summary = subparsers.add_parser(
896
977
  "summary",
@@ -931,6 +1012,7 @@ def main():
931
1012
  dispatch = {
932
1013
  "validate": cmd_validate,
933
1014
  "template": cmd_template,
1015
+ "generate": cmd_generate,
934
1016
  "summary": cmd_summary,
935
1017
  "grade": cmd_grade,
936
1018
  }
@@ -40,6 +40,9 @@ For each unchecked task in plan.md, in order:
40
40
 
41
41
  1. Read L1/L2 doc for the target file's module — check TRAPS and DECISIONS before modifying files
42
42
  2. Apply TDD where applicable: write a failing test first, then implement until it passes. For UI components or configuration changes where unit tests don't apply, skip the test-first step.
43
+ - **Cover three paths for each function**: happy path (valid inputs producing expected behavior), edge cases (boundary values specific to the parameter type and domain — e.g., zero/min/max for numeric, empty collection, boundary index), and error conditions (inputs that should trigger error handling). Determine edge cases from the function's parameter types and domain logic, not from a fixed checklist. If a function has no edge or error paths, don't force them.
44
+ - **No redundant tests**: Check if a test for this behavior already exists before writing. Each test must verify a uniquely different code path — don't write multiple tests that exercise the same logic.
45
+ - **Test your own code only**: Don't test framework behavior, third-party library internals, or language built-ins. For library calls, test the integration point (correct parameters passed, return value correctly handled), not the library itself.
43
46
  3. Mark task as `[x]` in `plan.md` immediately after completion — not batched at the end. Immediate marking means plan.md always reflects true progress, even if the session is interrupted.
44
47
  4. **Parallel tasks**: If task has `[P]` marker, it can run in parallel with other `[P]` tasks in the same group. Sequential tasks stop on failure (later tasks may depend on this one). Parallel `[P]` tasks continue — report all failures at the end.
45
48
  5. **Checkpoint tasks** (`CP:` prefix in plan.md): When a checkpoint task is reached, verify build passes and tests pass before continuing. Checkpoints catch integration errors early — skipping them means cascading failures in later phases.