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.
- package/lib/agents.js +7 -0
- package/lib/bmad-cache/bmb/.claude-plugin/marketplace.json +3 -3
- package/lib/bmad-cache/bmb/_git_preserved/index +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.idx +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/{pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.pack → pack-b388526edf307cf56f8e65673466cff074105abf.pack} +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.rev +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/packed-refs +1 -1
- package/lib/bmad-cache/bmb/_git_preserved/refs/heads/main +1 -1
- package/lib/bmad-cache/bmb/_git_preserved/shallow +1 -1
- package/lib/bmad-cache/bmb/package-lock.json +2 -2
- package/lib/bmad-cache/bmb/package.json +1 -1
- package/lib/bmad-cache/bmb/samples/bmad-agent-code-coach/scripts/wake.py +78 -0
- package/lib/bmad-cache/bmb/samples/bmad-agent-creative-muse/scripts/wake.py +78 -0
- package/lib/bmad-cache/bmb/samples/bmad-agent-sentinel/scripts/wake.py +78 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/customize-template.toml +3 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/init-sanctum-template.py +11 -5
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/report-shell.html +1073 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/wake-template.py +78 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/customize.toml +48 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/count_tokens.py +78 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass.py +258 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/process-template.py +20 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/render_report.py +387 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/adapter-claude-code.json +9 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/aggregate_benchmark.py +236 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/memlog.py +197 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_evals.py +513 -390
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_triggers.py +347 -251
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_env_isolation.py +79 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_trigger_detection.py +123 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/tests/test-validate-module.py +82 -4
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/validate-module.py +55 -11
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/customize-template.toml +3 -5
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/report-shell.html +860 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/customize.toml +59 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/count_tokens.py +77 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/init_skill.py +132 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-prompt-metrics.py +186 -221
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/quick_validate.py +117 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/render_report.py +387 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_canon_sync.py +46 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_count_tokens.py +180 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_render_report.py +171 -0
- package/lib/bmad-cache/cache-manifest.json +4 -4
- package/lib/bmad-extension/.claude-plugin/marketplace.json.template +1 -1
- package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +2 -2
- package/lib/bmad.js +6 -1
- package/lib/templates/instruction-block-git.template.md +25 -25
- package/lib/templates/instruction-block-onprem.template.md +86 -86
- package/lib/templates/instruction-block-universal.template.md +29 -29
- package/package.json +3 -3
- package/lib/.bmad-extension-plugin.build-22612-1782454014750/.claude-plugin/marketplace.json +0 -109
- package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module-help.csv +0 -62
- package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module.yaml +0 -20
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.idx +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.rev +0 -0
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/references/sample-init-sanctum.py +0 -274
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/generate-html-report.py +0 -534
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-execution-deps.py +0 -337
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-prompt-metrics.py +0 -425
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-sanctum-architecture.py +0 -385
- package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-structure-capabilities.py +0 -482
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/Dockerfile +0 -29
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/docker_setup.py +0 -115
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/generate_report.py +0 -184
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/pty_runner.py +0 -171
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/utils.py +0 -260
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/extract-report-json.py +0 -287
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-html-report.py +0 -588
- 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)} • ${m.timestamp ? m.timestamp.split('T')[0] : ''} • ${m.scanner_count || 0} scanners • <a href="quality-report.md">Full Report ↗</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">▶</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">▶</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">▶</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">▶</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">▶</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">▶</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())
|