bmad-method 6.6.1-next.8 → 6.7.0

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 (27) hide show
  1. package/README.md +3 -3
  2. package/{tools/installer/modules/registry-fallback.yaml → bmad-modules.yaml} +29 -15
  3. package/package.json +4 -4
  4. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +18 -11
  5. package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +13 -8
  6. package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +54 -57
  7. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
  8. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-template.md +40 -30
  9. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-validation-checklist.md +126 -21
  10. package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/validation-report-template.html +193 -58
  11. package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +47 -13
  12. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +27 -12
  13. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +97 -0
  14. package/src/bmm-skills/module.yaml +2 -2
  15. package/src/core-skills/module.yaml +1 -1
  16. package/tools/installer/core/installer.js +1 -22
  17. package/tools/installer/core/manifest.js +0 -22
  18. package/tools/installer/modules/channel-plan.js +1 -1
  19. package/tools/installer/modules/external-manager.js +9 -27
  20. package/tools/installer/modules/official-modules.js +9 -48
  21. package/tools/installer/prompts.js +149 -0
  22. package/tools/installer/ui.js +13 -197
  23. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/facilitation-guide.md +0 -79
  24. package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validation-render.md +0 -58
  25. package/src/bmm-skills/2-plan-workflows/bmad-prd/scripts/render-validation-html.py +0 -290
  26. package/tools/installer/modules/community-manager.js +0 -704
  27. package/tools/installer/modules/registry-client.js +0 -187
@@ -1,290 +0,0 @@
1
- #!/usr/bin/env python3
2
- # /// script
3
- # requires-python = ">=3.10"
4
- # ///
5
- """Render a PRD validation findings JSON into HTML + markdown reports.
6
-
7
- Reads structured findings produced by the validator subagent, groups them by
8
- category (explicit `category` field, else derived from ID prefix), computes a
9
- pass/warn/fail summary and grade, substitutes into the configured HTML
10
- template, writes a markdown companion at the same path with `.md` extension,
11
- and optionally opens the HTML in the default browser.
12
- """
13
-
14
- import argparse
15
- import html
16
- import json
17
- import string
18
- import sys
19
- import webbrowser
20
- from datetime import datetime
21
- from pathlib import Path
22
-
23
- CATEGORY_FROM_PREFIX = {
24
- "Q": "Quality",
25
- "D": "Discipline",
26
- "S": "Structural integrity",
27
- "STK": "Stakes-gated",
28
- "M": "Mechanical",
29
- }
30
-
31
- CATEGORY_ORDER = ["Quality", "Discipline", "Structural integrity", "Stakes-gated", "Mechanical"]
32
-
33
-
34
- def category_for(finding: dict) -> str:
35
- explicit = finding.get("category")
36
- if explicit:
37
- return explicit
38
- fid = finding.get("id", "")
39
- prefix = fid.split("-", 1)[0] if "-" in fid else fid
40
- return CATEGORY_FROM_PREFIX.get(prefix, prefix or "Other")
41
-
42
-
43
- def compute_stats(findings: list[dict]) -> dict:
44
- total = len(findings)
45
- by_status = {"pass": 0, "warn": 0, "fail": 0, "n/a": 0}
46
- failed_critical = 0
47
- failed_high = 0
48
- for f in findings:
49
- status = (f.get("status") or "n/a").lower()
50
- if status in by_status:
51
- by_status[status] += 1
52
- if status == "fail":
53
- sev = (f.get("severity") or "low").lower()
54
- if sev == "critical":
55
- failed_critical += 1
56
- elif sev == "high":
57
- failed_high += 1
58
- return {
59
- "total": total,
60
- "passed": by_status["pass"],
61
- "warned": by_status["warn"],
62
- "failed": by_status["fail"],
63
- "na": by_status["n/a"],
64
- "failed_critical": failed_critical,
65
- "failed_high": failed_high,
66
- }
67
-
68
-
69
- def grade_from(stats: dict) -> tuple[str, str]:
70
- if stats["failed_critical"] > 0:
71
- return "Poor", "grade-poor"
72
- if stats["failed_high"] >= 1 or stats["failed"] >= 4:
73
- return "Fair", "grade-fair"
74
- if stats["failed"] > 0 or stats["warned"] > 2:
75
- return "Good", "grade-good"
76
- return "Excellent", "grade-excellent"
77
-
78
-
79
- def render_score_bar(stats: dict, width: int = 480, height: int = 22) -> str:
80
- total = max(stats["total"], 1)
81
- p = stats["passed"] / total * width
82
- w = stats["warned"] / total * width
83
- f = stats["failed"] / total * width
84
- n = stats["na"] / total * width
85
- return (
86
- f'<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img" '
87
- f'aria-label="Pass / warn / fail / n-a breakdown">'
88
- f'<rect x="0" y="0" width="{p:.1f}" height="{height}" fill="#22c55e"/>'
89
- f'<rect x="{p:.1f}" y="0" width="{w:.1f}" height="{height}" fill="#eab308"/>'
90
- f'<rect x="{p + w:.1f}" y="0" width="{f:.1f}" height="{height}" fill="#ef4444"/>'
91
- f'<rect x="{p + w + f:.1f}" y="0" width="{n:.1f}" height="{height}" fill="#94a3b8"/>'
92
- f"</svg>"
93
- )
94
-
95
-
96
- def render_finding(f: dict) -> str:
97
- status = (f.get("status") or "n/a").lower()
98
- severity = (f.get("severity") or "low").lower()
99
- fid = html.escape(f.get("id") or "")
100
- title = html.escape(f.get("title") or fid)
101
- location = html.escape(f.get("location") or "")
102
- note = html.escape(f.get("note") or "")
103
- fix = html.escape(f.get("suggested_fix") or "")
104
-
105
- status_class = "na" if status == "n/a" else status
106
- parts = [
107
- f'<article class="finding finding-{status_class}">',
108
- '<header>',
109
- f'<span class="badge badge-status badge-{status_class}">{status.upper()}</span>',
110
- f'<span class="badge badge-severity badge-sev-{severity}">{severity}</span>',
111
- f'<span class="finding-id">{fid}</span>',
112
- f'<h3 class="finding-title">{title}</h3>',
113
- '</header>',
114
- ]
115
- if location:
116
- parts.append(f'<div class="finding-location"><strong>Location:</strong> {location}</div>')
117
- if note:
118
- parts.append(f'<div class="finding-note">{note}</div>')
119
- if fix:
120
- parts.append(f'<div class="finding-fix"><strong>Suggested fix:</strong> {fix}</div>')
121
- parts.append("</article>")
122
- return "\n".join(parts)
123
-
124
-
125
- def render_category(name: str, findings: list[dict]) -> str:
126
- items = "\n".join(render_finding(f) for f in findings)
127
- name_e = html.escape(name)
128
- return (
129
- f'<section class="category">'
130
- f"<details open>"
131
- f'<summary><h2>{name_e} <span class="count">({len(findings)})</span></h2></summary>'
132
- f"{items}"
133
- f"</details>"
134
- f"</section>"
135
- )
136
-
137
-
138
- SEVERITY_ORDER = ["critical", "high", "medium", "low"]
139
-
140
-
141
- def render_finding_md(f: dict) -> str:
142
- status = (f.get("status") or "n/a").upper()
143
- severity = (f.get("severity") or "low").lower()
144
- fid = f.get("id") or ""
145
- title = f.get("title") or fid
146
- location = f.get("location") or ""
147
- note = f.get("note") or ""
148
- fix = f.get("suggested_fix") or ""
149
-
150
- lines = [f"### [{status}] {fid} — {title} _(severity: {severity})_"]
151
- if location:
152
- lines.append(f"- **Location:** {location}")
153
- if note:
154
- lines.append(f"- **Finding:** {note}")
155
- if fix:
156
- lines.append(f"- **Suggested fix:** {fix}")
157
- return "\n".join(lines)
158
-
159
-
160
- def render_markdown_report(data: dict, findings: list[dict], stats: dict, grade: str) -> str:
161
- prd_name = data.get("prd_name") or "PRD"
162
- prd_path = data.get("prd_path") or ""
163
- checklist_path = data.get("checklist_path") or ""
164
- timestamp = data.get("timestamp") or datetime.now().isoformat(timespec="seconds")
165
- synthesis = data.get("overall_synthesis") or ""
166
-
167
- out = [
168
- f"# Validation Report — {prd_name}",
169
- "",
170
- f"- **PRD:** `{prd_path}`",
171
- f"- **Checklist:** `{checklist_path}`",
172
- f"- **Run at:** {timestamp}",
173
- f"- **Grade:** {grade}",
174
- "",
175
- f"**Summary:** {stats['passed']} pass · {stats['warned']} warn · {stats['failed']} fail · {stats['na']} n/a "
176
- f"(total {stats['total']}; critical fails: {stats['failed_critical']}, high fails: {stats['failed_high']})",
177
- ]
178
- if synthesis:
179
- out += ["", "## Overall synthesis", "", synthesis]
180
-
181
- # Group by severity then status: failed criticals first, then highs, etc.
182
- by_sev: dict[str, list[dict]] = {s: [] for s in SEVERITY_ORDER}
183
- other: list[dict] = []
184
- for f in findings:
185
- sev = (f.get("severity") or "low").lower()
186
- if sev in by_sev:
187
- by_sev[sev].append(f)
188
- else:
189
- other.append(f)
190
-
191
- out += ["", "## Findings by severity"]
192
- any_findings = False
193
- for sev in SEVERITY_ORDER:
194
- items = by_sev[sev]
195
- if not items:
196
- continue
197
- any_findings = True
198
- out += ["", f"### {sev.capitalize()} ({len(items)})", ""]
199
- out += [render_finding_md(f) for f in items]
200
- if other:
201
- any_findings = True
202
- out += ["", f"### Other ({len(other)})", ""]
203
- out += [render_finding_md(f) for f in other]
204
- if not any_findings:
205
- out += ["", "_No findings._"]
206
-
207
- return "\n".join(out) + "\n"
208
-
209
-
210
- def main(argv: list[str]) -> int:
211
- parser = argparse.ArgumentParser(description="Render PRD validation findings to HTML.")
212
- parser.add_argument("--findings", required=True, help="Path to validation-findings.json")
213
- parser.add_argument("--template", required=True, help="Path to HTML template")
214
- parser.add_argument("--output", required=True, help="Path to write the rendered HTML")
215
- parser.add_argument("--open", action="store_true", help="Open the rendered HTML in the default browser")
216
- args = parser.parse_args(argv)
217
-
218
- findings_path = Path(args.findings)
219
- template_path = Path(args.template)
220
- output_path = Path(args.output)
221
-
222
- try:
223
- data = json.loads(findings_path.read_text(encoding="utf-8"))
224
- except FileNotFoundError:
225
- print(f"error: findings file not found: {findings_path}", file=sys.stderr)
226
- return 1
227
- except json.JSONDecodeError as e:
228
- print(f"error: findings file is not valid JSON ({findings_path}): {e}", file=sys.stderr)
229
- return 1
230
- try:
231
- template = template_path.read_text(encoding="utf-8")
232
- except FileNotFoundError:
233
- print(f"error: template file not found: {template_path}", file=sys.stderr)
234
- return 1
235
-
236
- findings = data.get("findings", []) or []
237
-
238
- by_cat: dict[str, list[dict]] = {}
239
- for f in findings:
240
- by_cat.setdefault(category_for(f), []).append(f)
241
-
242
- sorted_cats = sorted(
243
- by_cat.keys(),
244
- key=lambda c: (CATEGORY_ORDER.index(c) if c in CATEGORY_ORDER else 99, c),
245
- )
246
- categories_html = "\n".join(render_category(c, by_cat[c]) for c in sorted_cats)
247
-
248
- stats = compute_stats(findings)
249
- grade, grade_class = grade_from(stats)
250
- score_svg = render_score_bar(stats)
251
-
252
- timestamp = data.get("timestamp") or datetime.now().isoformat(timespec="seconds")
253
- substitutions = {
254
- "prd_name": html.escape(str(data.get("prd_name") or "PRD")),
255
- "prd_path": html.escape(str(data.get("prd_path") or "")),
256
- "checklist_path": html.escape(str(data.get("checklist_path") or "")),
257
- "timestamp": html.escape(timestamp),
258
- "overall_synthesis": html.escape(str(data.get("overall_synthesis") or "")),
259
- "grade": grade,
260
- "grade_class": grade_class,
261
- "total": str(stats["total"]),
262
- "passed": str(stats["passed"]),
263
- "failed": str(stats["failed"]),
264
- "warned": str(stats["warned"]),
265
- "na": str(stats["na"]),
266
- "score_svg": score_svg,
267
- "categories_html": categories_html,
268
- }
269
-
270
- rendered = string.Template(template).safe_substitute(substitutions)
271
- output_path.write_text(rendered, encoding="utf-8")
272
-
273
- md_path = output_path.with_suffix(".md")
274
- md_path.write_text(render_markdown_report(data, findings, stats, grade), encoding="utf-8")
275
-
276
- print(json.dumps({
277
- "output": str(output_path),
278
- "markdown": str(md_path),
279
- "grade": grade,
280
- "stats": stats,
281
- }))
282
-
283
- if args.open:
284
- webbrowser.open(output_path.resolve().as_uri())
285
-
286
- return 0
287
-
288
-
289
- if __name__ == "__main__":
290
- sys.exit(main(sys.argv[1:]))