quackscore 0.2.5 → 0.4.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 (137) hide show
  1. package/README.md +20 -1
  2. package/dist/commands/create.d.ts +1 -0
  3. package/dist/commands/create.d.ts.map +1 -1
  4. package/dist/commands/create.js +28 -12
  5. package/dist/commands/create.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +19 -0
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/leaderboard.d.ts +5 -1
  10. package/dist/commands/leaderboard.d.ts.map +1 -1
  11. package/dist/commands/leaderboard.js +57 -29
  12. package/dist/commands/leaderboard.js.map +1 -1
  13. package/dist/commands/mock-report.js +21 -21
  14. package/dist/commands/mock-report.js.map +1 -1
  15. package/dist/commands/profile-options.d.ts +24 -0
  16. package/dist/commands/profile-options.d.ts.map +1 -0
  17. package/dist/commands/profile-options.js +38 -0
  18. package/dist/commands/profile-options.js.map +1 -0
  19. package/dist/commands/remove.d.ts +5 -1
  20. package/dist/commands/remove.d.ts.map +1 -1
  21. package/dist/commands/remove.js +14 -5
  22. package/dist/commands/remove.js.map +1 -1
  23. package/dist/commands/show.d.ts +5 -1
  24. package/dist/commands/show.d.ts.map +1 -1
  25. package/dist/commands/show.js +12 -3
  26. package/dist/commands/show.js.map +1 -1
  27. package/dist/commands/stats.d.ts +7 -0
  28. package/dist/commands/stats.d.ts.map +1 -0
  29. package/dist/commands/stats.js +59 -0
  30. package/dist/commands/stats.js.map +1 -0
  31. package/dist/commands/update-summary.d.ts +2 -0
  32. package/dist/commands/update-summary.d.ts.map +1 -1
  33. package/dist/commands/update-summary.js +12 -3
  34. package/dist/commands/update-summary.js.map +1 -1
  35. package/dist/commands/update.d.ts.map +1 -1
  36. package/dist/commands/update.js +148 -68
  37. package/dist/commands/update.js.map +1 -1
  38. package/dist/config/types.d.ts +1 -1
  39. package/dist/config/types.d.ts.map +1 -1
  40. package/dist/config/types.js +14 -0
  41. package/dist/config/types.js.map +1 -1
  42. package/dist/github/client.d.ts +5 -5
  43. package/dist/github/client.d.ts.map +1 -1
  44. package/dist/github/client.js +157 -57
  45. package/dist/github/client.js.map +1 -1
  46. package/dist/github/index.d.ts +2 -0
  47. package/dist/github/index.d.ts.map +1 -1
  48. package/dist/github/index.js +1 -0
  49. package/dist/github/index.js.map +1 -1
  50. package/dist/github/repository-stats.d.ts +8 -0
  51. package/dist/github/repository-stats.d.ts.map +1 -0
  52. package/dist/github/repository-stats.js +96 -0
  53. package/dist/github/repository-stats.js.map +1 -0
  54. package/dist/index.js +48 -7
  55. package/dist/index.js.map +1 -1
  56. package/dist/llm/analyzer.d.ts +3 -3
  57. package/dist/llm/analyzer.d.ts.map +1 -1
  58. package/dist/llm/analyzer.js +5 -5
  59. package/dist/llm/analyzer.js.map +1 -1
  60. package/dist/llm/character.d.ts +2 -2
  61. package/dist/llm/character.d.ts.map +1 -1
  62. package/dist/llm/character.js +3 -3
  63. package/dist/llm/character.js.map +1 -1
  64. package/dist/llm/client.d.ts +2 -2
  65. package/dist/llm/client.d.ts.map +1 -1
  66. package/dist/llm/client.js +1 -1
  67. package/dist/llm/client.js.map +1 -1
  68. package/dist/llm/prompts.d.ts.map +1 -1
  69. package/dist/llm/prompts.js +13 -1
  70. package/dist/llm/prompts.js.map +1 -1
  71. package/dist/llm/providers.d.ts +3 -3
  72. package/dist/llm/providers.d.ts.map +1 -1
  73. package/dist/llm/providers.js +25 -6
  74. package/dist/llm/providers.js.map +1 -1
  75. package/dist/report/generate.d.ts.map +1 -1
  76. package/dist/report/generate.js +9 -7
  77. package/dist/report/generate.js.map +1 -1
  78. package/dist/report/index.d.ts +1 -0
  79. package/dist/report/index.d.ts.map +1 -1
  80. package/dist/report/index.js +1 -0
  81. package/dist/report/index.js.map +1 -1
  82. package/dist/report/leaderboard.d.ts +5 -1
  83. package/dist/report/leaderboard.d.ts.map +1 -1
  84. package/dist/report/leaderboard.js +611 -865
  85. package/dist/report/leaderboard.js.map +1 -1
  86. package/dist/report/repository-stats.d.ts +3 -0
  87. package/dist/report/repository-stats.d.ts.map +1 -0
  88. package/dist/report/repository-stats.js +650 -0
  89. package/dist/report/repository-stats.js.map +1 -0
  90. package/dist/scoring/index.d.ts +1 -0
  91. package/dist/scoring/index.d.ts.map +1 -1
  92. package/dist/scoring/index.js +1 -0
  93. package/dist/scoring/index.js.map +1 -1
  94. package/dist/scoring/points.d.ts.map +1 -1
  95. package/dist/scoring/points.js +5 -10
  96. package/dist/scoring/points.js.map +1 -1
  97. package/dist/scoring/repository-stats.d.ts +7 -0
  98. package/dist/scoring/repository-stats.d.ts.map +1 -0
  99. package/dist/scoring/repository-stats.js +120 -0
  100. package/dist/scoring/repository-stats.js.map +1 -0
  101. package/dist/shared/profile-scope.d.ts +19 -0
  102. package/dist/shared/profile-scope.d.ts.map +1 -0
  103. package/dist/shared/profile-scope.js +101 -0
  104. package/dist/shared/profile-scope.js.map +1 -0
  105. package/dist/shared/profile-summary.js +6 -6
  106. package/dist/shared/profile-summary.js.map +1 -1
  107. package/dist/shared/repository-stats.d.ts +51 -0
  108. package/dist/shared/repository-stats.d.ts.map +1 -0
  109. package/dist/shared/repository-stats.js +2 -0
  110. package/dist/shared/repository-stats.js.map +1 -0
  111. package/dist/shared/types.d.ts +29 -1
  112. package/dist/shared/types.d.ts.map +1 -1
  113. package/dist/shared/ui.d.ts +1 -0
  114. package/dist/shared/ui.d.ts.map +1 -1
  115. package/dist/shared/ui.js +3 -0
  116. package/dist/shared/ui.js.map +1 -1
  117. package/dist/storage/index.d.ts +2 -2
  118. package/dist/storage/index.d.ts.map +1 -1
  119. package/dist/storage/index.js +2 -2
  120. package/dist/storage/index.js.map +1 -1
  121. package/dist/storage/leaderboard.d.ts +3 -2
  122. package/dist/storage/leaderboard.d.ts.map +1 -1
  123. package/dist/storage/leaderboard.js +80 -9
  124. package/dist/storage/leaderboard.js.map +1 -1
  125. package/dist/storage/paths.d.ts +6 -0
  126. package/dist/storage/paths.d.ts.map +1 -1
  127. package/dist/storage/paths.js +21 -0
  128. package/dist/storage/paths.js.map +1 -1
  129. package/dist/storage/report.d.ts +5 -2
  130. package/dist/storage/report.d.ts.map +1 -1
  131. package/dist/storage/report.js +38 -8
  132. package/dist/storage/report.js.map +1 -1
  133. package/dist/storage/user.d.ts +2 -1
  134. package/dist/storage/user.d.ts.map +1 -1
  135. package/dist/storage/user.js +27 -10
  136. package/dist/storage/user.js.map +1 -1
  137. package/package.json +13 -6
@@ -0,0 +1,650 @@
1
+ import { formatDuration } from '../scoring/repository-stats.js';
2
+ function escapeHtml(value) {
3
+ return value
4
+ .replace(/&/g, '&')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/'/g, '&#39;');
9
+ }
10
+ function serializeForScript(value) {
11
+ return JSON.stringify(value).replace(/</g, '\\u003c');
12
+ }
13
+ function formatNumber(value) {
14
+ return value.toLocaleString('en-US', { maximumFractionDigits: 1 });
15
+ }
16
+ export function generateRepositoryStatsHTML(data) {
17
+ const scopeLabel = data.team
18
+ ? `${data.org}/${data.repo} - ${data.team}`
19
+ : `${data.org}/${data.repo}`;
20
+ const generatedAt = new Date(data.generatedAt).toLocaleString('en-US', {
21
+ day: '2-digit',
22
+ month: 'short',
23
+ year: 'numeric',
24
+ hour: '2-digit',
25
+ minute: '2-digit',
26
+ });
27
+ return `<!DOCTYPE html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="UTF-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
32
+ <title>Quackscore Repository Stats - ${escapeHtml(scopeLabel)}</title>
33
+ <style>
34
+ @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@600;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap');
35
+
36
+ :root {
37
+ color-scheme: dark;
38
+ --bg: #070707;
39
+ --panel: #111111;
40
+ --panel-soft: #171717;
41
+ --panel-strong: #1f1f1f;
42
+ --border: rgba(255,255,255,0.10);
43
+ --text: #f4f4f4;
44
+ --muted: #a3a3a3;
45
+ --amber: #f5b331;
46
+ --amber-soft: rgba(245,179,49,0.16);
47
+ --blue: #7da2ff;
48
+ --blue-soft: rgba(125,162,255,0.16);
49
+ --violet: #b380ff;
50
+ --green: #7dd3a7;
51
+ --danger: #ff8d8d;
52
+ }
53
+ * { box-sizing: border-box; }
54
+ body {
55
+ margin: 0;
56
+ min-height: 100vh;
57
+ font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
58
+ color: var(--text);
59
+ background: linear-gradient(180deg, #050505 0%, #101010 58%, #080808 100%);
60
+ }
61
+ button, input {
62
+ font: inherit;
63
+ }
64
+ .shell {
65
+ width: min(1180px, calc(100vw - 32px));
66
+ margin: 0 auto;
67
+ padding: 28px 0 44px;
68
+ }
69
+ .hero {
70
+ display: grid;
71
+ grid-template-columns: minmax(0, 1fr) auto;
72
+ gap: 18px;
73
+ align-items: end;
74
+ padding: 24px;
75
+ border: 1px solid rgba(245,179,49,0.18);
76
+ border-radius: 8px;
77
+ background: linear-gradient(135deg, rgba(31,31,31,0.98), rgba(12,12,12,0.98));
78
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
79
+ }
80
+ .eyebrow {
81
+ margin: 0 0 8px;
82
+ color: var(--muted);
83
+ font-family: "JetBrains Mono", ui-monospace, monospace;
84
+ font-size: 0.78rem;
85
+ text-transform: uppercase;
86
+ letter-spacing: 0;
87
+ }
88
+ h1 {
89
+ margin: 0;
90
+ color: var(--amber);
91
+ font-family: "Cinzel", ui-serif, Georgia, serif;
92
+ font-size: clamp(2rem, 5vw, 4rem);
93
+ line-height: 1;
94
+ }
95
+ .hero-meta {
96
+ margin: 10px 0 0;
97
+ color: var(--muted);
98
+ line-height: 1.5;
99
+ }
100
+ .hero-count {
101
+ min-width: 150px;
102
+ padding: 14px 16px;
103
+ border: 1px solid var(--border);
104
+ border-radius: 8px;
105
+ background: rgba(255,255,255,0.04);
106
+ font-family: "JetBrains Mono", ui-monospace, monospace;
107
+ color: var(--muted);
108
+ }
109
+ .hero-count strong {
110
+ display: block;
111
+ color: var(--amber);
112
+ font-size: 1.8rem;
113
+ font-family: "Cinzel", ui-serif, Georgia, serif;
114
+ }
115
+ .filters {
116
+ display: grid;
117
+ grid-template-columns: minmax(0, 1fr) auto;
118
+ gap: 14px;
119
+ margin-top: 16px;
120
+ padding: 16px;
121
+ border: 1px solid var(--border);
122
+ border-radius: 8px;
123
+ background: var(--panel);
124
+ }
125
+ .range-buttons {
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ gap: 8px;
129
+ }
130
+ .range-btn,
131
+ .date-input {
132
+ min-height: 40px;
133
+ border: 1px solid rgba(255,255,255,0.12);
134
+ border-radius: 8px;
135
+ background: var(--panel-strong);
136
+ color: var(--text);
137
+ }
138
+ .range-btn {
139
+ padding: 0 14px;
140
+ cursor: pointer;
141
+ }
142
+ .range-btn:hover,
143
+ .range-btn.is-active {
144
+ border-color: rgba(245,179,49,0.42);
145
+ background: rgba(245,179,49,0.12);
146
+ color: #ffd98c;
147
+ }
148
+ .custom-range {
149
+ display: none;
150
+ grid-template-columns: repeat(2, minmax(150px, 1fr));
151
+ gap: 8px;
152
+ }
153
+ .custom-range.is-visible {
154
+ display: grid;
155
+ }
156
+ .date-input {
157
+ padding: 0 12px;
158
+ }
159
+ .date-input::-webkit-calendar-picker-indicator {
160
+ filter: invert(0.8) opacity(0.7);
161
+ }
162
+ .summary-grid {
163
+ display: grid;
164
+ grid-template-columns: repeat(5, minmax(0, 1fr));
165
+ gap: 12px;
166
+ margin-top: 16px;
167
+ }
168
+ .metric {
169
+ min-height: 112px;
170
+ padding: 16px;
171
+ border: 1px solid var(--border);
172
+ border-radius: 8px;
173
+ background: linear-gradient(180deg, var(--panel), var(--panel-soft));
174
+ }
175
+ .metric span {
176
+ display: block;
177
+ color: var(--muted);
178
+ font-size: 0.78rem;
179
+ font-family: "JetBrains Mono", ui-monospace, monospace;
180
+ text-transform: uppercase;
181
+ }
182
+ .metric strong {
183
+ display: block;
184
+ margin-top: 12px;
185
+ color: var(--text);
186
+ font-size: clamp(1.45rem, 3vw, 2rem);
187
+ }
188
+ .metric.accent strong { color: var(--amber); }
189
+ .metric.blue strong { color: var(--blue); }
190
+ .metric.green strong { color: var(--green); }
191
+ .content-grid {
192
+ display: grid;
193
+ grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr);
194
+ gap: 16px;
195
+ margin-top: 16px;
196
+ }
197
+ .section {
198
+ border: 1px solid var(--border);
199
+ border-radius: 8px;
200
+ background: var(--panel);
201
+ overflow: hidden;
202
+ }
203
+ .section-header {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ align-items: center;
207
+ gap: 12px;
208
+ padding: 16px 18px;
209
+ border-bottom: 1px solid rgba(255,255,255,0.08);
210
+ }
211
+ .section-header h2 {
212
+ margin: 0;
213
+ font-size: 1rem;
214
+ }
215
+ .section-header span {
216
+ color: var(--muted);
217
+ font-size: 0.86rem;
218
+ }
219
+ .chart {
220
+ padding: 18px;
221
+ }
222
+ .bar-row,
223
+ .trend-row {
224
+ display: grid;
225
+ grid-template-columns: 94px minmax(0, 1fr) 72px;
226
+ gap: 12px;
227
+ align-items: center;
228
+ min-height: 34px;
229
+ color: var(--muted);
230
+ font-size: 0.86rem;
231
+ }
232
+ .bar-track,
233
+ .trend-track {
234
+ height: 14px;
235
+ border-radius: 8px;
236
+ background: rgba(255,255,255,0.07);
237
+ overflow: hidden;
238
+ }
239
+ .bar-fill,
240
+ .approval-fill,
241
+ .merge-fill {
242
+ height: 100%;
243
+ border-radius: 8px;
244
+ }
245
+ .bar-fill {
246
+ background: linear-gradient(90deg, var(--amber), #ffe0a0);
247
+ }
248
+ .trend-track {
249
+ display: grid;
250
+ grid-template-columns: 1fr;
251
+ }
252
+ .approval-fill {
253
+ background: linear-gradient(90deg, var(--blue), #c6d5ff);
254
+ }
255
+ .merge-fill {
256
+ background: linear-gradient(90deg, var(--violet), #ddc8ff);
257
+ }
258
+ .trend-legend {
259
+ display: flex;
260
+ gap: 12px;
261
+ color: var(--muted);
262
+ font-size: 0.82rem;
263
+ }
264
+ .legend-dot {
265
+ display: inline-block;
266
+ width: 9px;
267
+ height: 9px;
268
+ margin-right: 6px;
269
+ border-radius: 999px;
270
+ }
271
+ .legend-dot.approval { background: var(--blue); }
272
+ .legend-dot.merge { background: var(--violet); }
273
+ .table-wrap {
274
+ overflow-x: auto;
275
+ }
276
+ table {
277
+ width: 100%;
278
+ border-collapse: collapse;
279
+ min-width: 620px;
280
+ }
281
+ th,
282
+ td {
283
+ padding: 13px 16px;
284
+ border-bottom: 1px solid rgba(255,255,255,0.07);
285
+ text-align: left;
286
+ }
287
+ th {
288
+ color: var(--muted);
289
+ font-family: "JetBrains Mono", ui-monospace, monospace;
290
+ font-size: 0.74rem;
291
+ text-transform: uppercase;
292
+ }
293
+ td {
294
+ color: var(--text);
295
+ }
296
+ td.numeric,
297
+ th.numeric {
298
+ text-align: right;
299
+ }
300
+ .mono {
301
+ font-family: "JetBrains Mono", ui-monospace, monospace;
302
+ }
303
+ .empty {
304
+ padding: 24px;
305
+ color: var(--muted);
306
+ text-align: center;
307
+ }
308
+ @media (max-width: 920px) {
309
+ .hero,
310
+ .filters,
311
+ .content-grid {
312
+ grid-template-columns: 1fr;
313
+ }
314
+ .summary-grid {
315
+ grid-template-columns: repeat(2, minmax(0, 1fr));
316
+ }
317
+ }
318
+ @media (max-width: 560px) {
319
+ .shell {
320
+ width: min(100vw - 20px, 1180px);
321
+ padding-top: 10px;
322
+ }
323
+ .hero,
324
+ .filters,
325
+ .metric,
326
+ .section-header,
327
+ .chart {
328
+ padding: 12px;
329
+ }
330
+ .summary-grid {
331
+ grid-template-columns: 1fr;
332
+ }
333
+ .bar-row,
334
+ .trend-row {
335
+ grid-template-columns: 72px minmax(0, 1fr) 54px;
336
+ gap: 8px;
337
+ }
338
+ table {
339
+ min-width: 560px;
340
+ }
341
+ }
342
+ </style>
343
+ </head>
344
+ <body>
345
+ <main class="shell">
346
+ <section class="hero">
347
+ <div>
348
+ <p class="eyebrow">Repository stats${data.team ? ' / Team authors' : ''}</p>
349
+ <h1>${escapeHtml(scopeLabel)}</h1>
350
+ <p class="hero-meta">Generated ${escapeHtml(generatedAt)}. Metrics update instantly when the date range changes.</p>
351
+ </div>
352
+ <div class="hero-count"><strong data-count>${data.summary.mergedPRs}</strong> merged PRs</div>
353
+ </section>
354
+
355
+ <section class="filters" aria-label="Report filters">
356
+ <div class="range-buttons" data-range-buttons>
357
+ <button class="range-btn is-active" type="button" data-range="all">All time</button>
358
+ <button class="range-btn" type="button" data-range="last_month">Last month</button>
359
+ <button class="range-btn" type="button" data-range="last_3_months">Last 3 months</button>
360
+ <button class="range-btn" type="button" data-range="last_6_months">Last 6 months</button>
361
+ <button class="range-btn" type="button" data-range="last_year">Last year</button>
362
+ <button class="range-btn" type="button" data-range="custom">Custom</button>
363
+ </div>
364
+ <div class="custom-range" data-custom-range>
365
+ <input class="date-input" type="date" aria-label="From date" data-from>
366
+ <input class="date-input" type="date" aria-label="To date" data-to>
367
+ </div>
368
+ </section>
369
+
370
+ <section class="summary-grid" aria-label="Summary metrics">
371
+ <div class="metric accent"><span>Merged PRs</span><strong data-metric="mergedPRs">${formatNumber(data.summary.mergedPRs)}</strong></div>
372
+ <div class="metric green"><span>Approved PRs</span><strong data-metric="approvedPRs">${formatNumber(data.summary.approvedPRs)}</strong></div>
373
+ <div class="metric blue"><span>Avg approval</span><strong data-metric="averageApprovalHours">${escapeHtml(formatDuration(data.summary.averageApprovalHours))}</strong></div>
374
+ <div class="metric"><span>Avg merge</span><strong data-metric="averageMergeHours">${escapeHtml(formatDuration(data.summary.averageMergeHours))}</strong></div>
375
+ <div class="metric"><span>Avg comments</span><strong data-metric="averageComments">${formatNumber(data.summary.averageComments)}</strong></div>
376
+ </section>
377
+
378
+ <section class="content-grid">
379
+ <div class="section">
380
+ <div class="section-header">
381
+ <h2>Weekly Contributions</h2>
382
+ <span>Merged PRs per week</span>
383
+ </div>
384
+ <div class="chart" data-weekly-chart></div>
385
+ </div>
386
+ <div class="section">
387
+ <div class="section-header">
388
+ <h2>Timing Trends</h2>
389
+ <div class="trend-legend">
390
+ <span><i class="legend-dot approval"></i>Approval</span>
391
+ <span><i class="legend-dot merge"></i>Merge</span>
392
+ </div>
393
+ </div>
394
+ <div class="chart" data-timing-chart></div>
395
+ </div>
396
+ </section>
397
+
398
+ <section class="content-grid">
399
+ <div class="section">
400
+ <div class="section-header">
401
+ <h2>Author Stats</h2>
402
+ <span>PR ownership and discussion load</span>
403
+ </div>
404
+ <div class="table-wrap">
405
+ <table>
406
+ <thead><tr><th>Author</th><th class="numeric">PRs</th><th class="numeric">Approved</th><th class="numeric">Avg approval</th><th class="numeric">Avg comments</th></tr></thead>
407
+ <tbody data-author-table></tbody>
408
+ </table>
409
+ </div>
410
+ </div>
411
+ <div class="section">
412
+ <div class="section-header">
413
+ <h2>Approver Speed</h2>
414
+ <span>Fastest reviewers first</span>
415
+ </div>
416
+ <div class="table-wrap">
417
+ <table>
418
+ <thead><tr><th>Approver</th><th class="numeric">Approvals</th><th class="numeric">Avg approval</th></tr></thead>
419
+ <tbody data-approver-table></tbody>
420
+ </table>
421
+ </div>
422
+ </div>
423
+ </section>
424
+ </main>
425
+
426
+ <script id="repository-stats-data" type="application/json">${serializeForScript(data)}</script>
427
+ <script>
428
+ (function() {
429
+ const HOUR_MS = 60 * 60 * 1000;
430
+ const DAY_MS = 24 * HOUR_MS;
431
+ const dataElement = document.getElementById('repository-stats-data');
432
+ if (!dataElement) return;
433
+ const report = JSON.parse(dataElement.textContent || '{}');
434
+ const prs = Array.isArray(report.pullRequests) ? report.pullRequests : [];
435
+ const generatedAtMs = Date.parse(report.generatedAt) || Date.now();
436
+ let range = 'all';
437
+
438
+ const buttons = Array.from(document.querySelectorAll('[data-range]'));
439
+ const customRange = document.querySelector('[data-custom-range]');
440
+ const fromInput = document.querySelector('[data-from]');
441
+ const toInput = document.querySelector('[data-to]');
442
+ const count = document.querySelector('[data-count]');
443
+ const weeklyChart = document.querySelector('[data-weekly-chart]');
444
+ const timingChart = document.querySelector('[data-timing-chart]');
445
+ const authorTable = document.querySelector('[data-author-table]');
446
+ const approverTable = document.querySelector('[data-approver-table]');
447
+
448
+ const escapeHtml = (value) => String(value)
449
+ .replace(/&/g, '&amp;')
450
+ .replace(/</g, '&lt;')
451
+ .replace(/>/g, '&gt;')
452
+ .replace(/"/g, '&quot;')
453
+ .replace(/'/g, '&#39;');
454
+
455
+ const formatNumber = (value) => Number(value || 0).toLocaleString('en-US', { maximumFractionDigits: 1 });
456
+ const formatDuration = (hours) => {
457
+ if (hours == null || Number.isNaN(hours)) return 'N/A';
458
+ if (hours < 1) return Math.round(hours * 60) + 'm';
459
+ if (hours < 48) return hours.toFixed(1) + 'h';
460
+ return (hours / 24).toFixed(1) + 'd';
461
+ };
462
+ const hoursBetween = (start, end) => {
463
+ const startMs = Date.parse(start);
464
+ const endMs = Date.parse(end);
465
+ if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs < startMs) return null;
466
+ return (endMs - startMs) / HOUR_MS;
467
+ };
468
+ const average = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
469
+ const getWeekStart = (value) => {
470
+ const date = new Date(value);
471
+ const day = date.getUTCDay();
472
+ const diff = date.getUTCDate() - day + (day === 0 ? -6 : 1);
473
+ const monday = new Date(date);
474
+ monday.setUTCDate(diff);
475
+ monday.setUTCHours(0, 0, 0, 0);
476
+ return monday.toISOString().slice(0, 10);
477
+ };
478
+ const firstApprovalHours = (pr) => {
479
+ const approvals = Array.isArray(pr.approvals) ? [...pr.approvals] : [];
480
+ approvals.sort((left, right) => Date.parse(left.submittedAt) - Date.parse(right.submittedAt));
481
+ return approvals[0] ? hoursBetween(pr.createdAt, approvals[0].submittedAt) : null;
482
+ };
483
+
484
+ const cutoffFor = () => {
485
+ if (range === 'all' || range === 'custom') return null;
486
+ const days = {
487
+ last_month: 30,
488
+ last_3_months: 90,
489
+ last_6_months: 180,
490
+ last_year: 365,
491
+ }[range];
492
+ return days ? generatedAtMs - days * DAY_MS : null;
493
+ };
494
+
495
+ const filteredPRs = () => {
496
+ const cutoff = cutoffFor();
497
+ const fromMs = range === 'custom' && fromInput && fromInput.value ? Date.parse(fromInput.value) : null;
498
+ const toMs = range === 'custom' && toInput && toInput.value ? Date.parse(toInput.value + 'T23:59:59.999') : null;
499
+ return prs.filter((pr) => {
500
+ const mergedAt = Date.parse(pr.mergedAt);
501
+ if (Number.isNaN(mergedAt)) return false;
502
+ if (cutoff != null && mergedAt < cutoff) return false;
503
+ if (fromMs != null && mergedAt < fromMs) return false;
504
+ if (toMs != null && mergedAt > toMs) return false;
505
+ return true;
506
+ });
507
+ };
508
+
509
+ const buildStats = (items) => {
510
+ const mergeHours = items.map((pr) => hoursBetween(pr.createdAt, pr.mergedAt)).filter((value) => value != null);
511
+ const approvalHours = items.map(firstApprovalHours).filter((value) => value != null);
512
+ const totalComments = items.reduce((sum, pr) => sum + (pr.issueComments || 0) + (pr.reviewComments || 0), 0);
513
+ const weekMap = new Map();
514
+ const trendMap = new Map();
515
+ const authorMap = new Map();
516
+ const approverMap = new Map();
517
+
518
+ items.forEach((pr) => {
519
+ const week = getWeekStart(pr.mergedAt);
520
+ weekMap.set(week, (weekMap.get(week) || 0) + 1);
521
+ const trend = trendMap.get(week) || { approval: [], merge: [] };
522
+ const firstApproval = firstApprovalHours(pr);
523
+ const mergeTime = hoursBetween(pr.createdAt, pr.mergedAt);
524
+ if (firstApproval != null) trend.approval.push(firstApproval);
525
+ if (mergeTime != null) trend.merge.push(mergeTime);
526
+ trendMap.set(week, trend);
527
+
528
+ const authorItems = authorMap.get(pr.author) || [];
529
+ authorItems.push(pr);
530
+ authorMap.set(pr.author, authorItems);
531
+
532
+ (pr.approvals || []).forEach((approval) => {
533
+ const approvalTime = hoursBetween(pr.createdAt, approval.submittedAt);
534
+ if (approvalTime == null) return;
535
+ const approverItems = approverMap.get(approval.approver) || [];
536
+ approverItems.push(approvalTime);
537
+ approverMap.set(approval.approver, approverItems);
538
+ });
539
+ });
540
+
541
+ const weekly = Array.from(weekMap.entries()).map(([week, pullRequests]) => ({ week, pullRequests })).sort((left, right) => left.week.localeCompare(right.week));
542
+ const trends = Array.from(trendMap.entries()).map(([week, values]) => ({
543
+ week,
544
+ approval: average(values.approval),
545
+ merge: average(values.merge),
546
+ })).sort((left, right) => left.week.localeCompare(right.week));
547
+ const authors = Array.from(authorMap.entries()).map(([author, authorPRs]) => {
548
+ const authorApprovals = authorPRs.map(firstApprovalHours).filter((value) => value != null);
549
+ const authorComments = authorPRs.reduce((sum, pr) => sum + (pr.issueComments || 0) + (pr.reviewComments || 0), 0);
550
+ return {
551
+ author,
552
+ pullRequests: authorPRs.length,
553
+ approvedPRs: authorApprovals.length,
554
+ averageApprovalHours: average(authorApprovals),
555
+ averageComments: authorPRs.length ? authorComments / authorPRs.length : 0,
556
+ };
557
+ }).sort((left, right) => right.pullRequests - left.pullRequests || String(left.author).localeCompare(String(right.author)));
558
+ const approvers = Array.from(approverMap.entries()).map(([approver, times]) => ({
559
+ approver,
560
+ approvals: times.length,
561
+ averageApprovalHours: average(times) || 0,
562
+ })).sort((left, right) => left.averageApprovalHours - right.averageApprovalHours || right.approvals - left.approvals || String(left.approver).localeCompare(String(right.approver)));
563
+
564
+ return {
565
+ summary: {
566
+ mergedPRs: items.length,
567
+ approvedPRs: approvalHours.length,
568
+ averageApprovalHours: average(approvalHours),
569
+ averageMergeHours: average(mergeHours),
570
+ averageComments: items.length ? totalComments / items.length : 0,
571
+ },
572
+ weekly,
573
+ trends,
574
+ authors,
575
+ approvers,
576
+ };
577
+ };
578
+
579
+ const renderBars = (items) => {
580
+ if (!weeklyChart) return;
581
+ if (!items.length) {
582
+ weeklyChart.innerHTML = '<div class="empty">No merged pull requests in this date range.</div>';
583
+ return;
584
+ }
585
+ const max = Math.max(...items.map((item) => item.pullRequests), 1);
586
+ weeklyChart.innerHTML = items.map((item) => {
587
+ const width = Math.max(5, Math.round(item.pullRequests / max * 100));
588
+ return '<div class="bar-row"><span class="mono">' + escapeHtml(item.week) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + width + '%"></div></div><strong>' + item.pullRequests + '</strong></div>';
589
+ }).join('');
590
+ };
591
+
592
+ const renderTrends = (items) => {
593
+ if (!timingChart) return;
594
+ if (!items.length) {
595
+ timingChart.innerHTML = '<div class="empty">No timing data in this date range.</div>';
596
+ return;
597
+ }
598
+ const max = Math.max(...items.flatMap((item) => [item.approval || 0, item.merge || 0]), 1);
599
+ timingChart.innerHTML = items.map((item) => {
600
+ const approvalWidth = item.approval == null ? 0 : Math.max(4, Math.round(item.approval / max * 100));
601
+ const mergeWidth = item.merge == null ? 0 : Math.max(4, Math.round(item.merge / max * 100));
602
+ return '<div class="trend-row"><span class="mono">' + escapeHtml(item.week) + '</span><div><div class="trend-track"><div class="approval-fill" style="width:' + approvalWidth + '%"></div></div><div class="trend-track" style="margin-top:6px"><div class="merge-fill" style="width:' + mergeWidth + '%"></div></div></div><strong>' + escapeHtml(formatDuration(item.approval)) + ' / ' + escapeHtml(formatDuration(item.merge)) + '</strong></div>';
603
+ }).join('');
604
+ };
605
+
606
+ const renderAuthors = (items) => {
607
+ if (!authorTable) return;
608
+ authorTable.innerHTML = items.length
609
+ ? items.map((item) => '<tr><td class="mono">' + escapeHtml(item.author) + '</td><td class="numeric">' + item.pullRequests + '</td><td class="numeric">' + item.approvedPRs + '</td><td class="numeric">' + escapeHtml(formatDuration(item.averageApprovalHours)) + '</td><td class="numeric">' + formatNumber(item.averageComments) + '</td></tr>').join('')
610
+ : '<tr><td colspan="5"><div class="empty">No authors in this date range.</div></td></tr>';
611
+ };
612
+
613
+ const renderApprovers = (items) => {
614
+ if (!approverTable) return;
615
+ approverTable.innerHTML = items.length
616
+ ? items.map((item) => '<tr><td class="mono">' + escapeHtml(item.approver) + '</td><td class="numeric">' + item.approvals + '</td><td class="numeric">' + escapeHtml(formatDuration(item.averageApprovalHours)) + '</td></tr>').join('')
617
+ : '<tr><td colspan="3"><div class="empty">No approvals in this date range.</div></td></tr>';
618
+ };
619
+
620
+ const render = () => {
621
+ const stats = buildStats(filteredPRs());
622
+ if (count) count.textContent = String(stats.summary.mergedPRs);
623
+ document.querySelector('[data-metric="mergedPRs"]').textContent = formatNumber(stats.summary.mergedPRs);
624
+ document.querySelector('[data-metric="approvedPRs"]').textContent = formatNumber(stats.summary.approvedPRs);
625
+ document.querySelector('[data-metric="averageApprovalHours"]').textContent = formatDuration(stats.summary.averageApprovalHours);
626
+ document.querySelector('[data-metric="averageMergeHours"]').textContent = formatDuration(stats.summary.averageMergeHours);
627
+ document.querySelector('[data-metric="averageComments"]').textContent = formatNumber(stats.summary.averageComments);
628
+ renderBars(stats.weekly);
629
+ renderTrends(stats.trends);
630
+ renderAuthors(stats.authors);
631
+ renderApprovers(stats.approvers);
632
+ };
633
+
634
+ buttons.forEach((button) => {
635
+ button.addEventListener('click', () => {
636
+ range = button.getAttribute('data-range') || 'all';
637
+ buttons.forEach((item) => item.classList.toggle('is-active', item === button));
638
+ if (customRange) customRange.classList.toggle('is-visible', range === 'custom');
639
+ render();
640
+ });
641
+ });
642
+ if (fromInput) fromInput.addEventListener('change', render);
643
+ if (toInput) toInput.addEventListener('change', render);
644
+ render();
645
+ })();
646
+ </script>
647
+ </body>
648
+ </html>`;
649
+ }
650
+ //# sourceMappingURL=repository-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-stats.js","sourceRoot":"","sources":["../../src/report/repository-stats.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
1
  export { calculatePRPoints, calculateLevel, LEVEL_THRESHOLDS, MAX_LEVEL } from './points.js';
2
2
  export { aggregateStats } from './stats.js';
3
+ export { aggregateRepositoryStats, formatDuration } from './repository-stats.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scoring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scoring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export { calculatePRPoints, calculateLevel, LEVEL_THRESHOLDS, MAX_LEVEL } from './points.js';
2
2
  export { aggregateStats } from './stats.js';
3
+ export { aggregateRepositoryStats, formatDuration } from './repository-stats.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scoring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scoring/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"points.d.ts","sourceRoot":"","sources":["../../src/scoring/points.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,eAAO,MAAM,SAAS,MAAM,CAAC;AAqB7B,eAAO,MAAM,gBAAgB,UAAoB,CAAC;AAElD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAQpD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAWhG"}
1
+ {"version":3,"file":"points.d.ts","sourceRoot":"","sources":["../../src/scoring/points.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,eAAO,MAAM,SAAS,MAAM,CAAC;AAqB7B,eAAO,MAAM,gBAAgB,UAAoB,CAAC;AAElD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAWhG"}