@iseoane/agent-depot 0.2.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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +465 -0
- package/package.json +69 -0
- package/skills/doctor-md-agents/README.md +51 -0
- package/skills/doctor-md-agents/SKILL.md +275 -0
- package/skills/doctor-md-agents/references/rule-model.md +56 -0
- package/skills/doctor-md-agents/references/structure.md +89 -0
- package/skills/doctor-md-agents/scorers/adherence.md +73 -0
- package/skills/doctor-md-agents/scorers/rule-value.md +49 -0
- package/skills/doctor-md-agents/scripts/collect_sessions.py +1135 -0
- package/skills/doctor-md-agents/scripts/extract_rules.py +704 -0
- package/skills/doctor-md-agents/scripts/render_report.py +644 -0
- package/skills/doctor-md-agents/scripts/scan_corrections.py +781 -0
- package/skills/doctor-md-agents/scripts/test_collect_sessions.py +780 -0
- package/skills/doctor-md-agents/scripts/test_extract_rules.py +530 -0
- package/skills/doctor-md-agents/scripts/test_render_report.py +383 -0
- package/skills/doctor-md-agents/scripts/test_scan_corrections.py +343 -0
- package/skills/doctor-md-agents/scripts/test_verify_citations.py +236 -0
- package/skills/doctor-md-agents/scripts/testdata/old_extract_rules_pre_t1.py.txt +507 -0
- package/skills/doctor-md-agents/scripts/verify_citations.py +217 -0
- package/skills/doctor-md-skill/README.md +27 -0
- package/skills/doctor-md-skill/SKILL.md +212 -0
- package/skills/doctor-md-skill/assets/pierre-diffs.js +1531 -0
- package/skills/doctor-md-skill/references/skill-improvements.md +32 -0
- package/skills/doctor-md-skill/references/supported-harnesses.md +44 -0
- package/skills/doctor-md-skill/scorers/code-quality.md +41 -0
- package/skills/doctor-md-skill/scorers/efficiency.md +38 -0
- package/skills/doctor-md-skill/scripts/collect_sessions.py +1112 -0
- package/skills/doctor-md-skill/scripts/detect_machine_sessions.py +315 -0
- package/skills/doctor-md-skill/scripts/render_report.py +545 -0
- package/skills/doctor-md-skill/scripts/test_collect_sessions.py +593 -0
- package/skills/doctor-md-skill/scripts/test_detect_machine_sessions.py +181 -0
- package/skills/doctor-md-skill/scripts/test_render_report.py +241 -0
- package/skills/doctor-md-skill/scripts/test_verify_citations.py +196 -0
- package/skills/doctor-md-skill/scripts/verify_citations.py +221 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render a doctor-md-skill report.json into one shareable HTML report.
|
|
3
|
+
|
|
4
|
+
Output (next to report.json):
|
|
5
|
+
report.html - scorecard, findings, and suggested skill edits in a single
|
|
6
|
+
self-contained page, with a "share as png" button that draws
|
|
7
|
+
a 1200x675 share image client-side and downloads it.
|
|
8
|
+
|
|
9
|
+
Python 3.9+, stdlib only. Uses system fonts so the page and the exported PNG
|
|
10
|
+
render the same everywhere.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import base64
|
|
15
|
+
import html
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import webbrowser
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
GRADES = [
|
|
24
|
+
(0.97, "A+"), (0.93, "A"), (0.90, "A-"),
|
|
25
|
+
(0.87, "B+"), (0.83, "B"), (0.80, "B-"),
|
|
26
|
+
(0.77, "C+"), (0.73, "C"), (0.70, "C-"),
|
|
27
|
+
(0.60, "D"), (0.0, "F"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
DIFFS_BUNDLE_PATH = (
|
|
31
|
+
Path(__file__).resolve().parent.parent / "assets" / "pierre-diffs.js"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Collapsed height of a diff before the "show more" toggle takes over.
|
|
35
|
+
DIFF_CLAMP_PX = 320
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def grade_for(score: float) -> str:
|
|
39
|
+
for threshold, letter in GRADES:
|
|
40
|
+
if score >= threshold:
|
|
41
|
+
return letter
|
|
42
|
+
return "F"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pct(score) -> int:
|
|
46
|
+
return round(float(score) * 100)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_generated_at(value) -> str:
|
|
50
|
+
if not value:
|
|
51
|
+
return ""
|
|
52
|
+
raw = str(value)
|
|
53
|
+
normalized = raw[:-1] + "+00:00" if raw.endswith("Z") else raw
|
|
54
|
+
if re.search(r"[+-]\d{2}$", normalized):
|
|
55
|
+
normalized += ":00"
|
|
56
|
+
try:
|
|
57
|
+
generated_at = datetime.fromisoformat(normalized)
|
|
58
|
+
except ValueError:
|
|
59
|
+
return raw
|
|
60
|
+
suffix = ""
|
|
61
|
+
if generated_at.tzinfo is not None:
|
|
62
|
+
generated_at = generated_at.astimezone(timezone.utc)
|
|
63
|
+
suffix = " UTC"
|
|
64
|
+
time = generated_at.strftime("%I:%M %p").lstrip("0")
|
|
65
|
+
return (
|
|
66
|
+
f"{generated_at.strftime('%B')} {generated_at.day}, "
|
|
67
|
+
f"{generated_at.year} at {time}{suffix}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def open_report(report_path: Path) -> bool:
|
|
72
|
+
try:
|
|
73
|
+
return bool(webbrowser.open(report_path.absolute().as_uri(), new=2))
|
|
74
|
+
except (OSError, webbrowser.Error):
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def esc(v) -> str:
|
|
79
|
+
value = v if v is not None else ""
|
|
80
|
+
return html.escape(str(value))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render_diff(diff_text: str, proposed_path: str = "") -> str:
|
|
84
|
+
if not diff_text:
|
|
85
|
+
return ""
|
|
86
|
+
encoded = base64.b64encode(diff_text.encode("utf-8")).decode("ascii")
|
|
87
|
+
filename = Path(proposed_path).name if proposed_path else "SKILL.md"
|
|
88
|
+
return (
|
|
89
|
+
'<div class="diff-wrap" data-collapsed="true">'
|
|
90
|
+
f'<div class="diff-view" data-pierre-diff data-diff="{encoded}" '
|
|
91
|
+
f'data-filename="{esc(filename)}">'
|
|
92
|
+
f'<pre class="diff-fallback">{esc(diff_text)}</pre></div>'
|
|
93
|
+
'<button class="diff-toggle" type="button" hidden>show more</button>'
|
|
94
|
+
"</div>"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def embedded_diffs_script() -> str:
|
|
99
|
+
if not DIFFS_BUNDLE_PATH.exists():
|
|
100
|
+
raise RuntimeError(
|
|
101
|
+
f"@pierre/diffs bundle missing: {DIFFS_BUNDLE_PATH}; "
|
|
102
|
+
"restore assets/pierre-diffs.js"
|
|
103
|
+
)
|
|
104
|
+
bundle = DIFFS_BUNDLE_PATH.read_text()
|
|
105
|
+
return re.sub(r"</script", r"<\\/script", bundle, flags=re.IGNORECASE)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Attribution shown only in the exported share image.
|
|
109
|
+
SHARE_STAMP_NAME = "Generated with doctor-md-skill"
|
|
110
|
+
SHARE_STAMP_SUB = "local run"
|
|
111
|
+
|
|
112
|
+
# Design tokens: white ground with a dot grid, Matter-Mono-ish monospace, #2a1eff accent,
|
|
113
|
+
# hairline rgba(13,10,61) rules, square corners, lowercase labels,
|
|
114
|
+
# uppercase wide-tracked meta bars.
|
|
115
|
+
PAGE_CSS = """
|
|
116
|
+
* { box-sizing: border-box; }
|
|
117
|
+
body {
|
|
118
|
+
--mono-font: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
119
|
+
--fg: #1a1522; --muted: #5d5966; --muted-2: #918d9a; --accent: #2a1eff;
|
|
120
|
+
--line: rgba(13, 10, 61, 0.16); --line-soft: rgba(13, 10, 61, 0.07);
|
|
121
|
+
--page-bg: #fff; --surface: #fff; --bg-panel: #f6f5fb; --yellow: #eef17c;
|
|
122
|
+
--button-fg: #1a1522;
|
|
123
|
+
--footer-shadow: rgba(13, 10, 61, 0.12);
|
|
124
|
+
font-family: var(--mono-font);
|
|
125
|
+
background: radial-gradient(circle at 1px 1px, var(--line-soft) 1px, transparent 0) 0 0 / 22px 22px, var(--page-bg);
|
|
126
|
+
color: var(--fg); max-width: 900px; margin: 0 auto; padding: 48px 24px;
|
|
127
|
+
line-height: 1.65; font-size: 13px; color-scheme: light;
|
|
128
|
+
}
|
|
129
|
+
@media (prefers-color-scheme: dark) {
|
|
130
|
+
body {
|
|
131
|
+
--fg: #f4f1f8; --muted: #bbb5c2; --muted-2: #928b9b; --accent: #9188ff;
|
|
132
|
+
--line: rgba(239, 235, 255, 0.2); --line-soft: rgba(239, 235, 255, 0.08);
|
|
133
|
+
--page-bg: #0f0d14; --surface: #17141d; --bg-panel: #211d29;
|
|
134
|
+
--footer-shadow: rgba(0, 0, 0, 0.45);
|
|
135
|
+
color-scheme: dark;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
::selection { background: var(--accent); color: #fff; }
|
|
139
|
+
h1 { font-weight: 500; letter-spacing: -2px; font-size: 34px; margin: 4px 0 0; }
|
|
140
|
+
h2 { font-weight: 500; letter-spacing: -1px; font-size: 20px; margin: 40px 0 8px; }
|
|
141
|
+
p { color: var(--muted); font-weight: 500; }
|
|
142
|
+
a { color: var(--accent); }
|
|
143
|
+
code { background: var(--bg-panel); border: 1px solid var(--line-soft); padding: 1px 5px; }
|
|
144
|
+
li { margin-bottom: 10px; }
|
|
145
|
+
.tag { font-size: 11px; color: var(--accent); text-transform: lowercase; }
|
|
146
|
+
.tag::before { content: "# "; }
|
|
147
|
+
.muted { color: var(--muted-2); font-size: 12px; }
|
|
148
|
+
.row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
149
|
+
.title-row { margin-top: 4px; }
|
|
150
|
+
.title-row h1 { margin: 0; }
|
|
151
|
+
.cta-button { font-family: inherit; font-size: 13px; font-weight: 600; color: var(--button-fg);
|
|
152
|
+
background: var(--yellow); border: 1px solid var(--button-fg); padding: 8px 14px;
|
|
153
|
+
text-decoration: none; white-space: nowrap; flex: none; cursor: pointer; }
|
|
154
|
+
.cta-button:hover { background: #f4f79f; }
|
|
155
|
+
.cta-button[disabled] { cursor: default; opacity: 0.65; }
|
|
156
|
+
.scorecard { display: flex; align-items: center; gap: 48px; border: 1px solid var(--line);
|
|
157
|
+
background: var(--surface); padding: 26px 28px; margin-top: 20px; }
|
|
158
|
+
.grade-col { text-align: center; flex: none; width: 170px; }
|
|
159
|
+
.grade { font-size: 96px; font-weight: 600; line-height: 1; letter-spacing: -5px; color: var(--accent); }
|
|
160
|
+
.grade-label { font-size: 11px; color: var(--muted-2); margin-top: 8px; text-transform: uppercase; letter-spacing: 0.14em; }
|
|
161
|
+
.bars { flex: 1; display: flex; flex-direction: column; gap: 20px; min-width: 0; }
|
|
162
|
+
.bar-head { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 7px; font-weight: 500; }
|
|
163
|
+
.bar-name { text-transform: lowercase; }
|
|
164
|
+
.bar-val { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
165
|
+
.bar-track { height: 8px; background: var(--line-soft); box-shadow: inset 0 0 0 1px var(--line); }
|
|
166
|
+
.bar-fill { height: 100%; background: var(--accent);
|
|
167
|
+
animation: report-fill 700ms cubic-bezier(0.22, 1, 0.36, 1) var(--metric-delay) both;
|
|
168
|
+
transform-origin: left; }
|
|
169
|
+
.stats { display: grid; grid-template-columns: repeat(3, 1fr); border: 1px solid var(--line);
|
|
170
|
+
border-top: none; background: var(--bg-panel); }
|
|
171
|
+
.stat { padding: 16px 24px 14px; border-left: 1px solid var(--line); }
|
|
172
|
+
.stat:first-child { border-left: none; }
|
|
173
|
+
.stat .num { font-size: 34px; font-weight: 600; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
|
174
|
+
.stat .lbl { font-size: 12px; color: var(--muted); margin-top: 2px; text-transform: lowercase; }
|
|
175
|
+
.evidence-note { border: 1px solid var(--line); border-top: none; background: var(--surface);
|
|
176
|
+
padding: 10px 24px; font-size: 12px; color: var(--muted); }
|
|
177
|
+
.evidence-note .meta-item { display: inline-block; margin-right: 24px; }
|
|
178
|
+
.evidence-note b { color: var(--fg); font-variant-numeric: tabular-nums; }
|
|
179
|
+
.diff-wrap { margin: 10px 0 4px; }
|
|
180
|
+
.diff-view { display: grid; gap: 10px; max-width: 100%;
|
|
181
|
+
--diffs-font-family: var(--mono-font); --diffs-header-font-family: var(--mono-font); }
|
|
182
|
+
.diff-view > * { min-width: 0; }
|
|
183
|
+
.diff-fallback { background: var(--bg-panel); border: 1px solid var(--line); padding: 13px 16px;
|
|
184
|
+
color: var(--muted); font-size: 12px; line-height: 1.7; overflow-x: auto; margin: 0; white-space: pre; }
|
|
185
|
+
.diff-wrap[data-overflowing="true"][data-collapsed="true"] .diff-view {
|
|
186
|
+
max-height: __CLAMP__px; overflow: hidden;
|
|
187
|
+
-webkit-mask-image: linear-gradient(#000 calc(100% - 72px), transparent);
|
|
188
|
+
mask-image: linear-gradient(#000 calc(100% - 72px), transparent);
|
|
189
|
+
}
|
|
190
|
+
.diff-toggle { font-family: inherit; font-size: 10px; font-weight: 600; letter-spacing: 0.1em;
|
|
191
|
+
text-transform: uppercase; color: var(--accent); background: var(--surface);
|
|
192
|
+
border: 1px solid var(--line); padding: 5px 10px; margin-top: 6px; cursor: pointer; }
|
|
193
|
+
.diff-toggle:hover { border-color: var(--accent); }
|
|
194
|
+
@keyframes report-fill {
|
|
195
|
+
from { transform: scaleX(0); }
|
|
196
|
+
to { transform: scaleX(1); }
|
|
197
|
+
}
|
|
198
|
+
@media (prefers-reduced-motion: reduce) {
|
|
199
|
+
.bar-fill { animation: none; }
|
|
200
|
+
}
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def render_page(r) -> str:
|
|
205
|
+
scores = r["scores"]
|
|
206
|
+
stats = r.get("stats", {})
|
|
207
|
+
grade = r.get("grade") or grade_for(scores["overall"])
|
|
208
|
+
generated_at = format_generated_at(r.get("generated_at"))
|
|
209
|
+
|
|
210
|
+
bars = "".join(
|
|
211
|
+
f'<div class="bar-row"><div class="bar-head"><span class="bar-name">{esc(name)}</span>'
|
|
212
|
+
f'<span class="bar-val">{pct(val)}</span></div>'
|
|
213
|
+
f'<div class="bar-track"><div class="bar-fill" '
|
|
214
|
+
f'style="width:{pct(val)}%;--metric-delay:{180 + index * 110}ms"></div></div></div>'
|
|
215
|
+
for index, (name, val) in enumerate([
|
|
216
|
+
("Efficiency", scores.get("efficiency", 0)),
|
|
217
|
+
("Code Quality", scores.get("code_quality", 0)),
|
|
218
|
+
("Skill Coverage", scores.get("skill_coverage", 0)),
|
|
219
|
+
])
|
|
220
|
+
)
|
|
221
|
+
stat_cells = "".join(
|
|
222
|
+
f'<div class="stat"><div class="num">{esc(value)}</div><div class="lbl">{esc(label)}</div></div>'
|
|
223
|
+
for value, label in [
|
|
224
|
+
(stats.get("sessions_analyzed", 0), "conversations scored"),
|
|
225
|
+
(stats.get("skills_found", 0), "skills installed"),
|
|
226
|
+
(stats.get("skills_used", 0), "skills used"),
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
evidence_note = (
|
|
230
|
+
'<div class="evidence-note">'
|
|
231
|
+
f'<span class="meta-item">sessions dropped as machine-driven: <b>{esc(stats.get("sessions_dropped_as_machine", 0))}</b></span>'
|
|
232
|
+
f'<span class="meta-item">unverified quotes discarded: <b>{esc(stats.get("unverified_quotes", 0))}</b></span>'
|
|
233
|
+
'</div>'
|
|
234
|
+
)
|
|
235
|
+
findings = "".join(f"<li>{esc(finding)}</li>" for finding in r.get("top_findings", []))
|
|
236
|
+
suggestions = "".join(
|
|
237
|
+
f"""<li><b><code>{esc(s.get('skill'))}</code></b> — {esc(s.get('change'))}
|
|
238
|
+
{('<div class="muted">Evidence: ' + esc(s['evidence']) + '</div>') if s.get('evidence') else ''}
|
|
239
|
+
{render_diff(s.get('diff', ''), s.get('proposed_path', ''))}</li>"""
|
|
240
|
+
for s in r.get("suggestions", [])
|
|
241
|
+
) or "<li>No skill change cleared the bar for this window.</li>"
|
|
242
|
+
|
|
243
|
+
card_data = json.dumps({
|
|
244
|
+
"title": r.get("title", "Agent Skill Report"),
|
|
245
|
+
"eyebrow": "doctor-md-skill",
|
|
246
|
+
"handle": r.get("handle") or "agent skill report",
|
|
247
|
+
"harness": r.get("harness", "codex"),
|
|
248
|
+
"grade": grade,
|
|
249
|
+
"grade_label": f"overall {pct(scores['overall'])}",
|
|
250
|
+
"bars": [
|
|
251
|
+
["Efficiency", pct(scores.get("efficiency", 0))],
|
|
252
|
+
["Code Quality", pct(scores.get("code_quality", 0))],
|
|
253
|
+
["Skill Coverage", pct(scores.get("skill_coverage", 0))],
|
|
254
|
+
],
|
|
255
|
+
"meta": f"{stats.get('sessions_scanned', 0)} conversations found \u00b7 "
|
|
256
|
+
f"last {stats.get('window_days', 45)} days",
|
|
257
|
+
"stats": [
|
|
258
|
+
[str(stats.get("sessions_analyzed", 0)), "conversations scored"],
|
|
259
|
+
[str(stats.get("skills_found", 0)), "skills installed"],
|
|
260
|
+
[str(stats.get("skills_used", 0)), "skills used"],
|
|
261
|
+
],
|
|
262
|
+
"stamp": [SHARE_STAMP_NAME, SHARE_STAMP_SUB],
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
return f"""<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
266
|
+
<meta name="color-scheme" content="light dark">
|
|
267
|
+
<title>{esc(r.get('title', 'Agent Skill Report'))}</title>
|
|
268
|
+
<style>{PAGE_CSS.replace('__CLAMP__', str(DIFF_CLAMP_PX))}</style></head><body>
|
|
269
|
+
<div class="tag">doctor-md-skill</div>
|
|
270
|
+
<div class="row title-row">
|
|
271
|
+
<h1>{esc(r.get('title', 'Agent Skill Report'))}</h1>
|
|
272
|
+
<button class="cta-button" id="share-png" type="button">Share</button>
|
|
273
|
+
</div>
|
|
274
|
+
<p class="muted">Generated {esc(generated_at)} · harness: {esc(r.get('harness', 'codex'))}</p>
|
|
275
|
+
<div class="scorecard">
|
|
276
|
+
<div class="grade-col"><div class="grade">{esc(grade)}</div>
|
|
277
|
+
<div class="grade-label">overall {pct(scores['overall'])}</div></div>
|
|
278
|
+
<div class="bars">{bars}</div>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="stats">{stat_cells}</div>
|
|
281
|
+
{evidence_note}
|
|
282
|
+
<h2>Findings</h2><ul>{findings}</ul>
|
|
283
|
+
<h2>Suggested skill changes</h2><ol>{suggestions}</ol>
|
|
284
|
+
<script>{embedded_diffs_script()}</script>
|
|
285
|
+
<script>{page_script(card_data)}</script>
|
|
286
|
+
</body></html>"""
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def page_script(card_data: str) -> str:
|
|
290
|
+
"""Diff collapsing plus a canvas-drawn 1200x675 share image."""
|
|
291
|
+
script = r"""
|
|
292
|
+
(function () {
|
|
293
|
+
var CARD = __CARD__;
|
|
294
|
+
var CLAMP = __CLAMP__;
|
|
295
|
+
|
|
296
|
+
// --- collapsible diffs -------------------------------------------------
|
|
297
|
+
// scrollHeight is the full content height whether or not the view is
|
|
298
|
+
// currently clamped, so this measures the same either way. Only diffs that
|
|
299
|
+
// actually overflow get clamped, so short ones never pick up the fade.
|
|
300
|
+
function syncToggle(wrap, button) {
|
|
301
|
+
var view = wrap.querySelector('.diff-view');
|
|
302
|
+
if (!view) return;
|
|
303
|
+
var overflowing = view.scrollHeight > CLAMP + 24;
|
|
304
|
+
wrap.dataset.overflowing = overflowing ? 'true' : 'false';
|
|
305
|
+
button.hidden = !overflowing;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
document.querySelectorAll('.diff-wrap').forEach(function (wrap) {
|
|
309
|
+
var button = wrap.querySelector('.diff-toggle');
|
|
310
|
+
var view = wrap.querySelector('.diff-view');
|
|
311
|
+
if (!button || !view) return;
|
|
312
|
+
button.addEventListener('click', function () {
|
|
313
|
+
var collapsed = wrap.dataset.collapsed === 'true';
|
|
314
|
+
wrap.dataset.collapsed = collapsed ? 'false' : 'true';
|
|
315
|
+
button.textContent = collapsed ? 'show less' : 'show more';
|
|
316
|
+
if (!collapsed) wrap.scrollIntoView({ block: 'nearest' });
|
|
317
|
+
});
|
|
318
|
+
syncToggle(wrap, button);
|
|
319
|
+
if (window.ResizeObserver) {
|
|
320
|
+
new ResizeObserver(function () { syncToggle(wrap, button); }).observe(view);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// --- share image -------------------------------------------------------
|
|
325
|
+
var MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
|
|
326
|
+
var FG = '#1a1522', MUTED = '#5d5966', MUTED2 = '#918d9a', ACCENT = '#2a1eff';
|
|
327
|
+
var LINE = 'rgba(13,10,61,0.16)', LINE_SOFT = 'rgba(13,10,61,0.07)';
|
|
328
|
+
var PANEL = '#f6f5fb';
|
|
329
|
+
var W = 1200, H = 675;
|
|
330
|
+
|
|
331
|
+
function drawCard(scale) {
|
|
332
|
+
var canvas = document.createElement('canvas');
|
|
333
|
+
canvas.width = W * scale;
|
|
334
|
+
canvas.height = H * scale;
|
|
335
|
+
var c = canvas.getContext('2d');
|
|
336
|
+
c.scale(scale, scale);
|
|
337
|
+
|
|
338
|
+
function font(weight, size) { c.font = weight + ' ' + size + 'px ' + MONO; }
|
|
339
|
+
function track(value) { try { c.letterSpacing = value; } catch (e) {} }
|
|
340
|
+
function rule(x, y, w, h) { c.fillStyle = LINE; c.fillRect(x, y, w, h); }
|
|
341
|
+
function text(str, x, y, align) {
|
|
342
|
+
c.textAlign = align || 'left';
|
|
343
|
+
c.textBaseline = 'middle';
|
|
344
|
+
c.fillText(str, x, y);
|
|
345
|
+
c.textAlign = 'left';
|
|
346
|
+
}
|
|
347
|
+
function dots(x, y, w, h, step) {
|
|
348
|
+
c.save();
|
|
349
|
+
c.beginPath();
|
|
350
|
+
c.rect(x, y, w, h);
|
|
351
|
+
c.clip();
|
|
352
|
+
c.fillStyle = LINE_SOFT;
|
|
353
|
+
for (var i = x; i < x + w; i += step) {
|
|
354
|
+
for (var j = y; j < y + h; j += step) {
|
|
355
|
+
c.beginPath();
|
|
356
|
+
c.arc(i + 1, j + 1, 1, 0, Math.PI * 2);
|
|
357
|
+
c.fill();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
c.restore();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
c.fillStyle = '#fff';
|
|
364
|
+
c.fillRect(0, 0, W, H);
|
|
365
|
+
dots(0, 0, W, H, 22);
|
|
366
|
+
|
|
367
|
+
var fx = 48, fy = 40, fw = 1104, fh = 595;
|
|
368
|
+
c.fillStyle = '#fff';
|
|
369
|
+
c.fillRect(fx, fy, fw, fh);
|
|
370
|
+
rule(fx, fy, fw, 1);
|
|
371
|
+
rule(fx, fy + fh - 1, fw, 1);
|
|
372
|
+
rule(fx, fy, 1, fh);
|
|
373
|
+
rule(fx + fw - 1, fy, 1, fh);
|
|
374
|
+
|
|
375
|
+
// meta bar
|
|
376
|
+
var barBottom = fy + 38;
|
|
377
|
+
rule(fx, barBottom, fw, 1);
|
|
378
|
+
font('400', 11);
|
|
379
|
+
track('1.1px');
|
|
380
|
+
c.fillStyle = MUTED2;
|
|
381
|
+
var handle = CARD.handle.toUpperCase();
|
|
382
|
+
text(handle, fx + 16, fy + 19);
|
|
383
|
+
var handleEnd = fx + 16 + c.measureText(handle).width + 14;
|
|
384
|
+
var harness = CARD.harness.toUpperCase();
|
|
385
|
+
var harnessW = c.measureText(harness).width + 12;
|
|
386
|
+
var harnessX = fx + fw - 16 - harnessW;
|
|
387
|
+
c.strokeStyle = LINE;
|
|
388
|
+
c.lineWidth = 1;
|
|
389
|
+
c.strokeRect(harnessX + 0.5, fy + 8.5, harnessW - 1, 21);
|
|
390
|
+
text(harness, harnessX + 6, fy + 19);
|
|
391
|
+
var meta = CARD.meta.toUpperCase();
|
|
392
|
+
var metaX = harnessX - 14 - c.measureText(meta).width;
|
|
393
|
+
text(meta, metaX, fy + 19);
|
|
394
|
+
rule(handleEnd, fy + 19, Math.max(0, metaX - 14 - handleEnd), 1);
|
|
395
|
+
track('normal');
|
|
396
|
+
|
|
397
|
+
// body
|
|
398
|
+
dots(fx + 1, barBottom + 1, fw - 2, 404, 26);
|
|
399
|
+
font('400', 11);
|
|
400
|
+
track('0.4px');
|
|
401
|
+
c.fillStyle = ACCENT;
|
|
402
|
+
text('# ' + CARD.eyebrow, fx + 36, barBottom + 18);
|
|
403
|
+
font('500', 34);
|
|
404
|
+
track('-2px');
|
|
405
|
+
c.fillStyle = FG;
|
|
406
|
+
text(CARD.title, fx + 36, barBottom + 52);
|
|
407
|
+
track('normal');
|
|
408
|
+
|
|
409
|
+
var mainMid = barBottom + 74 + (405 - 74) / 2;
|
|
410
|
+
|
|
411
|
+
font('600', 170);
|
|
412
|
+
track('-8px');
|
|
413
|
+
c.fillStyle = ACCENT;
|
|
414
|
+
text(CARD.grade, fx + 186, mainMid - 15, 'center');
|
|
415
|
+
track('normal');
|
|
416
|
+
font('400', 11);
|
|
417
|
+
track('1.5px');
|
|
418
|
+
c.fillStyle = MUTED2;
|
|
419
|
+
text(CARD.grade_label.toUpperCase(), fx + 186, mainMid + 88, 'center');
|
|
420
|
+
track('normal');
|
|
421
|
+
|
|
422
|
+
var bx = fx + 392;
|
|
423
|
+
var bw = fx + fw - 36 - bx;
|
|
424
|
+
var rowH = 35, gap = 28;
|
|
425
|
+
var top = mainMid - (3 * rowH + 2 * gap) / 2;
|
|
426
|
+
CARD.bars.forEach(function (bar, index) {
|
|
427
|
+
var y = top + index * (rowH + gap);
|
|
428
|
+
font('500', 14);
|
|
429
|
+
c.fillStyle = FG;
|
|
430
|
+
text(bar[0].toLowerCase(), bx, y + 9);
|
|
431
|
+
font('600', 14);
|
|
432
|
+
text(String(bar[1]), bx + bw, y + 9, 'right');
|
|
433
|
+
c.fillStyle = LINE_SOFT;
|
|
434
|
+
c.fillRect(bx, y + 27, bw, 8);
|
|
435
|
+
c.strokeStyle = LINE;
|
|
436
|
+
c.strokeRect(bx + 0.5, y + 27.5, bw - 1, 7);
|
|
437
|
+
c.fillStyle = ACCENT;
|
|
438
|
+
c.fillRect(bx, y + 27, bw * Math.max(0, Math.min(100, bar[1])) / 100, 8);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// stats
|
|
442
|
+
var sy = barBottom + 405;
|
|
443
|
+
c.fillStyle = PANEL;
|
|
444
|
+
c.fillRect(fx + 1, sy, fw - 2, 96);
|
|
445
|
+
rule(fx, sy, fw, 1);
|
|
446
|
+
var colW = (fw - 2) / 3;
|
|
447
|
+
CARD.stats.forEach(function (stat, index) {
|
|
448
|
+
var cx = fx + 1 + index * colW;
|
|
449
|
+
if (index) rule(cx, sy, 1, 96);
|
|
450
|
+
font('600', 40);
|
|
451
|
+
c.fillStyle = FG;
|
|
452
|
+
text(stat[0], cx + 24, sy + 40);
|
|
453
|
+
font('400', 12);
|
|
454
|
+
c.fillStyle = MUTED;
|
|
455
|
+
text(stat[1], cx + 24, sy + 72);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// footer
|
|
459
|
+
var gy = sy + 96;
|
|
460
|
+
c.fillStyle = '#fff';
|
|
461
|
+
c.fillRect(fx + 1, gy, fw - 2, fy + fh - gy - 1);
|
|
462
|
+
rule(fx, gy, fw, 1);
|
|
463
|
+
font('600', 15);
|
|
464
|
+
track('-0.45px');
|
|
465
|
+
c.fillStyle = FG;
|
|
466
|
+
text(CARD.stamp[0], fx + 16, gy + 20);
|
|
467
|
+
track('normal');
|
|
468
|
+
font('400', 11);
|
|
469
|
+
c.fillStyle = MUTED2;
|
|
470
|
+
text(CARD.stamp[1], fx + 16, gy + 37);
|
|
471
|
+
|
|
472
|
+
return canvas;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function slug(value) {
|
|
476
|
+
return (value || '').toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
477
|
+
.replace(/^-+|-+$/g, '') || 'agent';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
var button = document.getElementById('share-png');
|
|
481
|
+
button.addEventListener('click', function () {
|
|
482
|
+
var label = button.textContent;
|
|
483
|
+
button.disabled = true;
|
|
484
|
+
var ready = (document.fonts && document.fonts.ready) || Promise.resolve();
|
|
485
|
+
ready.then(function () {
|
|
486
|
+
drawCard(2).toBlob(function (blob) {
|
|
487
|
+
var url = URL.createObjectURL(blob);
|
|
488
|
+
var a = document.createElement('a');
|
|
489
|
+
a.href = url;
|
|
490
|
+
a.download = slug(CARD.handle) + '-skill-report.png';
|
|
491
|
+
a.click();
|
|
492
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 2000);
|
|
493
|
+
button.disabled = false;
|
|
494
|
+
button.textContent = 'saved \u2713';
|
|
495
|
+
setTimeout(function () { button.textContent = label; }, 2000);
|
|
496
|
+
}, 'image/png');
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
})();
|
|
500
|
+
"""
|
|
501
|
+
return script.replace("__CARD__", card_data.replace("</", "<\\/")).replace("__CLAMP__", str(DIFF_CLAMP_PX))
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def parse_args(argv=None):
|
|
505
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
506
|
+
parser.add_argument(
|
|
507
|
+
"report_path",
|
|
508
|
+
nargs="?",
|
|
509
|
+
default="./doctor-md-skill-report/report.json",
|
|
510
|
+
help="Path to the report.json file",
|
|
511
|
+
)
|
|
512
|
+
parser.add_argument(
|
|
513
|
+
"--open",
|
|
514
|
+
action="store_true",
|
|
515
|
+
dest="open_browser",
|
|
516
|
+
help="Open the generated report in the default browser",
|
|
517
|
+
)
|
|
518
|
+
return parser.parse_args(argv)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def main(argv=None):
|
|
522
|
+
args = parse_args(argv)
|
|
523
|
+
report_path = Path(args.report_path).expanduser()
|
|
524
|
+
if not report_path.exists():
|
|
525
|
+
print(f"error: {report_path} not found", file=sys.stderr)
|
|
526
|
+
sys.exit(1)
|
|
527
|
+
r = json.loads(report_path.read_text())
|
|
528
|
+
r.setdefault("grade", grade_for(r["scores"]["overall"]))
|
|
529
|
+
|
|
530
|
+
out_path = report_path.parent / "report.html"
|
|
531
|
+
out_path.write_text(render_page(r))
|
|
532
|
+
print(f"report: {out_path.absolute().as_uri()}")
|
|
533
|
+
if args.open_browser:
|
|
534
|
+
if open_report(out_path):
|
|
535
|
+
print(" opened in the default browser")
|
|
536
|
+
else:
|
|
537
|
+
print(
|
|
538
|
+
"warning: could not open the report in the default browser",
|
|
539
|
+
file=sys.stderr,
|
|
540
|
+
)
|
|
541
|
+
print(' use "share as png" for a 1200x675 share image')
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
main()
|