Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Shared terms match the skilld CLI glossary (`skilld-dev/skilld/GLOSSARY.md`). Th
| track | `/skills/<slug>`, `layers/registry` | track 1β€”N Skill | "track" |
| comparison | `/compare/<slug>`, `layers/marketing` | comparison Nβ€”N Skill | "comparison" |
| trending | `/skills/trending`, ADR-0004 | Repository 1β€”N social mention | "trending" |
| trending award | `skill_trending_awards`, ADR-0011 | Skill 1β€”N trending award | "#3 trending" |
| registry | skilld.dev | one | "skilld" |
| skilld token | `cli_tokens`, `layers/identity` | Author 1β€”N token | "skilld token" |
| provenance | Skill detail, cards | Skill 1β€”1 author and source link | "written by", "source" |
Expand Down Expand Up @@ -158,6 +159,14 @@ Collisions

**Never:** popular, hot, top, leaderboard.

### trending award

**Is:** the best rank a Skill reached on the `week` or `month` trending board in one calendar period (ADR-0011). It stays after the Skill leaves the board. Only a row with a post or a star surge earns one.

**Format:** `#3 trending Β· Sep 2026` on a chip or a README badge. `#3 trending, week of 29 Sep 2026` in full.

**Never:** trophy, medal, achievement, honor, top pick.

### registry

**Is:** skilld.dev, the curated index of Skills and the discovery MCP server.
Expand Down
2 changes: 1 addition & 1 deletion VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ The interface recedes; the content speaks. Warm stone, rare rose, mono chrome, b
1. **skilld-maintained Skills stay separate from registry provenance.** The skilld project may publish visible skilld-maintained Skills for generation, review, search, and install guidance. They name the project as author and remain readable source assets. Generated output never enters the registry as platform-authored maintainer content. Registry Skills retain their Repository owner and exact GitHub provenance. Machine-generated search metadata stays labeled and stays metadata.
2. **We are not AI-powered.** The product category is a curated registry. "AI" never modifies skilld itself in copy, positioning, or feature naming.
3. **No pay-to-play, ever.** No sponsored listings, promoted skills, paid placement, or ads. This is the money posture made permanent: curation that can be bought is not curation.
4. **Popularity is never the quality signal.** Unverifiable third-party counts (installs scraped from skills.sh) never order a surface, never feed trust tiers, and never appear as our numbers; we cannot vouch for a figure we cannot source. GitHub stars are the one sanctioned popularity signal: verifiable at the canonical source, synced for free, honest to display. Stars may order browse surfaces (catalog sort, the human-reviewed leaderboard) and may feed trust as a supporting input (a well-starred repository has earned community evidence), but stars alone never grant the top trust tier: admission and the highest trust signals stay human judgments, the homepage's organizing principle stays curation and freshness, and no star count ever substitutes for review. **Likes carve-out (ADR-0003, 2026-08-10):** Skill likes are first-party (a row we wrote, keyed to a GitHub-authenticated user, so we can state exactly what the number counts) and Skill-grained (stars measure a repository and lend the same count to all twenty Skills inside it). Like counts may be displayed and may back an opt-in `?sort=likes`. They never feed trust, never affect admission, never become a default order, and never order a trending surface: a board ranked by our own users' taste is a mirror. The carve-out covers display and that one sort key; every other clause above holds unchanged. **Trending carve-out (ADR-0004, 2026-08-19):** `/skills/trending` and the weekly email are ordered by social mentions we record ourselves from the public X and Bluesky APIs, and by star surges on repositories holding exactly one Skill. Both pass the test that admitted stars and rejected installs: we can state what the number counts, and a reader can check it, because every socially attributed row ships the post that named the Skill with its author and link. Each row states which route named it; a heading may never claim people named something when a star count did. Neither signal feeds trust or admission, and installs stay barred. **Track carve-out (ADR-0010, 2026-10-06):** a track page may lead with the Skills devs talked about, by posts alone and under the same rules, once at least five qualify; every other section names its own order.
4. **Popularity is never the quality signal.** Unverifiable third-party counts (installs scraped from skills.sh) never order a surface, never feed trust tiers, and never appear as our numbers; we cannot vouch for a figure we cannot source. GitHub stars are the one sanctioned popularity signal: verifiable at the canonical source, synced for free, honest to display. Stars may order browse surfaces (catalog sort, the human-reviewed leaderboard) and may feed trust as a supporting input (a well-starred repository has earned community evidence), but stars alone never grant the top trust tier: admission and the highest trust signals stay human judgments, the homepage's organizing principle stays curation and freshness, and no star count ever substitutes for review. **Likes carve-out (ADR-0003, 2026-08-10):** Skill likes are first-party (a row we wrote, keyed to a GitHub-authenticated user, so we can state exactly what the number counts) and Skill-grained (stars measure a repository and lend the same count to all twenty Skills inside it). Like counts may be displayed and may back an opt-in `?sort=likes`. They never feed trust, never affect admission, never become a default order, and never order a trending surface: a board ranked by our own users' taste is a mirror. The carve-out covers display and that one sort key; every other clause above holds unchanged. **Trending carve-out (ADR-0004, 2026-08-19):** `/skills/trending` and the weekly email are ordered by social mentions we record ourselves from the public X and Bluesky APIs, and by star surges on repositories holding exactly one Skill. Both pass the test that admitted stars and rejected installs: we can state what the number counts, and a reader can check it, because every socially attributed row ships the post that named the Skill with its author and link. Each row states which route named it; a heading may never claim people named something when a star count did. Neither signal feeds trust or admission, and installs stay barred. **Track carve-out (ADR-0010, 2026-10-06):** a track page may lead with the Skills devs talked about, by posts alone and under the same rules, once at least five qualify; every other section names its own order. **Awards carve-out (ADR-0011, 2026-10-06):** a trending award keeps the best rank an evidenced row reached on the week or month board, dated to its period, on the Skill page and on an opt-in README badge. It repeats a claim the board already made with its evidence, so no one can buy or farm it. Star filler and the all-time range earn none, and awards never feed trust, admission, indexability, or order.
5. **We are not the npm of skills.** No publish flow and no namespace ownership. GitHub is the registry of record. Artifact delivery may cache immutable signed bytes for installation. It never becomes the source of truth. Protocol work serves the skilld CLI and requires a dated ADR.
6. **We do not optimize engagement.** Sessions per week is not a goal. Time-to-install and digest usefulness are. No streaks, no notification escalation, no re-engagement campaigns beyond the digest the user asked for.
7. **We are not an enterprise product.** No private registries, SSO, seats, or compliance surface. Private GitHub Artifact delivery is transient and account scoped. The free posture cannot carry enterprise support obligations.
Expand Down
55 changes: 52 additions & 3 deletions app/components/BadgeEmbedControl.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<script setup lang="ts">
import type { SkillBadgeEmbedInput, SkillBadgeTheme } from '~~/shared/skill-badge'
import { skillBadgeEmbed, skillBadgeImagePath } from '~~/shared/skill-badge'
import type { TrendingAward } from '~~/shared/trending-award'
import { skillBadgeAwardWidth, skillBadgeEmbed, skillBadgeImagePath } from '~~/shared/skill-badge'
import { repoHubPath } from '~~/shared/skill-routes'
import { trendingAwardBadgeLabel, trendingAwardLabel } from '~~/shared/trending-award'

const props = defineProps<Pick<SkillBadgeEmbedInput, 'owner' | 'repo' | 'name' | 'registryPath'>>()
const props = defineProps<Pick<SkillBadgeEmbedInput, 'owner' | 'repo' | 'name' | 'registryPath'> & {
/** The award the badge would show. The award option appears only with one. */
trendingAward?: TrendingAward | null
}>()

const open = ref(false)
const showLabel = ref(true)
const showLikes = ref(false)
// On by default: an earned award is the usual reason to add the badge.
const showTrendingAward = ref(Boolean(props.trendingAward))
const previewTheme = ref<SkillBadgeTheme>('light')
const { copy, copied } = useClipboard()

Expand All @@ -21,12 +28,15 @@ const configuredInput = computed(() => ({
...badgeInput.value,
showLabel: showLabel.value,
showLikes: showLikes.value,
showTrendingAward: Boolean(props.trendingAward) && showTrendingAward.value,
}))
const minimalInput = computed(() => ({ ...badgeInput.value, showLabel: false }))
const minimalLightImage = computed(() => skillBadgeImagePath(minimalInput.value, 'light'))
const minimalDarkImage = computed(() => skillBadgeImagePath(minimalInput.value, 'dark'))
const previewImage = computed(() => skillBadgeImagePath(configuredInput.value, previewTheme.value))
const previewWidth = computed(() => (showLabel.value ? 153 : 81) + (showLikes.value ? 46 : 0))
const previewWidth = computed(() => (showLabel.value ? 153 : 81)
+ (showLikes.value ? 46 : 0)
+ (props.trendingAward && showTrendingAward.value ? skillBadgeAwardWidth(trendingAwardBadgeLabel(props.trendingAward)) : 0))
const targetLabel = computed(() => props.registryPath === repoHubPath(props.owner, props.repo)
? `${props.owner}/${props.repo}`
: `${props.owner}/${props.repo}/${props.name}`)
Expand Down Expand Up @@ -220,6 +230,45 @@ function copyEmbed(): void {
/>
</div>
</fieldset>

<fieldset
v-if="trendingAward"
class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4 py-3"
>
<legend class="sr-only">
Trending award
</legend>
<div>
<p class="font-mono text-sm text-highlighted" aria-hidden="true">
Trending award
</p>
<p class="mt-0.5 text-xs text-muted">
Add {{ trendingAwardLabel(trendingAward) }}.
</p>
</div>
<div class="flex gap-1" role="group" aria-label="Badge trending award">
<UButton
type="button"
label="Plain"
size="sm"
color="neutral"
:variant="showTrendingAward ? 'ghost' : 'soft'"
class="min-h-11"
:aria-pressed="!showTrendingAward"
@click="showTrendingAward = false"
/>
<UButton
type="button"
label="With award"
size="sm"
color="neutral"
:variant="showTrendingAward ? 'soft' : 'ghost'"
class="min-h-11"
:aria-pressed="showTrendingAward"
@click="showTrendingAward = true"
/>
</div>
</fieldset>
</div>
</template>

Expand Down
41 changes: 41 additions & 0 deletions docs/adr/0011-trending-awards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# ADR-0011: Trending awards

**Status:** Accepted
**Date:** 2026-10-06

## Context

The trending boards change every hour. A Skill that ranked third last week has no record of it
once it leaves the board. Maintainers ask for proof they can keep, and a README badge that links
back to the Skill page is the channel VISION already names: the install command spreading
through maintainers' own READMEs.

VISION names badges as something the growth-hacking Skill author wants. The concern there is a
badge a maintainer can farm or buy. A trending award repeats a claim the board already made in
public, with the post or the star surge that put the row there.

## Decision

Record a trending award for every evidenced row on the `week` and `month` boards.

- An evidenced row has a post that named the Skill or a star surge on a repository holding
exactly one Skill. Star filler earns nothing. The `all` range earns nothing, because it ranks
by lifetime stars.
- An award is keyed by Skill, board, and calendar period in UTC: the ISO week starting Monday,
or the month. It keeps the best rank the board showed during that period.
- `record-trending-awards` samples the boards hourly. It never removes a row and never worsens
a rank.
- The Skill page shows the best award beside the heading. The README badge adds it on request
with `?trending=1`. Both always show the period, because an undated rank reads as current.
- Awards are display only. They never feed trust, admission, indexability, or ordering.

## Consequences

The award inherits the board's evidence. If the board ranking changes, past awards keep the rank
the old board showed, which is what readers saw at the time.

Awards are separate from the SEO admission experiment in `trending-admission.ts`. Ending that
experiment does not end awards.

Cull path: drop the `record-trending-awards` task, remove the chip and the badge option, then
drop `skill_trending_awards`.
3 changes: 2 additions & 1 deletion docs/arch/cron.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Generated from literal `defineScheduledTask` declarations. Run `pnpm cron:docs` after schedule changes.

Tasks: 21. Unique Cloudflare triggers: 14.
Tasks: 22. Unique Cloudflare triggers: 14.

| Task | Cron | Source |
| --- | --- | --- |
Expand All @@ -18,6 +18,7 @@ Tasks: 21. Unique Cloudflare triggers: 14.
| `purge-personal-data` | `30 4 * * *` | `layers/identity/server/tasks/purge-personal-data.ts` |
| `recompute-skill-scores` | `0 3 * * *` | `layers/registry/server/tasks/recompute-skill-scores.ts` |
| `reconcile-rendered` | `20 */6 * * *` | `layers/registry/server/tasks/reconcile-rendered.ts` |
| `record-trending-awards` | `0 * * * *` | `layers/registry/server/tasks/record-trending-awards.ts` |
| `refresh-x-engagement` | `10 * * * *` | `layers/registry/server/tasks/refresh-x-engagement.ts` |
| `scheduled-cadence-watchdog` | `*/5 * * * *` | `server/tasks/scheduled-cadence-watchdog.ts` |
| `send-digests` | `0 9 1 * *` | `layers/identity/server/tasks/send-digests.ts` |
Expand Down
30 changes: 28 additions & 2 deletions layers/registry/app/components/SkillDetail.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<script setup lang="ts">
import type { SkillAudit } from '~~/app/utils/skill-audit-overview'
import type { TrendingAward } from '#shared/trending-award'
import type { ZipState } from '../utils/skill-zip'
import type { SkillBehavior } from './_SkillBehaviors.vue'
import { formatTimeAgo } from '@vueuse/core'
import { resolveSkillAuditOverview } from '~~/app/utils/skill-audit-overview'
import { comparisonLinkForSkill } from '#shared/comparison-navigation'
import { avatarProxyUrl, githubAvatarProxyUrl } from '#shared/image-proxy'
import { skillPageUrl as exactSkillPageUrl, skillInstallCmd, skillRunCmd, skillRunPrompt } from '#shared/skill-commands'
import { headlineTrendingAward, trendingAwardBadgeLabel, trendingAwardLabel, trendingAwardPath } from '#shared/trending-award'
import { behaviorIcon } from '../utils/skill-behaviors'
import { formatByteSize, formatTokenCount, resolveSkillContextCost, resolveSkillFileContext } from '../utils/skill-context-cost'
import { fileIcon, highlightLangFromPath } from '../utils/skill-file-tree'
Expand Down Expand Up @@ -201,6 +203,8 @@ const skillFetch = useFetch(
tags: SkillTag[]
keywords: string[]
likeCount: number
/** Best rank first. Optional while an edge-cached payload predates it. */
trendingAwards?: TrendingAward[]
faqs: FaqItem[]
summary: SkillSummary | null
dependencies: string[]
Expand Down Expand Up @@ -675,6 +679,7 @@ useResizeObserver(descriptionEl, () => {
descriptionClamped.value = el.scrollHeight > el.clientHeight + 1
})
const shortSha = computed(() => data.value?.provenance?.sourceCommitSha?.slice(0, 7) ?? null)
const trendingAward = computed(() => headlineTrendingAward(data.value?.trendingAwards ?? []))

// Frontmatter licences are free text ("Proprietary. LICENSE.txt has complete
// terms"). The chip keeps the first clause; the title carries the rest.
Expand Down Expand Up @@ -1406,6 +1411,16 @@ useHead(computed(() => ({
/>
official
</span>
<NuxtLink
v-if="trendingAward"
:to="trendingAwardPath(trendingAward)"
class="skill-award"
:aria-label="trendingAwardLabel(trendingAward)"
:title="trendingAwardLabel(trendingAward)"
>
<TrendingMark />
{{ trendingAwardBadgeLabel(trendingAward) }}
</NuxtLink>
</div>
<div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted">
<span>
Expand Down Expand Up @@ -2499,7 +2514,10 @@ useHead(computed(() => ({
<h2 id="readme-badge-heading" class="sr-only">
README badge
</h2>
<BadgeEmbedControl v-bind="badgeInput" />
<BadgeEmbedControl
v-bind="badgeInput"
:trending-award="trendingAward"
/>
</section>
</div>

Expand Down Expand Up @@ -2699,7 +2717,8 @@ useHead(computed(() => ({
}
/* The solid xs badge set 10px text on rose. This keeps the accent and reads
at chip size. */
.skill-official {
.skill-official,
.skill-award {
display: inline-flex;
align-items: center;
gap: 0.25rem;
Expand All @@ -2711,6 +2730,13 @@ useHead(computed(() => ({
line-height: 1.25rem;
color: var(--syntax-arg);
}
.skill-award {
font-variant-numeric: tabular-nums;
transition: border-color 150ms ease;
}
.skill-award:hover {
border-color: var(--syntax-arg);
}
.skill-sha {
color: var(--syntax-arg);
}
Expand Down
7 changes: 6 additions & 1 deletion layers/registry/server/api/skills/[...slug].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { parseSkillMd } from '../../utils/skill-md-render'
import { findDuplicateGroupForSkill, findSkillWithRow } from '../../utils/skills-registry'
import { tagLinkPath } from '../../utils/tag-quality'
import { isSkillIndexable, noteAdmissionFallback, SKILL_INDEX_INPUT_COLUMNS_SQL } from '../../utils/trending-admission'
import { parseSkillTrendingAwards, SKILL_TRENDING_AWARDS_SQL } from '../../utils/trending-awards'
import { fetchUpstreamText } from '../../utils/upstream-text'

interface FaqPayload { faqs: { question: string, answer: string }[] }
Expand Down Expand Up @@ -146,7 +147,8 @@ const DETAIL_COLUMNS_SQL = `r.forks, r.repo_created_at,
s.rendered_status, s.rendered_raw, s.rendered_frontmatter, s.rendered_html,
(SELECT sr.sha FROM skill_revisions sr
WHERE sr.owner = s.owner AND sr.repo = s.repo AND sr.name = s.name
ORDER BY sr.modified_at DESC LIMIT 1) AS latest_revision_sha`
ORDER BY sr.modified_at DESC LIMIT 1) AS latest_revision_sha,
${SKILL_TRENDING_AWARDS_SQL}`

interface SkillDetailRow {
// repo meta
Expand Down Expand Up @@ -177,6 +179,7 @@ interface SkillDetailRow {
probe_exception: number | null
admissions_populated: number | null
repo_kind: string | null
trending_awards: string | null
seo_index_reasons: string | null
seo_index_synced_at: number | null
curator_count: number | null
Expand Down Expand Up @@ -428,6 +431,8 @@ async function loadSkillDetail(event: H3Event, platform: Platform, slug: string)
// Deliberately top-level, not under `seo`: likes are displayed and back
// ?sort=likes, but never feed indexability or trust (ADR-0003).
likeCount: row.like_count ?? 0,
// Best rank first. Display only, like likes: never trust or indexability (ADR-0011).
trendingAwards: parseSkillTrendingAwards(row.trending_awards),
faqs: faqRow?.payload.faqs ?? [],
summary: summaryRow?.payload?.text
? { text: summaryRow.payload.text }
Expand Down
Loading