ma-agents 3.13.2 → 3.14.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 (70) hide show
  1. package/lib/agents.js +7 -0
  2. package/lib/bmad-cache/bmb/.claude-plugin/marketplace.json +3 -3
  3. package/lib/bmad-cache/bmb/_git_preserved/index +0 -0
  4. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.idx +0 -0
  5. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/{pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.pack → pack-b388526edf307cf56f8e65673466cff074105abf.pack} +0 -0
  6. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.rev +0 -0
  7. package/lib/bmad-cache/bmb/_git_preserved/packed-refs +1 -1
  8. package/lib/bmad-cache/bmb/_git_preserved/refs/heads/main +1 -1
  9. package/lib/bmad-cache/bmb/_git_preserved/shallow +1 -1
  10. package/lib/bmad-cache/bmb/package-lock.json +2 -2
  11. package/lib/bmad-cache/bmb/package.json +1 -1
  12. package/lib/bmad-cache/bmb/samples/bmad-agent-code-coach/scripts/wake.py +78 -0
  13. package/lib/bmad-cache/bmb/samples/bmad-agent-creative-muse/scripts/wake.py +78 -0
  14. package/lib/bmad-cache/bmb/samples/bmad-agent-sentinel/scripts/wake.py +78 -0
  15. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/customize-template.toml +3 -0
  16. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/init-sanctum-template.py +11 -5
  17. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/report-shell.html +1073 -0
  18. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/wake-template.py +78 -0
  19. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/customize.toml +48 -0
  20. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/count_tokens.py +78 -0
  21. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass.py +258 -0
  22. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/process-template.py +20 -0
  23. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/render_report.py +387 -0
  24. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/adapter-claude-code.json +9 -0
  25. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/aggregate_benchmark.py +236 -0
  26. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/memlog.py +197 -0
  27. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_evals.py +513 -390
  28. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_triggers.py +347 -251
  29. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_env_isolation.py +79 -0
  30. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_trigger_detection.py +123 -0
  31. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/tests/test-validate-module.py +82 -4
  32. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/validate-module.py +55 -11
  33. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/customize-template.toml +3 -5
  34. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/report-shell.html +860 -0
  35. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/customize.toml +59 -0
  36. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/count_tokens.py +77 -0
  37. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/init_skill.py +132 -0
  38. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-prompt-metrics.py +186 -221
  39. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/quick_validate.py +117 -0
  40. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/render_report.py +387 -0
  41. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_canon_sync.py +46 -0
  42. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_count_tokens.py +180 -0
  43. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_render_report.py +171 -0
  44. package/lib/bmad-cache/cache-manifest.json +4 -4
  45. package/lib/bmad-extension/.claude-plugin/marketplace.json.template +1 -1
  46. package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +2 -2
  47. package/lib/bmad.js +6 -1
  48. package/lib/templates/instruction-block-git.template.md +25 -25
  49. package/lib/templates/instruction-block-onprem.template.md +86 -86
  50. package/lib/templates/instruction-block-universal.template.md +29 -29
  51. package/package.json +3 -3
  52. package/lib/.bmad-extension-plugin.build-22612-1782454014750/.claude-plugin/marketplace.json +0 -109
  53. package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module-help.csv +0 -62
  54. package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module.yaml +0 -20
  55. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.idx +0 -0
  56. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.rev +0 -0
  57. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/references/sample-init-sanctum.py +0 -274
  58. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/generate-html-report.py +0 -534
  59. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-execution-deps.py +0 -337
  60. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-prompt-metrics.py +0 -425
  61. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-sanctum-architecture.py +0 -385
  62. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-structure-capabilities.py +0 -482
  63. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/Dockerfile +0 -29
  64. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/docker_setup.py +0 -115
  65. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/generate_report.py +0 -184
  66. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/pty_runner.py +0 -171
  67. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/utils.py +0 -260
  68. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/extract-report-json.py +0 -287
  69. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-html-report.py +0 -588
  70. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-execution-deps.py +0 -288
@@ -1,588 +0,0 @@
1
- # /// script
2
- # requires-python = ">=3.9"
3
- # ///
4
-
5
- #!/usr/bin/env python3
6
- """
7
- Generate an interactive HTML quality analysis report from report-data.json.
8
-
9
- Reads the structured report data produced by the report creator and renders
10
- a self-contained HTML report with:
11
- - Grade + narrative at top
12
- - Broken items with fix prompts
13
- - Opportunity themes with "Fix This Theme" prompt generation
14
- - Expandable strengths
15
- - Expandable detailed analysis per dimension
16
- - Link to full markdown report
17
-
18
- Usage:
19
- python3 generate-html-report.py {quality-report-dir} [--open]
20
- """
21
-
22
- from __future__ import annotations
23
-
24
- import argparse
25
- import json
26
- import platform
27
- import subprocess
28
- import sys
29
- from pathlib import Path
30
-
31
-
32
- def load_report_data(report_dir: Path) -> dict:
33
- """Load report-data.json from the report directory."""
34
- data_file = report_dir / 'report-data.json'
35
- if not data_file.exists():
36
- print(f'Error: {data_file} not found', file=sys.stderr)
37
- sys.exit(2)
38
- return json.loads(data_file.read_text(encoding='utf-8'))
39
-
40
-
41
- def build_fix_prompt(skill_path: str, theme: dict) -> str:
42
- """Build a coherent fix prompt for an entire opportunity theme."""
43
- prompt = f"## Task: {theme['name']}\n"
44
- prompt += f"Skill path: {skill_path}\n\n"
45
- prompt += f"### Problem\n{theme['description']}\n\n"
46
- prompt += f"### Fix\n{theme['action']}\n\n"
47
- if theme.get('findings'):
48
- prompt += "### Specific observations to address:\n\n"
49
- for i, f in enumerate(theme['findings'], 1):
50
- loc = f"{f['file']}:{f['line']}" if f.get('file') and f.get('line') else f.get('file', '')
51
- prompt += f"{i}. **{f['title']}**"
52
- if loc:
53
- prompt += f" ({loc})"
54
- if f.get('detail'):
55
- prompt += f"\n {f['detail']}"
56
- prompt += "\n"
57
- return prompt.strip()
58
-
59
-
60
- def build_broken_prompt(skill_path: str, items: list) -> str:
61
- """Build a fix prompt for all broken items."""
62
- prompt = f"## Task: Fix Critical Issues\nSkill path: {skill_path}\n\n"
63
- for i, item in enumerate(items, 1):
64
- loc = f"{item['file']}:{item['line']}" if item.get('file') and item.get('line') else item.get('file', '')
65
- prompt += f"{i}. **[{item.get('severity','high').upper()}] {item['title']}**\n"
66
- if loc:
67
- prompt += f" File: {loc}\n"
68
- if item.get('detail'):
69
- prompt += f" Context: {item['detail']}\n"
70
- if item.get('action'):
71
- prompt += f" Fix: {item['action']}\n"
72
- prompt += "\n"
73
- return prompt.strip()
74
-
75
-
76
- HTML_TEMPLATE = r"""<!DOCTYPE html>
77
- <html lang="en">
78
- <head>
79
- <meta charset="utf-8">
80
- <meta name="viewport" content="width=device-width, initial-scale=1">
81
- <title>BMad Method · Quality Analysis: SKILL_NAME</title>
82
- <style>
83
- :root {
84
- --bg: #0d1117; --surface: #161b22; --surface2: #21262d; --border: #30363d;
85
- --text: #e6edf3; --text-muted: #8b949e; --text-dim: #6e7681;
86
- --critical: #f85149; --high: #f0883e; --medium: #d29922; --low: #58a6ff;
87
- --strength: #3fb950; --suggestion: #a371f7;
88
- --accent: #58a6ff; --accent-hover: #79c0ff;
89
- --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
90
- --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
91
- }
92
- @media (prefers-color-scheme: light) {
93
- :root {
94
- --bg: #ffffff; --surface: #f6f8fa; --surface2: #eaeef2; --border: #d0d7de;
95
- --text: #1f2328; --text-muted: #656d76; --text-dim: #8c959f;
96
- --critical: #cf222e; --high: #bc4c00; --medium: #9a6700; --low: #0969da;
97
- --strength: #1a7f37; --suggestion: #8250df;
98
- --accent: #0969da; --accent-hover: #0550ae;
99
- }
100
- }
101
- * { margin: 0; padding: 0; box-sizing: border-box; }
102
- body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.5; padding: 2rem; max-width: 900px; margin: 0 auto; }
103
- h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
104
- .subtitle { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
105
- .subtitle a { color: var(--accent); text-decoration: none; }
106
- .subtitle a:hover { text-decoration: underline; }
107
- .grade { font-size: 2.5rem; font-weight: 700; margin: 0.5rem 0; }
108
- .grade-Excellent { color: var(--strength); }
109
- .grade-Good { color: var(--low); }
110
- .grade-Fair { color: var(--medium); }
111
- .grade-Poor { color: var(--critical); }
112
- .narrative { color: var(--text-muted); font-size: 0.95rem; margin-bottom: 1.5rem; line-height: 1.6; }
113
- .badge { display: inline-flex; align-items: center; padding: 0.15rem 0.5rem; border-radius: 2rem; font-size: 0.75rem; font-weight: 600; }
114
- .badge-critical { background: color-mix(in srgb, var(--critical) 20%, transparent); color: var(--critical); }
115
- .badge-high { background: color-mix(in srgb, var(--high) 20%, transparent); color: var(--high); }
116
- .badge-medium { background: color-mix(in srgb, var(--medium) 20%, transparent); color: var(--medium); }
117
- .badge-low { background: color-mix(in srgb, var(--low) 20%, transparent); color: var(--low); }
118
- .badge-strength { background: color-mix(in srgb, var(--strength) 20%, transparent); color: var(--strength); }
119
- .section { border: 1px solid var(--border); border-radius: 0.5rem; margin: 0.75rem 0; overflow: hidden; }
120
- .section-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; background: var(--surface); cursor: pointer; user-select: none; }
121
- .section-header:hover { background: var(--surface2); }
122
- .section-header .arrow { font-size: 0.7rem; transition: transform 0.15s; color: var(--text-muted); width: 1rem; }
123
- .section-header.open .arrow { transform: rotate(90deg); }
124
- .section-header .label { font-weight: 600; flex: 1; }
125
- .section-header .count { font-size: 0.8rem; color: var(--text-muted); }
126
- .section-header .actions { display: flex; gap: 0.5rem; }
127
- .section-body { display: none; }
128
- .section-body.open { display: block; }
129
- .item { padding: 0.75rem 1rem; border-top: 1px solid var(--border); }
130
- .item:hover { background: var(--surface); }
131
- .item-title { font-weight: 600; font-size: 0.9rem; }
132
- .item-file { font-family: var(--mono); font-size: 0.75rem; color: var(--text-muted); }
133
- .item-desc { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; }
134
- .item-action { font-size: 0.85rem; margin-top: 0.25rem; }
135
- .item-action strong { color: var(--strength); }
136
- .opp { padding: 1rem; border-top: 1px solid var(--border); }
137
- .opp-header { display: flex; align-items: center; gap: 0.75rem; }
138
- .opp-name { font-weight: 600; font-size: 1rem; flex: 1; }
139
- .opp-count { font-size: 0.8rem; color: var(--text-muted); }
140
- .opp-desc { font-size: 0.9rem; color: var(--text-muted); margin: 0.5rem 0; }
141
- .opp-impact { font-size: 0.85rem; color: var(--text-dim); font-style: italic; }
142
- .opp-findings { margin-top: 0.75rem; padding-left: 1rem; border-left: 2px solid var(--border); display: none; }
143
- .opp-findings.open { display: block; }
144
- .opp-finding { font-size: 0.85rem; padding: 0.25rem 0; color: var(--text-muted); }
145
- .opp-finding .source { font-size: 0.75rem; color: var(--text-dim); }
146
- .btn { background: none; border: 1px solid var(--border); border-radius: 0.25rem; padding: 0.3rem 0.7rem; cursor: pointer; color: var(--text-muted); font-size: 0.8rem; transition: all 0.15s; }
147
- .btn:hover { border-color: var(--accent); color: var(--accent); }
148
- .btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
149
- .btn-primary:hover { background: var(--accent-hover); }
150
- .btn.copied { border-color: var(--strength); color: var(--strength); }
151
- .strength-item { padding: 0.5rem 1rem; border-top: 1px solid var(--border); }
152
- .strength-item .title { font-weight: 600; font-size: 0.9rem; color: var(--strength); }
153
- .strength-item .detail { font-size: 0.85rem; color: var(--text-muted); }
154
- .analysis-section { padding: 0.75rem 1rem; border-top: 1px solid var(--border); }
155
- .analysis-section h4 { font-size: 0.9rem; margin-bottom: 0.25rem; }
156
- .analysis-section p { font-size: 0.85rem; color: var(--text-muted); }
157
- .analysis-finding { font-size: 0.85rem; padding: 0.25rem 0 0.25rem 1rem; border-left: 2px solid var(--border); margin: 0.25rem 0; color: var(--text-muted); }
158
- .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 200; align-items: center; justify-content: center; }
159
- .modal-overlay.visible { display: flex; }
160
- .modal { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1.5rem; width: 90%; max-width: 700px; max-height: 80vh; overflow-y: auto; }
161
- .modal h3 { margin-bottom: 0.75rem; }
162
- .modal pre { background: var(--bg); border: 1px solid var(--border); border-radius: 0.375rem; padding: 1rem; font-family: var(--mono); font-size: 0.8rem; white-space: pre-wrap; word-wrap: break-word; max-height: 50vh; overflow-y: auto; }
163
- .modal-actions { display: flex; gap: 0.75rem; margin-top: 1rem; justify-content: flex-end; }
164
- .recs { padding: 0.75rem 1rem; border-top: 1px solid var(--border); }
165
- .rec { padding: 0.3rem 0; font-size: 0.9rem; }
166
- .rec-rank { font-weight: 700; color: var(--accent); margin-right: 0.5rem; }
167
- .rec-resolves { font-size: 0.8rem; color: var(--text-dim); }
168
- </style>
169
- </head>
170
- <body>
171
-
172
- <div style="color:#a371f7;font-size:0.8rem;font-weight:600;letter-spacing:0.05em;text-transform:uppercase;margin-bottom:0.25rem">BMad Method</div>
173
- <h1>Quality Analysis: <span id="skill-name"></span></h1>
174
- <div class="subtitle" id="subtitle"></div>
175
-
176
- <div id="grade-area"></div>
177
- <div class="narrative" id="narrative"></div>
178
-
179
- <div id="broken-section"></div>
180
- <div id="opportunities-section"></div>
181
- <div id="strengths-section"></div>
182
- <div id="user-experience-section"></div>
183
- <div id="recommendations-section"></div>
184
- <div id="detailed-section"></div>
185
-
186
- <div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
187
- <div class="modal">
188
- <h3 id="modal-title">Generated Prompt</h3>
189
- <pre id="modal-content"></pre>
190
- <div class="modal-actions">
191
- <button class="btn" onclick="closeModal()">Close</button>
192
- <button class="btn btn-primary" onclick="copyModal()">Copy to Clipboard</button>
193
- </div>
194
- </div>
195
- </div>
196
-
197
- <script>
198
- const RAW = JSON.parse(document.getElementById('report-data').textContent);
199
- const DATA = normalize(RAW);
200
-
201
- function normalize(d) {
202
- // Fix meta field variants
203
- if (d.meta) {
204
- d.meta.skill_name = d.meta.skill_name || d.meta.skill || d.meta.name || 'Unknown';
205
- d.meta.scanner_count = typeof d.meta.scanner_count === 'number' ? d.meta.scanner_count
206
- : Array.isArray(d.meta.scanners_run) ? d.meta.scanners_run.length
207
- : d.meta.scanner_count || 0;
208
- }
209
- // Fix strengths: plain strings → objects
210
- d.strengths = (d.strengths || []).map(s =>
211
- typeof s === 'string' ? { title: s, detail: '' } : { title: s.title || '', detail: s.detail || '' }
212
- );
213
- // Fix opportunities: title→name, findings_resolved→findings
214
- (d.opportunities || []).forEach(o => {
215
- o.name = o.name || o.title || '';
216
- o.finding_count = o.finding_count || (o.findings || o.findings_resolved || []).length;
217
- if (!o.findings && o.findings_resolved) o.findings = [];
218
- o.action = o.action || o.fix || '';
219
- });
220
- // Fix broken: description→detail, fix→action
221
- (d.broken || []).forEach(b => {
222
- b.detail = b.detail || b.description || '';
223
- b.action = b.action || b.fix || '';
224
- });
225
- // Fix recommendations: description→action
226
- (d.recommendations || []).forEach((r, i) => {
227
- r.action = r.action || r.description || '';
228
- r.rank = r.rank || i + 1;
229
- });
230
- // Fix journeys: persona→archetype, friction→friction_points
231
- // Accept both `enhancement` (new) and `experience` (legacy) section keys
232
- const expSection = d.detailed_analysis && (d.detailed_analysis.enhancement || d.detailed_analysis.experience);
233
- if (expSection) {
234
- expSection.journeys = (expSection.journeys || []).map(j => ({
235
- archetype: j.archetype || j.persona || j.name || 'Unknown',
236
- summary: j.summary || j.journey_summary || j.description || j.friction || '',
237
- friction_points: j.friction_points || (j.friction ? [j.friction] : []),
238
- bright_spots: j.bright_spots || (j.bright ? [j.bright] : [])
239
- }));
240
- }
241
- return d;
242
- }
243
-
244
- function esc(s) {
245
- if (!s) return '';
246
- const d = document.createElement('div');
247
- d.textContent = String(s);
248
- return d.innerHTML;
249
- }
250
-
251
- function init() {
252
- const m = DATA.meta;
253
- document.getElementById('skill-name').textContent = m.skill_name;
254
- document.getElementById('subtitle').innerHTML =
255
- `${esc(m.skill_path)} &bull; ${m.timestamp ? m.timestamp.split('T')[0] : ''} &bull; ${m.scanner_count || 0} scanners &bull; <a href="quality-report.md">Full Report &nearr;</a>`;
256
-
257
- document.getElementById('grade-area').innerHTML =
258
- `<div class="grade grade-${DATA.grade}">${esc(DATA.grade)}</div>`;
259
- document.getElementById('narrative').textContent = DATA.narrative || '';
260
-
261
- renderBroken();
262
- renderOpportunities();
263
- renderStrengths();
264
- renderUserExperience();
265
- renderRecommendations();
266
- renderDetailed();
267
- }
268
-
269
- function renderBroken() {
270
- const items = DATA.broken || [];
271
- if (!items.length) return;
272
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
273
- html += `<span class="arrow">&#9654;</span><span class="label">Broken / Critical (${items.length})</span>`;
274
- html += `<div class="actions"><button class="btn btn-primary" onclick="event.stopPropagation();showBrokenPrompt()">Fix These</button></div>`;
275
- html += `</div><div class="section-body open">`;
276
- items.forEach(item => {
277
- const loc = item.file ? `${item.file}${item.line ? ':'+item.line : ''}` : '';
278
- html += `<div class="item">`;
279
- html += `<span class="badge badge-${item.severity || 'high'}">${esc(item.severity || 'high')}</span> `;
280
- if (loc) html += `<span class="item-file">${esc(loc)}</span>`;
281
- html += `<div class="item-title">${esc(item.title)}</div>`;
282
- if (item.detail) html += `<div class="item-desc">${esc(item.detail)}</div>`;
283
- if (item.action) html += `<div class="item-action"><strong>Fix:</strong> ${esc(item.action)}</div>`;
284
- html += `</div>`;
285
- });
286
- html += `</div></div>`;
287
- document.getElementById('broken-section').innerHTML = html;
288
- }
289
-
290
- function renderOpportunities() {
291
- const opps = DATA.opportunities || [];
292
- if (!opps.length) return;
293
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
294
- html += `<span class="arrow">&#9654;</span><span class="label">Opportunities (${opps.length})</span>`;
295
- html += `</div><div class="section-body open">`;
296
- opps.forEach((opp, idx) => {
297
- html += `<div class="opp">`;
298
- html += `<div class="opp-header">`;
299
- html += `<span class="badge badge-${opp.severity || 'medium'}">${esc(opp.severity || 'medium')}</span>`;
300
- html += `<span class="opp-name">${idx+1}. ${esc(opp.name)}</span>`;
301
- html += `<span class="opp-count">${opp.finding_count || (opp.findings||[]).length} observations</span>`;
302
- html += `<button class="btn" onclick="toggleFindings(${idx})">Details</button>`;
303
- html += `<button class="btn btn-primary" onclick="showThemePrompt(${idx})">Fix This</button>`;
304
- html += `</div>`;
305
- html += `<div class="opp-desc">${esc(opp.description)}</div>`;
306
- if (opp.impact) html += `<div class="opp-impact">Impact: ${esc(opp.impact)}</div>`;
307
- html += `<div class="opp-findings" id="findings-${idx}">`;
308
- (opp.findings || []).forEach(f => {
309
- const loc = f.file ? `${f.file}${f.line ? ':'+f.line : ''}` : '';
310
- html += `<div class="opp-finding">`;
311
- html += `<strong>${esc(f.title)}</strong>`;
312
- if (loc) html += ` <span class="item-file">${esc(loc)}</span>`;
313
- if (f.source) html += ` <span class="source">[${esc(f.source)}]</span>`;
314
- if (f.detail) html += `<br>${esc(f.detail)}`;
315
- html += `</div>`;
316
- });
317
- html += `</div></div>`;
318
- });
319
- html += `</div></div>`;
320
- document.getElementById('opportunities-section').innerHTML = html;
321
- }
322
-
323
- function renderStrengths() {
324
- const items = DATA.strengths || [];
325
- if (!items.length) return;
326
- let html = `<div class="section"><div class="section-header" onclick="toggleSection(this)">`;
327
- html += `<span class="arrow">&#9654;</span><span class="label">Strengths (${items.length})</span>`;
328
- html += `</div><div class="section-body">`;
329
- items.forEach(s => {
330
- html += `<div class="strength-item"><div class="title">${esc(s.title)}</div>`;
331
- if (s.detail) html += `<div class="detail">${esc(s.detail)}</div>`;
332
- html += `</div>`;
333
- });
334
- html += `</div></div>`;
335
- document.getElementById('strengths-section').innerHTML = html;
336
- }
337
-
338
- function renderRecommendations() {
339
- const recs = DATA.recommendations || [];
340
- if (!recs.length) return;
341
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
342
- html += `<span class="arrow">&#9654;</span><span class="label">Recommendations</span>`;
343
- html += `</div><div class="section-body open"><div class="recs">`;
344
- recs.forEach(r => {
345
- html += `<div class="rec">`;
346
- html += `<span class="rec-rank">#${r.rank}</span>`;
347
- html += `${esc(r.action)}`;
348
- if (r.resolves) html += ` <span class="rec-resolves">(resolves ${r.resolves} observations)</span>`;
349
- html += `</div>`;
350
- });
351
- html += `</div></div></div>`;
352
- document.getElementById('recommendations-section').innerHTML = html;
353
- }
354
-
355
- function renderUserExperience() {
356
- const ux = DATA.detailed_analysis && DATA.detailed_analysis.enhancement;
357
- if (!ux) return;
358
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
359
- html += `<span class="arrow">&#9654;</span><span class="label">User Experience</span>`;
360
- html += `</div><div class="section-body open">`;
361
- if (ux.assessment) html += `<p>${esc(ux.assessment)}</p>`;
362
- if (ux.journeys && ux.journeys.length) {
363
- html += `<div style="margin:1rem 0"><strong>User Journeys:</strong></div>`;
364
- ux.journeys.forEach(j => {
365
- html += `<div style="margin:0.75rem 0;padding:0.75rem;border-left:3px solid var(--accent);background:var(--surface2);">`;
366
- html += `<div style="font-weight:600;margin-bottom:0.5rem">${esc(j.archetype)}</div>`;
367
- html += `<p style="margin:0 0 0.5rem 0;font-size:0.95rem">${esc(j.summary || '')}</p>`;
368
- if (j.friction_points && j.friction_points.length) {
369
- html += `<div style="color:var(--high);font-size:0.85rem;margin:0.25rem 0"><strong>Friction Points:</strong></div>`;
370
- html += `<ul style="margin:0.25rem 0 0.5rem 1.25rem;color:var(--high);font-size:0.85rem">`;
371
- j.friction_points.forEach(fp => { html += `<li>${esc(fp)}</li>`; });
372
- html += `</ul>`;
373
- }
374
- if (j.bright_spots && j.bright_spots.length) {
375
- html += `<div style="color:var(--strength);font-size:0.85rem;margin:0.25rem 0"><strong>Bright Spots:</strong></div>`;
376
- html += `<ul style="margin:0.25rem 0 0 1.25rem;color:var(--strength);font-size:0.85rem">`;
377
- j.bright_spots.forEach(bs => { html += `<li>${esc(bs)}</li>`; });
378
- html += `</ul>`;
379
- }
380
- html += `</div>`;
381
- });
382
- }
383
- if (ux.autonomous) {
384
- const a = ux.autonomous;
385
- html += `<div style="margin:1rem 0;padding:0.75rem;background:var(--surface2);border-left:3px solid var(--suggestion);">`;
386
- html += `<div style="font-weight:600;margin-bottom:0.5rem">Headless / Automation Potential</div>`;
387
- html += `<div><strong>${esc(a.potential || '')}</strong>`;
388
- if (a.notes) html += `: ${esc(a.notes)}`;
389
- html += `</div></div>`;
390
- }
391
- (ux.findings || []).forEach(f => {
392
- const loc = f.file ? `${f.file}${f.line ? ':'+f.line : ''}` : '';
393
- html += `<div class="analysis-finding">`;
394
- if (f.severity) html += `<span class="badge badge-${f.severity}">${esc(f.severity)}</span> `;
395
- html += `${esc(f.title)}`;
396
- if (loc) html += ` <span class="item-file">${esc(loc)}</span>`;
397
- html += `</div>`;
398
- });
399
- html += `</div></div>`;
400
- document.getElementById('user-experience-section').innerHTML = html;
401
- }
402
-
403
- function renderDetailed() {
404
- const da = DATA.detailed_analysis;
405
- if (!da) return;
406
- const dims = [
407
- ['architecture', 'Architecture (Structure, Craft, Cohesion)'],
408
- ['determinism', 'Determinism & Distribution'],
409
- ['customization', 'Customization Surface']
410
- ];
411
- let html = `<div class="section"><div class="section-header" onclick="toggleSection(this)">`;
412
- html += `<span class="arrow">&#9654;</span><span class="label">Detailed Analysis</span>`;
413
- html += `</div><div class="section-body">`;
414
- dims.forEach(([key, label]) => {
415
- const dim = da[key];
416
- if (!dim) return;
417
- html += `<div class="analysis-section"><h4>${label}</h4>`;
418
- if (dim.assessment) html += `<p>${esc(dim.assessment)}</p>`;
419
- if (dim.dimensions) {
420
- html += `<table style="width:100%;font-size:0.85rem;margin:0.5rem 0;border-collapse:collapse;">`;
421
- html += `<tr><th style="text-align:left;padding:0.3rem;border-bottom:1px solid var(--border)">Dimension</th><th style="text-align:left;padding:0.3rem;border-bottom:1px solid var(--border)">Score</th><th style="text-align:left;padding:0.3rem;border-bottom:1px solid var(--border)">Notes</th></tr>`;
422
- Object.entries(dim.dimensions).forEach(([d, v]) => {
423
- if (v && typeof v === 'object') {
424
- html += `<tr><td style="padding:0.3rem;border-bottom:1px solid var(--border)">${esc(d.replace(/_/g,' '))}</td><td style="padding:0.3rem;border-bottom:1px solid var(--border)">${esc(v.score||'')}</td><td style="padding:0.3rem;border-bottom:1px solid var(--border)">${esc(v.notes||'')}</td></tr>`;
425
- }
426
- });
427
- html += `</table>`;
428
- }
429
- if (dim.journeys && dim.journeys.length) {
430
- dim.journeys.forEach(j => {
431
- html += `<div style="margin:0.5rem 0"><strong>${esc(j.archetype)}</strong>: ${esc(j.summary || j.journey_summary || '')}`;
432
- if (j.friction_points && j.friction_points.length) {
433
- html += `<ul style="color:var(--high);font-size:0.85rem;padding-left:1.25rem">`;
434
- j.friction_points.forEach(fp => { html += `<li>${esc(fp)}</li>`; });
435
- html += `</ul>`;
436
- }
437
- html += `</div>`;
438
- });
439
- }
440
- if (dim.autonomous) {
441
- const a = dim.autonomous;
442
- html += `<p><strong>Headless Potential:</strong> ${esc(a.potential||'')}`;
443
- if (a.notes) html += ` — ${esc(a.notes)}`;
444
- html += `</p>`;
445
- }
446
- (dim.findings || []).forEach(f => {
447
- const loc = f.file ? `${f.file}${f.line ? ':'+f.line : ''}` : '';
448
- html += `<div class="analysis-finding">`;
449
- if (f.severity) html += `<span class="badge badge-${f.severity}">${esc(f.severity)}</span> `;
450
- html += `${esc(f.title)}`;
451
- if (loc) html += ` <span class="item-file">${esc(loc)}</span>`;
452
- html += `</div>`;
453
- });
454
- html += `</div>`;
455
- });
456
- html += `</div></div>`;
457
- document.getElementById('detailed-section').innerHTML = html;
458
- }
459
-
460
- // --- Interactions ---
461
- function toggleSection(el) {
462
- el.classList.toggle('open');
463
- el.nextElementSibling.classList.toggle('open');
464
- }
465
-
466
- function toggleFindings(idx) {
467
- document.getElementById('findings-'+idx).classList.toggle('open');
468
- }
469
-
470
- // --- Prompt Generation ---
471
- function showThemePrompt(idx) {
472
- const opp = DATA.opportunities[idx];
473
- if (!opp) return;
474
- let prompt = `## Task: ${opp.name}\nSkill path: ${DATA.meta.skill_path}\n\n`;
475
- prompt += `### Problem\n${opp.description}\n\n`;
476
- prompt += `### Fix\n${opp.action}\n\n`;
477
- if (opp.findings && opp.findings.length) {
478
- prompt += `### Specific observations to address:\n\n`;
479
- opp.findings.forEach((f, i) => {
480
- const loc = f.file ? (f.line ? `${f.file}:${f.line}` : f.file) : '';
481
- prompt += `${i+1}. **${f.title}**`;
482
- if (loc) prompt += ` (${loc})`;
483
- if (f.detail) prompt += `\n ${f.detail}`;
484
- prompt += `\n`;
485
- });
486
- }
487
- document.getElementById('modal-title').textContent = `Fix: ${opp.name}`;
488
- document.getElementById('modal-content').textContent = prompt.trim();
489
- document.getElementById('modal').classList.add('visible');
490
- }
491
-
492
- function showBrokenPrompt() {
493
- const items = DATA.broken || [];
494
- let prompt = `## Task: Fix Critical Issues\nSkill path: ${DATA.meta.skill_path}\n\n`;
495
- items.forEach((item, i) => {
496
- const loc = item.file ? (item.line ? `${item.file}:${item.line}` : item.file) : '';
497
- prompt += `${i+1}. **[${(item.severity||'high').toUpperCase()}] ${item.title}**\n`;
498
- if (loc) prompt += ` File: ${loc}\n`;
499
- if (item.detail) prompt += ` Context: ${item.detail}\n`;
500
- if (item.action) prompt += ` Fix: ${item.action}\n`;
501
- prompt += `\n`;
502
- });
503
- document.getElementById('modal-title').textContent = 'Fix Critical Issues';
504
- document.getElementById('modal-content').textContent = prompt.trim();
505
- document.getElementById('modal').classList.add('visible');
506
- }
507
-
508
- function closeModal() { document.getElementById('modal').classList.remove('visible'); }
509
-
510
- function copyModal() {
511
- const text = document.getElementById('modal-content').textContent;
512
- navigator.clipboard.writeText(text).then(() => {
513
- const btn = document.querySelector('.modal .btn-primary');
514
- btn.textContent = 'Copied!';
515
- setTimeout(() => { btn.textContent = 'Copy to Clipboard'; }, 1500);
516
- });
517
- }
518
-
519
- init();
520
- </script>
521
- </body>
522
- </html>"""
523
-
524
-
525
- def generate_html(report_data: dict) -> str:
526
- """Inject report data into the HTML template."""
527
- data_json = json.dumps(report_data, indent=None, ensure_ascii=False)
528
- data_tag = f'<script id="report-data" type="application/json">{data_json}</script>'
529
- html = HTML_TEMPLATE.replace('<script>\nconst RAW', f'{data_tag}\n<script>\nconst RAW')
530
- html = html.replace('SKILL_NAME', report_data.get('meta', {}).get('skill_name', 'Unknown'))
531
- return html
532
-
533
-
534
- def main() -> int:
535
- parser = argparse.ArgumentParser(
536
- description='Generate interactive HTML quality analysis report',
537
- )
538
- parser.add_argument(
539
- 'report_dir',
540
- type=Path,
541
- help='Directory containing report-data.json',
542
- )
543
- parser.add_argument(
544
- '--open',
545
- action='store_true',
546
- help='Open the HTML report in the default browser',
547
- )
548
- parser.add_argument(
549
- '--output', '-o',
550
- type=Path,
551
- help='Output HTML file path (default: {report_dir}/quality-report.html)',
552
- )
553
- args = parser.parse_args()
554
-
555
- if not args.report_dir.is_dir():
556
- print(f'Error: {args.report_dir} is not a directory', file=sys.stderr)
557
- return 2
558
-
559
- report_data = load_report_data(args.report_dir)
560
- html = generate_html(report_data)
561
-
562
- output_path = args.output or (args.report_dir / 'quality-report.html')
563
- output_path.write_text(html, encoding='utf-8')
564
-
565
- # Output summary
566
- opp_count = len(report_data.get('opportunities', []))
567
- broken_count = len(report_data.get('broken', []))
568
- print(json.dumps({
569
- 'html_report': str(output_path),
570
- 'grade': report_data.get('grade', 'Unknown'),
571
- 'opportunities': opp_count,
572
- 'broken': broken_count,
573
- }))
574
-
575
- if args.open:
576
- system = platform.system()
577
- if system == 'Darwin':
578
- subprocess.run(['open', str(output_path)])
579
- elif system == 'Linux':
580
- subprocess.run(['xdg-open', str(output_path)])
581
- elif system == 'Windows':
582
- subprocess.run(['start', str(output_path)], shell=True)
583
-
584
- return 0
585
-
586
-
587
- if __name__ == '__main__':
588
- sys.exit(main())