@urmzd/github-insights 2.0.1 → 2.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/.githooks/commit-msg +4 -0
- package/.githooks/pre-commit +4 -0
- package/AGENTS.md +32 -19
- package/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +18 -19
- package/README.md +21 -24
- package/action.yml +1 -1
- package/assets/insights/index.svg +45 -4
- package/assets/insights/metrics-constellation.svg +55 -0
- package/assets/insights/metrics-impact.svg +55 -0
- package/assets/insights/metrics-rhythm.svg +55 -0
- package/assets/insights/metrics-velocity.svg +55 -0
- package/examples/classic/README.md +36 -2
- package/examples/classic/index.svg +45 -4
- package/examples/classic/metrics-constellation.svg +55 -0
- package/examples/classic/metrics-impact.svg +55 -0
- package/examples/classic/metrics-rhythm.svg +55 -0
- package/examples/classic/metrics-velocity.svg +55 -0
- package/examples/ecosystem/README.md +39 -28
- package/examples/ecosystem/index.svg +45 -4
- package/examples/ecosystem/metrics-constellation.svg +55 -0
- package/examples/ecosystem/metrics-impact.svg +55 -0
- package/examples/ecosystem/metrics-rhythm.svg +55 -0
- package/examples/ecosystem/metrics-velocity.svg +55 -0
- package/examples/minimal/README.md +36 -2
- package/examples/minimal/index.svg +45 -4
- package/examples/minimal/metrics-constellation.svg +55 -0
- package/examples/minimal/metrics-impact.svg +55 -0
- package/examples/minimal/metrics-rhythm.svg +55 -0
- package/examples/minimal/metrics-velocity.svg +55 -0
- package/examples/modern/README.md +62 -50
- package/examples/modern/index.svg +45 -4
- package/examples/modern/metrics-constellation.svg +55 -0
- package/examples/modern/metrics-impact.svg +55 -0
- package/examples/modern/metrics-rhythm.svg +55 -0
- package/examples/modern/metrics-velocity.svg +55 -0
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/skills/github-insights/SKILL.md +35 -81
- package/sr.yaml +9 -0
- package/src/api.ts +3 -141
- package/src/components/contribution-rhythm.tsx +152 -0
- package/src/components/full-svg.test.tsx +4 -1
- package/src/components/full-svg.tsx +14 -7
- package/src/components/impact-trail.tsx +90 -0
- package/src/components/language-velocity.tsx +181 -0
- package/src/components/project-constellation.tsx +97 -0
- package/src/components/section.test.tsx +5 -3
- package/src/components/section.tsx +5 -13
- package/src/components/style-defs.tsx +44 -3
- package/src/index.ts +34 -47
- package/src/metrics.test.ts +50 -57
- package/src/metrics.ts +293 -97
- package/src/readme.test.ts +2 -4
- package/src/templates.test.ts +116 -16
- package/src/templates.ts +68 -27
- package/src/theme.ts +11 -1
- package/src/types.ts +31 -7
- package/assets/insights/metrics-calendar.svg +0 -14
- package/assets/insights/metrics-complexity.svg +0 -14
- package/assets/insights/metrics-contributions.svg +0 -14
- package/assets/insights/metrics-expertise.svg +0 -14
- package/assets/insights/metrics-languages.svg +0 -14
- package/assets/insights/metrics-pulse.svg +0 -14
- package/examples/classic/metrics-calendar.svg +0 -14
- package/examples/classic/metrics-complexity.svg +0 -14
- package/examples/classic/metrics-contributions.svg +0 -14
- package/examples/classic/metrics-expertise.svg +0 -14
- package/examples/classic/metrics-languages.svg +0 -14
- package/examples/classic/metrics-pulse.svg +0 -14
- package/examples/ecosystem/metrics-calendar.svg +0 -14
- package/examples/ecosystem/metrics-complexity.svg +0 -14
- package/examples/ecosystem/metrics-contributions.svg +0 -14
- package/examples/ecosystem/metrics-expertise.svg +0 -14
- package/examples/ecosystem/metrics-languages.svg +0 -14
- package/examples/ecosystem/metrics-pulse.svg +0 -14
- package/examples/minimal/metrics-calendar.svg +0 -14
- package/examples/minimal/metrics-complexity.svg +0 -14
- package/examples/minimal/metrics-contributions.svg +0 -14
- package/examples/minimal/metrics-expertise.svg +0 -14
- package/examples/minimal/metrics-languages.svg +0 -14
- package/examples/minimal/metrics-pulse.svg +0 -14
- package/examples/modern/metrics-calendar.svg +0 -14
- package/examples/modern/metrics-complexity.svg +0 -14
- package/examples/modern/metrics-contributions.svg +0 -14
- package/examples/modern/metrics-expertise.svg +0 -14
- package/examples/modern/metrics-languages.svg +0 -14
- package/examples/modern/metrics-pulse.svg +0 -14
- package/src/components/bar-chart.test.tsx +0 -38
- package/src/components/bar-chart.tsx +0 -54
- package/src/components/contribution-calendar.test.tsx +0 -44
- package/src/components/contribution-calendar.tsx +0 -94
- package/src/components/contribution-cards.test.tsx +0 -36
- package/src/components/contribution-cards.tsx +0 -58
- package/src/components/donut-chart.test.tsx +0 -36
- package/src/components/donut-chart.tsx +0 -102
- package/src/components/project-cards.test.tsx +0 -46
- package/src/components/project-cards.tsx +0 -66
- package/src/components/stat-cards.test.tsx +0 -32
- package/src/components/stat-cards.tsx +0 -57
- package/src/components/tech-highlights.test.tsx +0 -63
- package/src/components/tech-highlights.tsx +0 -109
- package/teasr.toml +0 -14
package/src/metrics.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { renderStatCards } from "./components/stat-cards.js";
|
|
6
|
-
import { renderTechHighlights } from "./components/tech-highlights.js";
|
|
1
|
+
import { renderContributionRhythm } from "./components/contribution-rhythm.js";
|
|
2
|
+
import { renderImpactTrail } from "./components/impact-trail.js";
|
|
3
|
+
import { renderLanguageVelocity } from "./components/language-velocity.js";
|
|
4
|
+
import { renderProjectConstellation } from "./components/project-constellation.js";
|
|
7
5
|
import { parseManifest } from "./parsers.js";
|
|
8
6
|
import type {
|
|
7
|
+
ConstellationNode,
|
|
9
8
|
ContributionData,
|
|
9
|
+
ContributionRhythm,
|
|
10
10
|
LanguageItem,
|
|
11
11
|
ManifestMap,
|
|
12
|
+
MonthlyLanguageBucket,
|
|
12
13
|
ProjectItem,
|
|
13
14
|
ProjectStatus,
|
|
14
15
|
RepoClassificationInput,
|
|
15
16
|
RepoClassificationOutput,
|
|
16
17
|
RepoNode,
|
|
17
18
|
SectionDef,
|
|
18
|
-
TechHighlight,
|
|
19
19
|
} from "./types.js";
|
|
20
20
|
|
|
21
21
|
// ── Category Sets ───────────────────────────────────────────────────────────
|
|
@@ -25,12 +25,10 @@ const EXCLUDED_LANGUAGES = new Set(["Jupyter Notebook"]);
|
|
|
25
25
|
// ── Section keys ────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
27
|
export const SECTION_KEYS: Record<string, string> = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
contributions: "metrics-contributions.svg",
|
|
33
|
-
calendar: "metrics-calendar.svg",
|
|
28
|
+
velocity: "metrics-velocity.svg",
|
|
29
|
+
rhythm: "metrics-rhythm.svg",
|
|
30
|
+
constellation: "metrics-constellation.svg",
|
|
31
|
+
impact: "metrics-impact.svg",
|
|
34
32
|
};
|
|
35
33
|
|
|
36
34
|
// ── Aggregation ─────────────────────────────────────────────────────────────
|
|
@@ -117,6 +115,7 @@ const toProjectItem = (repo: RepoNode): ProjectItem => ({
|
|
|
117
115
|
languageCount: repo.languages.edges.length,
|
|
118
116
|
codeSize: repo.diskUsage,
|
|
119
117
|
languages: repoLanguages(repo),
|
|
118
|
+
isArchived: repo.isArchived || undefined,
|
|
120
119
|
});
|
|
121
120
|
|
|
122
121
|
// ── Top Projects by Stars ───────────────────────────────────────────────────
|
|
@@ -190,6 +189,7 @@ export const splitProjectsByRecency = (
|
|
|
190
189
|
active: ProjectItem[];
|
|
191
190
|
maintained: ProjectItem[];
|
|
192
191
|
inactive: ProjectItem[];
|
|
192
|
+
archived: ProjectItem[];
|
|
193
193
|
} => {
|
|
194
194
|
const commitMap = new Map<string, number>();
|
|
195
195
|
for (const entry of contributionData.commitContributionsByRepository || []) {
|
|
@@ -206,8 +206,17 @@ export const splitProjectsByRecency = (
|
|
|
206
206
|
const activeRepos: RepoNode[] = [];
|
|
207
207
|
const maintainedRepos: RepoNode[] = [];
|
|
208
208
|
const inactiveRepos: RepoNode[] = [];
|
|
209
|
+
const archivedRepos: RepoNode[] = [];
|
|
209
210
|
|
|
210
211
|
for (const repo of repos) {
|
|
212
|
+
if (repo.isArchived) {
|
|
213
|
+
archivedRepos.push(repo);
|
|
214
|
+
console.info(
|
|
215
|
+
`[archived ] ${repo.name} (complexity=${complexityScore(repo).toFixed(1)})`,
|
|
216
|
+
);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
211
220
|
const commits = commitMap.get(repo.name) || 0;
|
|
212
221
|
const aiEntry = aiMap.get(repo.name);
|
|
213
222
|
const status = aiEntry?.status || heuristicStatus(commits, repo.createdAt);
|
|
@@ -227,7 +236,7 @@ export const splitProjectsByRecency = (
|
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
console.info(
|
|
230
|
-
`Split: ${activeRepos.length} active, ${maintainedRepos.length} maintained, ${inactiveRepos.length} inactive`,
|
|
239
|
+
`Split: ${activeRepos.length} active, ${maintainedRepos.length} maintained, ${inactiveRepos.length} inactive, ${archivedRepos.length} archived`,
|
|
231
240
|
);
|
|
232
241
|
|
|
233
242
|
const sortByComplexity = (a: RepoNode, b: RepoNode) =>
|
|
@@ -248,115 +257,302 @@ export const splitProjectsByRecency = (
|
|
|
248
257
|
const inactive: ProjectItem[] = inactiveRepos
|
|
249
258
|
.sort(sortByComplexity)
|
|
250
259
|
.map(toProjectItemWithSummary);
|
|
260
|
+
const archived: ProjectItem[] = archivedRepos
|
|
261
|
+
.sort(sortByComplexity)
|
|
262
|
+
.map(toProjectItemWithSummary);
|
|
263
|
+
|
|
264
|
+
return { active, maintained, inactive, archived };
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ── Language Velocity ────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
export const computeLanguageVelocity = (
|
|
270
|
+
contributionData: ContributionData,
|
|
271
|
+
repos: RepoNode[],
|
|
272
|
+
): MonthlyLanguageBucket[] => {
|
|
273
|
+
// Build a map of repo name → primary language + color
|
|
274
|
+
const repoLangMap = new Map<string, { name: string; color: string }>();
|
|
275
|
+
for (const repo of repos) {
|
|
276
|
+
if (repo.primaryLanguage) {
|
|
277
|
+
repoLangMap.set(repo.name, {
|
|
278
|
+
name: repo.primaryLanguage.name,
|
|
279
|
+
color: repo.primaryLanguage.color,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Build monthly commit counts per language from commitContributionsByRepository
|
|
285
|
+
const monthlyMap = new Map<
|
|
286
|
+
string,
|
|
287
|
+
Map<string, { commits: number; color: string }>
|
|
288
|
+
>();
|
|
289
|
+
|
|
290
|
+
// Use contribution calendar to get month boundaries
|
|
291
|
+
const calendar = contributionData.contributionCalendar;
|
|
292
|
+
if (!calendar) return [];
|
|
293
|
+
|
|
294
|
+
// Get the date range from the calendar
|
|
295
|
+
const allDays = calendar.weeks.flatMap((w) => w.contributionDays);
|
|
296
|
+
if (allDays.length === 0) return [];
|
|
297
|
+
|
|
298
|
+
// Create 12 monthly buckets from the calendar date range
|
|
299
|
+
const firstDate = new Date(allDays[0].date);
|
|
300
|
+
const lastDate = new Date(allDays[allDays.length - 1].date);
|
|
301
|
+
|
|
302
|
+
// Initialize month keys
|
|
303
|
+
const months: string[] = [];
|
|
304
|
+
const d = new Date(firstDate.getFullYear(), firstDate.getMonth(), 1);
|
|
305
|
+
while (d <= lastDate) {
|
|
306
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
307
|
+
months.push(key);
|
|
308
|
+
monthlyMap.set(key, new Map());
|
|
309
|
+
d.setMonth(d.getMonth() + 1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Compute monthly contribution weights from the calendar
|
|
313
|
+
// This gives us the actual activity shape across months
|
|
314
|
+
const monthWeights = new Map<string, number>();
|
|
315
|
+
for (const day of allDays) {
|
|
316
|
+
const date = new Date(day.date);
|
|
317
|
+
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
318
|
+
monthWeights.set(key, (monthWeights.get(key) || 0) + day.contributionCount);
|
|
319
|
+
}
|
|
320
|
+
const totalWeight =
|
|
321
|
+
[...monthWeights.values()].reduce((a, b) => a + b, 0) || 1;
|
|
322
|
+
|
|
323
|
+
// Distribute per-repo commits using monthly weights from the calendar
|
|
324
|
+
for (const entry of contributionData.commitContributionsByRepository || []) {
|
|
325
|
+
const repoName = entry.repository.name;
|
|
326
|
+
const lang = repoLangMap.get(repoName);
|
|
327
|
+
if (!lang) continue;
|
|
328
|
+
|
|
329
|
+
const totalCommits = entry.contributions.totalCount;
|
|
330
|
+
if (totalCommits === 0) continue;
|
|
331
|
+
|
|
332
|
+
for (const monthKey of months) {
|
|
333
|
+
const weight = monthWeights.get(monthKey) || 0;
|
|
334
|
+
const monthCommits = totalCommits * (weight / totalWeight);
|
|
335
|
+
|
|
336
|
+
const langMap = monthlyMap.get(monthKey);
|
|
337
|
+
if (!langMap) continue;
|
|
338
|
+
const existing = langMap.get(lang.name);
|
|
339
|
+
if (existing) {
|
|
340
|
+
existing.commits += monthCommits;
|
|
341
|
+
} else {
|
|
342
|
+
langMap.set(lang.name, {
|
|
343
|
+
commits: monthCommits,
|
|
344
|
+
color: lang.color,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Convert to output format
|
|
351
|
+
return months.map((month) => {
|
|
352
|
+
const langMap = monthlyMap.get(month) || new Map();
|
|
353
|
+
const languages = [...langMap.entries()]
|
|
354
|
+
.map(([name, data]) => ({
|
|
355
|
+
name,
|
|
356
|
+
commits: Math.round(data.commits),
|
|
357
|
+
color: data.color,
|
|
358
|
+
}))
|
|
359
|
+
.sort((a, b) => b.commits - a.commits);
|
|
360
|
+
return { month, languages };
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// ── Contribution Rhythm ─────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
export const computeContributionRhythm = (
|
|
367
|
+
contributionData: ContributionData,
|
|
368
|
+
): ContributionRhythm => {
|
|
369
|
+
const dayTotals: [number, number, number, number, number, number, number] = [
|
|
370
|
+
0, 0, 0, 0, 0, 0, 0,
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const calendar = contributionData.contributionCalendar;
|
|
374
|
+
let longestStreak = 0;
|
|
375
|
+
let currentStreak = 0;
|
|
376
|
+
|
|
377
|
+
if (calendar) {
|
|
378
|
+
for (const week of calendar.weeks) {
|
|
379
|
+
for (const day of week.contributionDays) {
|
|
380
|
+
const dayOfWeek = new Date(day.date).getDay();
|
|
381
|
+
dayTotals[dayOfWeek] += day.contributionCount;
|
|
382
|
+
|
|
383
|
+
if (day.contributionCount > 0) {
|
|
384
|
+
currentStreak++;
|
|
385
|
+
longestStreak = Math.max(longestStreak, currentStreak);
|
|
386
|
+
} else {
|
|
387
|
+
currentStreak = 0;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
251
392
|
|
|
252
|
-
|
|
393
|
+
const { contributions } = contributionData;
|
|
394
|
+
const stats = [
|
|
395
|
+
{
|
|
396
|
+
label: "COMMITS",
|
|
397
|
+
value: contributions.totalCommitContributions.toLocaleString(),
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
label: "PRS",
|
|
401
|
+
value: contributions.totalPullRequestContributions.toLocaleString(),
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
label: "REVIEWS",
|
|
405
|
+
value: contributions.totalPullRequestReviewContributions.toLocaleString(),
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
label: "REPOS",
|
|
409
|
+
value:
|
|
410
|
+
contributions.totalRepositoriesWithContributedCommits.toLocaleString(),
|
|
411
|
+
},
|
|
412
|
+
{ label: "STREAK", value: `${longestStreak}d` },
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
return { dayTotals, longestStreak, stats };
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// ── Project Constellation ───────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
export const computeConstellationLayout = (
|
|
421
|
+
projects: ProjectItem[],
|
|
422
|
+
repos: RepoNode[],
|
|
423
|
+
): ConstellationNode[] => {
|
|
424
|
+
if (projects.length === 0) return [];
|
|
425
|
+
|
|
426
|
+
const chartWidth = 760;
|
|
427
|
+
const chartHeight = 340;
|
|
428
|
+
const padX = 40;
|
|
429
|
+
const padY = 30;
|
|
430
|
+
|
|
431
|
+
// Build repo lookup for disk usage
|
|
432
|
+
const repoMap = new Map<string, RepoNode>();
|
|
433
|
+
for (const repo of repos) {
|
|
434
|
+
repoMap.set(repo.name, repo);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Group projects by primary language
|
|
438
|
+
const langGroups = new Map<string, number[]>();
|
|
439
|
+
for (let i = 0; i < projects.length; i++) {
|
|
440
|
+
const p = projects[i];
|
|
441
|
+
const lang = p.languages?.[0] || "Other";
|
|
442
|
+
if (!langGroups.has(lang)) langGroups.set(lang, []);
|
|
443
|
+
langGroups.get(lang)?.push(i);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const langKeys = [...langGroups.keys()].sort();
|
|
447
|
+
const bandWidth = (chartWidth - 2 * padX) / Math.max(langKeys.length, 1);
|
|
448
|
+
|
|
449
|
+
// Compute complexity range for y-axis normalization
|
|
450
|
+
const complexities = projects.map((p) => {
|
|
451
|
+
const repo = repoMap.get(p.name);
|
|
452
|
+
return repo ? complexityScore(repo) : 0;
|
|
453
|
+
});
|
|
454
|
+
const minC = Math.min(...complexities);
|
|
455
|
+
const maxC = Math.max(...complexities);
|
|
456
|
+
const rangeC = maxC - minC || 1;
|
|
457
|
+
|
|
458
|
+
const nodes: ConstellationNode[] = projects.map((p, i) => {
|
|
459
|
+
const lang = p.languages?.[0] || "Other";
|
|
460
|
+
const bandIndex = langKeys.indexOf(lang);
|
|
461
|
+
const groupIndices = langGroups.get(lang) || [];
|
|
462
|
+
const indexInGroup = groupIndices.indexOf(i);
|
|
463
|
+
|
|
464
|
+
// X: center of language band with jitter
|
|
465
|
+
const bandCenter = padX + bandIndex * bandWidth + bandWidth / 2;
|
|
466
|
+
const jitter =
|
|
467
|
+
groupIndices.length > 1
|
|
468
|
+
? ((indexInGroup - (groupIndices.length - 1) / 2) * bandWidth * 0.4) /
|
|
469
|
+
Math.max(groupIndices.length - 1, 1)
|
|
470
|
+
: 0;
|
|
471
|
+
const x = Math.max(padX, Math.min(chartWidth - padX, bandCenter + jitter));
|
|
472
|
+
|
|
473
|
+
// Y: complexity score (inverted so higher complexity = higher on chart)
|
|
474
|
+
const normC = (complexities[i] - minC) / rangeC;
|
|
475
|
+
const y = padY + (1 - normC) * (chartHeight - 2 * padY);
|
|
476
|
+
|
|
477
|
+
// Radius: based on disk usage
|
|
478
|
+
const repo = repoMap.get(p.name);
|
|
479
|
+
const diskKb = repo?.diskUsage || 100;
|
|
480
|
+
const radius = Math.max(6, Math.min(22, 3 + Math.log2(diskKb / 100) * 3));
|
|
481
|
+
|
|
482
|
+
// Color: primary language color
|
|
483
|
+
const color = repo?.primaryLanguage?.color || "#8b949e";
|
|
484
|
+
|
|
485
|
+
return { name: p.name, url: p.url, x, y, radius, color, connections: [] };
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Connect projects that share 2+ languages
|
|
489
|
+
for (let i = 0; i < projects.length; i++) {
|
|
490
|
+
for (let j = i + 1; j < projects.length; j++) {
|
|
491
|
+
const langsA = new Set(projects[i].languages || []);
|
|
492
|
+
const langsB = projects[j].languages || [];
|
|
493
|
+
const shared = langsB.filter((l) => langsA.has(l)).length;
|
|
494
|
+
if (shared >= 2) {
|
|
495
|
+
nodes[i].connections.push(j);
|
|
496
|
+
nodes[j].connections.push(i);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return nodes;
|
|
253
502
|
};
|
|
254
503
|
|
|
255
504
|
// ── Section definitions ─────────────────────────────────────────────────────
|
|
256
505
|
|
|
257
506
|
export const buildSections = ({
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
507
|
+
velocity,
|
|
508
|
+
rhythm,
|
|
509
|
+
constellation,
|
|
261
510
|
contributionData,
|
|
262
511
|
}: {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
512
|
+
velocity: MonthlyLanguageBucket[];
|
|
513
|
+
rhythm: ContributionRhythm;
|
|
514
|
+
constellation: ConstellationNode[];
|
|
266
515
|
contributionData: ContributionData;
|
|
267
516
|
}): SectionDef[] => {
|
|
268
517
|
const sections: SectionDef[] = [];
|
|
269
518
|
|
|
270
|
-
// 1.
|
|
271
|
-
|
|
272
|
-
filename: "metrics-pulse.svg",
|
|
273
|
-
title: "At a Glance",
|
|
274
|
-
subtitle: "Contribution activity over the past year",
|
|
275
|
-
renderBody: (y: number) => {
|
|
276
|
-
const stats = [
|
|
277
|
-
{
|
|
278
|
-
label: "COMMITS",
|
|
279
|
-
value:
|
|
280
|
-
contributionData.contributions.totalCommitContributions.toLocaleString(),
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
label: "PRS",
|
|
284
|
-
value:
|
|
285
|
-
contributionData.contributions.totalPullRequestContributions.toLocaleString(),
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
label: "REVIEWS",
|
|
289
|
-
value:
|
|
290
|
-
contributionData.contributions.totalPullRequestReviewContributions.toLocaleString(),
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
label: "REPOS",
|
|
294
|
-
value:
|
|
295
|
-
contributionData.contributions.totalRepositoriesWithContributedCommits.toLocaleString(),
|
|
296
|
-
},
|
|
297
|
-
];
|
|
298
|
-
return renderStatCards(stats, y);
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// 2. Languages
|
|
303
|
-
sections.push({
|
|
304
|
-
filename: "metrics-languages.svg",
|
|
305
|
-
title: "Languages",
|
|
306
|
-
subtitle: "By bytes of code across all public repos",
|
|
307
|
-
renderBody: (y: number) => renderDonutChart(languages, y),
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// 3. Expertise
|
|
311
|
-
if (techHighlights.length > 0) {
|
|
519
|
+
// 1. Language Velocity
|
|
520
|
+
if (velocity.length > 0) {
|
|
312
521
|
sections.push({
|
|
313
|
-
filename: "metrics-
|
|
314
|
-
title: "
|
|
315
|
-
subtitle:
|
|
316
|
-
|
|
317
|
-
renderBody: (y: number) => renderTechHighlights(techHighlights, y),
|
|
522
|
+
filename: "metrics-velocity.svg",
|
|
523
|
+
title: "Language Velocity",
|
|
524
|
+
subtitle: "How language usage has evolved over the past year",
|
|
525
|
+
renderBody: (y: number) => renderLanguageVelocity(velocity, y),
|
|
318
526
|
});
|
|
319
527
|
}
|
|
320
528
|
|
|
321
|
-
//
|
|
529
|
+
// 2. Contribution Rhythm
|
|
322
530
|
sections.push({
|
|
323
|
-
filename: "metrics-
|
|
324
|
-
title: "
|
|
325
|
-
subtitle: "
|
|
326
|
-
renderBody: (y: number) =>
|
|
531
|
+
filename: "metrics-rhythm.svg",
|
|
532
|
+
title: "Contribution Rhythm",
|
|
533
|
+
subtitle: "Activity patterns and statistics over the past year",
|
|
534
|
+
renderBody: (y: number) => renderContributionRhythm(rhythm, y),
|
|
327
535
|
});
|
|
328
536
|
|
|
329
|
-
//
|
|
330
|
-
if (
|
|
331
|
-
const calendarData = contributionData.contributionCalendar;
|
|
537
|
+
// 3. Project Constellation
|
|
538
|
+
if (constellation.length > 0) {
|
|
332
539
|
sections.push({
|
|
333
|
-
filename: "metrics-
|
|
334
|
-
title: "
|
|
335
|
-
subtitle:
|
|
336
|
-
renderBody: (y: number) =>
|
|
540
|
+
filename: "metrics-constellation.svg",
|
|
541
|
+
title: "Project Constellation",
|
|
542
|
+
subtitle: "Projects mapped by language ecosystem and complexity",
|
|
543
|
+
renderBody: (y: number) => renderProjectConstellation(constellation, y),
|
|
337
544
|
});
|
|
338
545
|
}
|
|
339
546
|
|
|
340
|
-
//
|
|
547
|
+
// 4. Impact Trail
|
|
341
548
|
if (contributionData.externalRepos.nodes.length > 0) {
|
|
342
549
|
sections.push({
|
|
343
|
-
filename: "metrics-
|
|
344
|
-
title: "Open Source
|
|
345
|
-
subtitle: "External repositories contributed to
|
|
550
|
+
filename: "metrics-impact.svg",
|
|
551
|
+
title: "Open Source Impact",
|
|
552
|
+
subtitle: "External repositories contributed to",
|
|
346
553
|
renderBody: (y: number) => {
|
|
347
|
-
const repos = contributionData.externalRepos.nodes.slice(0,
|
|
348
|
-
|
|
349
|
-
project: r.nameWithOwner,
|
|
350
|
-
detail: [
|
|
351
|
-
r.stargazerCount > 0
|
|
352
|
-
? `\u2605 ${r.stargazerCount.toLocaleString()}`
|
|
353
|
-
: null,
|
|
354
|
-
r.primaryLanguage?.name,
|
|
355
|
-
]
|
|
356
|
-
.filter(Boolean)
|
|
357
|
-
.join(" \u00b7 "),
|
|
358
|
-
}));
|
|
359
|
-
return renderContributionCards(highlights, y);
|
|
554
|
+
const repos = contributionData.externalRepos.nodes.slice(0, 8);
|
|
555
|
+
return renderImpactTrail(repos, y);
|
|
360
556
|
},
|
|
361
557
|
});
|
|
362
558
|
}
|
package/src/readme.test.ts
CHANGED
|
@@ -152,7 +152,7 @@ describe("generateReadme", () => {
|
|
|
152
152
|
svgs: [
|
|
153
153
|
{ label: "Languages", path: "assets/insights/metrics-languages.svg" },
|
|
154
154
|
{ label: "Projects", path: "assets/insights/metrics-projects.svg" },
|
|
155
|
-
{ label: "
|
|
155
|
+
{ label: "Rhythm", path: "assets/insights/metrics-rhythm.svg" },
|
|
156
156
|
],
|
|
157
157
|
});
|
|
158
158
|
expect(result).toContain(
|
|
@@ -161,9 +161,7 @@ describe("generateReadme", () => {
|
|
|
161
161
|
expect(result).toContain(
|
|
162
162
|
"",
|
|
163
163
|
);
|
|
164
|
-
expect(result).toContain(
|
|
165
|
-
"",
|
|
166
|
-
);
|
|
164
|
+
expect(result).toContain("");
|
|
167
165
|
});
|
|
168
166
|
|
|
169
167
|
it("renders all sections combined", () => {
|
package/src/templates.test.ts
CHANGED
|
@@ -20,9 +20,10 @@ const makeContext = (
|
|
|
20
20
|
preamble: "A software developer in Austin, TX.",
|
|
21
21
|
svgs: [{ label: "GitHub Metrics", path: "assets/insights/index.svg" }],
|
|
22
22
|
sectionSvgs: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
velocity: "assets/insights/metrics-velocity.svg",
|
|
24
|
+
rhythm: "assets/insights/metrics-rhythm.svg",
|
|
25
|
+
constellation: "assets/insights/metrics-constellation.svg",
|
|
26
|
+
impact: "assets/insights/metrics-impact.svg",
|
|
26
27
|
},
|
|
27
28
|
profile: makeUserProfile(),
|
|
28
29
|
activeProjects: [
|
|
@@ -44,7 +45,9 @@ const makeContext = (
|
|
|
44
45
|
},
|
|
45
46
|
],
|
|
46
47
|
inactiveProjects: [],
|
|
48
|
+
archivedProjects: [],
|
|
47
49
|
allProjects: [],
|
|
50
|
+
templateName: "classic",
|
|
48
51
|
categorizedProjects: {
|
|
49
52
|
Applications: [
|
|
50
53
|
{
|
|
@@ -69,7 +72,9 @@ const makeContext = (
|
|
|
69
72
|
{ name: "TypeScript", value: 100, percent: "60.0", color: "#3178c6" },
|
|
70
73
|
{ name: "Rust", value: 50, percent: "30.0", color: "#dea584" },
|
|
71
74
|
],
|
|
72
|
-
|
|
75
|
+
velocity: [],
|
|
76
|
+
rhythm: { dayTotals: [0, 0, 0, 0, 0, 0, 0], longestStreak: 0, stats: [] },
|
|
77
|
+
constellation: [],
|
|
73
78
|
contributionData: makeContributionData(),
|
|
74
79
|
socialBadges:
|
|
75
80
|
"[](https://urmzd.dev)",
|
|
@@ -241,6 +246,28 @@ describe("classicTemplate", () => {
|
|
|
241
246
|
expect(output).toContain("/ˈʊrm.zəd/");
|
|
242
247
|
});
|
|
243
248
|
|
|
249
|
+
it("includes archived section for archived projects", () => {
|
|
250
|
+
const output = getTemplate("classic")(
|
|
251
|
+
makeContext({
|
|
252
|
+
archivedProjects: [
|
|
253
|
+
{
|
|
254
|
+
name: "old-project",
|
|
255
|
+
url: "https://github.com/urmzd/old-project",
|
|
256
|
+
description: "An archived project",
|
|
257
|
+
stars: 2,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
expect(output).toContain("## Archived");
|
|
263
|
+
expect(output).toContain("[old-project]");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("omits archived section when no archived projects", () => {
|
|
267
|
+
const output = getTemplate("classic")(makeContext());
|
|
268
|
+
expect(output).not.toContain("## Archived");
|
|
269
|
+
});
|
|
270
|
+
|
|
244
271
|
it("ends with trailing newline", () => {
|
|
245
272
|
const output = getTemplate("classic")(makeContext());
|
|
246
273
|
expect(output.endsWith("\n")).toBe(true);
|
|
@@ -294,17 +321,34 @@ describe("modernTemplate", () => {
|
|
|
294
321
|
expect(output).toContain("AI-generated summary of the project.");
|
|
295
322
|
});
|
|
296
323
|
|
|
297
|
-
it("includes GitHub Stats section with
|
|
324
|
+
it("includes GitHub Stats section with velocity and rhythm", () => {
|
|
298
325
|
const output = getTemplate("modern")(makeContext());
|
|
299
326
|
expect(output).toContain("## GitHub Stats");
|
|
300
|
-
expect(output).toContain("assets/insights/metrics-
|
|
301
|
-
expect(output).toContain("assets/insights/metrics-
|
|
327
|
+
expect(output).toContain("assets/insights/metrics-velocity.svg");
|
|
328
|
+
expect(output).toContain("assets/insights/metrics-rhythm.svg");
|
|
302
329
|
});
|
|
303
330
|
|
|
304
|
-
it("includes
|
|
331
|
+
it("includes constellation section", () => {
|
|
305
332
|
const output = getTemplate("modern")(makeContext());
|
|
306
|
-
expect(output).toContain("##
|
|
307
|
-
expect(output).toContain("assets/insights/metrics-
|
|
333
|
+
expect(output).toContain("## Project Map");
|
|
334
|
+
expect(output).toContain("assets/insights/metrics-constellation.svg");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("includes archived section separate from active/maintained", () => {
|
|
338
|
+
const output = getTemplate("modern")(
|
|
339
|
+
makeContext({
|
|
340
|
+
archivedProjects: [
|
|
341
|
+
{
|
|
342
|
+
name: "legacy-lib",
|
|
343
|
+
url: "https://github.com/urmzd/legacy-lib",
|
|
344
|
+
description: "A legacy library",
|
|
345
|
+
stars: 1,
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
expect(output).toContain("## Archived");
|
|
351
|
+
expect(output).toContain("[legacy-lib]");
|
|
308
352
|
});
|
|
309
353
|
|
|
310
354
|
it("includes social badges", () => {
|
|
@@ -346,6 +390,23 @@ describe("minimalTemplate", () => {
|
|
|
346
390
|
expect(output).toContain("@urmzd/github-insights");
|
|
347
391
|
});
|
|
348
392
|
|
|
393
|
+
it("includes archived section for archived projects", () => {
|
|
394
|
+
const output = getTemplate("minimal")(
|
|
395
|
+
makeContext({
|
|
396
|
+
archivedProjects: [
|
|
397
|
+
{
|
|
398
|
+
name: "old-util",
|
|
399
|
+
url: "https://github.com/urmzd/old-util",
|
|
400
|
+
description: "A retired utility",
|
|
401
|
+
stars: 0,
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
expect(output).toContain("## Archived");
|
|
407
|
+
expect(output).toContain("[old-util]");
|
|
408
|
+
});
|
|
409
|
+
|
|
349
410
|
it("ends with trailing newline", () => {
|
|
350
411
|
const output = getTemplate("minimal")(makeContext());
|
|
351
412
|
expect(output.endsWith("\n")).toBe(true);
|
|
@@ -382,17 +443,17 @@ describe("ecosystemTemplate", () => {
|
|
|
382
443
|
);
|
|
383
444
|
});
|
|
384
445
|
|
|
385
|
-
it("includes GitHub Stats section", () => {
|
|
446
|
+
it("includes GitHub Stats section with velocity and rhythm", () => {
|
|
386
447
|
const output = getTemplate("ecosystem")(makeContext());
|
|
387
448
|
expect(output).toContain("## GitHub Stats");
|
|
388
|
-
expect(output).toContain("assets/insights/metrics-
|
|
389
|
-
expect(output).toContain("assets/insights/metrics-
|
|
449
|
+
expect(output).toContain("assets/insights/metrics-velocity.svg");
|
|
450
|
+
expect(output).toContain("assets/insights/metrics-rhythm.svg");
|
|
390
451
|
});
|
|
391
452
|
|
|
392
|
-
it("includes
|
|
453
|
+
it("includes constellation section", () => {
|
|
393
454
|
const output = getTemplate("ecosystem")(makeContext());
|
|
394
|
-
expect(output).toContain("##
|
|
395
|
-
expect(output).toContain("assets/insights/metrics-
|
|
455
|
+
expect(output).toContain("## Project Map");
|
|
456
|
+
expect(output).toContain("assets/insights/metrics-constellation.svg");
|
|
396
457
|
});
|
|
397
458
|
|
|
398
459
|
it("includes social badges", () => {
|
|
@@ -405,6 +466,45 @@ describe("ecosystemTemplate", () => {
|
|
|
405
466
|
expect(output).toContain("@urmzd/github-insights");
|
|
406
467
|
});
|
|
407
468
|
|
|
469
|
+
it("separates archived projects from category tables", () => {
|
|
470
|
+
const output = getTemplate("ecosystem")(
|
|
471
|
+
makeContext({
|
|
472
|
+
archivedProjects: [
|
|
473
|
+
{
|
|
474
|
+
name: "old-app",
|
|
475
|
+
url: "https://github.com/urmzd/old-app",
|
|
476
|
+
description: "A retired application",
|
|
477
|
+
stars: 3,
|
|
478
|
+
category: "Applications",
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
categorizedProjects: {
|
|
482
|
+
Applications: [
|
|
483
|
+
{
|
|
484
|
+
name: "resume-generator",
|
|
485
|
+
url: "https://github.com/urmzd/resume-generator",
|
|
486
|
+
description: "CLI tool for professional resumes",
|
|
487
|
+
stars: 42,
|
|
488
|
+
category: "Applications",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: "old-app",
|
|
492
|
+
url: "https://github.com/urmzd/old-app",
|
|
493
|
+
description: "A retired application",
|
|
494
|
+
stars: 3,
|
|
495
|
+
category: "Applications",
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
expect(output).toContain("### Archived");
|
|
502
|
+
expect(output).toContain("[old-app]");
|
|
503
|
+
// old-app should NOT appear in the Applications table
|
|
504
|
+
const appSection = output.split("### Applications")[1].split("###")[0];
|
|
505
|
+
expect(appSection).not.toContain("old-app");
|
|
506
|
+
});
|
|
507
|
+
|
|
408
508
|
it("ends with trailing newline", () => {
|
|
409
509
|
const output = getTemplate("ecosystem")(makeContext());
|
|
410
510
|
expect(output.endsWith("\n")).toBe(true);
|