@urmzd/github-insights 2.1.0 → 2.3.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 (116) hide show
  1. package/.githooks/.sr-hooks-hash +1 -0
  2. package/.githooks/commit-msg +3 -0
  3. package/.githooks/pre-commit +3 -0
  4. package/AGENTS.md +32 -19
  5. package/CHANGELOG.md +62 -0
  6. package/CONTRIBUTING.md +18 -19
  7. package/README.md +21 -24
  8. package/action.yml +1 -1
  9. package/assets/insights/index.svg +45 -4
  10. package/assets/insights/metrics-constellation.svg +55 -0
  11. package/assets/insights/metrics-growth.svg +55 -0
  12. package/assets/insights/metrics-heatmap.svg +55 -0
  13. package/assets/insights/metrics-impact.svg +55 -0
  14. package/assets/insights/metrics-rhythm.svg +55 -0
  15. package/assets/insights/metrics-velocity.svg +55 -0
  16. package/examples/classic/README.md +36 -2
  17. package/examples/classic/index.svg +45 -4
  18. package/examples/classic/metrics-constellation.svg +55 -0
  19. package/examples/classic/metrics-growth.svg +55 -0
  20. package/examples/classic/metrics-heatmap.svg +55 -0
  21. package/examples/classic/metrics-impact.svg +55 -0
  22. package/examples/classic/metrics-rhythm.svg +55 -0
  23. package/examples/classic/metrics-velocity.svg +55 -0
  24. package/examples/ecosystem/README.md +39 -28
  25. package/examples/ecosystem/index.svg +45 -4
  26. package/examples/ecosystem/metrics-constellation.svg +55 -0
  27. package/examples/ecosystem/metrics-growth.svg +55 -0
  28. package/examples/ecosystem/metrics-heatmap.svg +55 -0
  29. package/examples/ecosystem/metrics-impact.svg +55 -0
  30. package/examples/ecosystem/metrics-rhythm.svg +55 -0
  31. package/examples/ecosystem/metrics-velocity.svg +55 -0
  32. package/examples/minimal/README.md +36 -2
  33. package/examples/minimal/index.svg +45 -4
  34. package/examples/minimal/metrics-constellation.svg +55 -0
  35. package/examples/minimal/metrics-growth.svg +55 -0
  36. package/examples/minimal/metrics-heatmap.svg +55 -0
  37. package/examples/minimal/metrics-impact.svg +55 -0
  38. package/examples/minimal/metrics-rhythm.svg +55 -0
  39. package/examples/minimal/metrics-velocity.svg +55 -0
  40. package/examples/modern/README.md +62 -50
  41. package/examples/modern/index.svg +45 -4
  42. package/examples/modern/metrics-constellation.svg +55 -0
  43. package/examples/modern/metrics-growth.svg +55 -0
  44. package/examples/modern/metrics-heatmap.svg +55 -0
  45. package/examples/modern/metrics-impact.svg +55 -0
  46. package/examples/modern/metrics-rhythm.svg +55 -0
  47. package/examples/modern/metrics-velocity.svg +55 -0
  48. package/llms.txt +4 -4
  49. package/package.json +1 -1
  50. package/skills/github-insights/SKILL.md +35 -81
  51. package/sr.yaml +9 -0
  52. package/src/api.ts +2 -140
  53. package/src/components/contribution-heatmap.tsx +43 -0
  54. package/src/components/contribution-rhythm.tsx +152 -0
  55. package/src/components/full-svg.test.tsx +4 -1
  56. package/src/components/full-svg.tsx +14 -7
  57. package/src/components/growth-arc.tsx +119 -0
  58. package/src/components/impact-trail.tsx +90 -0
  59. package/src/components/language-velocity.tsx +181 -0
  60. package/src/components/project-constellation.tsx +97 -0
  61. package/src/components/section.test.tsx +5 -3
  62. package/src/components/section.tsx +5 -13
  63. package/src/components/style-defs.tsx +44 -3
  64. package/src/index.ts +28 -47
  65. package/src/metrics.test.ts +50 -57
  66. package/src/metrics.ts +277 -95
  67. package/src/readme.test.ts +2 -4
  68. package/src/templates.test.ts +19 -16
  69. package/src/templates.ts +30 -16
  70. package/src/theme.ts +11 -1
  71. package/src/types.ts +34 -7
  72. package/assets/insights/metrics-calendar.svg +0 -14
  73. package/assets/insights/metrics-complexity.svg +0 -14
  74. package/assets/insights/metrics-contributions.svg +0 -14
  75. package/assets/insights/metrics-expertise.svg +0 -14
  76. package/assets/insights/metrics-languages.svg +0 -14
  77. package/assets/insights/metrics-pulse.svg +0 -14
  78. package/examples/classic/metrics-calendar.svg +0 -14
  79. package/examples/classic/metrics-complexity.svg +0 -14
  80. package/examples/classic/metrics-contributions.svg +0 -14
  81. package/examples/classic/metrics-expertise.svg +0 -14
  82. package/examples/classic/metrics-languages.svg +0 -14
  83. package/examples/classic/metrics-pulse.svg +0 -14
  84. package/examples/ecosystem/metrics-calendar.svg +0 -14
  85. package/examples/ecosystem/metrics-complexity.svg +0 -14
  86. package/examples/ecosystem/metrics-contributions.svg +0 -14
  87. package/examples/ecosystem/metrics-expertise.svg +0 -14
  88. package/examples/ecosystem/metrics-languages.svg +0 -14
  89. package/examples/ecosystem/metrics-pulse.svg +0 -14
  90. package/examples/minimal/metrics-calendar.svg +0 -14
  91. package/examples/minimal/metrics-complexity.svg +0 -14
  92. package/examples/minimal/metrics-contributions.svg +0 -14
  93. package/examples/minimal/metrics-expertise.svg +0 -14
  94. package/examples/minimal/metrics-languages.svg +0 -14
  95. package/examples/minimal/metrics-pulse.svg +0 -14
  96. package/examples/modern/metrics-calendar.svg +0 -14
  97. package/examples/modern/metrics-complexity.svg +0 -14
  98. package/examples/modern/metrics-contributions.svg +0 -14
  99. package/examples/modern/metrics-expertise.svg +0 -14
  100. package/examples/modern/metrics-languages.svg +0 -14
  101. package/examples/modern/metrics-pulse.svg +0 -14
  102. package/src/components/bar-chart.test.tsx +0 -38
  103. package/src/components/bar-chart.tsx +0 -54
  104. package/src/components/contribution-calendar.test.tsx +0 -44
  105. package/src/components/contribution-calendar.tsx +0 -94
  106. package/src/components/contribution-cards.test.tsx +0 -36
  107. package/src/components/contribution-cards.tsx +0 -58
  108. package/src/components/donut-chart.test.tsx +0 -36
  109. package/src/components/donut-chart.tsx +0 -102
  110. package/src/components/project-cards.test.tsx +0 -46
  111. package/src/components/project-cards.tsx +0 -66
  112. package/src/components/stat-cards.test.tsx +0 -32
  113. package/src/components/stat-cards.tsx +0 -57
  114. package/src/components/tech-highlights.test.tsx +0 -63
  115. package/src/components/tech-highlights.tsx +0 -109
  116. 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 ─────────────────────────────────────────────────────────────
@@ -266,111 +264,295 @@ export const splitProjectsByRecency = (
266
264
  return { active, maintained, inactive, archived };
267
265
  };
268
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
+ }
392
+
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;
502
+ };
503
+
269
504
  // ── Section definitions ─────────────────────────────────────────────────────
270
505
 
271
506
  export const buildSections = ({
272
- languages,
273
- techHighlights,
274
- projects,
507
+ velocity,
508
+ rhythm,
509
+ constellation,
275
510
  contributionData,
276
511
  }: {
277
- languages: LanguageItem[];
278
- techHighlights: TechHighlight[];
279
- projects: ProjectItem[];
512
+ velocity: MonthlyLanguageBucket[];
513
+ rhythm: ContributionRhythm;
514
+ constellation: ConstellationNode[];
280
515
  contributionData: ContributionData;
281
516
  }): SectionDef[] => {
282
517
  const sections: SectionDef[] = [];
283
518
 
284
- // 1. At a Glance
285
- sections.push({
286
- filename: "metrics-pulse.svg",
287
- title: "At a Glance",
288
- subtitle: "Contribution activity over the past year",
289
- renderBody: (y: number) => {
290
- const stats = [
291
- {
292
- label: "COMMITS",
293
- value:
294
- contributionData.contributions.totalCommitContributions.toLocaleString(),
295
- },
296
- {
297
- label: "PRS",
298
- value:
299
- contributionData.contributions.totalPullRequestContributions.toLocaleString(),
300
- },
301
- {
302
- label: "REVIEWS",
303
- value:
304
- contributionData.contributions.totalPullRequestReviewContributions.toLocaleString(),
305
- },
306
- {
307
- label: "REPOS",
308
- value:
309
- contributionData.contributions.totalRepositoriesWithContributedCommits.toLocaleString(),
310
- },
311
- ];
312
- return renderStatCards(stats, y);
313
- },
314
- });
315
-
316
- // 2. Languages
317
- sections.push({
318
- filename: "metrics-languages.svg",
319
- title: "Languages",
320
- subtitle: "By bytes of code across all public repos",
321
- renderBody: (y: number) => renderDonutChart(languages, y),
322
- });
323
-
324
- // 3. Expertise
325
- if (techHighlights.length > 0) {
519
+ // 1. Language Velocity
520
+ if (velocity.length > 0) {
326
521
  sections.push({
327
- filename: "metrics-expertise.svg",
328
- title: "Expertise",
329
- subtitle:
330
- "Curated from dependencies, topics, and languages via AI analysis",
331
- 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),
332
526
  });
333
527
  }
334
528
 
335
- // 4. Signature Projects
529
+ // 2. Contribution Rhythm
336
530
  sections.push({
337
- filename: "metrics-complexity.svg",
338
- title: "Signature Projects",
339
- subtitle: "Top projects by technical complexity",
340
- 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),
341
535
  });
342
536
 
343
- // 5. Contribution Calendar
344
- if (contributionData.contributionCalendar) {
345
- const calendarData = contributionData.contributionCalendar;
537
+ // 3. Project Constellation
538
+ if (constellation.length > 0) {
346
539
  sections.push({
347
- filename: "metrics-calendar.svg",
348
- title: "Contribution Calendar",
349
- subtitle: `${calendarData.totalContributions.toLocaleString()} contributions in the last year`,
350
- 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),
351
544
  });
352
545
  }
353
546
 
354
- // 6. Open Source Contributions
547
+ // 4. Impact Trail
355
548
  if (contributionData.externalRepos.nodes.length > 0) {
356
549
  sections.push({
357
- filename: "metrics-contributions.svg",
358
- title: "Open Source Contributions",
359
- subtitle: "External repositories contributed to (all time)",
550
+ filename: "metrics-impact.svg",
551
+ title: "Open Source Impact",
552
+ subtitle: "External repositories contributed to",
360
553
  renderBody: (y: number) => {
361
- const repos = contributionData.externalRepos.nodes.slice(0, 5);
362
- const highlights = repos.map((r) => ({
363
- project: r.nameWithOwner,
364
- detail: [
365
- r.stargazerCount > 0
366
- ? `\u2605 ${r.stargazerCount.toLocaleString()}`
367
- : null,
368
- r.primaryLanguage?.name,
369
- ]
370
- .filter(Boolean)
371
- .join(" \u00b7 "),
372
- }));
373
- return renderContributionCards(highlights, y);
554
+ const repos = contributionData.externalRepos.nodes.slice(0, 8);
555
+ return renderImpactTrail(repos, y);
374
556
  },
375
557
  });
376
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: [
@@ -71,7 +72,9 @@ const makeContext = (
71
72
  { name: "TypeScript", value: 100, percent: "60.0", color: "#3178c6" },
72
73
  { name: "Rust", value: 50, percent: "30.0", color: "#dea584" },
73
74
  ],
74
- techHighlights: [],
75
+ velocity: [],
76
+ rhythm: { dayTotals: [0, 0, 0, 0, 0, 0, 0], longestStreak: 0, stats: [] },
77
+ constellation: [],
75
78
  contributionData: makeContributionData(),
76
79
  socialBadges:
77
80
  "[![urmzd.dev](https://img.shields.io/badge/urmzd.dev-4285F4?style=flat&logo=google-chrome&logoColor=white)](https://urmzd.dev)",
@@ -318,17 +321,17 @@ describe("modernTemplate", () => {
318
321
  expect(output).toContain("AI-generated summary of the project.");
319
322
  });
320
323
 
321
- it("includes GitHub Stats section with pulse and calendar", () => {
324
+ it("includes GitHub Stats section with velocity and rhythm", () => {
322
325
  const output = getTemplate("modern")(makeContext());
323
326
  expect(output).toContain("## GitHub Stats");
324
- expect(output).toContain("assets/insights/metrics-pulse.svg");
325
- 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");
326
329
  });
327
330
 
328
- it("includes expertise section", () => {
331
+ it("includes constellation section", () => {
329
332
  const output = getTemplate("modern")(makeContext());
330
- expect(output).toContain("## Other Areas of Interest");
331
- expect(output).toContain("assets/insights/metrics-expertise.svg");
333
+ expect(output).toContain("## Project Map");
334
+ expect(output).toContain("assets/insights/metrics-constellation.svg");
332
335
  });
333
336
 
334
337
  it("includes archived section separate from active/maintained", () => {
@@ -440,17 +443,17 @@ describe("ecosystemTemplate", () => {
440
443
  );
441
444
  });
442
445
 
443
- it("includes GitHub Stats section", () => {
446
+ it("includes GitHub Stats section with velocity and rhythm", () => {
444
447
  const output = getTemplate("ecosystem")(makeContext());
445
448
  expect(output).toContain("## GitHub Stats");
446
- expect(output).toContain("assets/insights/metrics-pulse.svg");
447
- 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");
448
451
  });
449
452
 
450
- it("includes expertise section", () => {
453
+ it("includes constellation section", () => {
451
454
  const output = getTemplate("ecosystem")(makeContext());
452
- expect(output).toContain("## Other Areas of Interest");
453
- expect(output).toContain("assets/insights/metrics-expertise.svg");
455
+ expect(output).toContain("## Project Map");
456
+ expect(output).toContain("assets/insights/metrics-constellation.svg");
454
457
  });
455
458
 
456
459
  it("includes social badges", () => {
package/src/templates.ts CHANGED
@@ -183,22 +183,29 @@ function modernTemplate(ctx: TemplateContext): string {
183
183
  );
184
184
  if (archivedSection) parts.push(archivedSection);
185
185
 
186
- // GitHub Stats section: pulse + calendar
186
+ // Constellation
187
+ if (ctx.sectionSvgs.constellation) {
188
+ parts.push(
189
+ `## Project Map\n\n![Project Constellation](${ctx.sectionSvgs.constellation})`,
190
+ );
191
+ }
192
+
193
+ // GitHub Stats section: rhythm + velocity
187
194
  const statsImages: string[] = [];
188
- if (ctx.sectionSvgs.pulse) {
189
- statsImages.push(`![At a Glance](${ctx.sectionSvgs.pulse})`);
195
+ if (ctx.sectionSvgs.velocity) {
196
+ statsImages.push(`![Language Velocity](${ctx.sectionSvgs.velocity})`);
190
197
  }
191
- if (ctx.sectionSvgs.calendar) {
192
- statsImages.push(`![Contributions](${ctx.sectionSvgs.calendar})`);
198
+ if (ctx.sectionSvgs.rhythm) {
199
+ statsImages.push(`![Contribution Rhythm](${ctx.sectionSvgs.rhythm})`);
193
200
  }
194
201
  if (statsImages.length > 0) {
195
202
  parts.push(`## GitHub Stats\n\n${statsImages.join("\n")}`);
196
203
  }
197
204
 
198
- // Other areas of interest: expertise
199
- if (ctx.sectionSvgs.expertise) {
205
+ // Impact
206
+ if (ctx.sectionSvgs.impact) {
200
207
  parts.push(
201
- `## Other Areas of Interest\n\n![Expertise](${ctx.sectionSvgs.expertise})`,
208
+ `## Open Source Impact\n\n![Impact Trail](${ctx.sectionSvgs.impact})`,
202
209
  );
203
210
  }
204
211
 
@@ -285,22 +292,29 @@ function ecosystemTemplate(ctx: TemplateContext): string {
285
292
  parts.push(renderProjectTable("Archived", ctx.archivedProjects));
286
293
  }
287
294
 
288
- // GitHub Stats section: pulse + calendar
295
+ // Constellation
296
+ if (ctx.sectionSvgs.constellation) {
297
+ parts.push(
298
+ `## Project Map\n\n![Project Constellation](${ctx.sectionSvgs.constellation})`,
299
+ );
300
+ }
301
+
302
+ // GitHub Stats section: velocity + rhythm
289
303
  const statsImages: string[] = [];
290
- if (ctx.sectionSvgs.pulse) {
291
- statsImages.push(`![At a Glance](${ctx.sectionSvgs.pulse})`);
304
+ if (ctx.sectionSvgs.velocity) {
305
+ statsImages.push(`![Language Velocity](${ctx.sectionSvgs.velocity})`);
292
306
  }
293
- if (ctx.sectionSvgs.calendar) {
294
- statsImages.push(`![Contributions](${ctx.sectionSvgs.calendar})`);
307
+ if (ctx.sectionSvgs.rhythm) {
308
+ statsImages.push(`![Contribution Rhythm](${ctx.sectionSvgs.rhythm})`);
295
309
  }
296
310
  if (statsImages.length > 0) {
297
311
  parts.push(`## GitHub Stats\n\n${statsImages.join("\n")}`);
298
312
  }
299
313
 
300
- // Other areas of interest: expertise
301
- if (ctx.sectionSvgs.expertise) {
314
+ // Impact
315
+ if (ctx.sectionSvgs.impact) {
302
316
  parts.push(
303
- `## Other Areas of Interest\n\n![Expertise](${ctx.sectionSvgs.expertise})`,
317
+ `## Open Source Impact\n\n![Impact Trail](${ctx.sectionSvgs.impact})`,
304
318
  );
305
319
  }
306
320
 
package/src/theme.ts CHANGED
@@ -8,6 +8,16 @@ export const THEME = {
8
8
  muted: "#6e7681",
9
9
  } as const;
10
10
 
11
+ export const THEME_LIGHT = {
12
+ bg: "#ffffff",
13
+ cardBg: "#f6f8fa",
14
+ border: "#d0d7de",
15
+ link: "#0969da",
16
+ text: "#1f2328",
17
+ secondary: "#656d76",
18
+ muted: "#656d76",
19
+ } as const;
20
+
11
21
  export const FONT =
12
22
  "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif";
13
23
 
@@ -15,7 +25,7 @@ export const LAYOUT = {
15
25
  width: 808,
16
26
  padX: 24,
17
27
  padY: 24,
18
- sectionGap: 30,
28
+ sectionGap: 40,
19
29
  barHeight: 18,
20
30
  barRowHeight: 48,
21
31
  barMaxWidth: 700,
package/src/types.ts CHANGED
@@ -62,8 +62,6 @@ export interface SectionDef {
62
62
  title: string;
63
63
  subtitle: string;
64
64
  renderBody?: (y: number) => RenderResult;
65
- items?: BarItem[];
66
- options?: Record<string, unknown>;
67
65
  }
68
66
 
69
67
  // ── GitHub API types ────────────────────────────────────────────────────────
@@ -151,10 +149,37 @@ export interface PackageParser {
151
149
  parseDependencies(text: string): string[];
152
150
  }
153
151
 
154
- export interface TechHighlight {
155
- category: string;
156
- items: string[];
157
- score: number; // 0-100 proficiency level
152
+ // ── Language velocity ──────────────────────────────────────────────────────
153
+
154
+ export interface MonthlyLanguageBucket {
155
+ month: string; // "2025-04"
156
+ languages: { name: string; commits: number; color: string }[];
157
+ }
158
+
159
+ // ── Contribution rhythm ───────────────────────────────────────────────────
160
+
161
+ export interface ContributionRhythm {
162
+ dayTotals: [number, number, number, number, number, number, number];
163
+ longestStreak: number;
164
+ stats: StatItem[];
165
+ }
166
+
167
+ // ── Project constellation ─────────────────────────────────────────────────
168
+
169
+ export interface GrowthArcPoint {
170
+ label: string;
171
+ avgComplexity: number;
172
+ repoCount: number;
173
+ }
174
+
175
+ export interface ConstellationNode {
176
+ name: string;
177
+ url: string;
178
+ x: number;
179
+ y: number;
180
+ radius: number;
181
+ color: string;
182
+ connections: number[]; // indices of connected nodes
158
183
  }
159
184
 
160
185
  export interface UserConfig {
@@ -229,7 +254,9 @@ export interface TemplateContext {
229
254
  allProjects: ProjectItem[];
230
255
  categorizedProjects: Record<string, ProjectItem[]>;
231
256
  languages: LanguageItem[];
232
- techHighlights: TechHighlight[];
257
+ velocity: MonthlyLanguageBucket[];
258
+ rhythm: ContributionRhythm;
259
+ constellation: ConstellationNode[];
233
260
  contributionData: ContributionData;
234
261
  socialBadges: string;
235
262
  svgDir: string;