Skip to content

feat: gist-based personalization config#7

Open
Copilot wants to merge 5 commits intomainfrom
copilot/add-gist-personalization-config
Open

feat: gist-based personalization config#7
Copilot wants to merge 5 commits intomainfrom
copilot/add-gist-personalization-config

Conversation

Copy link

Copilot AI commented Feb 17, 2026

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() in client.ts — scans user's public gists for profiles-sh.json, parses and validates content
  • KV cached with 1h TTL; negative results cached as "none" to avoid repeated API calls on users without a gist

Override application (src/lib/gist-config.ts, new file)

  • applyPersonaOverrides() — filters hidden_categories, substitutes tagline_overrides
  • applyProjectOverrides() — sorts featured_repos to top of projects list
  • gistConfigToCustomizationRow() — maps gist config → D1 customizations row

Scoring boost

  • featured_topics passed through computeFullProfilecomputeCategoryScores / computeOwnedRepoScoresscoreRepo
  • +2 bonus per repo topic that matches both a featured topic and a category topic

Schema & persistence

  • Migration 0004_gist_config.sql adds featured_repos and featured_topics columns to customizations
  • upsertCustomizations updated to persist all gist-derived fields
  • CustomizationRow type extended

Pipeline integration

  • processUsername() fetches gist config in parallel with profile/repos/stars
  • Overrides applied after engine computation, before D1 write
  • sort_order reassigned on both persona and project rows post-override

Warning

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
    • Triggering command: /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)
    • Triggering command: /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

This section details on the original issue you should resolve

<issue_title>Gist-based personalization config</issue_title>
<issue_description>## Summary

Allow users to personalize their profiles.sh profile by creating a profiles-sh.json gist.

Details

Users create a public gist named profiles-sh.json with preferences:

{
  "featured_repos": ["repo-a", "repo-b"],
  "featured_topics": ["rust", "nushell"],
  "hidden_categories": ["tinkerer"],
  "theme": "dark",
  "tagline_overrides": {
    "systems": "I write kernels for fun."
  }
}

The app fetches and applies this config on profile load:

  1. On profile request, check if user has a profiles-sh.json gist via GitHub API
  2. Cache the gist content in KV with a 1-hour TTL
  3. Apply overrides to the generated profile (featured repos sort to top, hidden categories are removed, custom taglines replace defaults)
  4. Store merged config in the customizations D1 table

Acceptance Criteria

  • Fetch gist by convention name profiles-sh.json
  • Cache gist content in KV (1h TTL)
  • Apply featured_repos — pin repos to top of projects section
  • Apply featured_topics — boost topic scores in domain scoring
  • Apply hidden_categories — remove personas from display
  • Apply tagline_overrides — replace persona taglines
  • Graceful fallback if no gist exists (no error, use defaults)
  • Store resolved config in customizations table</issue_description>

<agent_instructions>@copilot please review this issue and scope / triage it and add more details, comments, and sub-issues.
</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com>
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 17, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

Copilot AI and others added 3 commits February 17, 2026 22:43
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>
Copilot AI changed the title [WIP] Add gist-based personalization configuration for profiles feat: gist-based personalization config Feb 17, 2026
Copilot AI requested a review from danielbodnar February 17, 2026 22:50
@danielbodnar danielbodnar marked this pull request as ready for review February 18, 2026 00:05
Copilot AI review requested due to automatic review settings February 18, 2026 00:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 featuredTopics parameter, adding +2 bonus per featured topic match in scoreRepo()
  • Implemented override application functions in new gist-config.ts module for personas and projects
  • Extended D1 schema with featured_repos and featured_topics columns 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()));
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}),
);

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +66
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];
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +33
* 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()));
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
return null;
}

const config: GistConfig = JSON.parse(content);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
await putCached(env.KV, cacheKey, "none", GIST_CACHE_TTL);
return null;
}

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.).

Suggested change
// 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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +38 to 44
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
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +39
for (const p of filtered) {
const override = taglines[p.persona_id];
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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()];

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gist-based personalization config

3 participants