@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.
Files changed (103) hide show
  1. package/.githooks/commit-msg +4 -0
  2. package/.githooks/pre-commit +4 -0
  3. package/AGENTS.md +32 -19
  4. package/CHANGELOG.md +54 -0
  5. package/CONTRIBUTING.md +18 -19
  6. package/README.md +21 -24
  7. package/action.yml +1 -1
  8. package/assets/insights/index.svg +45 -4
  9. package/assets/insights/metrics-constellation.svg +55 -0
  10. package/assets/insights/metrics-impact.svg +55 -0
  11. package/assets/insights/metrics-rhythm.svg +55 -0
  12. package/assets/insights/metrics-velocity.svg +55 -0
  13. package/examples/classic/README.md +36 -2
  14. package/examples/classic/index.svg +45 -4
  15. package/examples/classic/metrics-constellation.svg +55 -0
  16. package/examples/classic/metrics-impact.svg +55 -0
  17. package/examples/classic/metrics-rhythm.svg +55 -0
  18. package/examples/classic/metrics-velocity.svg +55 -0
  19. package/examples/ecosystem/README.md +39 -28
  20. package/examples/ecosystem/index.svg +45 -4
  21. package/examples/ecosystem/metrics-constellation.svg +55 -0
  22. package/examples/ecosystem/metrics-impact.svg +55 -0
  23. package/examples/ecosystem/metrics-rhythm.svg +55 -0
  24. package/examples/ecosystem/metrics-velocity.svg +55 -0
  25. package/examples/minimal/README.md +36 -2
  26. package/examples/minimal/index.svg +45 -4
  27. package/examples/minimal/metrics-constellation.svg +55 -0
  28. package/examples/minimal/metrics-impact.svg +55 -0
  29. package/examples/minimal/metrics-rhythm.svg +55 -0
  30. package/examples/minimal/metrics-velocity.svg +55 -0
  31. package/examples/modern/README.md +62 -50
  32. package/examples/modern/index.svg +45 -4
  33. package/examples/modern/metrics-constellation.svg +55 -0
  34. package/examples/modern/metrics-impact.svg +55 -0
  35. package/examples/modern/metrics-rhythm.svg +55 -0
  36. package/examples/modern/metrics-velocity.svg +55 -0
  37. package/llms.txt +4 -4
  38. package/package.json +1 -1
  39. package/skills/github-insights/SKILL.md +35 -81
  40. package/sr.yaml +9 -0
  41. package/src/api.ts +3 -141
  42. package/src/components/contribution-rhythm.tsx +152 -0
  43. package/src/components/full-svg.test.tsx +4 -1
  44. package/src/components/full-svg.tsx +14 -7
  45. package/src/components/impact-trail.tsx +90 -0
  46. package/src/components/language-velocity.tsx +181 -0
  47. package/src/components/project-constellation.tsx +97 -0
  48. package/src/components/section.test.tsx +5 -3
  49. package/src/components/section.tsx +5 -13
  50. package/src/components/style-defs.tsx +44 -3
  51. package/src/index.ts +34 -47
  52. package/src/metrics.test.ts +50 -57
  53. package/src/metrics.ts +293 -97
  54. package/src/readme.test.ts +2 -4
  55. package/src/templates.test.ts +116 -16
  56. package/src/templates.ts +68 -27
  57. package/src/theme.ts +11 -1
  58. package/src/types.ts +31 -7
  59. package/assets/insights/metrics-calendar.svg +0 -14
  60. package/assets/insights/metrics-complexity.svg +0 -14
  61. package/assets/insights/metrics-contributions.svg +0 -14
  62. package/assets/insights/metrics-expertise.svg +0 -14
  63. package/assets/insights/metrics-languages.svg +0 -14
  64. package/assets/insights/metrics-pulse.svg +0 -14
  65. package/examples/classic/metrics-calendar.svg +0 -14
  66. package/examples/classic/metrics-complexity.svg +0 -14
  67. package/examples/classic/metrics-contributions.svg +0 -14
  68. package/examples/classic/metrics-expertise.svg +0 -14
  69. package/examples/classic/metrics-languages.svg +0 -14
  70. package/examples/classic/metrics-pulse.svg +0 -14
  71. package/examples/ecosystem/metrics-calendar.svg +0 -14
  72. package/examples/ecosystem/metrics-complexity.svg +0 -14
  73. package/examples/ecosystem/metrics-contributions.svg +0 -14
  74. package/examples/ecosystem/metrics-expertise.svg +0 -14
  75. package/examples/ecosystem/metrics-languages.svg +0 -14
  76. package/examples/ecosystem/metrics-pulse.svg +0 -14
  77. package/examples/minimal/metrics-calendar.svg +0 -14
  78. package/examples/minimal/metrics-complexity.svg +0 -14
  79. package/examples/minimal/metrics-contributions.svg +0 -14
  80. package/examples/minimal/metrics-expertise.svg +0 -14
  81. package/examples/minimal/metrics-languages.svg +0 -14
  82. package/examples/minimal/metrics-pulse.svg +0 -14
  83. package/examples/modern/metrics-calendar.svg +0 -14
  84. package/examples/modern/metrics-complexity.svg +0 -14
  85. package/examples/modern/metrics-contributions.svg +0 -14
  86. package/examples/modern/metrics-expertise.svg +0 -14
  87. package/examples/modern/metrics-languages.svg +0 -14
  88. package/examples/modern/metrics-pulse.svg +0 -14
  89. package/src/components/bar-chart.test.tsx +0 -38
  90. package/src/components/bar-chart.tsx +0 -54
  91. package/src/components/contribution-calendar.test.tsx +0 -44
  92. package/src/components/contribution-calendar.tsx +0 -94
  93. package/src/components/contribution-cards.test.tsx +0 -36
  94. package/src/components/contribution-cards.tsx +0 -58
  95. package/src/components/donut-chart.test.tsx +0 -36
  96. package/src/components/donut-chart.tsx +0 -102
  97. package/src/components/project-cards.test.tsx +0 -46
  98. package/src/components/project-cards.tsx +0 -66
  99. package/src/components/stat-cards.test.tsx +0 -32
  100. package/src/components/stat-cards.tsx +0 -57
  101. package/src/components/tech-highlights.test.tsx +0 -63
  102. package/src/components/tech-highlights.tsx +0 -109
  103. package/teasr.toml +0 -14
package/src/metrics.ts CHANGED
@@ -1,21 +1,21 @@
1
- import { renderContributionCalendar } from "./components/contribution-calendar.js";
2
- import { renderContributionCards } from "./components/contribution-cards.js";
3
- import { renderDonutChart } from "./components/donut-chart.js";
4
- import { renderProjectCards } from "./components/project-cards.js";
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
- pulse: "metrics-pulse.svg",
29
- languages: "metrics-languages.svg",
30
- expertise: "metrics-expertise.svg",
31
- projects: "metrics-complexity.svg",
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
- return { active, maintained, inactive };
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
- languages,
259
- techHighlights,
260
- projects,
507
+ velocity,
508
+ rhythm,
509
+ constellation,
261
510
  contributionData,
262
511
  }: {
263
- languages: LanguageItem[];
264
- techHighlights: TechHighlight[];
265
- projects: ProjectItem[];
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. At a Glance
271
- sections.push({
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-expertise.svg",
314
- title: "Expertise",
315
- subtitle:
316
- "Curated from dependencies, topics, and languages via AI analysis",
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
- // 4. Signature Projects
529
+ // 2. Contribution Rhythm
322
530
  sections.push({
323
- filename: "metrics-complexity.svg",
324
- title: "Signature Projects",
325
- subtitle: "Top projects by technical complexity",
326
- renderBody: (y: number) => renderProjectCards(projects, y),
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
- // 5. Contribution Calendar
330
- if (contributionData.contributionCalendar) {
331
- const calendarData = contributionData.contributionCalendar;
537
+ // 3. Project Constellation
538
+ if (constellation.length > 0) {
332
539
  sections.push({
333
- filename: "metrics-calendar.svg",
334
- title: "Contribution Calendar",
335
- subtitle: `${calendarData.totalContributions.toLocaleString()} contributions in the last year`,
336
- renderBody: (y: number) => renderContributionCalendar(calendarData, y),
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
- // 6. Open Source Contributions
547
+ // 4. Impact Trail
341
548
  if (contributionData.externalRepos.nodes.length > 0) {
342
549
  sections.push({
343
- filename: "metrics-contributions.svg",
344
- title: "Open Source Contributions",
345
- subtitle: "External repositories contributed to (all time)",
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, 5);
348
- const highlights = repos.map((r) => ({
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
  }
@@ -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: "Expertise", path: "assets/insights/metrics-expertise.svg" },
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
  "![Projects](assets/insights/metrics-projects.svg)",
163
163
  );
164
- expect(result).toContain(
165
- "![Expertise](assets/insights/metrics-expertise.svg)",
166
- );
164
+ expect(result).toContain("![Rhythm](assets/insights/metrics-rhythm.svg)");
167
165
  });
168
166
 
169
167
  it("renders all sections combined", () => {
@@ -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
- pulse: "assets/insights/metrics-pulse.svg",
24
- calendar: "assets/insights/metrics-calendar.svg",
25
- expertise: "assets/insights/metrics-expertise.svg",
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
- techHighlights: [],
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
  "[![urmzd.dev](https://img.shields.io/badge/urmzd.dev-4285F4?style=flat&logo=google-chrome&logoColor=white)](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 pulse and calendar", () => {
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-pulse.svg");
301
- expect(output).toContain("assets/insights/metrics-calendar.svg");
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 expertise section", () => {
331
+ it("includes constellation section", () => {
305
332
  const output = getTemplate("modern")(makeContext());
306
- expect(output).toContain("## Other Areas of Interest");
307
- expect(output).toContain("assets/insights/metrics-expertise.svg");
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-pulse.svg");
389
- expect(output).toContain("assets/insights/metrics-calendar.svg");
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 expertise section", () => {
453
+ it("includes constellation section", () => {
393
454
  const output = getTemplate("ecosystem")(makeContext());
394
- expect(output).toContain("## Other Areas of Interest");
395
- expect(output).toContain("assets/insights/metrics-expertise.svg");
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);