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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/agent-adapter.js +18 -0
- package/bundled/adapters/claude/command-adapter.js +1 -27
- package/bundled/agents/prizm-dev-team-critic.md +2 -0
- package/bundled/agents/prizm-dev-team-dev.md +2 -0
- package/bundled/agents/prizm-dev-team-reviewer.md +2 -0
- package/bundled/dev-pipeline/README.md +63 -63
- package/bundled/dev-pipeline/assets/feature-list-example.json +1 -1
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
- package/bundled/dev-pipeline/{launch-daemon.sh → launch-feature-daemon.sh} +33 -33
- package/bundled/dev-pipeline/launch-refactor-daemon.sh +454 -0
- package/bundled/dev-pipeline/lib/branch.sh +1 -1
- package/bundled/dev-pipeline/reset-feature.sh +3 -3
- package/bundled/dev-pipeline/reset-refactor.sh +312 -0
- package/bundled/dev-pipeline/{retry-bug.sh → retry-bugfix.sh} +47 -59
- package/bundled/dev-pipeline/retry-feature.sh +41 -54
- package/bundled/dev-pipeline/retry-refactor.sh +358 -0
- package/bundled/dev-pipeline/run-bugfix.sh +6 -0
- package/bundled/dev-pipeline/{run.sh → run-feature.sh} +31 -31
- package/bundled/dev-pipeline/run-refactor.sh +787 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +177 -10
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +419 -0
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +393 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +726 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-code-challenge.md +13 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +7 -0
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +7 -0
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +26 -0
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +5 -0
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-analyze.md +5 -0
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +12 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +29 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +8 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +11 -10
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -3
- package/bundled/dev-pipeline/templates/feature-list-schema.json +1 -1
- package/bundled/dev-pipeline/templates/refactor-list-schema.json +159 -0
- package/bundled/dev-pipeline/templates/sections/ac-verification-checklist.md +13 -0
- package/bundled/dev-pipeline/templates/sections/feature-context.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-analyze-agent.md +9 -8
- package/bundled/dev-pipeline/templates/sections/phase-analyze-full.md +9 -8
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +2 -1
- package/bundled/dev-pipeline/templates/sections/phase-critic-code.md +8 -10
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +9 -10
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +8 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -10
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +8 -15
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +7 -12
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +8 -19
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery.md +75 -0
- package/bundled/skills/_metadata.json +33 -6
- package/bundled/skills/app-planner/SKILL.md +105 -320
- package/bundled/skills/app-planner/assets/app-design-guide.md +101 -0
- package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
- package/bundled/skills/app-planner/references/project-brief-guide.md +49 -80
- package/bundled/skills/bug-fix-workflow/SKILL.md +2 -2
- package/bundled/skills/bug-planner/SKILL.md +68 -5
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +3 -2
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +19 -5
- package/bundled/skills/{dev-pipeline-launcher → feature-pipeline-launcher}/SKILL.md +32 -32
- package/bundled/skills/feature-planner/SKILL.md +337 -0
- package/bundled/skills/{app-planner → feature-planner}/assets/evaluation-guide.md +4 -4
- package/bundled/skills/{app-planner → feature-planner}/assets/planning-guide.md +3 -171
- package/bundled/skills/{app-planner → feature-planner}/references/browser-interaction.md +6 -5
- package/bundled/skills/feature-planner/references/decomposition-patterns.md +75 -0
- package/bundled/skills/{app-planner → feature-planner}/references/error-recovery.md +8 -8
- package/bundled/skills/{app-planner → feature-planner}/references/incremental-feature-planning.md +1 -1
- package/bundled/skills/{app-planner/references/new-app-planning.md → feature-planner/references/new-project-planning.md} +1 -1
- package/bundled/skills/{app-planner → feature-planner}/scripts/validate-and-generate.py +4 -4
- package/bundled/skills/feature-workflow/SKILL.md +23 -23
- package/bundled/skills/prizm-kit/SKILL.md +1 -3
- package/bundled/skills/prizmkit-analyze/SKILL.md +2 -5
- package/bundled/skills/prizmkit-code-review/SKILL.md +2 -2
- package/bundled/skills/prizmkit-committer/SKILL.md +4 -8
- package/bundled/skills/prizmkit-deploy/SKILL.md +1 -5
- package/bundled/skills/prizmkit-implement/SKILL.md +3 -50
- package/bundled/skills/prizmkit-init/SKILL.md +5 -77
- package/bundled/skills/prizmkit-plan/SKILL.md +1 -12
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +6 -24
- package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +21 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +12 -117
- package/bundled/skills/recovery-workflow/SKILL.md +166 -316
- package/bundled/skills/recovery-workflow/evals/evals.json +29 -13
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +232 -274
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +352 -0
- package/bundled/skills/refactor-planner/SKILL.md +436 -0
- package/bundled/skills/refactor-planner/assets/planning-guide.md +292 -0
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +301 -0
- package/bundled/skills/refactor-planner/references/refactor-scoping-guide.md +221 -0
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +786 -0
- package/bundled/skills/refactor-workflow/SKILL.md +299 -319
- package/package.json +1 -1
- package/src/clean.js +3 -3
- package/src/scaffold.js +6 -6
- package/bundled/skills/prizmkit-plan/assets/spec-template.md +0 -56
- package/bundled/skills/prizmkit-plan/references/clarify-guide.md +0 -67
- package/src/config.js +0 -504
- package/src/prompts.js +0 -210
- /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())
|