@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
@@ -8,9 +8,9 @@ description: >-
8
8
  argument-hint: [setup | customize | generate | debug | extend]
9
9
  ---
10
10
 
11
- # GitHub Metrics — Agent Skill
11
+ # GitHub Insights — Agent Skill
12
12
 
13
- Generate beautiful dark-themed SVG metrics for GitHub profile READMEs. Sections include language breakdowns (donut chart), AI expertise analysis (proficiency bars), contribution pulse (stat cards), contribution calendar (heatmap), signature projects (by stars), and open source contributions.
13
+ Generate beautiful SVG metrics for GitHub profile READMEs. Sections include language velocity streamgraph, contribution rhythm radar chart, project constellation map, and open source impact trail. SVGs support light/dark theme switching and CSS animations.
14
14
 
15
15
  ## Quick Reference
16
16
 
@@ -18,7 +18,7 @@ Generate beautiful dark-themed SVG metrics for GitHub profile READMEs. Sections
18
18
  |------|---------|
19
19
  | Generate locally | `npm run generate` (requires `gh auth login`) |
20
20
  | Full CI check | `npm run ci` (fmt + lint + typecheck + test + build) |
21
- | Build bundle | `npm run build` (ncc `dist/`) |
21
+ | Build bundle | `npm run build` (ncc -> `dist/`) |
22
22
  | Run tests | `npm test` (vitest) |
23
23
  | Type-check | `npm run typecheck` |
24
24
  | Lint | `npm run lint` (biome) |
@@ -66,8 +66,7 @@ npm run generate
66
66
  Local mode differences:
67
67
  - `commit-push` defaults to `false` (no git operations)
68
68
  - README writes to `_README.md` (not `README.md`)
69
- - All three template previews are generated in `examples/{classic,modern,minimal}/`
70
- - Both preamble variants (full + short) are generated for preview
69
+ - All four template previews are generated in `examples/{classic,modern,minimal,ecosystem}/`
71
70
 
72
71
  ## Configuration
73
72
 
@@ -76,18 +75,16 @@ Local mode differences:
76
75
  ```yaml
77
76
  name: Display Name # overrides GitHub profile name
78
77
  pronunciation: pronunciation # shown as subscript in heading
79
- title: Software Engineer # blockquote under heading; guides AI expertise
80
- desired_title: Senior SWE # AI context only — biases expertise categories
78
+ title: Software Engineer # blockquote under heading
79
+ desired_title: Senior SWE # AI context only
81
80
  bio: Short bio text. # footer text in classic template
82
81
  preamble: PREAMBLE.md # path to custom preamble (bypasses AI generation)
83
- template: classic # "classic" | "modern" | "minimal"
82
+ template: classic # "classic" | "modern" | "minimal" | "ecosystem"
84
83
  sections:
85
- - pulse
86
- - languages
87
- - expertise
88
- - projects
89
- - contributions
90
- - calendar
84
+ - velocity
85
+ - rhythm
86
+ - constellation
87
+ - impact
91
88
  ```
92
89
 
93
90
  ### Action inputs
@@ -109,41 +106,38 @@ sections:
109
106
 
110
107
  | Key | SVG filename | What it renders |
111
108
  |-----|-------------|-----------------|
112
- | `pulse` | `metrics-pulse.svg` | 4 stat cards: commits, PRs, reviews, active repos |
113
- | `languages` | `metrics-languages.svg` | Donut chart of top 10 languages by bytes |
114
- | `expertise` | `metrics-expertise.svg` | AI-generated proficiency bars by category |
115
- | `projects` | `metrics-complexity.svg` | Top 5 repos by stars with descriptions |
116
- | `calendar` | `metrics-calendar.svg` | GitHub contribution heatmap (1 year) |
117
- | `contributions` | `metrics-contributions.svg` | External repos contributed to |
109
+ | `velocity` | `metrics-velocity.svg` | Streamgraph of language usage over 12 months |
110
+ | `rhythm` | `metrics-rhythm.svg` | 7-spoke radar chart + contribution stats |
111
+ | `constellation` | `metrics-constellation.svg` | Project map by language and complexity |
112
+ | `impact` | `metrics-impact.svg` | External contributions with impact bars |
118
113
 
119
114
  ### Templates
120
115
 
121
- - **`classic`** (default): Formal layout — `# Name`, blockquote title, full AI preamble (2–4 paragraphs), social badges, SVG metrics, bio footer, attribution.
122
- - **`modern`**: Friendly — `# Hi, I'm {firstName} 👋`, short preamble, active/maintained/inactive project lists in markdown, selective SVG sections.
123
- - **`minimal`**: Clean — `# {firstName}`, short preamble, social badges, SVG metrics, attribution.
116
+ - **`classic`** (default): Formal layout — `# Name`, blockquote title, preamble, social badges, SVG metrics, bio footer.
117
+ - **`modern`**: Friendly — `# Hi, I'm {firstName}`, projects by activity, Project Map, GitHub Stats, Impact.
118
+ - **`minimal`**: Clean — `# {firstName}`, preamble, social badges, SVG metrics.
119
+ - **`ecosystem`**: Categorized — projects by purpose (Developer Tools/SDKs/Applications/Research), Project Map, GitHub Stats, Impact.
124
120
 
125
121
  ## Architecture
126
122
 
127
123
  ### Execution flow
128
124
 
129
125
  ```
130
- Inputs Fetch (parallel) AI calls (sequential) Transform Render sections Write SVGs Generate README Commit
126
+ Inputs -> Fetch (parallel) -> AI calls -> Transform -> Compute velocity/rhythm/constellation -> Render sections -> Write SVGs -> Generate README -> Commit
131
127
  ```
132
128
 
133
129
  ### Key source files
134
130
 
135
131
  | File | Role |
136
132
  |------|------|
137
- | `src/index.ts` | Orchestration: fetch transform render write commit |
133
+ | `src/index.ts` | Orchestration: fetch -> transform -> render -> write -> commit |
138
134
  | `src/api.ts` | GitHub GraphQL queries + GitHub Models AI calls |
139
- | `src/metrics.ts` | Data aggregation, complexity scoring, section building |
140
- | `src/config.ts` | TOML config loading |
141
- | `src/types.ts` | All TypeScript interfaces (`UserConfig`, `SectionDef`, `TemplateContext`, etc.) |
142
- | `src/templates.ts` | Three README template functions + social badge builder |
143
- | `src/theme.ts` | `THEME` colors, `LAYOUT` dimensions, `BAR_COLORS` palette |
144
- | `src/parsers.ts` | Dependency manifest parsers (package.json, Cargo.toml, go.mod, etc.) |
145
- | `src/components/` | SVG rendering components (custom JSX → SVG strings) |
146
- | `src/jsx-factory.ts` | Custom `h()` / `Fragment()` JSX runtime (no React) |
135
+ | `src/metrics.ts` | Data aggregation, velocity/rhythm/constellation computation, section building |
136
+ | `src/config.ts` | YAML/TOML config loading |
137
+ | `src/types.ts` | All TypeScript interfaces |
138
+ | `src/templates.ts` | Four README template functions + social badge builder |
139
+ | `src/theme.ts` | `THEME`/`THEME_LIGHT` colors, `LAYOUT` dimensions, `BAR_COLORS` palette |
140
+ | `src/components/` | SVG rendering components (custom JSX -> SVG strings) |
147
141
 
148
142
  ### Component rendering pattern
149
143
 
@@ -155,25 +149,6 @@ function renderXxx(data: ..., y: number): { svg: string; height: number }
155
149
 
156
150
  Components return SVG string fragments and their rendered height. The `y` parameter is the vertical cursor; heights are accumulated to stack sections vertically.
157
151
 
158
- ### Theme constants (hardcoded in `src/theme.ts`)
159
-
160
- ```
161
- THEME.bg = "#0d1117" (dark background)
162
- THEME.cardBg = "#161b22" (card backgrounds)
163
- THEME.border = "#30363d" (card borders)
164
- THEME.link = "#58a6ff" (card titles - blue)
165
- THEME.text = "#c9d1d9" (primary text)
166
- THEME.secondary= "#8b949e" (labels)
167
- THEME.muted = "#6e7681" (values)
168
-
169
- LAYOUT.width = 808 (fixed SVG canvas width)
170
- LAYOUT.padX = 24
171
- LAYOUT.padY = 24
172
- LAYOUT.sectionGap = 30
173
- ```
174
-
175
- These are not configurable via inputs or TOML — changing them requires editing source.
176
-
177
152
  ## Extending
178
153
 
179
154
  ### Add a new section
@@ -183,16 +158,7 @@ These are not configurable via inputs or TOML — changing them requires editing
183
158
  3. Add the section key to `SECTION_KEYS` map in `src/metrics.ts`
184
159
  4. Add a `SectionDef` entry in the `buildSections()` function in `src/metrics.ts`
185
160
  5. Fetch any new data needed in `src/index.ts`
186
- 6. Add a `*.test.ts` file alongside the component
187
- 7. Update `action.yml` if new inputs are needed
188
-
189
- ### Add a new dependency parser
190
-
191
- 1. Implement `PackageParser` interface in `src/parsers.ts`:
192
- ```ts
193
- { filenames: string[]; parseDependencies(text: string): string[] }
194
- ```
195
- 2. Add it to the `PARSERS` array — it auto-registers in `PARSER_MAP`
161
+ 6. Add a `*.test.tsx` file alongside the component
196
162
 
197
163
  ### Add a new README template
198
164
 
@@ -202,36 +168,24 @@ These are not configurable via inputs or TOML — changing them requires editing
202
168
 
203
169
  ## Troubleshooting
204
170
 
205
- ### "Expertise section missing"
206
- The expertise section only appears when the AI call succeeds. Check:
207
- - Token has `models: read` permission
208
- - The workflow has `permissions: models: read`
209
- - GitHub Models endpoint is reachable
171
+ ### "Velocity section is flat"
172
+ The streamgraph distributes per-repo commits using the contribution calendar's monthly activity weights. If there's no calendar data, velocity will be empty.
210
173
 
211
- ### "Calendar section missing"
212
- Only rendered when contribution calendar data exists. The user must have public contributions.
174
+ ### "Constellation section missing"
175
+ Only rendered when there are projects with language data. Ensure repos have detectable languages.
213
176
 
214
- ### "Contributions section missing"
215
- Only rendered when the user has contributed to external (non-owned) repositories in the past year.
177
+ ### "Impact section missing"
178
+ Only rendered when the user has contributed to external (non-owned) repositories.
216
179
 
217
180
  ### AI preamble is empty or generic
218
181
  - The AI call uses `gpt-4.1` via GitHub Models — it needs diverse profile data to generate good output
219
- - Provide `title` and `desired_title` in TOML config for better results
182
+ - Provide `title` in config for better results
220
183
  - Create a custom `PREAMBLE.md` to bypass AI entirely
221
184
 
222
185
  ### Local generation fails
223
186
  - Ensure `gh auth login` is done and `gh auth token` returns a valid token
224
187
  - Ensure Node.js 22+ (`node --version`)
225
- - The `GITHUB_TOKEN` and `GITHUB_REPOSITORY_OWNER` env vars are set automatically by `npm run generate` via `gh`
226
188
 
227
189
  ### SVG looks wrong after changes
228
190
  - Run `npm run build` to rebuild the `dist/` bundle — the action runs `dist/index.js`, not source directly
229
191
  - SVG width is fixed at 808px; all layout math depends on this
230
-
231
- ## Code Style Rules
232
-
233
- - TypeScript strict mode, ES modules
234
- - Biome for formatting and linting (not ESLint/Prettier)
235
- - Tests colocated as `*.test.ts` / `*.test.tsx` alongside source
236
- - Custom JSX factory (`h`, `Fragment`) — NOT React
237
- - All AI calls and contribution fetches are non-fatal (catch → return empty)
package/sr.yaml CHANGED
@@ -14,3 +14,12 @@ commit_types:
14
14
 
15
15
  artifacts:
16
16
  - dist.tar.gz
17
+
18
+ hooks:
19
+ commit-msg:
20
+ - sr hook commit-msg
21
+ pre-commit:
22
+ - npm run fmt
23
+ - npm run lint
24
+ - npm run typecheck
25
+ - npm run test
package/src/api.ts CHANGED
@@ -7,7 +7,6 @@ import type {
7
7
  RepoClassificationInput,
8
8
  RepoClassificationOutput,
9
9
  RepoNode,
10
- TechHighlight,
11
10
  UserConfig,
12
11
  UserProfile,
13
12
  } from "./types.js";
@@ -320,7 +319,6 @@ export interface PreambleContext {
320
319
  profile: UserProfile;
321
320
  userConfig: UserConfig;
322
321
  languages: { name: string; percent: string }[];
323
- techHighlights: TechHighlight[];
324
322
  activeProjects: ProjectItem[];
325
323
  complexProjects: ProjectItem[];
326
324
  }
@@ -330,21 +328,12 @@ export const fetchAIPreamble = async (
330
328
  context: PreambleContext,
331
329
  ): Promise<string | undefined> => {
332
330
  try {
333
- const {
334
- profile,
335
- userConfig,
336
- languages,
337
- techHighlights,
338
- activeProjects,
339
- complexProjects,
340
- } = context;
331
+ const { profile, userConfig, languages, activeProjects, complexProjects } =
332
+ context;
341
333
 
342
334
  const langLines = languages
343
335
  .map((l) => `- ${l.name}: ${l.percent}%`)
344
336
  .join("\n");
345
- const techLines = techHighlights
346
- .map((h) => `- ${h.category}: ${h.items.join(", ")} (score: ${h.score})`)
347
- .join("\n");
348
337
 
349
338
  const formatProject = (p: ProjectItem): string => {
350
339
  const langs = p.languages?.length ? ` [${p.languages.join(", ")}]` : "";
@@ -373,9 +362,6 @@ ${profileLines}
373
362
  Languages (by code volume):
374
363
  ${langLines}
375
364
 
376
- Expertise areas:
377
- ${techLines}
378
-
379
365
  Most technically complex projects (by language diversity, codebase size, and depth):
380
366
  ${complexProjectLines || "None"}
381
367
 
@@ -474,130 +460,6 @@ Generate 1-2 sentences that:
474
460
  }
475
461
  };
476
462
 
477
- export const fetchExpertiseAnalysis = async (
478
- token: string,
479
- languages: { name: string; percent: string }[],
480
- allDeps: string[],
481
- allTopics: string[],
482
- repos: RepoNode[],
483
- readmeMap: ReadmeMap,
484
- userConfig: UserConfig = {},
485
- ): Promise<TechHighlight[]> => {
486
- try {
487
- const langLines = languages
488
- .map((l) => `- ${l.name}: ${l.percent}%`)
489
- .join("\n");
490
-
491
- const repoSummaries = repos
492
- .slice(0, 20)
493
- .map((r) => {
494
- const readme = readmeMap.get(r.name) || "";
495
- const snippet = readme.slice(0, 500).replace(/\n/g, " ");
496
- const desc = r.description || "";
497
- return `- ${r.name}: ${desc} | ${snippet}`;
498
- })
499
- .join("\n");
500
-
501
- const desiredTitle = userConfig.desired_title || userConfig.title;
502
- let titleContext = "";
503
- if (userConfig.title) {
504
- titleContext = `\nDeveloper context:\n- Current title: ${userConfig.title}`;
505
- if (desiredTitle && desiredTitle !== userConfig.title) {
506
- titleContext += `\n- Desired title: ${desiredTitle}`;
507
- }
508
- titleContext += `\n- Tailor the expertise categories to highlight skills most relevant to ${desiredTitle}. Prioritize domains and technologies that align with this role.\n`;
509
- }
510
-
511
- const prompt = `You are analyzing a developer's GitHub profile to create a curated expertise showcase.
512
- ${titleContext}
513
- Languages (by code volume):
514
- ${langLines}
515
-
516
- Dependencies found across repositories:
517
- ${allDeps.join(", ")}
518
-
519
- Repository topics:
520
- ${allTopics.join(", ")}
521
-
522
- Repository descriptions and README excerpts:
523
- ${repoSummaries}
524
-
525
- From this data, produce a curated expertise profile:
526
- - Group the most notable technologies into 3-6 expertise categories
527
- - Use domain-oriented category names (e.g., "Machine Learning", "Web Development", "DevOps", "Backend & APIs", "Data Science", "Systems Programming")
528
- - Include 3-6 of the most relevant technologies/tools per category
529
- - Normalize names to their common display form (e.g., "pg" → "PostgreSQL", "torch" → "PyTorch", "boto3" → "AWS SDK")
530
- - Skip trivial utility libraries (lodash, uuid, etc.) that don't showcase meaningful expertise
531
- - Only include categories where there's meaningful evidence of usage
532
- - Assign each category a proficiency score from 0 to 100 based on evidence strength:
533
- language code volume, dependency count, topic mentions, and README depth.
534
- Use the full range (e.g. 80-95 for primary stack, 50-70 for secondary, 30-50 for minor).`;
535
-
536
- const res = await fetchWithRetry(
537
- "https://models.github.ai/inference/chat/completions",
538
- {
539
- method: "POST",
540
- headers: {
541
- Authorization: `bearer ${token}`,
542
- "Content-Type": "application/json",
543
- },
544
- body: JSON.stringify({
545
- model: "gpt-4.1",
546
- messages: [{ role: "user", content: prompt }],
547
- temperature: 0.1,
548
- response_format: {
549
- type: "json_schema",
550
- json_schema: {
551
- name: "tech_highlights",
552
- strict: true,
553
- schema: {
554
- type: "object",
555
- properties: {
556
- highlights: {
557
- type: "array",
558
- items: {
559
- type: "object",
560
- properties: {
561
- category: { type: "string" },
562
- items: { type: "array", items: { type: "string" } },
563
- score: { type: "number" },
564
- },
565
- required: ["category", "items", "score"],
566
- additionalProperties: false,
567
- },
568
- },
569
- },
570
- required: ["highlights"],
571
- additionalProperties: false,
572
- },
573
- },
574
- },
575
- }),
576
- },
577
- "Expertise",
578
- );
579
-
580
- if (!res?.ok) {
581
- if (res) console.warn(`GitHub Models API error: ${res.status}`);
582
- return [];
583
- }
584
-
585
- const json = (await res.json()) as {
586
- choices?: { message?: { content?: string } }[];
587
- };
588
- const content = json.choices?.[0]?.message?.content || "{}";
589
- const parsed = JSON.parse(content) as { highlights?: TechHighlight[] };
590
- return (parsed.highlights || [])
591
- .filter((h) => h.category && Array.isArray(h.items) && h.items.length > 0)
592
- .map((h) => ({ ...h, score: Math.max(0, Math.min(100, h.score || 0)) }))
593
- .sort((a, b) => b.score - a.score);
594
- } catch (err: unknown) {
595
- const msg = err instanceof Error ? err.message : String(err);
596
- console.warn(`Expertise analysis failed (non-fatal): ${msg}`);
597
- return [];
598
- }
599
- };
600
-
601
463
  export const fetchProjectClassifications = async (
602
464
  token: string,
603
465
  repos: RepoClassificationInput[],
@@ -0,0 +1,43 @@
1
+ import { Fragment, h } from "../jsx-factory.js";
2
+ import { LAYOUT } from "../theme.js";
3
+ import type { ContributionCalendar, RenderResult } from "../types.js";
4
+
5
+ export function renderContributionHeatmap(
6
+ calendar: ContributionCalendar,
7
+ y: number,
8
+ ): RenderResult {
9
+ if (calendar.weeks.length === 0) return { svg: "", height: 0 };
10
+
11
+ const { padX } = LAYOUT;
12
+ const cellSize = 11;
13
+ const cellGap = 2;
14
+ const step = cellSize + cellGap;
15
+
16
+ const rows = 7;
17
+ const chartHeight = rows * step;
18
+ const totalHeight = chartHeight + 4;
19
+
20
+ const svg = (
21
+ <>
22
+ {/* Heatmap cells */}
23
+ {calendar.weeks.map((week, wi) =>
24
+ week.contributionDays.map((day, di) => (
25
+ <rect
26
+ x={padX + wi * step}
27
+ y={y + di * step}
28
+ width={cellSize}
29
+ height={cellSize}
30
+ rx="2"
31
+ fill={day.color}
32
+ opacity="0.85"
33
+ className={`fade-${Math.min((wi % 6) + 1, 6)}`}
34
+ />
35
+ )),
36
+ )}
37
+ </>
38
+ );
39
+
40
+ return { svg, height: totalHeight };
41
+ }
42
+
43
+ void Fragment;
@@ -0,0 +1,152 @@
1
+ import { Fragment, h } from "../jsx-factory.js";
2
+ import { escapeXml } from "../svg-utils.js";
3
+ import { BAR_COLORS, LAYOUT, THEME } from "../theme.js";
4
+ import type { ContributionRhythm, RenderResult } from "../types.js";
5
+
6
+ const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
7
+
8
+ export function renderContributionRhythm(
9
+ rhythm: ContributionRhythm,
10
+ y: number,
11
+ ): RenderResult {
12
+ const { padX } = LAYOUT;
13
+
14
+ // Radar chart dimensions
15
+ const radarCx = padX + 120;
16
+ const radarCy = y + 120;
17
+ const radarR = 90;
18
+ const maxVal = Math.max(...rhythm.dayTotals, 1);
19
+
20
+ // Guide circles
21
+ const guides = [0.25, 0.5, 0.75, 1.0];
22
+ const guidesSvg = guides.map((pct) => (
23
+ <circle
24
+ cx={radarCx}
25
+ cy={radarCy}
26
+ r={radarR * pct}
27
+ fill="none"
28
+ stroke={THEME.border}
29
+ stroke-width="1"
30
+ stroke-opacity="0.4"
31
+ />
32
+ ));
33
+
34
+ // Spoke lines
35
+ const spokesSvg = DAY_NAMES.map((_, i) => {
36
+ const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
37
+ const x2 = radarCx + radarR * Math.cos(angle);
38
+ const y2 = radarCy + radarR * Math.sin(angle);
39
+ return (
40
+ <line
41
+ x1={radarCx}
42
+ y1={radarCy}
43
+ x2={x2}
44
+ y2={y2}
45
+ stroke={THEME.border}
46
+ stroke-width="1"
47
+ stroke-opacity="0.3"
48
+ />
49
+ );
50
+ });
51
+
52
+ // Day labels
53
+ const labelsSvg = DAY_NAMES.map((name, i) => {
54
+ const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
55
+ const labelR = radarR + 16;
56
+ const lx = radarCx + labelR * Math.cos(angle);
57
+ const ly = radarCy + labelR * Math.sin(angle) + 4;
58
+ return (
59
+ <text x={lx} y={ly} className="t t-value" text-anchor="middle">
60
+ {escapeXml(name)}
61
+ </text>
62
+ );
63
+ });
64
+
65
+ // Data polygon
66
+ const points = rhythm.dayTotals
67
+ .map((val, i) => {
68
+ const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
69
+ const r = (val / maxVal) * radarR;
70
+ const px = radarCx + r * Math.cos(angle);
71
+ const py = radarCy + r * Math.sin(angle);
72
+ return `${px},${py}`;
73
+ })
74
+ .join(" ");
75
+
76
+ // Stats section (right side)
77
+ const statsX = padX + 300;
78
+ const statsStartY = y + 30;
79
+ const statColors = [
80
+ BAR_COLORS[0],
81
+ BAR_COLORS[1],
82
+ BAR_COLORS[2],
83
+ BAR_COLORS[4],
84
+ BAR_COLORS[5],
85
+ ];
86
+
87
+ const statsSvg = rhythm.stats.map((stat, i) => {
88
+ const sy = statsStartY + i * 42;
89
+ const color = statColors[i % statColors.length];
90
+ return (
91
+ <>
92
+ <text x={statsX} y={sy} className="t t-stat-label">
93
+ {escapeXml(stat.label)}
94
+ </text>
95
+ <text x={statsX} y={sy + 22} fill={color} className="t t-stat-value">
96
+ {escapeXml(stat.value)}
97
+ </text>
98
+ </>
99
+ );
100
+ });
101
+
102
+ const height = 250;
103
+
104
+ const svg = (
105
+ <>
106
+ {/* Guide circles */}
107
+ {guidesSvg.join("")}
108
+
109
+ {/* Spokes */}
110
+ {spokesSvg.join("")}
111
+
112
+ {/* Data polygon */}
113
+ <polygon
114
+ points={points}
115
+ fill={BAR_COLORS[0]}
116
+ fill-opacity="0.2"
117
+ stroke={BAR_COLORS[0]}
118
+ stroke-width="2"
119
+ stroke-opacity="0.8"
120
+ className="fade-2"
121
+ style={`transform-origin: ${radarCx}px ${radarCy}px`}
122
+ />
123
+
124
+ {/* Data points */}
125
+ {rhythm.dayTotals.map((val, i) => {
126
+ const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
127
+ const r = (val / maxVal) * radarR;
128
+ const px = radarCx + r * Math.cos(angle);
129
+ const py = radarCy + r * Math.sin(angle);
130
+ return (
131
+ <circle
132
+ cx={px}
133
+ cy={py}
134
+ r="3"
135
+ fill={BAR_COLORS[0]}
136
+ className={`fade-${Math.min(i + 1, 6)}`}
137
+ />
138
+ );
139
+ })}
140
+
141
+ {/* Day labels */}
142
+ {labelsSvg.join("")}
143
+
144
+ {/* Stats */}
145
+ {statsSvg.join("")}
146
+ </>
147
+ );
148
+
149
+ return { svg, height };
150
+ }
151
+
152
+ void Fragment;
@@ -35,7 +35,10 @@ describe("generateFullSvg", () => {
35
35
  filename: "b.svg",
36
36
  title: "Section B",
37
37
  subtitle: "Subtitle B",
38
- items: [{ name: "Go", value: 10 }],
38
+ renderBody: (y) => ({
39
+ svg: `<text y="${y}">Go</text>`,
40
+ height: 30,
41
+ }),
39
42
  },
40
43
  ];
41
44
  const result = generateFullSvg(sections);
@@ -1,7 +1,6 @@
1
1
  import { Fragment, h } from "../jsx-factory.js";
2
2
  import { LAYOUT, THEME } from "../theme.js";
3
3
  import type { SectionDef } from "../types.js";
4
- import { renderBarChart } from "./bar-chart.js";
5
4
  import { renderSectionHeader } from "./section.js";
6
5
  import { StyleDefs } from "./style-defs.js";
7
6
 
@@ -15,7 +14,13 @@ export function wrapSectionSvg(bodySvg: string, height: number): string {
15
14
  viewBox={`0 0 ${width} ${height}`}
16
15
  >
17
16
  <StyleDefs />
18
- <rect width={width} height={height} rx="12" fill={THEME.bg} />
17
+ <rect
18
+ width={width}
19
+ height={height}
20
+ rx="12"
21
+ className="bg-fill"
22
+ fill={THEME.bg}
23
+ />
19
24
  {bodySvg}
20
25
  </svg>
21
26
  );
@@ -35,10 +40,6 @@ export function generateFullSvg(sections: SectionDef[]): string {
35
40
  const body = section.renderBody(y);
36
41
  bodySvg += body.svg;
37
42
  y += body.height + sectionGap;
38
- } else if (section.items) {
39
- const bars = renderBarChart(section.items, y, section.options || {});
40
- bodySvg += bars.svg;
41
- y += bars.height + sectionGap;
42
43
  }
43
44
  }
44
45
 
@@ -52,7 +53,13 @@ export function generateFullSvg(sections: SectionDef[]): string {
52
53
  viewBox={`0 0 ${width} ${totalHeight}`}
53
54
  >
54
55
  <StyleDefs />
55
- <rect width={width} height={totalHeight} rx="12" fill={THEME.bg} />
56
+ <rect
57
+ width={width}
58
+ height={totalHeight}
59
+ rx="12"
60
+ className="bg-fill"
61
+ fill={THEME.bg}
62
+ />
56
63
  {bodySvg}
57
64
  </svg>
58
65
  );