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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[]
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -246,6 +247,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<Sea
{r.owner}/{r.name}
</Link>
<HostPill host={r.host} />
{r.badge_embedded ? <BadgeAdoptedTag /> : null}
</td>
<td className="text-right tabular-nums text-ink-dim">{compactStars(r.stars)}</td>
<td className="text-right">
Expand Down
13 changes: 13 additions & 0 deletions components/BadgeAdoptedTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CheckCircle } from "@phosphor-icons/react/dist/ssr";

export function BadgeAdoptedTag() {
return (
<span
title="This repo embeds its Agent Friendly Code badge in its README"
className="ml-2 inline-flex items-center gap-1 rounded border border-ok/40 bg-ok/10 px-2 py-px align-middle text-[10.5px] font-semibold uppercase tracking-wider text-ok"
>
<CheckCircle size={12} weight="fill" aria-hidden="true" />
Badge
</span>
);
}
2 changes: 2 additions & 0 deletions components/RepoHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,6 +21,7 @@ export function RepoHero({ repo }: { repo: RepoRow }) {
<h1 className="m-0 break-words text-[20px] font-semibold tracking-tight sm:text-[22px]">
{repo.owner}/{repo.name}
<HostPill host={repo.host} />
{repo.badge_embedded ? <BadgeAdoptedTag /> : null}
</h1>

<a
Expand Down
Binary file modified data/rank.db
Binary file not shown.
12 changes: 12 additions & 0 deletions lib/badge-adoption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { firstExisting, readSafe } from "./scoring/signals/helpers";

const README_CANDIDATES = ["README.md", "README.rst", "README.txt", "README"];

export function detectBadgeEmbed(repoPath: string, slug: string): boolean {
const p = firstExisting(repoPath, README_CANDIDATES);
if (!p) {
return false;
}

return readSafe(p).includes(`/api/badge/${slug}`);
}
10 changes: 7 additions & 3 deletions lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ db.exec(`
overall_score REAL,
previous_overall_score REAL,
language TEXT,
badge_embedded INTEGER,
UNIQUE(host, owner, name)
);
CREATE TABLE IF NOT EXISTS model_score (
Expand Down Expand Up @@ -73,22 +74,24 @@ export function saveScoredRepo(args: {
owner: string;
overall: number;
stars?: number | null;
badgeEmbedded?: boolean;
signals: SignalResult[];
language?: string | null;
modelScores: ModelScore[];
defaultBranch?: string | null;
}): number {
const tx = db.transaction(() => {
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,
Expand All @@ -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 };
Expand Down
1 change: 1 addition & 0 deletions lib/types/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions scripts/score.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -61,12 +62,14 @@ async function scoreCommand(target: string): Promise<void> {

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,
Expand Down
77 changes: 77 additions & 0 deletions scripts/seed-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
{
Expand Down Expand Up @@ -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 ---
{
Expand Down Expand Up @@ -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 ---
{
Expand Down Expand Up @@ -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 ---
{
Expand All @@ -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) ---
{
Expand All @@ -317,13 +372,19 @@ 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" },
{
note: "Vapor — Swift web framework",
url: "https://github.com/vapor/vapor",
},
{ url: "https://github.com/Alamofire/Alamofire", note: "Alamofire — Swift HTTP networking" },

// --- GitHub, Ruby ---
{
Expand Down Expand Up @@ -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" },
Expand Down Expand Up @@ -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 ---
{
Expand Down
63 changes: 63 additions & 0 deletions tests/badge-adoption.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading