@urmzd/github-insights 2.1.0 → 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 +45 -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 +2 -140
  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 +28 -47
  52. package/src/metrics.test.ts +50 -57
  53. package/src/metrics.ts +277 -95
  54. package/src/readme.test.ts +2 -4
  55. package/src/templates.test.ts +19 -16
  56. package/src/templates.ts +30 -16
  57. package/src/theme.ts +11 -1
  58. package/src/types.ts +28 -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,56 +85,33 @@ 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,
@@ -145,17 +120,20 @@ async function run(): Promise<void> {
145
120
  archived: archivedProjects,
146
121
  } = splitProjectsByRecency(repos, contributionData, aiClassifications);
147
122
 
123
+ // ── Compute new visualization data ───────────────────────────────────
124
+ const velocity = computeLanguageVelocity(contributionData, repos);
125
+ const rhythm = computeContributionRhythm(contributionData);
126
+ const constellation = computeConstellationLayout(complexProjects, repos);
127
+
148
128
  const sectionDefs = buildSections({
149
- languages,
150
- techHighlights,
151
- projects,
129
+ velocity,
130
+ rhythm,
131
+ constellation,
152
132
  contributionData,
153
133
  });
154
134
 
155
135
  // Filter sections by requested keys if specified
156
- let activeSections = sectionDefs.filter(
157
- (s) => s.renderBody || (s.items && s.items.length > 0),
158
- );
136
+ let activeSections = sectionDefs.filter((s) => s.renderBody);
159
137
  if (requestedSections.length > 0) {
160
138
  const allowedFilenames = new Set(
161
139
  requestedSections.map((key) => SECTION_KEYS[key]).filter(Boolean),
@@ -169,11 +147,11 @@ async function run(): Promise<void> {
169
147
  mkdirSync(outputDir, { recursive: true });
170
148
 
171
149
  for (const section of activeSections) {
150
+ if (!section.renderBody) continue;
172
151
  const { svg, height } = renderSection(
173
152
  section.title,
174
153
  section.subtitle,
175
- section.renderBody || section.items || [],
176
- section.options || {},
154
+ section.renderBody,
177
155
  );
178
156
  writeFileSync(
179
157
  `${outputDir}/${section.filename}`,
@@ -199,7 +177,6 @@ async function run(): Promise<void> {
199
177
  profile: userProfile,
200
178
  userConfig,
201
179
  languages,
202
- techHighlights,
203
180
  activeProjects,
204
181
  complexProjects,
205
182
  });
@@ -258,7 +235,9 @@ async function run(): Promise<void> {
258
235
  allProjects: complexProjects,
259
236
  categorizedProjects,
260
237
  languages,
261
- techHighlights,
238
+ velocity,
239
+ rhythm,
240
+ constellation,
262
241
  contributionData,
263
242
  socialBadges,
264
243
  svgDir,
@@ -323,7 +302,9 @@ async function run(): Promise<void> {
323
302
  allProjects: complexProjects,
324
303
  categorizedProjects,
325
304
  languages,
326
- techHighlights,
305
+ velocity,
306
+ rhythm,
307
+ constellation,
327
308
  contributionData,
328
309
  socialBadges,
329
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