prizmkit 1.0.0 → 1.0.1

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 (89) hide show
  1. package/bundled/VERSION.json +5 -0
  2. package/bundled/adapters/claude/agent-adapter.js +108 -0
  3. package/bundled/adapters/claude/command-adapter.js +104 -0
  4. package/bundled/adapters/claude/paths.js +35 -0
  5. package/bundled/adapters/claude/rules-adapter.js +77 -0
  6. package/bundled/adapters/claude/settings-adapter.js +73 -0
  7. package/bundled/adapters/claude/team-adapter.js +183 -0
  8. package/bundled/adapters/codebuddy/agent-adapter.js +43 -0
  9. package/bundled/adapters/codebuddy/paths.js +29 -0
  10. package/bundled/adapters/codebuddy/settings-adapter.js +47 -0
  11. package/bundled/adapters/codebuddy/skill-adapter.js +68 -0
  12. package/bundled/adapters/codebuddy/team-adapter.js +46 -0
  13. package/bundled/adapters/shared/frontmatter.js +77 -0
  14. package/bundled/agents/prizm-dev-team-coordinator.md +142 -0
  15. package/bundled/agents/prizm-dev-team-dev.md +99 -0
  16. package/bundled/agents/prizm-dev-team-pm.md +114 -0
  17. package/bundled/agents/prizm-dev-team-reviewer.md +119 -0
  18. package/bundled/dev-pipeline/README.md +482 -0
  19. package/bundled/dev-pipeline/assets/feature-list-example.json +147 -0
  20. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +138 -0
  21. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +425 -0
  22. package/bundled/dev-pipeline/launch-daemon.sh +549 -0
  23. package/bundled/dev-pipeline/reset-feature.sh +209 -0
  24. package/bundled/dev-pipeline/retry-bug.sh +344 -0
  25. package/bundled/dev-pipeline/retry-feature.sh +338 -0
  26. package/bundled/dev-pipeline/run-bugfix.sh +638 -0
  27. package/bundled/dev-pipeline/run.sh +845 -0
  28. package/bundled/dev-pipeline/scripts/check-session-status.py +158 -0
  29. package/bundled/dev-pipeline/scripts/detect-stuck.py +385 -0
  30. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +598 -0
  31. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +402 -0
  32. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +294 -0
  33. package/bundled/dev-pipeline/scripts/init-dev-team.py +134 -0
  34. package/bundled/dev-pipeline/scripts/init-pipeline.py +335 -0
  35. package/bundled/dev-pipeline/scripts/update-bug-status.py +748 -0
  36. package/bundled/dev-pipeline/scripts/update-feature-status.py +1076 -0
  37. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +262 -0
  38. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +159 -0
  39. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +291 -0
  40. package/bundled/dev-pipeline/templates/feature-list-schema.json +112 -0
  41. package/bundled/dev-pipeline/templates/session-status-schema.json +77 -0
  42. package/bundled/skills/_metadata.json +267 -0
  43. package/bundled/skills/app-planner/SKILL.md +580 -0
  44. package/bundled/skills/app-planner/assets/planning-guide.md +313 -0
  45. package/bundled/skills/app-planner/scripts/validate-and-generate.py +758 -0
  46. package/bundled/skills/bug-planner/SKILL.md +235 -0
  47. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +252 -0
  48. package/bundled/skills/dev-pipeline-launcher/SKILL.md +223 -0
  49. package/bundled/skills/prizm-kit/SKILL.md +151 -0
  50. package/bundled/skills/prizm-kit/assets/claude-md-template.md +38 -0
  51. package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +35 -0
  52. package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +15 -0
  53. package/bundled/skills/prizmkit-adr-manager/SKILL.md +68 -0
  54. package/bundled/skills/prizmkit-adr-manager/assets/adr-template.md +26 -0
  55. package/bundled/skills/prizmkit-analyze/SKILL.md +194 -0
  56. package/bundled/skills/prizmkit-api-doc-generator/SKILL.md +56 -0
  57. package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +351 -0
  58. package/bundled/skills/prizmkit-bug-reproducer/SKILL.md +62 -0
  59. package/bundled/skills/prizmkit-ci-cd-generator/SKILL.md +54 -0
  60. package/bundled/skills/prizmkit-clarify/SKILL.md +52 -0
  61. package/bundled/skills/prizmkit-code-review/SKILL.md +70 -0
  62. package/bundled/skills/prizmkit-committer/SKILL.md +117 -0
  63. package/bundled/skills/prizmkit-db-migration/SKILL.md +65 -0
  64. package/bundled/skills/prizmkit-dependency-health/SKILL.md +123 -0
  65. package/bundled/skills/prizmkit-deployment-strategy/SKILL.md +58 -0
  66. package/bundled/skills/prizmkit-error-triage/SKILL.md +55 -0
  67. package/bundled/skills/prizmkit-implement/SKILL.md +47 -0
  68. package/bundled/skills/prizmkit-init/SKILL.md +156 -0
  69. package/bundled/skills/prizmkit-log-analyzer/SKILL.md +55 -0
  70. package/bundled/skills/prizmkit-monitoring-setup/SKILL.md +75 -0
  71. package/bundled/skills/prizmkit-onboarding-generator/SKILL.md +70 -0
  72. package/bundled/skills/prizmkit-perf-profiler/SKILL.md +55 -0
  73. package/bundled/skills/prizmkit-plan/SKILL.md +54 -0
  74. package/bundled/skills/prizmkit-plan/assets/plan-template.md +37 -0
  75. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +140 -0
  76. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +943 -0
  77. package/bundled/skills/prizmkit-retrospective/SKILL.md +79 -0
  78. package/bundled/skills/prizmkit-security-audit/SKILL.md +130 -0
  79. package/bundled/skills/prizmkit-specify/SKILL.md +52 -0
  80. package/bundled/skills/prizmkit-specify/assets/spec-template.md +37 -0
  81. package/bundled/skills/prizmkit-summarize/SKILL.md +51 -0
  82. package/bundled/skills/prizmkit-summarize/assets/registry-template.md +18 -0
  83. package/bundled/skills/prizmkit-tasks/SKILL.md +50 -0
  84. package/bundled/skills/prizmkit-tasks/assets/tasks-template.md +21 -0
  85. package/bundled/skills/prizmkit-tech-debt-tracker/SKILL.md +139 -0
  86. package/bundled/team/prizm-dev-team.json +47 -0
  87. package/bundled/templates/claude-md-template.md +38 -0
  88. package/bundled/templates/codebuddy-md-template.md +35 -0
  89. package/package.json +2 -1
@@ -0,0 +1,758 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate-and-generate.py - Validate and generate feature-list.json files
4
+ for the dev-pipeline system.
5
+
6
+ Commands:
7
+ validate Validate an existing feature-list.json
8
+ template Generate a blank template feature-list.json
9
+ summary Print a summary table of features from a feature-list.json
10
+
11
+ Usage:
12
+ python3 validate-and-generate.py validate --input feature-list.json [--output validated.json]
13
+ python3 validate-and-generate.py template --output feature-list.json
14
+ python3 validate-and-generate.py summary --input feature-list.json [--format markdown|json]
15
+
16
+ Python 3.6+ required. No external dependencies.
17
+ """
18
+
19
+ import argparse
20
+ import collections
21
+ import json
22
+ import os
23
+ import re
24
+ import sys
25
+ from datetime import datetime, timezone
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Constants
29
+ # ---------------------------------------------------------------------------
30
+
31
+ SCHEMA_VERSION = "dev-pipeline-feature-list-v1"
32
+
33
+ VALID_STATUSES = {"pending", "in_progress", "completed", "failed", "skipped", "split"}
34
+ VALID_COMPLEXITIES = {"low", "medium", "high"}
35
+ VALID_GRANULARITIES = {"feature", "sub_feature", "auto"}
36
+
37
+ FEATURE_ID_RE = re.compile(r"^F-\d{3}(-[A-Z])?$")
38
+ SUB_FEATURE_ID_RE = re.compile(r"^F-\d{3}-[A-Z]$")
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Helpers
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ def _err(msg):
46
+ """Print an error message to stderr."""
47
+ print("ERROR: {}".format(msg), file=sys.stderr)
48
+
49
+
50
+ def _warn(msg):
51
+ """Print a warning message to stderr."""
52
+ print("WARNING: {}".format(msg), file=sys.stderr)
53
+
54
+
55
+ def _info(msg):
56
+ """Print an informational message to stderr."""
57
+ print("INFO: {}".format(msg), file=sys.stderr)
58
+
59
+
60
+ def _load_json(path):
61
+ """Load and return parsed JSON from *path*.
62
+
63
+ Returns (data, error_message). On success error_message is None.
64
+ """
65
+ if not os.path.isfile(path):
66
+ return None, "File not found: {}".format(path)
67
+ try:
68
+ with open(path, "r", encoding="utf-8") as fh:
69
+ data = json.load(fh)
70
+ return data, None
71
+ except json.JSONDecodeError as exc:
72
+ return None, "JSON parse error in {}: {}".format(path, exc)
73
+ except Exception as exc:
74
+ return None, "Failed to read {}: {}".format(path, exc)
75
+
76
+
77
+ def _write_json(path, data):
78
+ """Write *data* as pretty-printed JSON to *path*."""
79
+ parent = os.path.dirname(path)
80
+ if parent and not os.path.isdir(parent):
81
+ os.makedirs(parent, exist_ok=True)
82
+ with open(path, "w", encoding="utf-8") as fh:
83
+ json.dump(data, fh, indent=2, ensure_ascii=False)
84
+ fh.write("\n")
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Cycle detection (Kahn's algorithm)
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ def _detect_cycles(features):
93
+ """Return (has_cycles: bool, max_depth: int) using Kahn's topological sort.
94
+
95
+ *features* is the list of feature dicts. We build a graph from the
96
+ ``dependencies`` field and run Kahn's algorithm.
97
+
98
+ Returns a tuple ``(has_cycles, max_depth)`` where *max_depth* is the
99
+ longest path in the DAG (0 if there are cycles or a single node).
100
+ """
101
+ id_set = {f["id"] for f in features}
102
+ # Build adjacency list and in-degree map.
103
+ adj = {fid: [] for fid in id_set} # dependency -> [dependent]
104
+ in_degree = {fid: 0 for fid in id_set}
105
+
106
+ for feat in features:
107
+ fid = feat["id"]
108
+ for dep in feat.get("dependencies", []):
109
+ if dep in id_set:
110
+ adj[dep].append(fid)
111
+ in_degree[fid] += 1
112
+
113
+ # Kahn's algorithm
114
+ queue = collections.deque()
115
+ for fid, deg in in_degree.items():
116
+ if deg == 0:
117
+ queue.append(fid)
118
+
119
+ sorted_order = []
120
+ # Track depth for each node to compute max dependency depth.
121
+ depth = {fid: 0 for fid in id_set}
122
+
123
+ while queue:
124
+ node = queue.popleft()
125
+ sorted_order.append(node)
126
+ for neighbour in adj[node]:
127
+ in_degree[neighbour] -= 1
128
+ new_depth = depth[node] + 1
129
+ if new_depth > depth[neighbour]:
130
+ depth[neighbour] = new_depth
131
+ if in_degree[neighbour] == 0:
132
+ queue.append(neighbour)
133
+
134
+ has_cycles = len(sorted_order) != len(id_set)
135
+ max_depth = max(depth.values()) if depth else 0
136
+ return has_cycles, max_depth
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Validation
141
+ # ---------------------------------------------------------------------------
142
+
143
+
144
+ def validate_feature_list(data):
145
+ """Validate a parsed feature-list data structure.
146
+
147
+ Returns a dict with keys ``valid``, ``errors``, ``warnings``, ``stats``.
148
+ """
149
+ errors = []
150
+ warnings = []
151
+
152
+ # ------------------------------------------------------------------
153
+ # 1. Top-level schema validation
154
+ # ------------------------------------------------------------------
155
+ schema = data.get("$schema")
156
+ if schema != SCHEMA_VERSION:
157
+ errors.append(
158
+ "$schema must be '{}', got '{}'".format(SCHEMA_VERSION, schema)
159
+ )
160
+
161
+ app_name = data.get("app_name")
162
+ if not isinstance(app_name, str) or not app_name.strip():
163
+ errors.append("app_name must be a non-empty string")
164
+
165
+ features = data.get("features")
166
+ if not isinstance(features, list) or len(features) == 0:
167
+ errors.append("features must be a non-empty array")
168
+ # Early-out: nothing else to validate if features are missing.
169
+ return {
170
+ "valid": False,
171
+ "errors": errors,
172
+ "warnings": warnings,
173
+ "stats": {
174
+ "total_features": 0,
175
+ "total_sub_features": 0,
176
+ "complexity_distribution": {},
177
+ "max_dependency_depth": 0,
178
+ "has_cycles": False,
179
+ },
180
+ }
181
+
182
+ # ------------------------------------------------------------------
183
+ # 2. Per-feature validation
184
+ # ------------------------------------------------------------------
185
+ required_keys = {
186
+ "id", "title", "description", "priority",
187
+ "dependencies", "acceptance_criteria", "status",
188
+ }
189
+
190
+ seen_ids = set()
191
+ priorities = []
192
+ complexity_dist = {"low": 0, "medium": 0, "high": 0}
193
+ total_sub_features = 0
194
+
195
+ for idx, feat in enumerate(features):
196
+ label = "features[{}]".format(idx)
197
+
198
+ # -- Required keys --
199
+ if not isinstance(feat, dict):
200
+ errors.append("{} is not an object".format(label))
201
+ continue
202
+
203
+ missing = required_keys - set(feat.keys())
204
+ if missing:
205
+ errors.append("{} missing required keys: {}".format(
206
+ label, ", ".join(sorted(missing))
207
+ ))
208
+
209
+ # -- ID format & uniqueness --
210
+ fid = feat.get("id", "")
211
+ if not FEATURE_ID_RE.match(str(fid)):
212
+ errors.append(
213
+ "{}: id '{}' does not match pattern F-NNN or F-NNN-X".format(label, fid)
214
+ )
215
+ if fid in seen_ids:
216
+ errors.append("{}: duplicate id '{}'".format(label, fid))
217
+ seen_ids.add(fid)
218
+
219
+ # -- Title / description --
220
+ for key in ("title", "description"):
221
+ val = feat.get(key)
222
+ if not isinstance(val, str) or not val.strip():
223
+ errors.append("{}: {} must be a non-empty string".format(label, key))
224
+
225
+ # -- Priority --
226
+ priority = feat.get("priority")
227
+ if isinstance(priority, int) and priority > 0:
228
+ priorities.append(priority)
229
+ else:
230
+ errors.append("{}: priority must be a positive integer, got {}".format(
231
+ label, repr(priority)
232
+ ))
233
+
234
+ # -- Dependencies (list of strings) --
235
+ deps = feat.get("dependencies")
236
+ if not isinstance(deps, list):
237
+ errors.append("{}: dependencies must be an array".format(label))
238
+
239
+ # -- Acceptance criteria --
240
+ criteria = feat.get("acceptance_criteria")
241
+ if isinstance(criteria, list):
242
+ if len(criteria) < 1:
243
+ errors.append("{}: must have at least 1 acceptance criterion".format(label))
244
+ elif len(criteria) < 3:
245
+ warnings.append(
246
+ "{}: only {} acceptance criteria (recommend at least 3)".format(
247
+ label, len(criteria)
248
+ )
249
+ )
250
+ else:
251
+ errors.append("{}: acceptance_criteria must be an array".format(label))
252
+
253
+ # -- Status --
254
+ status = feat.get("status")
255
+ if status not in VALID_STATUSES:
256
+ errors.append(
257
+ "{}: status '{}' invalid, must be one of: {}".format(
258
+ label, status, ", ".join(sorted(VALID_STATUSES))
259
+ )
260
+ )
261
+ if status and status != "pending":
262
+ warnings.append(
263
+ "{}: status is '{}' (expected 'pending' for new plans)".format(label, status)
264
+ )
265
+
266
+ # -- Complexity (optional but validated if present) --
267
+ complexity = feat.get("estimated_complexity")
268
+ if complexity is not None:
269
+ if complexity not in VALID_COMPLEXITIES:
270
+ errors.append(
271
+ "{}: estimated_complexity '{}' invalid, must be one of: {}".format(
272
+ label, complexity, ", ".join(sorted(VALID_COMPLEXITIES))
273
+ )
274
+ )
275
+ else:
276
+ complexity_dist[complexity] += 1
277
+
278
+ # -- Granularity (optional but validated if present) --
279
+ granularity = feat.get("session_granularity")
280
+ if granularity is not None:
281
+ if granularity not in VALID_GRANULARITIES:
282
+ errors.append(
283
+ "{}: session_granularity '{}' invalid, must be one of: {}".format(
284
+ label, granularity, ", ".join(sorted(VALID_GRANULARITIES))
285
+ )
286
+ )
287
+ if granularity == "auto":
288
+ subs = feat.get("sub_features")
289
+ if not isinstance(subs, list) or len(subs) == 0:
290
+ warnings.append(
291
+ "{}: granularity is 'auto' but no sub_features defined".format(label)
292
+ )
293
+
294
+ # -- Sub-features --
295
+ subs = feat.get("sub_features")
296
+ if isinstance(subs, list):
297
+ for sidx, sub in enumerate(subs):
298
+ sub_label = "{}->sub_features[{}]".format(label, sidx)
299
+ if not isinstance(sub, dict):
300
+ errors.append("{} is not an object".format(sub_label))
301
+ continue
302
+
303
+ sub_missing = {"id", "title", "description"} - set(sub.keys())
304
+ if sub_missing:
305
+ errors.append("{} missing required keys: {}".format(
306
+ sub_label, ", ".join(sorted(sub_missing))
307
+ ))
308
+
309
+ sub_id = sub.get("id", "")
310
+ if not SUB_FEATURE_ID_RE.match(str(sub_id)):
311
+ errors.append(
312
+ "{}: id '{}' must be F-NNN-X format".format(sub_label, sub_id)
313
+ )
314
+
315
+ # Sub-feature ID should share parent prefix
316
+ parent_prefix = str(fid).rstrip("ABCDEFGHIJKLMNOPQRSTUVWXYZ").rstrip("-")
317
+ sub_prefix = str(sub_id)[:5] # e.g. "F-001"
318
+ if parent_prefix and sub_prefix != parent_prefix:
319
+ warnings.append(
320
+ "{}: sub-feature '{}' does not share parent prefix '{}'".format(
321
+ sub_label, sub_id, parent_prefix
322
+ )
323
+ )
324
+
325
+ if sub_id in seen_ids:
326
+ errors.append("{}: duplicate id '{}'".format(sub_label, sub_id))
327
+ seen_ids.add(sub_id)
328
+ total_sub_features += 1
329
+
330
+ # -- Priority uniqueness --
331
+ if len(priorities) != len(set(priorities)):
332
+ dup_prios = [
333
+ p for p, c in collections.Counter(priorities).items() if c > 1
334
+ ]
335
+ warnings.append(
336
+ "Duplicate priorities found: {}".format(
337
+ ", ".join(str(p) for p in sorted(dup_prios))
338
+ )
339
+ )
340
+
341
+ # ------------------------------------------------------------------
342
+ # 3. Dependency validation
343
+ # ------------------------------------------------------------------
344
+ all_ids = {f.get("id") for f in features}
345
+ for idx, feat in enumerate(features):
346
+ label = "features[{}]".format(idx)
347
+ deps = feat.get("dependencies", [])
348
+ if isinstance(deps, list):
349
+ for dep in deps:
350
+ if dep not in all_ids:
351
+ errors.append(
352
+ "{}: dependency '{}' does not exist in feature list".format(label, dep)
353
+ )
354
+
355
+ # -- Cycle detection --
356
+ has_cycles, max_depth = _detect_cycles(features)
357
+ if has_cycles:
358
+ errors.append("Dependency graph contains cycles (not a valid DAG)")
359
+
360
+ # ------------------------------------------------------------------
361
+ # 4. Build result
362
+ # ------------------------------------------------------------------
363
+ is_valid = len(errors) == 0
364
+
365
+ return {
366
+ "valid": is_valid,
367
+ "errors": errors,
368
+ "warnings": warnings,
369
+ "stats": {
370
+ "total_features": len(features),
371
+ "total_sub_features": total_sub_features,
372
+ "complexity_distribution": complexity_dist,
373
+ "max_dependency_depth": max_depth,
374
+ "has_cycles": has_cycles,
375
+ },
376
+ }
377
+
378
+
379
+ # ---------------------------------------------------------------------------
380
+ # Template generation
381
+ # ---------------------------------------------------------------------------
382
+
383
+
384
+ def generate_template():
385
+ """Return a template feature-list dict with placeholder values."""
386
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
387
+
388
+ return {
389
+ "$schema": SCHEMA_VERSION,
390
+ "app_name": "YOUR_APP_NAME",
391
+ "app_description": "YOUR_APP_DESCRIPTION",
392
+ "created_at": now,
393
+ "created_by": "app-planner",
394
+ "source_spec": "",
395
+ "features": [
396
+ {
397
+ "id": "F-001",
398
+ "title": "Project Infrastructure Setup",
399
+ "description": (
400
+ "Initialize project structure, configure build tools, "
401
+ "set up development environment."
402
+ ),
403
+ "priority": 1,
404
+ "estimated_complexity": "medium",
405
+ "dependencies": [],
406
+ "acceptance_criteria": [
407
+ "Project builds successfully",
408
+ "Development server starts",
409
+ "Linting and formatting configured",
410
+ ],
411
+ "status": "pending",
412
+ "session_granularity": "feature",
413
+ "sub_features": [],
414
+ }
415
+ ],
416
+ "global_context": {
417
+ "tech_stack": "",
418
+ "design_system": "",
419
+ "testing_strategy": "",
420
+ },
421
+ }
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Summary
426
+ # ---------------------------------------------------------------------------
427
+
428
+
429
+ def _build_dependency_graph_text(features):
430
+ """Build a human-readable text representation of the dependency graph.
431
+
432
+ Produces an arrow-chain format that shows all dependency paths,
433
+ including convergent edges (where multiple paths lead to the same node).
434
+
435
+ Returns a list of lines.
436
+ """
437
+ all_ids = [f["id"] for f in features]
438
+
439
+ # Build adjacency: dependency -> list of dependents (forward edges)
440
+ dependents = {fid: [] for fid in all_ids}
441
+ has_parent = set()
442
+ for feat in features:
443
+ for dep in feat.get("dependencies", []):
444
+ if dep in dependents:
445
+ dependents[dep].append(feat["id"])
446
+ has_parent.add(feat["id"])
447
+
448
+ # Sort children for deterministic output
449
+ for fid in dependents:
450
+ dependents[fid] = sorted(set(dependents[fid]))
451
+
452
+ # Roots: features with no incoming dependencies
453
+ roots = [fid for fid in all_ids if fid not in has_parent]
454
+ if not roots:
455
+ return ["(cycle detected - no root nodes)"]
456
+ if not any(dependents[r] for r in all_ids):
457
+ # No dependencies at all
458
+ return ["(no dependencies)"]
459
+
460
+ result_lines = []
461
+
462
+ def _render(node, prefix, is_continuation):
463
+ """Render a node and its dependents recursively.
464
+
465
+ *prefix*: whitespace to print before " -> node" on branch lines.
466
+ *is_continuation*: True if this node is appended on the same line
467
+ as its parent (first child).
468
+ """
469
+ children = dependents.get(node, [])
470
+ if not children:
471
+ return
472
+
473
+ for i, child in enumerate(children):
474
+ if i == 0:
475
+ # First child: continue on the same line
476
+ result_lines[-1] += " -> {}".format(child)
477
+ _render(child, prefix + " " * (len(node) + 4), True)
478
+ else:
479
+ # Subsequent children: new line, indented under the arrow
480
+ line = "{}-> {}".format(prefix, child)
481
+ result_lines.append(line)
482
+ child_prefix = prefix + " " * (len(child) + 4)
483
+ _render(child, child_prefix, True)
484
+
485
+ for root in sorted(roots):
486
+ result_lines.append(root)
487
+ _render(root, " " * len(root), False)
488
+
489
+ return result_lines
490
+
491
+
492
+ def generate_summary_markdown(data):
493
+ """Generate a markdown summary of the feature list."""
494
+ app_name = data.get("app_name", "Unknown")
495
+ features = data.get("features", [])
496
+
497
+ lines = []
498
+ lines.append("# Feature Summary: {}".format(app_name))
499
+ lines.append("")
500
+
501
+ # Table header
502
+ lines.append("| ID | Title | Complexity | Priority | Dependencies | Criteria | Granularity |")
503
+ lines.append("|----|-------|------------|----------|--------------|----------|-------------|")
504
+
505
+ for feat in features:
506
+ fid = feat.get("id", "?")
507
+ title = feat.get("title", "?")
508
+ complexity = feat.get("estimated_complexity", "-")
509
+ priority = feat.get("priority", "?")
510
+ deps = feat.get("dependencies", [])
511
+ deps_str = ", ".join(deps) if deps else "-"
512
+ criteria_count = len(feat.get("acceptance_criteria", []))
513
+ granularity = feat.get("session_granularity", "-")
514
+
515
+ lines.append("| {} | {} | {} | {} | {} | {} | {} |".format(
516
+ fid, title, complexity, priority, deps_str, criteria_count, granularity
517
+ ))
518
+
519
+ lines.append("")
520
+
521
+ # Dependency graph
522
+ lines.append("## Dependency Graph")
523
+ graph_lines = _build_dependency_graph_text(features)
524
+ for gl in graph_lines:
525
+ lines.append(gl)
526
+ lines.append("")
527
+
528
+ # Statistics
529
+ complexity_dist = {"low": 0, "medium": 0, "high": 0}
530
+ total_sub = 0
531
+ for feat in features:
532
+ c = feat.get("estimated_complexity")
533
+ if c in complexity_dist:
534
+ complexity_dist[c] += 1
535
+ subs = feat.get("sub_features")
536
+ if isinstance(subs, list):
537
+ total_sub += len(subs)
538
+
539
+ _, max_depth = _detect_cycles(features)
540
+
541
+ lines.append("## Statistics")
542
+ lines.append("- Total features: {}".format(len(features)))
543
+ if total_sub > 0:
544
+ lines.append("- Total sub-features: {}".format(total_sub))
545
+ lines.append("- Complexity: {} low, {} medium, {} high".format(
546
+ complexity_dist["low"], complexity_dist["medium"], complexity_dist["high"]
547
+ ))
548
+ lines.append("- Max dependency depth: {}".format(max_depth))
549
+
550
+ return "\n".join(lines)
551
+
552
+
553
+ def generate_summary_json(data):
554
+ """Generate a JSON summary of the feature list."""
555
+ features = data.get("features", [])
556
+
557
+ complexity_dist = {"low": 0, "medium": 0, "high": 0}
558
+ total_sub = 0
559
+ for feat in features:
560
+ c = feat.get("estimated_complexity")
561
+ if c in complexity_dist:
562
+ complexity_dist[c] += 1
563
+ subs = feat.get("sub_features")
564
+ if isinstance(subs, list):
565
+ total_sub += len(subs)
566
+
567
+ has_cycles, max_depth = _detect_cycles(features)
568
+
569
+ feature_summaries = []
570
+ for feat in features:
571
+ feature_summaries.append({
572
+ "id": feat.get("id"),
573
+ "title": feat.get("title"),
574
+ "priority": feat.get("priority"),
575
+ "estimated_complexity": feat.get("estimated_complexity"),
576
+ "dependencies": feat.get("dependencies", []),
577
+ "acceptance_criteria_count": len(feat.get("acceptance_criteria", [])),
578
+ "session_granularity": feat.get("session_granularity"),
579
+ "status": feat.get("status"),
580
+ })
581
+
582
+ return {
583
+ "app_name": data.get("app_name", ""),
584
+ "features": feature_summaries,
585
+ "stats": {
586
+ "total_features": len(features),
587
+ "total_sub_features": total_sub,
588
+ "complexity_distribution": complexity_dist,
589
+ "max_dependency_depth": max_depth,
590
+ "has_cycles": has_cycles,
591
+ },
592
+ }
593
+
594
+
595
+ # ---------------------------------------------------------------------------
596
+ # CLI
597
+ # ---------------------------------------------------------------------------
598
+
599
+
600
+ def cmd_validate(args):
601
+ """Handle the 'validate' command."""
602
+ if not args.input:
603
+ _err("--input is required for the validate command")
604
+ return 2
605
+
606
+ data, load_err = _load_json(args.input)
607
+ if load_err:
608
+ _err(load_err)
609
+ result = {
610
+ "valid": False,
611
+ "errors": [load_err],
612
+ "warnings": [],
613
+ "stats": {
614
+ "total_features": 0,
615
+ "total_sub_features": 0,
616
+ "complexity_distribution": {},
617
+ "max_dependency_depth": 0,
618
+ "has_cycles": False,
619
+ },
620
+ }
621
+ print(json.dumps(result, indent=2, ensure_ascii=False))
622
+ return 2
623
+
624
+ result = validate_feature_list(data)
625
+
626
+ # Print results to stdout
627
+ print(json.dumps(result, indent=2, ensure_ascii=False))
628
+
629
+ # Log to stderr for humans
630
+ if result["valid"]:
631
+ _info("Validation passed with {} warning(s)".format(len(result["warnings"])))
632
+ else:
633
+ _err("Validation failed with {} error(s) and {} warning(s)".format(
634
+ len(result["errors"]), len(result["warnings"])
635
+ ))
636
+
637
+ for e in result["errors"]:
638
+ _err(" " + e)
639
+ for w in result["warnings"]:
640
+ _warn(" " + w)
641
+
642
+ # Optionally write validated/cleaned output
643
+ if args.output and result["valid"]:
644
+ _write_json(args.output, data)
645
+ _info("Validated output written to {}".format(args.output))
646
+
647
+ return 0 if result["valid"] else 1
648
+
649
+
650
+ def cmd_template(args):
651
+ """Handle the 'template' command."""
652
+ if not args.output:
653
+ _err("--output is required for the template command")
654
+ return 2
655
+
656
+ template = generate_template()
657
+ _write_json(args.output, template)
658
+ _info("Template written to {}".format(args.output))
659
+ return 0
660
+
661
+
662
+ def cmd_summary(args):
663
+ """Handle the 'summary' command."""
664
+ if not args.input:
665
+ _err("--input is required for the summary command")
666
+ return 2
667
+
668
+ data, load_err = _load_json(args.input)
669
+ if load_err:
670
+ _err(load_err)
671
+ return 2
672
+
673
+ output_format = getattr(args, "format", "markdown") or "markdown"
674
+
675
+ if output_format == "json":
676
+ summary = generate_summary_json(data)
677
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
678
+ else:
679
+ summary = generate_summary_markdown(data)
680
+ print(summary)
681
+
682
+ return 0
683
+
684
+
685
+ def main():
686
+ parser = argparse.ArgumentParser(
687
+ description="Validate and generate feature-list.json files for the dev-pipeline system.",
688
+ formatter_class=argparse.RawDescriptionHelpFormatter,
689
+ epilog=(
690
+ "Examples:\n"
691
+ " %(prog)s validate --input feature-list.json\n"
692
+ " %(prog)s validate --input feature-list.json --output validated.json\n"
693
+ " %(prog)s template --output feature-list.json\n"
694
+ " %(prog)s summary --input feature-list.json\n"
695
+ " %(prog)s summary --input feature-list.json --format json\n"
696
+ ),
697
+ )
698
+
699
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
700
+
701
+ # -- validate --
702
+ p_validate = subparsers.add_parser(
703
+ "validate",
704
+ help="Validate an existing feature-list.json",
705
+ )
706
+ p_validate.add_argument(
707
+ "--input", required=True, help="Path to input feature-list.json"
708
+ )
709
+ p_validate.add_argument(
710
+ "--output", help="Path to write validated output (optional)"
711
+ )
712
+
713
+ # -- template --
714
+ p_template = subparsers.add_parser(
715
+ "template",
716
+ help="Generate a blank template feature-list.json",
717
+ )
718
+ p_template.add_argument(
719
+ "--output", required=True, help="Path to write template file"
720
+ )
721
+
722
+ # -- summary --
723
+ p_summary = subparsers.add_parser(
724
+ "summary",
725
+ help="Print a summary table of features from a feature-list.json",
726
+ )
727
+ p_summary.add_argument(
728
+ "--input", required=True, help="Path to input feature-list.json"
729
+ )
730
+ p_summary.add_argument(
731
+ "--format",
732
+ choices=["json", "markdown"],
733
+ default="markdown",
734
+ help="Output format (default: markdown)",
735
+ )
736
+
737
+ args = parser.parse_args()
738
+
739
+ if not args.command:
740
+ parser.print_help(sys.stderr)
741
+ return 2
742
+
743
+ dispatch = {
744
+ "validate": cmd_validate,
745
+ "template": cmd_template,
746
+ "summary": cmd_summary,
747
+ }
748
+
749
+ handler = dispatch.get(args.command)
750
+ if handler is None:
751
+ _err("Unknown command: {}".format(args.command))
752
+ return 2
753
+
754
+ return handler(args)
755
+
756
+
757
+ if __name__ == "__main__":
758
+ sys.exit(main())