diff --git a/AGENTS.md b/AGENTS.md index 15b2040..a5fc38c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ components/ # Tailwind-styled React components RepoHero.tsx, ScoreDeltaPopover.tsx, SignalListCard.tsx, ModelSuggestions.tsx, PerModelScores.tsx, AlternativesStrip.tsx, BreadcrumbJsonLd.tsx, HomeJsonLd.tsx, ExternalLink.tsx, BadgeEmbed.tsx, ActionEmbed.tsx, PeerlistCard.tsx, CopySnippet.tsx, PackageLookupForm.tsx, - BackToTop.tsx, GoogleAnalytics.tsx + BadgeAdoptedTag.tsx, BackToTop.tsx, GoogleAnalytics.tsx lib/ constants/ scoring.ts # score thresholds, visible limits @@ -93,6 +93,7 @@ lib/ types/ db.ts # shared row-shape types for lib/db.ts (RepoRow, LeaderboardRow, …) package-lookup.ts # shared registry → repo lookup (used by /api/package + /package page) + badge-adoption.ts # detectBadgeEmbed — reads the cloned README for an embedded AFC badge (dashboard metadata, NOT a scored signal; never vendored to siblings) db.ts # better-sqlite3 schema + queries version.ts # APP_NAME, APP_VERSION, IS_PRE_RELEASE, APP_URL, APP_DESCRIPTION, REPO_URL, SIBLING_VERSION, ACTION_REPO_URL, ACTION_USES, SKILL_REPO_URL, SKILL_INSTALL_CMD, OG_DEFAULTS, TWITTER_DEFAULTS (spread into per-page openGraph / twitter — Next.js shallow-merges these objects so defaults must be re-spread on every page) changelog.ts # typed ChangelogEntry[] @@ -105,6 +106,7 @@ tests/ format.test.ts # compactStars, relativeTime, hostLabel parse-repo-url.test.ts # GH / GL / BB parsing + edge cases scorer.test.ts # scoreRepo, topImprovements + badge-adoption.test.ts # detectBadgeEmbed — README badge-embed detection signals/ # one *.test.ts per signal tasks/ README.md diff --git a/app/page.tsx b/app/page.tsx index 3200f6a..0ade962 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,6 +2,7 @@ import { ArrowUpRight } from "@phosphor-icons/react/dist/ssr"; import type { Metadata } from "next"; import { headers } from "next/headers"; import Link from "next/link"; +import { BadgeAdoptedTag } from "@/components/BadgeAdoptedTag"; import { HomeJsonLd } from "@/components/HomeJsonLd"; import { HostPill } from "@/components/HostPill"; import { HostSelect } from "@/components/HostSelect"; @@ -246,6 +247,7 @@ export default async function Page({ searchParams }: { searchParams: Promise + {r.badge_embedded ? : null} {compactStars(r.stars)} diff --git a/components/BadgeAdoptedTag.tsx b/components/BadgeAdoptedTag.tsx new file mode 100644 index 0000000..71bdeb0 --- /dev/null +++ b/components/BadgeAdoptedTag.tsx @@ -0,0 +1,13 @@ +import { CheckCircle } from "@phosphor-icons/react/dist/ssr"; + +export function BadgeAdoptedTag() { + return ( + + + ); +} diff --git a/components/RepoHero.tsx b/components/RepoHero.tsx index 5aa3839..5e91255 100644 --- a/components/RepoHero.tsx +++ b/components/RepoHero.tsx @@ -2,6 +2,7 @@ import type { RepoRow } from "@/lib/types/db"; import { compactStars, relativeTime } from "@/lib/utils/format"; import { scoreTier, TIER_TEXT_CLASS } from "@/lib/utils/score"; +import { BadgeAdoptedTag } from "./BadgeAdoptedTag"; import { HostPill } from "./HostPill"; import { Panel } from "./Panel"; import { ScoreDeltaPopover } from "./ScoreDeltaPopover"; @@ -20,6 +21,7 @@ export function RepoHero({ repo }: { repo: RepoRow }) {

{repo.owner}/{repo.name} + {repo.badge_embedded ? : null}

{ db.prepare( - `INSERT INTO repo (host, owner, name, url, default_branch, stars, last_scored_at, overall_score, language) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `INSERT INTO repo (host, owner, name, url, default_branch, stars, last_scored_at, overall_score, language, badge_embedded) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(url) DO UPDATE SET default_branch = excluded.default_branch, stars = excluded.stars, last_scored_at = excluded.last_scored_at, previous_overall_score = repo.overall_score, overall_score = excluded.overall_score, - language = COALESCE(excluded.language, repo.language)`, + language = COALESCE(excluded.language, repo.language), + badge_embedded = excluded.badge_embedded`, ).run( args.host, args.owner, @@ -99,6 +102,7 @@ export function saveScoredRepo(args: { Math.floor(Date.now() / 1000), args.overall, args.language ?? null, + args.badgeEmbedded ? 1 : 0, ); const row = db.prepare("SELECT id FROM repo WHERE url = ?").get(args.url) as { id: number }; diff --git a/lib/types/db.ts b/lib/types/db.ts index 7d88579..61666dc 100644 --- a/lib/types/db.ts +++ b/lib/types/db.ts @@ -7,6 +7,7 @@ export type RepoRow = { stars: number | null; language: string | null; overall_score: number | null; + badge_embedded: number | null; default_branch: string | null; last_scored_at: number | null; previous_overall_score: number | null; diff --git a/scripts/score.ts b/scripts/score.ts index 45f5a27..867de86 100644 --- a/scripts/score.ts +++ b/scripts/score.ts @@ -1,6 +1,7 @@ import { existsSync, mkdirSync, statSync } from "node:fs"; import { join } from "node:path"; +import { detectBadgeEmbed } from "../lib/badge-adoption"; import { shallowClone } from "../lib/clients/git"; import { fetchRepoMeta, parseRepoUrl } from "../lib/clients/github"; import { saveScoredRepo } from "../lib/db"; @@ -61,12 +62,14 @@ async function scoreCommand(target: string): Promise { console.log(`[score] scanning ${repoPath}`); const result = scoreRepo(repoPath); + const badgeEmbedded = detectBadgeEmbed(repoPath, `${host}/${owner}/${name}`); saveScoredRepo({ url, host, name, owner, + badgeEmbedded, stars: stars ?? null, overall: result.overall, signals: result.signals, diff --git a/scripts/seed-list.ts b/scripts/seed-list.ts index 76c80b8..4a5764c 100644 --- a/scripts/seed-list.ts +++ b/scripts/seed-list.ts @@ -119,6 +119,26 @@ export const SEEDS: Seed[] = [ url: "https://github.com/payloadcms/payload", }, { url: "https://github.com/strapi/strapi", note: "Strapi — Node.js headless CMS" }, + { url: "https://github.com/solidjs/solid", note: "Solid — reactive UI library" }, + { url: "https://github.com/preactjs/preact", note: "Preact — 3kB React alternative" }, + { url: "https://github.com/mui/material-ui", note: "MUI — React component library" }, + { + url: "https://github.com/chakra-ui/chakra-ui", + note: "Chakra UI — accessible React component library", + }, + { url: "https://github.com/nrwl/nx", note: "Nx — smart monorepo build system" }, + { + url: "https://github.com/directus/directus", + note: "Directus — headless CMS + data platform", + }, + { + url: "https://github.com/medusajs/medusa", + note: "Medusa — open-source commerce platform", + }, + { + url: "https://github.com/novuhq/novu", + note: "Novu — open-source notification infrastructure", + }, // --- GitHub, Python --- { @@ -177,6 +197,15 @@ export const SEEDS: Seed[] = [ url: "https://github.com/sqlfluff/sqlfluff", note: "SQLFluff — multi-dialect SQL linter / formatter", }, + { url: "https://github.com/encode/httpx", note: "HTTPX — next-gen Python HTTP client" }, + { url: "https://github.com/encode/starlette", note: "Starlette — lightweight ASGI framework" }, + { url: "https://github.com/celery/celery", note: "Celery — distributed task queue" }, + { url: "https://github.com/scrapy/scrapy", note: "Scrapy — web crawling framework" }, + { + url: "https://github.com/python-poetry/poetry", + note: "Poetry — Python packaging & dependency manager", + }, + { url: "https://github.com/mkdocs/mkdocs", note: "MkDocs — Markdown project documentation" }, // --- GitHub, Rust --- { @@ -226,6 +255,19 @@ export const SEEDS: Seed[] = [ note: "reqwest — ergonomic Rust HTTP client", url: "https://github.com/seanmonstar/reqwest", }, + { url: "https://github.com/clap-rs/clap", note: "clap — Rust command-line argument parser" }, + { + url: "https://github.com/alacritty/alacritty", + note: "Alacritty — GPU-accelerated terminal emulator", + }, + { + url: "https://github.com/surrealdb/surrealdb", + note: "SurrealDB — multi-model database (Rust)", + }, + { + url: "https://github.com/rustdesk/rustdesk", + note: "RustDesk — open-source remote desktop", + }, // --- GitHub, Go --- { @@ -269,6 +311,16 @@ export const SEEDS: Seed[] = [ url: "https://github.com/charmbracelet/bubbletea", note: "Bubble Tea — Go TUI framework based on The Elm Architecture", }, + { + url: "https://github.com/caddyserver/caddy", + note: "Caddy — web server with automatic HTTPS", + }, + { + url: "https://github.com/traefik/traefik", + note: "Traefik — cloud-native reverse proxy / load balancer", + }, + { url: "https://github.com/minio/minio", note: "MinIO — high-performance object storage" }, + { url: "https://github.com/go-gorm/gorm", note: "GORM — Go ORM library" }, // --- GitHub, C / C++ / systems --- { @@ -295,6 +347,9 @@ export const SEEDS: Seed[] = [ { url: "https://github.com/postgres/postgres", note: "PostgreSQL — relational database (mirror)" }, { url: "https://github.com/duckdb/duckdb", note: "DuckDB — in-process analytical database" }, { url: "https://github.com/ml-explore/mlx", note: "MLX — Apple's array framework for ML on Apple silicon" }, + { url: "https://github.com/fmtlib/fmt", note: "fmt — modern C++ formatting library" }, + { url: "https://github.com/nlohmann/json", note: "JSON for Modern C++" }, + { url: "https://github.com/opencv/opencv", note: "OpenCV — computer vision library" }, // --- GitHub, JVM (Java / Kotlin) --- { @@ -317,6 +372,11 @@ export const SEEDS: Seed[] = [ url: "https://github.com/Anuken/Mindustry", note: "Mindustry — open-source factory / tower-defense game (Java + libGDX)", }, + { url: "https://github.com/square/okhttp", note: "OkHttp — HTTP client for JVM / Android" }, + { + url: "https://github.com/netty/netty", + note: "Netty — async event-driven network framework", + }, // --- GitHub, Swift --- { url: "https://github.com/apple/swift", note: "Swift language" }, @@ -324,6 +384,7 @@ export const SEEDS: Seed[] = [ note: "Vapor — Swift web framework", url: "https://github.com/vapor/vapor", }, + { url: "https://github.com/Alamofire/Alamofire", note: "Alamofire — Swift HTTP networking" }, // --- GitHub, Ruby --- { @@ -368,6 +429,10 @@ export const SEEDS: Seed[] = [ url: "https://github.com/jellyfin/jellyfin", note: "Jellyfin — open-source media server (.NET)", }, + { + url: "https://github.com/PowerShell/PowerShell", + note: "PowerShell — cross-platform shell + scripting (.NET)", + }, // --- GitHub, PHP --- { url: "https://github.com/laravel/laravel", note: "Laravel — PHP web framework starter" }, @@ -467,6 +532,18 @@ export const SEEDS: Seed[] = [ url: "https://github.com/earendil-works/pi", note: "Pi — self-extensible coding agent CLI + unified multi-provider LLM API (TypeScript)", }, + { + url: "https://github.com/block/goose", + note: "Goose — open-source on-machine AI coding agent", + }, + { + url: "https://github.com/cline/cline", + note: "Cline — autonomous coding agent for VS Code", + }, + { + url: "https://github.com/continuedev/continue", + note: "Continue — open-source AI code assistant for IDEs", + }, // --- AI-native: models + infra --- { diff --git a/tests/badge-adoption.test.ts b/tests/badge-adoption.test.ts new file mode 100644 index 0000000..10d351d --- /dev/null +++ b/tests/badge-adoption.test.ts @@ -0,0 +1,63 @@ +import { strict as assert } from "node:assert"; +import { afterEach, describe, test } from "node:test"; + +import { detectBadgeEmbed } from "../lib/badge-adoption"; +import { makeFixture, removeFixture } from "./_helpers"; + +const SLUG = "github/honojs/hono"; + +describe("detectBadgeEmbed", () => { + let fixture = ""; + + afterEach(() => { + if (fixture) { + removeFixture(fixture); + fixture = ""; + } + }); + + test("false when no README exists", () => { + fixture = makeFixture({ "AGENTS.md": "unrelated" }); + assert.equal(detectBadgeEmbed(fixture, SLUG), false); + }); + + test("false when the README has no AFC badge", () => { + fixture = makeFixture({ + "README.md": "[![CI](https://example.com/ci.svg)](https://example.com)", + }); + + assert.equal(detectBadgeEmbed(fixture, SLUG), false); + }); + + test("true when the README embeds this repo's badge endpoint", () => { + fixture = makeFixture({ + "README.md": `# hono\n![Agent Friendly](https://agent-friendly-code.vercel.app/api/badge/${SLUG}.svg)`, + }); + + assert.equal(detectBadgeEmbed(fixture, SLUG), true); + }); + + test("host-agnostic — matches the endpoint path regardless of domain", () => { + fixture = makeFixture({ + "README.md": `![badge](https://afc.example.dev/api/badge/${SLUG}.svg?model=claude-code)`, + }); + + assert.equal(detectBadgeEmbed(fixture, SLUG), true); + }); + + test("false when the badge belongs to a different repo slug", () => { + fixture = makeFixture({ + "README.md": "![badge](https://agent-friendly-code.vercel.app/api/badge/github/other/repo.svg)", + }); + + assert.equal(detectBadgeEmbed(fixture, SLUG), false); + }); + + test("reads alternative README filenames", () => { + fixture = makeFixture({ + "README.rst": `.. image:: https://agent-friendly-code.vercel.app/api/badge/${SLUG}.svg`, + }); + + assert.equal(detectBadgeEmbed(fixture, SLUG), true); + }); +});