@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.
- package/.githooks/.sr-hooks-hash +1 -0
- package/.githooks/commit-msg +3 -0
- package/.githooks/pre-commit +3 -0
- package/AGENTS.md +32 -19
- package/CHANGELOG.md +62 -0
- package/CONTRIBUTING.md +18 -19
- package/README.md +21 -24
- package/action.yml +1 -1
- package/assets/insights/index.svg +45 -4
- package/assets/insights/metrics-constellation.svg +55 -0
- package/assets/insights/metrics-growth.svg +55 -0
- package/assets/insights/metrics-heatmap.svg +55 -0
- package/assets/insights/metrics-impact.svg +55 -0
- package/assets/insights/metrics-rhythm.svg +55 -0
- package/assets/insights/metrics-velocity.svg +55 -0
- package/examples/classic/README.md +36 -2
- package/examples/classic/index.svg +45 -4
- package/examples/classic/metrics-constellation.svg +55 -0
- package/examples/classic/metrics-growth.svg +55 -0
- package/examples/classic/metrics-heatmap.svg +55 -0
- package/examples/classic/metrics-impact.svg +55 -0
- package/examples/classic/metrics-rhythm.svg +55 -0
- package/examples/classic/metrics-velocity.svg +55 -0
- package/examples/ecosystem/README.md +39 -28
- package/examples/ecosystem/index.svg +45 -4
- package/examples/ecosystem/metrics-constellation.svg +55 -0
- package/examples/ecosystem/metrics-growth.svg +55 -0
- package/examples/ecosystem/metrics-heatmap.svg +55 -0
- package/examples/ecosystem/metrics-impact.svg +55 -0
- package/examples/ecosystem/metrics-rhythm.svg +55 -0
- package/examples/ecosystem/metrics-velocity.svg +55 -0
- package/examples/minimal/README.md +36 -2
- package/examples/minimal/index.svg +45 -4
- package/examples/minimal/metrics-constellation.svg +55 -0
- package/examples/minimal/metrics-growth.svg +55 -0
- package/examples/minimal/metrics-heatmap.svg +55 -0
- package/examples/minimal/metrics-impact.svg +55 -0
- package/examples/minimal/metrics-rhythm.svg +55 -0
- package/examples/minimal/metrics-velocity.svg +55 -0
- package/examples/modern/README.md +62 -50
- package/examples/modern/index.svg +45 -4
- package/examples/modern/metrics-constellation.svg +55 -0
- package/examples/modern/metrics-growth.svg +55 -0
- package/examples/modern/metrics-heatmap.svg +55 -0
- package/examples/modern/metrics-impact.svg +55 -0
- package/examples/modern/metrics-rhythm.svg +55 -0
- package/examples/modern/metrics-velocity.svg +55 -0
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/skills/github-insights/SKILL.md +35 -81
- package/sr.yaml +9 -0
- package/src/api.ts +2 -140
- package/src/components/contribution-heatmap.tsx +43 -0
- package/src/components/contribution-rhythm.tsx +152 -0
- package/src/components/full-svg.test.tsx +4 -1
- package/src/components/full-svg.tsx +14 -7
- package/src/components/growth-arc.tsx +119 -0
- package/src/components/impact-trail.tsx +90 -0
- package/src/components/language-velocity.tsx +181 -0
- package/src/components/project-constellation.tsx +97 -0
- package/src/components/section.test.tsx +5 -3
- package/src/components/section.tsx +5 -13
- package/src/components/style-defs.tsx +44 -3
- package/src/index.ts +28 -47
- package/src/metrics.test.ts +50 -57
- package/src/metrics.ts +277 -95
- package/src/readme.test.ts +2 -4
- package/src/templates.test.ts +19 -16
- package/src/templates.ts +30 -16
- package/src/theme.ts +11 -1
- package/src/types.ts +34 -7
- package/assets/insights/metrics-calendar.svg +0 -14
- package/assets/insights/metrics-complexity.svg +0 -14
- package/assets/insights/metrics-contributions.svg +0 -14
- package/assets/insights/metrics-expertise.svg +0 -14
- package/assets/insights/metrics-languages.svg +0 -14
- package/assets/insights/metrics-pulse.svg +0 -14
- package/examples/classic/metrics-calendar.svg +0 -14
- package/examples/classic/metrics-complexity.svg +0 -14
- package/examples/classic/metrics-contributions.svg +0 -14
- package/examples/classic/metrics-expertise.svg +0 -14
- package/examples/classic/metrics-languages.svg +0 -14
- package/examples/classic/metrics-pulse.svg +0 -14
- package/examples/ecosystem/metrics-calendar.svg +0 -14
- package/examples/ecosystem/metrics-complexity.svg +0 -14
- package/examples/ecosystem/metrics-contributions.svg +0 -14
- package/examples/ecosystem/metrics-expertise.svg +0 -14
- package/examples/ecosystem/metrics-languages.svg +0 -14
- package/examples/ecosystem/metrics-pulse.svg +0 -14
- package/examples/minimal/metrics-calendar.svg +0 -14
- package/examples/minimal/metrics-complexity.svg +0 -14
- package/examples/minimal/metrics-contributions.svg +0 -14
- package/examples/minimal/metrics-expertise.svg +0 -14
- package/examples/minimal/metrics-languages.svg +0 -14
- package/examples/minimal/metrics-pulse.svg +0 -14
- package/examples/modern/metrics-calendar.svg +0 -14
- package/examples/modern/metrics-complexity.svg +0 -14
- package/examples/modern/metrics-contributions.svg +0 -14
- package/examples/modern/metrics-expertise.svg +0 -14
- package/examples/modern/metrics-languages.svg +0 -14
- package/examples/modern/metrics-pulse.svg +0 -14
- package/src/components/bar-chart.test.tsx +0 -38
- package/src/components/bar-chart.tsx +0 -54
- package/src/components/contribution-calendar.test.tsx +0 -44
- package/src/components/contribution-calendar.tsx +0 -94
- package/src/components/contribution-cards.test.tsx +0 -36
- package/src/components/contribution-cards.tsx +0 -58
- package/src/components/donut-chart.test.tsx +0 -36
- package/src/components/donut-chart.tsx +0 -102
- package/src/components/project-cards.test.tsx +0 -46
- package/src/components/project-cards.tsx +0 -66
- package/src/components/stat-cards.test.tsx +0 -32
- package/src/components/stat-cards.tsx +0 -57
- package/src/components/tech-highlights.test.tsx +0 -63
- package/src/components/tech-highlights.tsx +0 -109
- package/teasr.toml +0 -14
package/src/metrics.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { renderStatCards } from "./components/stat-cards.js";
|
|
6
|
-
import { renderTechHighlights } from "./components/tech-highlights.js";
|
|
1
|
+
import { renderContributionRhythm } from "./components/contribution-rhythm.js";
|
|
2
|
+
import { renderImpactTrail } from "./components/impact-trail.js";
|
|
3
|
+
import { renderLanguageVelocity } from "./components/language-velocity.js";
|
|
4
|
+
import { renderProjectConstellation } from "./components/project-constellation.js";
|
|
7
5
|
import { parseManifest } from "./parsers.js";
|
|
8
6
|
import type {
|
|
7
|
+
ConstellationNode,
|
|
9
8
|
ContributionData,
|
|
9
|
+
ContributionRhythm,
|
|
10
10
|
LanguageItem,
|
|
11
11
|
ManifestMap,
|
|
12
|
+
MonthlyLanguageBucket,
|
|
12
13
|
ProjectItem,
|
|
13
14
|
ProjectStatus,
|
|
14
15
|
RepoClassificationInput,
|
|
15
16
|
RepoClassificationOutput,
|
|
16
17
|
RepoNode,
|
|
17
18
|
SectionDef,
|
|
18
|
-
TechHighlight,
|
|
19
19
|
} from "./types.js";
|
|
20
20
|
|
|
21
21
|
// ── Category Sets ───────────────────────────────────────────────────────────
|
|
@@ -25,12 +25,10 @@ const EXCLUDED_LANGUAGES = new Set(["Jupyter Notebook"]);
|
|
|
25
25
|
// ── Section keys ────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
27
|
export const SECTION_KEYS: Record<string, string> = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
contributions: "metrics-contributions.svg",
|
|
33
|
-
calendar: "metrics-calendar.svg",
|
|
28
|
+
velocity: "metrics-velocity.svg",
|
|
29
|
+
rhythm: "metrics-rhythm.svg",
|
|
30
|
+
constellation: "metrics-constellation.svg",
|
|
31
|
+
impact: "metrics-impact.svg",
|
|
34
32
|
};
|
|
35
33
|
|
|
36
34
|
// ── Aggregation ─────────────────────────────────────────────────────────────
|
|
@@ -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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
507
|
+
velocity,
|
|
508
|
+
rhythm,
|
|
509
|
+
constellation,
|
|
275
510
|
contributionData,
|
|
276
511
|
}: {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
285
|
-
|
|
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-
|
|
328
|
-
title: "
|
|
329
|
-
subtitle:
|
|
330
|
-
|
|
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
|
-
//
|
|
529
|
+
// 2. Contribution Rhythm
|
|
336
530
|
sections.push({
|
|
337
|
-
filename: "metrics-
|
|
338
|
-
title: "
|
|
339
|
-
subtitle: "
|
|
340
|
-
renderBody: (y: number) =>
|
|
531
|
+
filename: "metrics-rhythm.svg",
|
|
532
|
+
title: "Contribution Rhythm",
|
|
533
|
+
subtitle: "Activity patterns and statistics over the past year",
|
|
534
|
+
renderBody: (y: number) => renderContributionRhythm(rhythm, y),
|
|
341
535
|
});
|
|
342
536
|
|
|
343
|
-
//
|
|
344
|
-
if (
|
|
345
|
-
const calendarData = contributionData.contributionCalendar;
|
|
537
|
+
// 3. Project Constellation
|
|
538
|
+
if (constellation.length > 0) {
|
|
346
539
|
sections.push({
|
|
347
|
-
filename: "metrics-
|
|
348
|
-
title: "
|
|
349
|
-
subtitle:
|
|
350
|
-
renderBody: (y: number) =>
|
|
540
|
+
filename: "metrics-constellation.svg",
|
|
541
|
+
title: "Project Constellation",
|
|
542
|
+
subtitle: "Projects mapped by language ecosystem and complexity",
|
|
543
|
+
renderBody: (y: number) => renderProjectConstellation(constellation, y),
|
|
351
544
|
});
|
|
352
545
|
}
|
|
353
546
|
|
|
354
|
-
//
|
|
547
|
+
// 4. Impact Trail
|
|
355
548
|
if (contributionData.externalRepos.nodes.length > 0) {
|
|
356
549
|
sections.push({
|
|
357
|
-
filename: "metrics-
|
|
358
|
-
title: "Open Source
|
|
359
|
-
subtitle: "External repositories contributed to
|
|
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,
|
|
362
|
-
|
|
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
|
}
|
package/src/readme.test.ts
CHANGED
|
@@ -152,7 +152,7 @@ describe("generateReadme", () => {
|
|
|
152
152
|
svgs: [
|
|
153
153
|
{ label: "Languages", path: "assets/insights/metrics-languages.svg" },
|
|
154
154
|
{ label: "Projects", path: "assets/insights/metrics-projects.svg" },
|
|
155
|
-
{ label: "
|
|
155
|
+
{ label: "Rhythm", path: "assets/insights/metrics-rhythm.svg" },
|
|
156
156
|
],
|
|
157
157
|
});
|
|
158
158
|
expect(result).toContain(
|
|
@@ -161,9 +161,7 @@ describe("generateReadme", () => {
|
|
|
161
161
|
expect(result).toContain(
|
|
162
162
|
"",
|
|
163
163
|
);
|
|
164
|
-
expect(result).toContain(
|
|
165
|
-
"",
|
|
166
|
-
);
|
|
164
|
+
expect(result).toContain("");
|
|
167
165
|
});
|
|
168
166
|
|
|
169
167
|
it("renders all sections combined", () => {
|
package/src/templates.test.ts
CHANGED
|
@@ -20,9 +20,10 @@ const makeContext = (
|
|
|
20
20
|
preamble: "A software developer in Austin, TX.",
|
|
21
21
|
svgs: [{ label: "GitHub Metrics", path: "assets/insights/index.svg" }],
|
|
22
22
|
sectionSvgs: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
velocity: "assets/insights/metrics-velocity.svg",
|
|
24
|
+
rhythm: "assets/insights/metrics-rhythm.svg",
|
|
25
|
+
constellation: "assets/insights/metrics-constellation.svg",
|
|
26
|
+
impact: "assets/insights/metrics-impact.svg",
|
|
26
27
|
},
|
|
27
28
|
profile: makeUserProfile(),
|
|
28
29
|
activeProjects: [
|
|
@@ -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
|
-
|
|
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
|
"[](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
|
|
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-
|
|
325
|
-
expect(output).toContain("assets/insights/metrics-
|
|
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
|
|
331
|
+
it("includes constellation section", () => {
|
|
329
332
|
const output = getTemplate("modern")(makeContext());
|
|
330
|
-
expect(output).toContain("##
|
|
331
|
-
expect(output).toContain("assets/insights/metrics-
|
|
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-
|
|
447
|
-
expect(output).toContain("assets/insights/metrics-
|
|
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
|
|
453
|
+
it("includes constellation section", () => {
|
|
451
454
|
const output = getTemplate("ecosystem")(makeContext());
|
|
452
|
-
expect(output).toContain("##
|
|
453
|
-
expect(output).toContain("assets/insights/metrics-
|
|
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
|
-
//
|
|
186
|
+
// Constellation
|
|
187
|
+
if (ctx.sectionSvgs.constellation) {
|
|
188
|
+
parts.push(
|
|
189
|
+
`## Project Map\n\n`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// GitHub Stats section: rhythm + velocity
|
|
187
194
|
const statsImages: string[] = [];
|
|
188
|
-
if (ctx.sectionSvgs.
|
|
189
|
-
statsImages.push(``);
|
|
190
197
|
}
|
|
191
|
-
if (ctx.sectionSvgs.
|
|
192
|
-
statsImages.push(``);
|
|
193
200
|
}
|
|
194
201
|
if (statsImages.length > 0) {
|
|
195
202
|
parts.push(`## GitHub Stats\n\n${statsImages.join("\n")}`);
|
|
196
203
|
}
|
|
197
204
|
|
|
198
|
-
//
|
|
199
|
-
if (ctx.sectionSvgs.
|
|
205
|
+
// Impact
|
|
206
|
+
if (ctx.sectionSvgs.impact) {
|
|
200
207
|
parts.push(
|
|
201
|
-
`##
|
|
208
|
+
`## Open Source Impact\n\n`,
|
|
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
|
-
//
|
|
295
|
+
// Constellation
|
|
296
|
+
if (ctx.sectionSvgs.constellation) {
|
|
297
|
+
parts.push(
|
|
298
|
+
`## Project Map\n\n`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// GitHub Stats section: velocity + rhythm
|
|
289
303
|
const statsImages: string[] = [];
|
|
290
|
-
if (ctx.sectionSvgs.
|
|
291
|
-
statsImages.push(``);
|
|
292
306
|
}
|
|
293
|
-
if (ctx.sectionSvgs.
|
|
294
|
-
statsImages.push(``);
|
|
295
309
|
}
|
|
296
310
|
if (statsImages.length > 0) {
|
|
297
311
|
parts.push(`## GitHub Stats\n\n${statsImages.join("\n")}`);
|
|
298
312
|
}
|
|
299
313
|
|
|
300
|
-
//
|
|
301
|
-
if (ctx.sectionSvgs.
|
|
314
|
+
// Impact
|
|
315
|
+
if (ctx.sectionSvgs.impact) {
|
|
302
316
|
parts.push(
|
|
303
|
-
`##
|
|
317
|
+
`## Open Source Impact\n\n`,
|
|
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:
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
257
|
+
velocity: MonthlyLanguageBucket[];
|
|
258
|
+
rhythm: ContributionRhythm;
|
|
259
|
+
constellation: ConstellationNode[];
|
|
233
260
|
contributionData: ContributionData;
|
|
234
261
|
socialBadges: string;
|
|
235
262
|
svgDir: string;
|