@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/index.ts CHANGED
@@ -6,10 +6,7 @@ import {
6
6
  fetchAIPreamble,
7
7
  fetchAllRepoData,
8
8
  fetchContributionData,
9
- fetchExpertiseAnalysis,
10
- fetchManifestsForRepos,
11
9
  fetchProjectClassifications,
12
- fetchReadmeForRepos,
13
10
  fetchUserProfile,
14
11
  makeGraphql,
15
12
  } from "./api.js";
@@ -20,8 +17,9 @@ import {
20
17
  aggregateLanguages,
21
18
  buildClassificationInputs,
22
19
  buildSections,
23
- collectAllDependencies,
24
- collectAllTopics,
20
+ computeConstellationLayout,
21
+ computeContributionRhythm,
22
+ computeLanguageVelocity,
25
23
  getTopProjectsByComplexity,
26
24
  SECTION_KEYS,
27
25
  splitProjectsByRecency,
@@ -87,74 +85,55 @@ async function run(): Promise<void> {
87
85
  const repos = await fetchAllRepoData(graphql, username);
88
86
  core.info(`Found ${repos.length} public repos`);
89
87
 
90
- core.info("Fetching dependency manifests...");
91
88
  core.info("Fetching contribution data...");
92
- core.info("Fetching READMEs...");
93
89
  core.info("Fetching user profile...");
94
- const [manifests, contributionData, readmeMap, userProfile] =
95
- await Promise.all([
96
- fetchManifestsForRepos(graphql, username, repos),
97
- fetchContributionData(graphql, username),
98
- fetchReadmeForRepos(graphql, username, repos),
99
- fetchUserProfile(graphql, username),
100
- ]);
101
- core.info(`Fetched manifests for ${manifests.size} repos`);
90
+ const [contributionData, userProfile] = await Promise.all([
91
+ fetchContributionData(graphql, username),
92
+ fetchUserProfile(graphql, username),
93
+ ]);
102
94
  core.info(
103
95
  `Contributions: ${contributionData.contributions.totalCommitContributions} commits, ${contributionData.contributions.totalPullRequestContributions} PRs`,
104
96
  );
105
- core.info(`Fetched READMEs for ${readmeMap.size} repos`);
106
97
  core.info(`User profile: ${userProfile.name || username}`);
107
98
 
108
99
  // ── Transform ─────────────────────────────────────────────────────────
109
100
  const languages = aggregateLanguages(repos);
110
101
  const complexProjects = getTopProjectsByComplexity(repos);
111
- const projects = complexProjects.slice(0, 5);
112
-
113
- const allDeps = collectAllDependencies(repos, manifests);
114
- const allTopics = collectAllTopics(repos);
115
102
 
116
103
  core.info("Fetching project classifications from GitHub Models...");
117
104
  const classificationInputs = buildClassificationInputs(
118
105
  repos,
119
106
  contributionData,
120
107
  );
121
- const [aiClassifications, techHighlights] = await Promise.all([
122
- fetchProjectClassifications(token, classificationInputs),
123
- (async () => {
124
- core.info("Fetching expertise analysis from GitHub Models...");
125
- return fetchExpertiseAnalysis(
126
- token,
127
- languages,
128
- allDeps,
129
- allTopics,
130
- repos,
131
- readmeMap,
132
- userConfig,
133
- );
134
- })(),
135
- ]);
108
+ const aiClassifications = await fetchProjectClassifications(
109
+ token,
110
+ classificationInputs,
111
+ );
136
112
  core.info(
137
113
  `Project classifications: ${aiClassifications.length} AI-classified (${repos.length - aiClassifications.length} heuristic fallback)`,
138
114
  );
139
- core.info(`Expertise analysis: ${techHighlights.length} categories`);
140
115
 
141
116
  const {
142
117
  active: activeProjects,
143
118
  maintained: maintainedProjects,
144
119
  inactive: inactiveProjects,
120
+ archived: archivedProjects,
145
121
  } = splitProjectsByRecency(repos, contributionData, aiClassifications);
146
122
 
123
+ // ── Compute new visualization data ───────────────────────────────────
124
+ const velocity = computeLanguageVelocity(contributionData, repos);
125
+ const rhythm = computeContributionRhythm(contributionData);
126
+ const constellation = computeConstellationLayout(complexProjects, repos);
127
+
147
128
  const sectionDefs = buildSections({
148
- languages,
149
- techHighlights,
150
- projects,
129
+ velocity,
130
+ rhythm,
131
+ constellation,
151
132
  contributionData,
152
133
  });
153
134
 
154
135
  // Filter sections by requested keys if specified
155
- let activeSections = sectionDefs.filter(
156
- (s) => s.renderBody || (s.items && s.items.length > 0),
157
- );
136
+ let activeSections = sectionDefs.filter((s) => s.renderBody);
158
137
  if (requestedSections.length > 0) {
159
138
  const allowedFilenames = new Set(
160
139
  requestedSections.map((key) => SECTION_KEYS[key]).filter(Boolean),
@@ -168,11 +147,11 @@ async function run(): Promise<void> {
168
147
  mkdirSync(outputDir, { recursive: true });
169
148
 
170
149
  for (const section of activeSections) {
150
+ if (!section.renderBody) continue;
171
151
  const { svg, height } = renderSection(
172
152
  section.title,
173
153
  section.subtitle,
174
- section.renderBody || section.items || [],
175
- section.options || {},
154
+ section.renderBody,
176
155
  );
177
156
  writeFileSync(
178
157
  `${outputDir}/${section.filename}`,
@@ -198,7 +177,6 @@ async function run(): Promise<void> {
198
177
  profile: userProfile,
199
178
  userConfig,
200
179
  languages,
201
- techHighlights,
202
180
  activeProjects,
203
181
  complexProjects,
204
182
  });
@@ -227,6 +205,7 @@ async function run(): Promise<void> {
227
205
  ...activeProjects,
228
206
  ...maintainedProjects,
229
207
  ...inactiveProjects,
208
+ ...archivedProjects,
230
209
  ];
231
210
  const categorizedProjects: Record<string, typeof allProjectItems> = {};
232
211
  for (const project of allProjectItems) {
@@ -245,16 +224,20 @@ async function run(): Promise<void> {
245
224
  title: userConfig.title,
246
225
  bio: userConfig.bio,
247
226
  preamble,
227
+ templateName,
248
228
  svgs,
249
229
  sectionSvgs,
250
230
  profile: userProfile,
251
231
  activeProjects,
252
232
  maintainedProjects,
253
233
  inactiveProjects,
234
+ archivedProjects,
254
235
  allProjects: complexProjects,
255
236
  categorizedProjects,
256
237
  languages,
257
- techHighlights,
238
+ velocity,
239
+ rhythm,
240
+ constellation,
258
241
  contributionData,
259
242
  socialBadges,
260
243
  svgDir,
@@ -308,16 +291,20 @@ async function run(): Promise<void> {
308
291
  title: userConfig.title,
309
292
  bio: userConfig.bio,
310
293
  preamble,
294
+ templateName: tplName,
311
295
  svgs: previewSvgs,
312
296
  sectionSvgs: previewSectionSvgs,
313
297
  profile: userProfile,
314
298
  activeProjects,
315
299
  maintainedProjects,
316
300
  inactiveProjects,
301
+ archivedProjects,
317
302
  allProjects: complexProjects,
318
303
  categorizedProjects,
319
304
  languages,
320
- techHighlights,
305
+ velocity,
306
+ rhythm,
307
+ constellation,
321
308
  contributionData,
322
309
  socialBadges,
323
310
  svgDir: ".",
@@ -13,7 +13,11 @@ import {
13
13
  SECTION_KEYS,
14
14
  splitProjectsByRecency,
15
15
  } from "./metrics.js";
16
- import type { ManifestMap, TechHighlight } from "./types.js";
16
+ import type {
17
+ ContributionRhythm,
18
+ ManifestMap,
19
+ MonthlyLanguageBucket,
20
+ } from "./types.js";
17
21
 
18
22
  // ── aggregateLanguages ──────────────────────────────────────────────────────
19
23
 
@@ -534,37 +538,48 @@ describe("splitProjectsByRecency", () => {
534
538
 
535
539
  describe("SECTION_KEYS", () => {
536
540
  it("maps all known section names to filenames", () => {
537
- expect(SECTION_KEYS.pulse).toBe("metrics-pulse.svg");
538
- expect(SECTION_KEYS.languages).toBe("metrics-languages.svg");
539
- expect(SECTION_KEYS.expertise).toBe("metrics-expertise.svg");
540
- expect(SECTION_KEYS.projects).toBe("metrics-complexity.svg");
541
- expect(SECTION_KEYS.contributions).toBe("metrics-contributions.svg");
542
- expect(SECTION_KEYS.calendar).toBe("metrics-calendar.svg");
541
+ expect(SECTION_KEYS.velocity).toBe("metrics-velocity.svg");
542
+ expect(SECTION_KEYS.rhythm).toBe("metrics-rhythm.svg");
543
+ expect(SECTION_KEYS.constellation).toBe("metrics-constellation.svg");
544
+ expect(SECTION_KEYS.impact).toBe("metrics-impact.svg");
543
545
  });
544
546
  });
545
547
 
546
548
  // ── buildSections ───────────────────────────────────────────────────────────
547
549
 
548
550
  describe("buildSections", () => {
549
- const baseSectionsInput = () => ({
550
- languages: [
551
- { name: "TypeScript", value: 100, percent: "80.0", color: "#3178c6" },
552
- { name: "JavaScript", value: 25, percent: "20.0", color: "#f1e05a" },
551
+ const makeRhythm = (): ContributionRhythm => ({
552
+ dayTotals: [10, 20, 15, 25, 18, 12, 5],
553
+ longestStreak: 7,
554
+ stats: [
555
+ { label: "COMMITS", value: "100" },
556
+ { label: "PRS", value: "10" },
553
557
  ],
554
- techHighlights: [
555
- {
556
- category: "Frontend",
557
- items: ["React", "TypeScript", "Next.js"],
558
- score: 90,
559
- },
560
- { category: "Backend", items: ["Express", "PostgreSQL"], score: 75 },
561
- ] as TechHighlight[],
562
- projects: [
558
+ });
559
+
560
+ const makeVelocity = (): MonthlyLanguageBucket[] => [
561
+ {
562
+ month: "2025-01",
563
+ languages: [{ name: "TypeScript", commits: 50, color: "#3178c6" }],
564
+ },
565
+ {
566
+ month: "2025-02",
567
+ languages: [{ name: "TypeScript", commits: 60, color: "#3178c6" }],
568
+ },
569
+ ];
570
+
571
+ const baseSectionsInput = () => ({
572
+ velocity: makeVelocity(),
573
+ rhythm: makeRhythm(),
574
+ constellation: [
563
575
  {
564
576
  name: "big-project",
565
577
  url: "https://github.com/user/big-project",
566
- description: "A complex project",
567
- stars: 85,
578
+ x: 100,
579
+ y: 100,
580
+ radius: 10,
581
+ color: "#3178c6",
582
+ connections: [],
568
583
  },
569
584
  ],
570
585
  contributionData: makeContributionData(),
@@ -573,22 +588,21 @@ describe("buildSections", () => {
573
588
  it("returns correct filenames", () => {
574
589
  const sections = buildSections(baseSectionsInput());
575
590
  const filenames = sections.map((s) => s.filename);
576
- expect(filenames).toContain("metrics-languages.svg");
577
- expect(filenames).toContain("metrics-expertise.svg");
578
- expect(filenames).toContain("metrics-complexity.svg");
579
- expect(filenames).toContain("metrics-pulse.svg");
591
+ expect(filenames).toContain("metrics-velocity.svg");
592
+ expect(filenames).toContain("metrics-rhythm.svg");
593
+ expect(filenames).toContain("metrics-constellation.svg");
580
594
  });
581
595
 
582
- it("expertise section is conditional on non-empty techHighlights", () => {
596
+ it("velocity section is conditional on non-empty velocity data", () => {
583
597
  const input = baseSectionsInput();
584
- input.techHighlights = [];
598
+ input.velocity = [];
585
599
  const sections = buildSections(input);
586
600
  expect(sections.map((s) => s.filename)).not.toContain(
587
- "metrics-expertise.svg",
601
+ "metrics-velocity.svg",
588
602
  );
589
603
  });
590
604
 
591
- it("contributions section conditional on externalRepos", () => {
605
+ it("impact section conditional on externalRepos", () => {
592
606
  const input = baseSectionsInput();
593
607
  input.contributionData = makeContributionData({
594
608
  externalRepos: {
@@ -605,41 +619,20 @@ describe("buildSections", () => {
605
619
  },
606
620
  });
607
621
  const sections = buildSections(input);
608
- expect(sections.map((s) => s.filename)).toContain(
609
- "metrics-contributions.svg",
610
- );
622
+ expect(sections.map((s) => s.filename)).toContain("metrics-impact.svg");
611
623
  });
612
624
 
613
- it("contributions section omitted when no external repos", () => {
625
+ it("impact section omitted when no external repos", () => {
614
626
  const sections = buildSections(baseSectionsInput());
615
- expect(sections.map((s) => s.filename)).not.toContain(
616
- "metrics-contributions.svg",
617
- );
627
+ expect(sections.map((s) => s.filename)).not.toContain("metrics-impact.svg");
618
628
  });
619
629
 
620
- it("calendar section included when contributionCalendar exists", () => {
630
+ it("constellation section conditional on non-empty nodes", () => {
621
631
  const input = baseSectionsInput();
622
- input.contributionData = makeContributionData({
623
- contributionCalendar: makeContributionCalendar(),
624
- });
632
+ input.constellation = [];
625
633
  const sections = buildSections(input);
626
- expect(sections.map((s) => s.filename)).toContain("metrics-calendar.svg");
627
- });
628
-
629
- it("calendar section omitted when no contributionCalendar", () => {
630
- const sections = buildSections(baseSectionsInput());
631
634
  expect(sections.map((s) => s.filename)).not.toContain(
632
- "metrics-calendar.svg",
633
- );
634
- });
635
-
636
- it("uses complexity-based subtitle for signature projects", () => {
637
- const sections = buildSections(baseSectionsInput());
638
- const projectSection = sections.find(
639
- (s) => s.filename === "metrics-complexity.svg",
640
- );
641
- expect(projectSection?.subtitle).toBe(
642
- "Top projects by technical complexity",
635
+ "metrics-constellation.svg",
643
636
  );
644
637
  });
645
638