Conversation
Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
profiles-sh | bd37a62 | Commit Preview URL Branch Preview URL |
Feb 17 2026, 10:46 PM |
Implement profiles-sh.json gist fetching, parsing, and application: - Add GistConfig and GitHubGist types - Add fetchGistConfig() with KV caching (1h TTL) - Add applyPersonaOverrides() for hidden_categories and tagline_overrides - Add applyProjectOverrides() for featured_repos pinning - Add gistConfigToCustomizationRow() for D1 persistence - Add migration 0004_gist_config.sql for featured_repos/featured_topics columns - Integrate gist fetching into processUsername() pipeline - Store resolved config in customizations table Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com>
Pass featured_topics from gist config through to scoring functions. When a repo topic matches a featured topic AND a category topic, a +2 bonus per match is applied to boost relevant categories. Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com>
Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements gist-based personalization, allowing users to customize their profiles.sh profile by creating a public gist named profiles-sh.json. The system fetches, caches, and applies user preferences during profile computation, including featured repositories, featured topics for scoring boosts, hidden personas, custom taglines, and theme preferences.
Changes:
- Added gist config fetching with KV caching (1h TTL) via
fetchGistConfig()in the GitHub client - Extended scoring engine to accept
featuredTopicsparameter, adding +2 bonus per featured topic match inscoreRepo() - Implemented override application functions in new
gist-config.tsmodule for personas and projects - Extended D1 schema with
featured_reposandfeatured_topicscolumns in customizations table - Integrated gist config into the queue consumer pipeline, applying overrides post-computation
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/github/types.ts |
Added GistConfig and GitHubGist interfaces for gist API responses and config structure |
src/lib/github/client.ts |
Implemented fetchGistConfig() to scan user gists, fetch content, validate, and cache with negative result handling |
src/lib/gist-config.ts |
New module with override application logic for personas (filtering/tagline replacement) and projects (featured pinning) |
src/lib/engine/scoring.ts |
Enhanced scoreRepo(), computeCategoryScores(), and computeOwnedRepoScores() to accept and apply featured topic bonuses |
src/lib/engine/index.ts |
Updated computeFullProfile() to accept and thread through featuredTopics parameter |
src/lib/db/types.ts |
Extended CustomizationRow interface with featured_repos and featured_topics fields |
src/lib/db/queries.ts |
Updated upsertCustomizations() to persist featured_repos and featured_topics columns |
migrations/0004_gist_config.sql |
Added featured_repos and featured_topics TEXT columns to customizations table |
src/lib/queue-consumer.ts |
Integrated gist config fetching in parallel, applied overrides post-computation, persisted to D1 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const featured = config.featured_repos; | ||
| if (!featured || featured.length === 0) return projects; | ||
|
|
||
| const featuredSet = new Set(featured.map((r) => r.toLowerCase())); |
There was a problem hiding this comment.
There's a potential mismatch between the featured_repos array values and the project name field. The featured_repos are compared using lowercase (line 63), and the project name is also lowercased for comparison (line 64). However, looking at src/lib/engine/projects.ts line 103, the project name is extracted from the full_name by splitting on "/" and taking the last part. If a user specifies a featured repo as "owner/repo-name" in their gist config instead of just "repo-name", the match will fail. Consider documenting that featured_repos should only contain the repository name (not the full "owner/repo" format), or handle both formats in the matching logic.
| const featuredSet = new Set(featured.map((r) => r.toLowerCase())); | |
| const featuredSet = new Set( | |
| featured.map((r) => { | |
| const lower = r.toLowerCase(); | |
| const parts = lower.split("/"); | |
| return parts[parts.length - 1] || lower; | |
| }), | |
| ); |
| const featuredSet = new Set(featured.map((r) => r.toLowerCase())); | ||
| const pinned = projects.filter((p) => featuredSet.has(p.name.toLowerCase())); | ||
| const rest = projects.filter((p) => !featuredSet.has(p.name.toLowerCase())); | ||
| return [...pinned, ...rest]; |
There was a problem hiding this comment.
The applyProjectOverrides function doesn't preserve the order specified in the featured_repos array. Users may want to pin repos in a specific order (e.g., most important project first), but the current implementation filters pinned projects based on Set lookup, which doesn't maintain the original array order. Consider iterating through the featured array and finding matching projects in order, so that the pinned projects appear in the same sequence as specified in the gist config.
| * Apply gist config overrides to persona rows (in-place mutation for efficiency). | ||
| * | ||
| * - Removes personas in `hidden_categories` | ||
| * - Replaces taglines from `tagline_overrides` | ||
| * | ||
| * Returns the filtered persona list. | ||
| */ | ||
| export function applyPersonaOverrides< | ||
| T extends { persona_id: string; tagline: string | null }, | ||
| >(personas: T[], config: GistConfig): T[] { | ||
| const hidden = new Set( | ||
| (config.hidden_categories ?? []).map((c) => c.toLowerCase()), | ||
| ); | ||
|
|
||
| let filtered = personas; | ||
| if (hidden.size > 0) { | ||
| filtered = personas.filter((p) => !hidden.has(p.persona_id.toLowerCase())); |
There was a problem hiding this comment.
The comment at line 17 states "in-place mutation for efficiency" but the actual implementation doesn't mutate the input array when filtering (line 33 creates a new filtered array). The tagline mutations (lines 38-42) do mutate the objects in place, but the filtering operation creates a new array. This comment is misleading. Either update the comment to accurately describe the behavior, or if in-place mutation for the entire array was intended, consider a different implementation approach.
| return null; | ||
| } | ||
|
|
||
| const config: GistConfig = JSON.parse(content); |
There was a problem hiding this comment.
JSON.parse can throw a SyntaxError if the gist content is malformed JSON, but this call is not wrapped in a try-catch block within the parsing logic itself. While the outer try-catch at line 387 will catch this error, the catch handler caches "none" and logs a warning for any error, which means both malformed JSON and legitimate API errors are treated the same way. This could make debugging difficult if users have syntactically invalid JSON in their gist. Consider adding more specific error handling for JSON parsing errors with a more descriptive log message, or validating the JSON structure before caching the negative result.
| const config: GistConfig = JSON.parse(content); | |
| let config: GistConfig; | |
| try { | |
| config = JSON.parse(content); | |
| } catch (parseErr) { | |
| if (parseErr instanceof SyntaxError) { | |
| console.warn( | |
| `[github/client] Invalid JSON in gist config for ${username}:`, | |
| parseErr, | |
| ); | |
| // Treat invalid JSON as "no config" but with a clearer diagnostic. | |
| await putCached(env.KV, cacheKey, "none", GIST_CACHE_TTL); | |
| return null; | |
| } | |
| // Re-throw non-syntax errors to be handled by the outer catch. | |
| throw parseErr; | |
| } |
| await putCached(env.KV, cacheKey, "none", GIST_CACHE_TTL); | ||
| return null; | ||
| } | ||
|
|
There was a problem hiding this comment.
The validation only checks if the parsed config is a plain object, but doesn't validate the types of the nested properties. For example, if a user sets "featured_repos" to a string instead of an array, or "tagline_overrides" to an array instead of an object, the invalid data will be cached and passed through to the application logic. This could cause runtime errors in applyPersonaOverrides or applyProjectOverrides when they attempt to iterate over or access these properties. Consider adding validation for each expected property type (e.g., checking if featured_repos is an array, if tagline_overrides is an object, etc.).
| // Nested property validation for known fields that downstream logic depends on. | |
| const cfgAny = config as any; | |
| // If present, featured_repos must be an array. | |
| if ( | |
| Object.prototype.hasOwnProperty.call(cfgAny, "featured_repos") && | |
| !Array.isArray(cfgAny.featured_repos) | |
| ) { | |
| await putCached(env.KV, cacheKey, "none", GIST_CACHE_TTL); | |
| return null; | |
| } | |
| // If present, tagline_overrides must be a non-null, non-array object. | |
| if ( | |
| Object.prototype.hasOwnProperty.call(cfgAny, "tagline_overrides") && | |
| (typeof cfgAny.tagline_overrides !== "object" || | |
| cfgAny.tagline_overrides === null || | |
| Array.isArray(cfgAny.tagline_overrides)) | |
| ) { | |
| await putCached(env.KV, cacheKey, "none", GIST_CACHE_TTL); | |
| return null; | |
| } |
| const featuredMatches = topics.filter((t) => | ||
| featuredTopics.has(t) && cat.topics.some((st) => t.includes(st) || st.includes(t)), | ||
| ).length; | ||
| score += featuredMatches * 2; | ||
| } | ||
|
|
||
| // Description keyword match: +1.5 per keyword |
There was a problem hiding this comment.
The featured topic boost logic requires a topic to match BOTH a featured topic AND a category topic to receive the bonus. This is overly restrictive and may not work as intended. If a user features "rust" but the repo has the topic "rust" and the category doesn't explicitly include "rust" in its topics list, the repo won't get the bonus. This defeats the purpose of featured_topics which should boost repos with those topics regardless of category. Consider changing the logic to: if a topic matches a featured topic, add the bonus (remove the category topic requirement from the condition).
| const featuredMatches = topics.filter((t) => | |
| featuredTopics.has(t) && cat.topics.some((st) => t.includes(st) || st.includes(t)), | |
| ).length; | |
| score += featuredMatches * 2; | |
| } | |
| // Description keyword match: +1.5 per keyword | |
| const featuredMatches = topics.filter((t) => featuredTopics.has(t)).length; | |
| score += featuredMatches * 2; | |
| } | |
| // Description keyword match: +1.5 per keyword | |
| // Description keyword match: +1.5 per keyword |
| for (const p of filtered) { | ||
| const override = taglines[p.persona_id]; |
There was a problem hiding this comment.
The tagline_overrides lookup uses exact key matching (taglines[p.persona_id]), but there's a case-sensitivity inconsistency. The hidden_categories are lowercased for comparison (line 28), but tagline_overrides are not. This means if a user specifies a tagline override with a different case than the persona_id (e.g., "Rust" vs "rust"), the override won't be applied. For consistency with the hidden_categories logic and to make the feature more user-friendly, consider normalizing the persona_id key lookup to lowercase.
| for (const p of filtered) { | |
| const override = taglines[p.persona_id]; | |
| const lowerTaglines: Record<string, string | null> = {}; | |
| for (const [key, value] of Object.entries(taglines)) { | |
| lowerTaglines[key.toLowerCase()] = value; | |
| } | |
| for (const p of filtered) { | |
| const override = lowerTaglines[p.persona_id.toLowerCase()]; |
Users can now personalize their profiles.sh profile by creating a public gist named
profiles-sh.json. The system fetches, caches, and applies overrides during profile computation.{ "featured_repos": ["repo-a", "repo-b"], "featured_topics": ["rust", "nushell"], "hidden_categories": ["tinkerer"], "theme": "dark", "tagline_overrides": { "systems": "I write kernels for fun." } }Gist fetching & caching
fetchGistConfig()inclient.ts— scans user's public gists forprofiles-sh.json, parses and validates content"none"to avoid repeated API calls on users without a gistOverride application (
src/lib/gist-config.ts, new file)applyPersonaOverrides()— filtershidden_categories, substitutestagline_overridesapplyProjectOverrides()— sortsfeatured_reposto top of projects listgistConfigToCustomizationRow()— maps gist config → D1customizationsrowScoring boost
featured_topicspassed throughcomputeFullProfile→computeCategoryScores/computeOwnedRepoScores→scoreRepoSchema & persistence
0004_gist_config.sqladdsfeatured_reposandfeatured_topicscolumns tocustomizationsupsertCustomizationsupdated to persist all gist-derived fieldsCustomizationRowtype extendedPipeline integration
processUsername()fetches gist config in parallel with profile/repos/starssort_orderreassigned on both persona and project rows post-overrideWarning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
telemetry.astro.build/home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/professional-persona-cards/professional-persona-cards/node_modules/.bin/astro build(dns block)/home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/professional-persona-cards/professional-persona-cards/node_modules/.bin/astro build git conf�� get --global p/bin/git http.https://gitgit(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.