prizmkit 1.1.57 → 1.1.60

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