prizmkit 1.1.1 → 1.1.3

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 (99) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/agent-adapter.js +18 -0
  3. package/bundled/adapters/claude/command-adapter.js +1 -27
  4. package/bundled/agents/prizm-dev-team-critic.md +2 -0
  5. package/bundled/agents/prizm-dev-team-dev.md +2 -0
  6. package/bundled/agents/prizm-dev-team-reviewer.md +2 -0
  7. package/bundled/dev-pipeline/README.md +63 -63
  8. package/bundled/dev-pipeline/assets/feature-list-example.json +1 -1
  9. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
  10. package/bundled/dev-pipeline/{launch-daemon.sh → launch-feature-daemon.sh} +33 -33
  11. package/bundled/dev-pipeline/launch-refactor-daemon.sh +454 -0
  12. package/bundled/dev-pipeline/lib/branch.sh +1 -1
  13. package/bundled/dev-pipeline/reset-feature.sh +3 -3
  14. package/bundled/dev-pipeline/reset-refactor.sh +312 -0
  15. package/bundled/dev-pipeline/{retry-bug.sh → retry-bugfix.sh} +47 -59
  16. package/bundled/dev-pipeline/retry-feature.sh +41 -54
  17. package/bundled/dev-pipeline/retry-refactor.sh +358 -0
  18. package/bundled/dev-pipeline/run-bugfix.sh +6 -0
  19. package/bundled/dev-pipeline/{run.sh → run-feature.sh} +31 -31
  20. package/bundled/dev-pipeline/run-refactor.sh +787 -0
  21. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +177 -10
  22. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +419 -0
  23. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +393 -0
  24. package/bundled/dev-pipeline/scripts/update-refactor-status.py +726 -0
  25. package/bundled/dev-pipeline/templates/agent-prompts/critic-code-challenge.md +13 -0
  26. package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +7 -0
  27. package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +7 -0
  28. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +26 -0
  29. package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +5 -0
  30. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-analyze.md +5 -0
  31. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +12 -0
  32. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +29 -2
  33. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +8 -7
  34. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +11 -10
  35. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -3
  36. package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
  37. package/bundled/dev-pipeline/templates/refactor-list-schema.json +159 -0
  38. package/bundled/dev-pipeline/templates/sections/ac-verification-checklist.md +13 -0
  39. package/bundled/dev-pipeline/templates/sections/feature-context.md +1 -1
  40. package/bundled/dev-pipeline/templates/sections/phase-analyze-agent.md +9 -8
  41. package/bundled/dev-pipeline/templates/sections/phase-analyze-full.md +9 -8
  42. package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +2 -1
  43. package/bundled/dev-pipeline/templates/sections/phase-critic-code.md +8 -10
  44. package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +9 -10
  45. package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +8 -9
  46. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -10
  47. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +8 -15
  48. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +7 -12
  49. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +8 -19
  50. package/bundled/dev-pipeline/templates/sections/test-failure-recovery.md +75 -0
  51. package/bundled/skills/_metadata.json +33 -6
  52. package/bundled/skills/app-planner/SKILL.md +105 -320
  53. package/bundled/skills/app-planner/assets/app-design-guide.md +101 -0
  54. package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
  55. package/bundled/skills/app-planner/references/project-brief-guide.md +49 -80
  56. package/bundled/skills/bug-fix-workflow/SKILL.md +2 -2
  57. package/bundled/skills/bug-planner/SKILL.md +68 -5
  58. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +3 -2
  59. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +19 -5
  60. package/bundled/skills/{dev-pipeline-launcher → feature-pipeline-launcher}/SKILL.md +32 -32
  61. package/bundled/skills/feature-planner/SKILL.md +337 -0
  62. package/bundled/skills/{app-planner → feature-planner}/assets/evaluation-guide.md +4 -4
  63. package/bundled/skills/{app-planner → feature-planner}/assets/planning-guide.md +3 -171
  64. package/bundled/skills/{app-planner → feature-planner}/references/browser-interaction.md +6 -5
  65. package/bundled/skills/feature-planner/references/decomposition-patterns.md +75 -0
  66. package/bundled/skills/{app-planner → feature-planner}/references/error-recovery.md +8 -8
  67. package/bundled/skills/{app-planner → feature-planner}/references/incremental-feature-planning.md +1 -1
  68. package/bundled/skills/{app-planner/references/new-app-planning.md → feature-planner/references/new-project-planning.md} +1 -1
  69. package/bundled/skills/{app-planner → feature-planner}/scripts/validate-and-generate.py +4 -4
  70. package/bundled/skills/feature-workflow/SKILL.md +23 -23
  71. package/bundled/skills/prizm-kit/SKILL.md +1 -3
  72. package/bundled/skills/prizmkit-analyze/SKILL.md +2 -5
  73. package/bundled/skills/prizmkit-code-review/SKILL.md +2 -2
  74. package/bundled/skills/prizmkit-committer/SKILL.md +4 -8
  75. package/bundled/skills/prizmkit-deploy/SKILL.md +1 -5
  76. package/bundled/skills/prizmkit-implement/SKILL.md +3 -50
  77. package/bundled/skills/prizmkit-init/SKILL.md +5 -77
  78. package/bundled/skills/prizmkit-plan/SKILL.md +1 -12
  79. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +6 -24
  80. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +21 -0
  81. package/bundled/skills/prizmkit-retrospective/SKILL.md +12 -117
  82. package/bundled/skills/recovery-workflow/SKILL.md +166 -316
  83. package/bundled/skills/recovery-workflow/evals/evals.json +29 -13
  84. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +232 -274
  85. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +352 -0
  86. package/bundled/skills/refactor-planner/SKILL.md +436 -0
  87. package/bundled/skills/refactor-planner/assets/planning-guide.md +292 -0
  88. package/bundled/skills/refactor-planner/references/behavior-preservation.md +301 -0
  89. package/bundled/skills/refactor-planner/references/refactor-scoping-guide.md +221 -0
  90. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +786 -0
  91. package/bundled/skills/refactor-workflow/SKILL.md +299 -319
  92. package/package.json +1 -1
  93. package/src/clean.js +3 -3
  94. package/src/scaffold.js +6 -6
  95. package/bundled/skills/prizmkit-plan/assets/spec-template.md +0 -56
  96. package/bundled/skills/prizmkit-plan/references/clarify-guide.md +0 -67
  97. package/src/config.js +0 -504
  98. package/src/prompts.js +0 -210
  99. /package/bundled/skills/{dev-pipeline-launcher → feature-pipeline-launcher}/scripts/preflight-check.py +0 -0
@@ -0,0 +1,786 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate-and-generate-refactor.py - Validate and generate refactor-list.json files
4
+ for the dev-pipeline system.
5
+
6
+ Commands:
7
+ validate Validate an existing refactor-list.json
8
+ template Generate a blank template refactor-list.json
9
+ summary Print a summary table of refactors from a refactor-list.json
10
+
11
+ Usage:
12
+ python3 validate-and-generate-refactor.py validate --input refactor-list.json [--output validated.json]
13
+ python3 validate-and-generate-refactor.py template --output refactor-list.json
14
+ python3 validate-and-generate-refactor.py summary --input refactor-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
+
26
+ # ---------------------------------------------------------------------------
27
+ # Constants
28
+ # ---------------------------------------------------------------------------
29
+
30
+ SCHEMA_VERSION = "dev-pipeline-refactor-list-v1"
31
+
32
+ VALID_STATUSES = {"pending", "in_progress", "completed", "failed", "skipped"}
33
+ VALID_TYPES = {"extract", "rename", "restructure", "simplify", "decouple", "migrate"}
34
+ VALID_PRIORITIES = {"critical", "high", "medium", "low"}
35
+ VALID_COMPLEXITIES = {"low", "medium", "high"}
36
+ VALID_PRESERVATION_STRATEGIES = {"test-gate", "snapshot", "manual"}
37
+
38
+ REFACTOR_ID_RE = re.compile(r"^R-\d{3}$")
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(refactors):
93
+ """Return (has_cycles: bool, max_depth: int) using Kahn's topological sort.
94
+
95
+ *refactors* is the list of refactor 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 = {r["id"] for r in refactors}
102
+ # Build adjacency list and in-degree map.
103
+ adj = {rid: [] for rid in id_set} # dependency -> [dependent]
104
+ in_degree = {rid: 0 for rid in id_set}
105
+
106
+ for refactor in refactors:
107
+ rid = refactor["id"]
108
+ for dep in refactor.get("dependencies", []):
109
+ if dep in id_set:
110
+ adj[dep].append(rid)
111
+ in_degree[rid] += 1
112
+
113
+ # Kahn's algorithm
114
+ queue = collections.deque()
115
+ for rid, deg in in_degree.items():
116
+ if deg == 0:
117
+ queue.append(rid)
118
+
119
+ sorted_order = []
120
+ # Track depth for each node to compute max dependency depth.
121
+ depth = {rid: 0 for rid 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_refactor_list(data):
145
+ """Validate a parsed refactor-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
+ project_name = data.get("project_name")
162
+ if not isinstance(project_name, str) or not project_name.strip():
163
+ errors.append("project_name must be a non-empty string")
164
+
165
+ refactors = data.get("refactors")
166
+ if not isinstance(refactors, list) or len(refactors) == 0:
167
+ errors.append("refactors must be a non-empty array")
168
+ # Early-out: nothing else to validate if refactors are missing.
169
+ return {
170
+ "valid": False,
171
+ "errors": errors,
172
+ "warnings": warnings,
173
+ "stats": {
174
+ "total_refactors": 0,
175
+ "type_distribution": {},
176
+ "complexity_distribution": {},
177
+ "max_dependency_depth": 0,
178
+ "has_cycles": False,
179
+ },
180
+ }
181
+
182
+ # ------------------------------------------------------------------
183
+ # 2. Per-refactor validation
184
+ # ------------------------------------------------------------------
185
+ required_keys = {
186
+ "id", "title", "description", "scope", "type", "priority",
187
+ "complexity", "behavior_preservation", "acceptance_criteria",
188
+ "dependencies", "status",
189
+ }
190
+
191
+ seen_ids = set()
192
+ type_dist = {t: 0 for t in VALID_TYPES}
193
+ complexity_dist = {"low": 0, "medium": 0, "high": 0}
194
+
195
+ for idx, refactor in enumerate(refactors):
196
+ label = "refactors[{}]".format(idx)
197
+
198
+ # -- Required keys --
199
+ if not isinstance(refactor, dict):
200
+ errors.append("{} is not an object".format(label))
201
+ continue
202
+
203
+ missing = required_keys - set(refactor.keys())
204
+ if missing:
205
+ errors.append("{} missing required keys: {}".format(
206
+ label, ", ".join(sorted(missing))
207
+ ))
208
+
209
+ # -- ID format & uniqueness --
210
+ rid = refactor.get("id", "")
211
+ if not REFACTOR_ID_RE.match(str(rid)):
212
+ errors.append(
213
+ "{}: id '{}' does not match pattern R-NNN".format(label, rid)
214
+ )
215
+ if rid in seen_ids:
216
+ errors.append("{}: duplicate id '{}'".format(label, rid))
217
+ seen_ids.add(rid)
218
+
219
+ # -- Title --
220
+ title = refactor.get("title")
221
+ if not isinstance(title, str) or not title.strip():
222
+ errors.append("{}: title must be a non-empty string".format(label))
223
+
224
+ # -- Description depth check --
225
+ desc = refactor.get("description", "")
226
+ if not isinstance(desc, str) or not desc.strip():
227
+ errors.append("{}: description must be a non-empty string".format(label))
228
+ elif isinstance(desc, str) and desc.strip():
229
+ word_count = len(desc.split())
230
+ complexity = refactor.get("complexity", "medium")
231
+ min_words = {"low": 30, "medium": 50, "high": 80}.get(complexity, 50)
232
+ if word_count < 15:
233
+ errors.append(
234
+ "{}: description too short ({} words, minimum 15). "
235
+ "Include: what to refactor, why, affected components, "
236
+ "and expected outcome.".format(label, word_count)
237
+ )
238
+ elif word_count < min_words:
239
+ warnings.append(
240
+ "{}: description only {} words (recommend {}+ for {} complexity). "
241
+ "Richer descriptions produce better pipeline results.".format(
242
+ label, word_count, min_words, complexity
243
+ )
244
+ )
245
+
246
+ # -- Scope --
247
+ scope = refactor.get("scope")
248
+ if isinstance(scope, dict):
249
+ scope_files = scope.get("files")
250
+ if not isinstance(scope_files, list):
251
+ errors.append("{}: scope.files must be an array of strings".format(label))
252
+ elif not all(isinstance(f, str) for f in scope_files):
253
+ errors.append("{}: scope.files must contain only strings".format(label))
254
+
255
+ scope_modules = scope.get("modules")
256
+ if not isinstance(scope_modules, list):
257
+ errors.append("{}: scope.modules must be an array of strings".format(label))
258
+ elif not all(isinstance(m, str) for m in scope_modules):
259
+ errors.append("{}: scope.modules must contain only strings".format(label))
260
+ else:
261
+ errors.append("{}: scope must be an object with 'files' and 'modules'".format(label))
262
+
263
+ # -- Type --
264
+ rtype = refactor.get("type")
265
+ if isinstance(rtype, str) and rtype in VALID_TYPES:
266
+ type_dist[rtype] += 1
267
+ else:
268
+ errors.append(
269
+ "{}: type must be one of {}, got {}".format(
270
+ label, ", ".join(sorted(VALID_TYPES)), repr(rtype)
271
+ )
272
+ )
273
+
274
+ # -- Priority --
275
+ priority = refactor.get("priority")
276
+ if not isinstance(priority, str) or priority not in VALID_PRIORITIES:
277
+ errors.append(
278
+ "{}: priority must be one of {}, got {}".format(
279
+ label, ", ".join(sorted(VALID_PRIORITIES)), repr(priority)
280
+ )
281
+ )
282
+
283
+ # -- Complexity --
284
+ complexity = refactor.get("complexity")
285
+ if isinstance(complexity, str) and complexity in VALID_COMPLEXITIES:
286
+ complexity_dist[complexity] += 1
287
+ else:
288
+ errors.append(
289
+ "{}: complexity must be one of {}, got {}".format(
290
+ label, ", ".join(sorted(VALID_COMPLEXITIES)), repr(complexity)
291
+ )
292
+ )
293
+
294
+ # -- Behavior preservation --
295
+ bp = refactor.get("behavior_preservation")
296
+ if isinstance(bp, dict):
297
+ strategy = bp.get("strategy")
298
+ if not isinstance(strategy, str) or strategy not in VALID_PRESERVATION_STRATEGIES:
299
+ errors.append(
300
+ "{}: behavior_preservation.strategy must be one of {}, got {}".format(
301
+ label,
302
+ ", ".join(sorted(VALID_PRESERVATION_STRATEGIES)),
303
+ repr(strategy),
304
+ )
305
+ )
306
+
307
+ # Optional fields validation
308
+ existing_tests = bp.get("existing_tests")
309
+ if existing_tests is not None and not isinstance(existing_tests, bool):
310
+ errors.append(
311
+ "{}: behavior_preservation.existing_tests must be a boolean, got {}".format(
312
+ label, type(existing_tests).__name__
313
+ )
314
+ )
315
+
316
+ new_tests = bp.get("new_tests_needed")
317
+ if new_tests is not None:
318
+ if not isinstance(new_tests, list):
319
+ errors.append(
320
+ "{}: behavior_preservation.new_tests_needed must be an array of strings".format(label)
321
+ )
322
+ elif not all(isinstance(t, str) for t in new_tests):
323
+ errors.append(
324
+ "{}: behavior_preservation.new_tests_needed must contain only strings".format(label)
325
+ )
326
+ else:
327
+ errors.append(
328
+ "{}: behavior_preservation must be an object with 'strategy'".format(label)
329
+ )
330
+
331
+ # -- Acceptance criteria --
332
+ criteria = refactor.get("acceptance_criteria")
333
+ if isinstance(criteria, list):
334
+ if len(criteria) < 1:
335
+ errors.append("{}: must have at least 1 acceptance criterion".format(label))
336
+ elif len(criteria) < 3:
337
+ warnings.append(
338
+ "{}: only {} acceptance criteria (recommend at least 3)".format(
339
+ label, len(criteria)
340
+ )
341
+ )
342
+ for ci, c in enumerate(criteria):
343
+ if not isinstance(c, str) or not c.strip():
344
+ errors.append(
345
+ "{}: acceptance_criteria[{}] must be a non-empty string".format(label, ci)
346
+ )
347
+ else:
348
+ errors.append("{}: acceptance_criteria must be an array".format(label))
349
+
350
+ # -- Dependencies (list of strings matching R-NNN) --
351
+ deps = refactor.get("dependencies")
352
+ if isinstance(deps, list):
353
+ for dep in deps:
354
+ if not isinstance(dep, str) or not REFACTOR_ID_RE.match(dep):
355
+ errors.append(
356
+ "{}: dependency '{}' does not match R-NNN pattern".format(label, dep)
357
+ )
358
+ else:
359
+ errors.append("{}: dependencies must be an array".format(label))
360
+
361
+ # -- Status --
362
+ status = refactor.get("status")
363
+ if status not in VALID_STATUSES:
364
+ errors.append(
365
+ "{}: status '{}' invalid, must be one of: {}".format(
366
+ label, status, ", ".join(sorted(VALID_STATUSES))
367
+ )
368
+ )
369
+
370
+ # ------------------------------------------------------------------
371
+ # 3. Dependency validation
372
+ # ------------------------------------------------------------------
373
+ all_ids = {r.get("id") for r in refactors}
374
+ for idx, refactor in enumerate(refactors):
375
+ label = "refactors[{}]".format(idx)
376
+ deps = refactor.get("dependencies", [])
377
+ if isinstance(deps, list):
378
+ for dep in deps:
379
+ if isinstance(dep, str) and REFACTOR_ID_RE.match(dep) and dep not in all_ids:
380
+ errors.append(
381
+ "{}: dependency '{}' does not exist in refactor list".format(label, dep)
382
+ )
383
+
384
+ # -- Cycle detection --
385
+ has_cycles, max_depth = _detect_cycles(refactors)
386
+ if has_cycles:
387
+ errors.append("Dependency graph contains cycles (not a valid DAG)")
388
+
389
+ # ------------------------------------------------------------------
390
+ # 4. Build result
391
+ # ------------------------------------------------------------------
392
+ is_valid = len(errors) == 0
393
+
394
+ return {
395
+ "valid": is_valid,
396
+ "errors": errors,
397
+ "warnings": warnings,
398
+ "stats": {
399
+ "total_refactors": len(refactors),
400
+ "type_distribution": type_dist,
401
+ "complexity_distribution": complexity_dist,
402
+ "max_dependency_depth": max_depth,
403
+ "has_cycles": has_cycles,
404
+ },
405
+ }
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # Template generation
410
+ # ---------------------------------------------------------------------------
411
+
412
+
413
+ def generate_template():
414
+ """Return a template refactor-list dict with placeholder values."""
415
+ return {
416
+ "$schema": SCHEMA_VERSION,
417
+ "project_name": "YOUR_PROJECT_NAME",
418
+ "refactors": [
419
+ {
420
+ "id": "R-001",
421
+ "title": "Extract authentication module",
422
+ "description": (
423
+ "Extract authentication logic from the monolithic user service "
424
+ "into a dedicated auth module. This will improve separation of "
425
+ "concerns, make the auth logic independently testable, and reduce "
426
+ "coupling between user management and authentication flows."
427
+ ),
428
+ "scope": {
429
+ "files": [
430
+ "src/services/user-service.js",
431
+ "src/middleware/auth.js",
432
+ ],
433
+ "modules": ["user-service", "auth"],
434
+ },
435
+ "type": "extract",
436
+ "priority": "high",
437
+ "complexity": "medium",
438
+ "behavior_preservation": {
439
+ "strategy": "test-gate",
440
+ "existing_tests": True,
441
+ "new_tests_needed": [
442
+ "Auth module unit tests",
443
+ "Integration test for login flow",
444
+ ],
445
+ },
446
+ "acceptance_criteria": [
447
+ "Auth logic moved to dedicated module",
448
+ "All existing auth tests pass without modification",
449
+ "No changes to public API surface",
450
+ ],
451
+ "dependencies": [],
452
+ "status": "pending",
453
+ }
454
+ ],
455
+ }
456
+
457
+
458
+ # ---------------------------------------------------------------------------
459
+ # Summary
460
+ # ---------------------------------------------------------------------------
461
+
462
+
463
+ def _build_dependency_graph_text(refactors):
464
+ """Build a human-readable text representation of the dependency graph.
465
+
466
+ Returns a list of lines.
467
+ """
468
+ all_ids = [r["id"] for r in refactors]
469
+
470
+ # Build adjacency: dependency -> list of dependents (forward edges)
471
+ dependents = {rid: [] for rid in all_ids}
472
+ has_parent = set()
473
+ for refactor in refactors:
474
+ for dep in refactor.get("dependencies", []):
475
+ if dep in dependents:
476
+ dependents[dep].append(refactor["id"])
477
+ has_parent.add(refactor["id"])
478
+
479
+ # Sort children for deterministic output
480
+ for rid in dependents:
481
+ dependents[rid] = sorted(set(dependents[rid]))
482
+
483
+ # Roots: refactors with no incoming dependencies
484
+ roots = [rid for rid in all_ids if rid not in has_parent]
485
+ if not roots:
486
+ return ["(cycle detected - no root nodes)"]
487
+ if not any(dependents[r] for r in all_ids):
488
+ return ["(no dependencies)"]
489
+
490
+ result_lines = []
491
+
492
+ def _render(node, prefix, is_continuation):
493
+ """Render a node and its dependents recursively."""
494
+ children = dependents.get(node, [])
495
+ if not children:
496
+ return
497
+
498
+ for i, child in enumerate(children):
499
+ if i == 0:
500
+ result_lines[-1] += " -> {}".format(child)
501
+ _render(child, prefix + " " * (len(node) + 4), True)
502
+ else:
503
+ line = "{}-> {}".format(prefix, child)
504
+ result_lines.append(line)
505
+ child_prefix = prefix + " " * (len(child) + 4)
506
+ _render(child, child_prefix, True)
507
+
508
+ for root in sorted(roots):
509
+ result_lines.append(root)
510
+ _render(root, " " * len(root), False)
511
+
512
+ return result_lines
513
+
514
+
515
+ def generate_summary_markdown(data):
516
+ """Generate a markdown summary of the refactor list."""
517
+ project_name = data.get("project_name", "Unknown")
518
+ refactors = data.get("refactors", [])
519
+
520
+ lines = []
521
+ lines.append("# Refactor Summary: {}".format(project_name))
522
+ lines.append("")
523
+
524
+ # Table header
525
+ lines.append("| ID | Title | Type | Complexity | Priority | Dependencies | Criteria | Strategy |")
526
+ lines.append("|----|-------|------|------------|----------|--------------|----------|----------|")
527
+
528
+ for refactor in refactors:
529
+ rid = refactor.get("id", "?")
530
+ title = refactor.get("title", "?")
531
+ rtype = refactor.get("type", "-")
532
+ complexity = refactor.get("complexity", "-")
533
+ priority = refactor.get("priority", "?")
534
+ deps = refactor.get("dependencies", [])
535
+ deps_str = ", ".join(deps) if deps else "-"
536
+ criteria_count = len(refactor.get("acceptance_criteria", []))
537
+ bp = refactor.get("behavior_preservation", {})
538
+ strategy = bp.get("strategy", "-") if isinstance(bp, dict) else "-"
539
+
540
+ lines.append("| {} | {} | {} | {} | {} | {} | {} | {} |".format(
541
+ rid, title, rtype, complexity, priority, deps_str, criteria_count, strategy
542
+ ))
543
+
544
+ lines.append("")
545
+
546
+ # Dependency graph
547
+ lines.append("## Dependency Graph")
548
+ graph_lines = _build_dependency_graph_text(refactors)
549
+ for gl in graph_lines:
550
+ lines.append(gl)
551
+ lines.append("")
552
+
553
+ # Statistics
554
+ type_dist = {t: 0 for t in VALID_TYPES}
555
+ complexity_dist = {"low": 0, "medium": 0, "high": 0}
556
+ for refactor in refactors:
557
+ t = refactor.get("type")
558
+ if t in type_dist:
559
+ type_dist[t] += 1
560
+ c = refactor.get("complexity")
561
+ if c in complexity_dist:
562
+ complexity_dist[c] += 1
563
+
564
+ _, max_depth = _detect_cycles(refactors)
565
+
566
+ lines.append("## Statistics")
567
+ lines.append("- Total refactors: {}".format(len(refactors)))
568
+ lines.append("- Complexity: {} low, {} medium, {} high".format(
569
+ complexity_dist["low"], complexity_dist["medium"], complexity_dist["high"]
570
+ ))
571
+ type_parts = ["{} {}".format(v, k) for k, v in sorted(type_dist.items()) if v > 0]
572
+ if type_parts:
573
+ lines.append("- Types: {}".format(", ".join(type_parts)))
574
+ lines.append("- Max dependency depth: {}".format(max_depth))
575
+
576
+ return "\n".join(lines)
577
+
578
+
579
+ def generate_summary_json(data):
580
+ """Generate a JSON summary of the refactor list."""
581
+ refactors = data.get("refactors", [])
582
+
583
+ type_dist = {t: 0 for t in VALID_TYPES}
584
+ complexity_dist = {"low": 0, "medium": 0, "high": 0}
585
+ for refactor in refactors:
586
+ t = refactor.get("type")
587
+ if t in type_dist:
588
+ type_dist[t] += 1
589
+ c = refactor.get("complexity")
590
+ if c in complexity_dist:
591
+ complexity_dist[c] += 1
592
+
593
+ has_cycles, max_depth = _detect_cycles(refactors)
594
+
595
+ refactor_summaries = []
596
+ for refactor in refactors:
597
+ bp = refactor.get("behavior_preservation", {})
598
+ refactor_summaries.append({
599
+ "id": refactor.get("id"),
600
+ "title": refactor.get("title"),
601
+ "type": refactor.get("type"),
602
+ "priority": refactor.get("priority"),
603
+ "complexity": refactor.get("complexity"),
604
+ "dependencies": refactor.get("dependencies", []),
605
+ "acceptance_criteria_count": len(refactor.get("acceptance_criteria", [])),
606
+ "preservation_strategy": bp.get("strategy") if isinstance(bp, dict) else None,
607
+ "status": refactor.get("status"),
608
+ })
609
+
610
+ return {
611
+ "project_name": data.get("project_name", ""),
612
+ "refactors": refactor_summaries,
613
+ "stats": {
614
+ "total_refactors": len(refactors),
615
+ "type_distribution": type_dist,
616
+ "complexity_distribution": complexity_dist,
617
+ "max_dependency_depth": max_depth,
618
+ "has_cycles": has_cycles,
619
+ },
620
+ }
621
+
622
+
623
+ # ---------------------------------------------------------------------------
624
+ # CLI
625
+ # ---------------------------------------------------------------------------
626
+
627
+
628
+ def cmd_validate(args):
629
+ """Handle the 'validate' command."""
630
+ if not args.input:
631
+ _err("--input is required for the validate command")
632
+ return 2
633
+
634
+ data, load_err = _load_json(args.input)
635
+ if load_err:
636
+ _err(load_err)
637
+ result = {
638
+ "valid": False,
639
+ "errors": [load_err],
640
+ "warnings": [],
641
+ "stats": {
642
+ "total_refactors": 0,
643
+ "type_distribution": {},
644
+ "complexity_distribution": {},
645
+ "max_dependency_depth": 0,
646
+ "has_cycles": False,
647
+ },
648
+ }
649
+ print(json.dumps(result, indent=2, ensure_ascii=False))
650
+ return 2
651
+
652
+ result = validate_refactor_list(data)
653
+
654
+ # Print results to stdout
655
+ print(json.dumps(result, indent=2, ensure_ascii=False))
656
+
657
+ # Log to stderr for humans
658
+ if result["valid"]:
659
+ _info("Validation passed with {} warning(s)".format(len(result["warnings"])))
660
+ else:
661
+ _err("Validation failed with {} error(s) and {} warning(s)".format(
662
+ len(result["errors"]), len(result["warnings"])
663
+ ))
664
+
665
+ for e in result["errors"]:
666
+ _err(" " + e)
667
+ for w in result["warnings"]:
668
+ _warn(" " + w)
669
+
670
+ # Optionally write validated/cleaned output
671
+ if args.output and result["valid"]:
672
+ _write_json(args.output, data)
673
+ _info("Validated output written to {}".format(args.output))
674
+
675
+ return 0 if result["valid"] else 1
676
+
677
+
678
+ def cmd_template(args):
679
+ """Handle the 'template' command."""
680
+ if not args.output:
681
+ _err("--output is required for the template command")
682
+ return 2
683
+
684
+ template = generate_template()
685
+ _write_json(args.output, template)
686
+ _info("Template written to {}".format(args.output))
687
+ return 0
688
+
689
+
690
+ def cmd_summary(args):
691
+ """Handle the 'summary' command."""
692
+ if not args.input:
693
+ _err("--input is required for the summary command")
694
+ return 2
695
+
696
+ data, load_err = _load_json(args.input)
697
+ if load_err:
698
+ _err(load_err)
699
+ return 2
700
+
701
+ output_format = getattr(args, "format", "markdown") or "markdown"
702
+
703
+ if output_format == "json":
704
+ summary = generate_summary_json(data)
705
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
706
+ else:
707
+ summary = generate_summary_markdown(data)
708
+ print(summary)
709
+
710
+ return 0
711
+
712
+
713
+ def main():
714
+ parser = argparse.ArgumentParser(
715
+ description="Validate and generate refactor-list.json files for the dev-pipeline system.",
716
+ formatter_class=argparse.RawDescriptionHelpFormatter,
717
+ epilog=(
718
+ "Examples:\n"
719
+ " %(prog)s validate --input refactor-list.json\n"
720
+ " %(prog)s validate --input refactor-list.json --output validated.json\n"
721
+ " %(prog)s template --output refactor-list.json\n"
722
+ " %(prog)s summary --input refactor-list.json\n"
723
+ " %(prog)s summary --input refactor-list.json --format json\n"
724
+ ),
725
+ )
726
+
727
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
728
+
729
+ # -- validate --
730
+ p_validate = subparsers.add_parser(
731
+ "validate",
732
+ help="Validate an existing refactor-list.json",
733
+ )
734
+ p_validate.add_argument(
735
+ "--input", required=True, help="Path to input refactor-list.json"
736
+ )
737
+ p_validate.add_argument(
738
+ "--output", help="Path to write validated output (optional)"
739
+ )
740
+
741
+ # -- template --
742
+ p_template = subparsers.add_parser(
743
+ "template",
744
+ help="Generate a blank template refactor-list.json",
745
+ )
746
+ p_template.add_argument(
747
+ "--output", required=True, help="Path to write template file"
748
+ )
749
+
750
+ # -- summary --
751
+ p_summary = subparsers.add_parser(
752
+ "summary",
753
+ help="Print a summary table of refactors from a refactor-list.json",
754
+ )
755
+ p_summary.add_argument(
756
+ "--input", required=True, help="Path to input refactor-list.json"
757
+ )
758
+ p_summary.add_argument(
759
+ "--format",
760
+ choices=["json", "markdown"],
761
+ default="markdown",
762
+ help="Output format (default: markdown)",
763
+ )
764
+
765
+ args = parser.parse_args()
766
+
767
+ if not args.command:
768
+ parser.print_help(sys.stderr)
769
+ return 2
770
+
771
+ dispatch = {
772
+ "validate": cmd_validate,
773
+ "template": cmd_template,
774
+ "summary": cmd_summary,
775
+ }
776
+
777
+ handler = dispatch.get(args.command)
778
+ if handler is None:
779
+ _err("Unknown command: {}".format(args.command))
780
+ return 2
781
+
782
+ return handler(args)
783
+
784
+
785
+ if __name__ == "__main__":
786
+ sys.exit(main())