All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.4.0 - 2026-09-15
- Tier thresholds are more generous. The overall-score lower bound of every
tier drops by a few points — Maru 90 → 87, Master 80 → 76, Diamond 70 → 65,
Platinum 58 → 53, Gold 45 → 40, Silver 30 → 26, Bronze 15 → 12 (Seed stays 0).
The axis scores, the overall number and the confidence caps are untouched, so
the radar and the printed score do not move; only the band a given score
lands in does. The worked example in
docs/SCORING.md(overall 53.0) moves from Gold to Platinum.SCORE_VERSIONis bumped 0.4.0 → 0.5.0, so every cached summary is rebuilt on first request.
1.3.0 - 2026-09-03
- Opening a card URL directly no longer drops the tier nameplate. The card
SVG's
default-src 'none'CSP had noimg-src, so the nameplate — an embedded base64 PNG — was blocked whenever the SVG was loaded straight in a browser tab. The policy now allowsimg-src data:, mirroring GitHub Camo's own SVG policy. - A degraded card recovers in a minute instead of a day. A stale-fallback
copy kept the full
CACHE_TTL_SECONDSin the app cache (its restoredoverallStatusisok, so only thestaleflag gives it away), and every card — degraded or not — was sents-maxage=3600, stale-while-revalidate=86400at the CDN, so an outage stayed pinned at the edge long after the platform came back. Stale and partial summaries now get the short negative TTL plusmax-age=60ands-maxage=60, stale-while-revalidate=300. - A failed GitHub repository page no longer wipes out the whole profile. A
timeout or network error on page 2+ discarded the already-fetched first page,
degrading GitHub to
unavailable— a Seed card for an active account. The failure now keeps page 1 (followers, contributions, top repos) and degrades topartialwith a note. Follow-up pages also get a tighter timeout — the lesser of 3s (MAX_FOLLOWUP_PAGE_TIMEOUT) and what is left of the build deadline, and none is started with under a second to go — since their repos-only query is light. - GitHub API failures are no longer cached as "user not found". A non-200
first GraphQL page — an expired token (401), a rate limit (403) or a GitHub
outage (5xx) — was noted
user not found, and that note carries the 10-minuteNOT_FOUND_CACHE_TTL_SECONDS, so one outage pinned every handle requested during it as a missing user. Such failures are now notedhttp <status>and keep the 60s negative TTL; only a real "no such user" (HTTP 200 withdata.user: null) earns the long TTL. - Generator: an animation toggle. The API (
animate=false), the CLI (--no-animate) and the Action (animate: false) all supported it, but the generator page had no control for it; it now has one, wired to the preview, the URL and every snippet. - The error card honours
compact. It always rendered at 640×300, so a compact embed — which reserves 250×270 — blew out the README layout whenever anything failed. /api/card.svgnever returns a 500. Any unexpected exception (an adapter bug, the KV client, a renderer edge case) surfaced as a FastAPI 500, which is a broken image in someone's README. It now returns the visible error card (HTTP 200,no-store) and logs the traceback as acard_errorevent, so the branch can't swallow real bugs silently./docsand/redocrender again. The generator page'sscript-src 'self'CSP applied to them too, and Swagger UI / ReDoc load their bundles from jsDelivr, so both pages came up blank. Those paths now get a docs-only policy that allows jsDelivr, Google Fonts (ReDoc pulls its text fonts from there) and inline scripts, which FastAPI's Swagger UI page needs to boot and offers no nonce hook for. The generator page and the card endpoints keep the strict policy.- Restored
sync-requirements.ymland CI's Dependabot skip guard, both deleted by accident. Dependabot'suvPRs bumppyproject.toml/uv.lockbut can't regenerate the derivedrequirements.txt, so every one of them failed the drift check. - GitHub GraphQL errors are no longer cached as "user not found". An HTTP 200
with a top-level
errorsarray (rate limiting, authorization failures) and nodata.usernow gets the short negative TTL; only aNOT_FOUNDerror keeps the long not-found TTL. - JungOl: a missing or malformed solved history marks the snapshot
partialinstead of anoksnapshot with zero solves that could be cached for the full TTL and overwrite the last-good copy.
- Card text is outlined from pre-instanced static fonts. Instancing a
variable font costs ~200 ms per (family, weight) and a card needs five of them,
so a cold serverless start spent about a second inside fontTools before drawing
a single glyph. The instances are now built at development time and shipped in
the package (
codemaru/render/assets/fonts/<Family>-<weight>.ttf, regenerated withuv run python scripts/instance_fonts.py): a first render on a fresh process drops from ~940 ms to ~25 ms. Saving a TTF rounds outline coordinates to integers (glyfstores int16) — at most half a font unit, invisible at card scale — and the shorter path data makes cards ~27% smaller. A weight nobody pre-instanced still falls back to instancing at request time, so the variable fonts stay shipped. - The
<picture>snippet follows the reader's GitHub theme. It now pairs a(prefers-color-scheme: dark)<source>(theme=dark) with the selected theme on the<img>fallback. Pickingdarkin the generator makes the<img>fall back todefault, so the pair really is light/dark;transparentstays as chosen, since it suits either scheme. - Generator polish. The handle inputs carry mobile keyboard attributes
(
inputmode, autocapitalize / autocorrect / spellcheck off), the address bar is kept in sync with the form viahistory.replaceStateso a reload or a shared link reproduces what's on screen, and the logo asset went from 265 KB to 22 KB. InMemoryCacheis bounded at 1024 entries. A serverless instance can be reused for hours and the cache is keyed by profile, so an unbounded dict grew with every distinct handle ever requested on it. Eviction drops everything already expired first, then the oldest writes (FIFO, no per-read bookkeeping)./api/healthreports the deploy facts needed to diagnose a bad environment:version, whetherkvandgithubTokenare configured (never their values), andcacheEntries.?deep=trueadds a KVPINGround-trip askvPing— opt-in, so a KV outage doesn't page anyone while cards render fine without it.- A GitHub handle that doesn't exist is cached for
NOT_FOUND_CACHE_TTL_SECONDS(default 600) instead of 60s: it isn't a transient failure, and re-asking the GraphQL API every minute only burns quota. - Judge platforms are a registry. One row per judge in
codemaru/adapters/registry.pycarries its snapshot key, query param, label, confidence trust / saturation / weight and whether it shares the httpx client; scoring, confidence, the summary builder, the service, the query parser, the snippets and the CLI iterate it instead of naming judges, so adding one is a row plus an adapter. Tier bands shared by judges on the solved.ac scale moved toadapters/tiers.py, and snapshots expose a commonJudgeView. Confidence weights stay additive and are never renormalized — sharing a fixed budget would lower existing users' cards whenever a judge is added. The refactor changes no score by itself;SCORE_VERSIONmoves to0.4.0with the JungOl entry below. - A whole-card build budget (
CARD_BUILD_TIMEOUT_SECONDS, default 6s).ADAPTER_TIMEOUT_SECONDSbounds one request, but GitHub paginates sequentially, so the worst case ran well past a serverless function's limit — and a function killed by the platform returns no error card and writes no negative cache entry. The fetch phase now runs under one ceiling: the platforms that finished are kept, the rest are cancelled and markedunavailablewith a "timed out (card build budget)" note (the card renders aspartial), and GitHub stops requesting repository pages cooperatively before the deadline instead of losing the pages it already paid for. The budget covers the fetch phase only, so it is sized against the KV calls bracketing it —KV_TIMEOUT_SECONDS + CARD_BUILD_TIMEOUT_SECONDS + 2 * KV_TIMEOUT_SECONDS, 9s at the defaults — which has to stay under the function timeout. - Structured JSON logging on stdout (
codemaru/telemetry.py): one line peradapter,build,cache,kv_errorandcard_errorevent, so "this card is slow" becomes a query overevent/platform/msinstead of a guess. Only public handles are logged, and a caught exception contributes its class name rather than its message, which can carry a credentialed KV URL. The one deliberate exception is the card route's catch-all, which logs a full traceback because nothing else would ever surface that bug.configure_logging()always sets thecodemarulogger toINFO, even on a host that already configured root logging (uvicorn, pytest, Vercel's runtime) — only the stdout handler is conditional on that. - The GitHub Action installs with
uv(astral-sh/setup-uv) instead ofpip; the install step runs on every card refresh in every consumer's repo. - Removed dead code:
LIVE_ADAPTERS_AVAILABLE,LiveDataUnavailableError, the unusedREDIS_URLsetting,render/fonts.pyandDEFAULT_RENDER_OPTIONS.
-
JungOl (정올) as a third judge. Pass
jungol=<handle>to/api/card.svgand/api/summary.json,--jungolto the CLI, orjungol:to the Action; the generator has a matching field. The adapter reads the__data.jsonthat JungOl's own SvelteKit pages already serve — two GETs, no login, no HTML parsing and no new dependency — and decodes its devalue-flattened payload with a small pure function tested against saved live payloads. Any failure (HTTP error, timeout, schema drift, an oversized body) degrades tounavailablelike every other adapter.JungOl uses the same 0–30 tier scale as solved.ac, so difficulty bands and tier names carry over; its rating signal is scaled by 0.80 because its problem pool is much smaller. The card shows one tier row, never two:
BOJ Tierwhen solved.ac has data,JungOl Tierotherwise. Solved counts fold into the existing combinedSolvedmetric.Additive throughout — solved counts are summed, rating evidence is a max, and the confidence weight (trust 0.60, saturation 900, weight 0.10) is added rather than carved out of the other judges' — so linking JungOl can never lower an existing card, and
snapshots.jungol/input.jungolare new keys on a wire format that is otherwise unchanged.SCORE_VERSIONmoves to0.4.0, which expires every cache entry once (the new handle segment in the cache key does that anyway). -
Documented why Programmers, CodeTree and SWEA are not collected (
docs/SCORING.md): none exposes a public profile or API without logging in, and codemaru will not ask anyone for platform credentials. -
Two new workflows.
major-tag.ymlforce-moves the floatingv1tag onto every published release, souses: bnbong/codemaru@v1— what the README and the generator tell people to write — actually resolves; only plainvX.Y.Zreleases move it, and the initialv1has to be created once by hand (see CONTRIBUTING).action-smoke.ymlruns the composite action straight from the checkout on every PR that touches it, so a brokenaction.ymlsurfaces here instead of in a consumer's repo after a release. -
Golden-digest render tests. A render is byte-deterministic, so a SHA-256 per demo variant pins layout, palette, animation CSS and font loading; after an intentional visual change, eyeball the SVG and refresh the digests with
uv run python -m tests.render.golden --update. The suite also fails when the shipped static font instances drift from a fresh build (scripts/instance_fonts.py --check).
1.2.1 - 2026-06-12
- Summary cache is shared across instances via Vercel KV (Upstash Redis).
When
KV_REST_API_URL/KV_REST_API_TOKENare set, computed summaries and the stale-fallback store live in Redis instead of per-instance memory, so a cold serverless instance reuses a warm cache and skips the live fetch — cutting the first-view "broken image until refresh" timeout. Best-effort: missing creds (local / CI) or any KV error transparently falls back to the in-memory cache and never affects rendering — and every KV write is mirrored locally, so a transient KV read outage or a remote miss (e.g. a dropped write / eviction) still serves a warm instance from the mirror instead of rebuilding every request. Reuses the same KV store as adoption tracking (disjoint key namespaces). Cache keys are scoped by deploy env (VERCEL_ENV) and mode (fixture/live) so a preview deploy or fixture data can't pollute production, and a cached payload that fails to deserialize (e.g. written by a different deploy's schema) is treated as a miss and rebuilt rather than surfacing a 500. - Measured impact. Local CPU work is not the bottleneck (
build_summaryis about 0.05 ms,render_cardabout 0.77 ms, and cache JSON parse/store stays below 0.02 ms); the expensive path is live platform fetches. In observed live requests, GitHub-dominated fetches took roughly 2.0-5.5 s, while a warm shared KV hit only needs a small JSON payload read plus parsing (typically tens of ms or less in-region). This mainly helps cold/new serverless instances serving an already-cached profile; CDN hits remain edge-fast, and a truly first-time cache miss still has to perform the live fetch.
1.2.0 - 2026-06-11
- Animated tier emblem. Cards now play a one-shot entrance animation: the
hex crest stamps in, then the score, the wing-rest and the wings swinging in
from each side, the crown spikes rising left-to-right, the apex gem dropping
in, and the nameplate wiping in. It's pure CSS
@keyframesembedded in the SVG, so it runs even when the card is an<img>in a README (no scripts). Nothing is hidden by base styles, so any renderer that ignores the stylesheet — or a viewer withprefers-reduced-motion— still gets the full static card. Opt out with?animate=false(API),--no-animate(CLI), or the Action'sanimate: falseinput. - Generator preview: replay + loading spinner. The preview now shows a spinner while a card (re)loads, and a Replay button re-runs the entrance animation without changing inputs.
1.1.1 - 2026-06-11
- Adoption tracking can no longer be spoofed or used to exhaust KV quota.
Card embeds are now recorded only when the request is from Camo and the
GitHub snapshot is real (not
unavailable), so a forgedUser-Agent: camoagainst non-existent handles can't inflate the badge. The distinct-user store moved from a Redis SET to a HyperLogLog (PFADD/PFCOUNT): a fixed ~12KB ceiling regardless of cardinality, so a flood can't grow storage without bound. (The badge count restarts from 0 on a new key — expected for a HLL switch.) - Cloudflare-proxy bypass guard. An optional
ORIGIN_SHARED_SECRETmakes the app reject any request that lacks a matchingX-Origin-Authheader (injected by a Cloudflare request-header Transform Rule), so direct hits on the raw*.vercel.apporigin — which skip the WAF / rate limits — are blocked. Unlike a Host-name check it can't be bypassed by spoofing Host. Disabled by default; set only in production with the Cloudflare rule deployed first. - Security response headers. All responses send
X-Content-Type-Options: nosniff; the generator page adds aContent-Security-Policy,X-Frame-Options: DENY, andReferrer-Policy; card SVGs send a locked-downdefault-src 'none'CSP (defense-in-depth for direct opens). - Supply chain hardening.
requirements.txtis now fully pinned fromuv.lock(no more>=ranges that let Vercel install untested versions), with a CI check that fails if it drifts from the lockfile. All GitHub Actions are pinned to commit SHAs, and Dependabot keeps both Actions and Python deps updated.
1.1.0 - 2026-06-10
- Scoring overhaul (
SCORE_VERSION→0.3.0). A batch of fairness fixes to how profiles are ranked:- Confidence now scales with each platform's verifiable solve volume (weighted by source trust), not mere account presence — so linking a brand-new judge account with a handful of solves no longer bumps the tier. GitHub confidence also credits a standout owned project, so a historically significant flagship isn't capped low just for being recently quiet.
- Tier caps gained a distinct Master step and were re-tuned: a strong single-source profile (e.g. GitHub-only) can now reach up to Master, while the top tier Maru is reserved for an all-round, multi-platform pentagon (deep across both open-source and algorithm activity).
- Open Source weights commits and contributed repos most (0.40 / 0.20) and leans less on PRs/reviews/issues, so a prolific direct-commit maintainer who rarely opens PRs isn't scored into the ground.
- Depth is redesigned into three pillars — algorithmic depth (judges), a
representative-project signal (the most-starred owned repo, a new
snapshot field), and technical breadth — combined so deep algorithms or
one significant built project can carry it (breadth only fills ≤15% of the
headroom). Org-owned flagships (e.g.
python/cpython) aren't attributed — a known public-data limitation.
- Card metrics: LeetCode folded into a combined "Solved". The standalone LeetCode metric is removed; "Solved" is now the total problems solved across all judges (BOJ + LeetCode, and future platforms). LeetCode still feeds the scores. The Solved metric shows even for a LeetCode-only profile.
- Adoption tracking + README badge. A new
GET /api/stats/badgeshields.io endpoint reports how many distinct developers have embedded a codemaru card. Card requests coming from GitHub's image proxy (Camo) — real README embeds — record the handle in a Vercel KV (Upstash Redis) set; the badge shows theSCARD. Best-effort: withoutKV_REST_API_URL/KV_REST_API_TOKEN(local / CI) tracking is a no-op and a failing KV never affects card rendering. Only the public, lower-cased handle is stored — no viewer IPs/headers.
- High-activity GitHub profiles no longer drop to "unavailable". The live
adapter's per-request read timeout (3s) was too tight for accounts whose
GraphQL query is heavy (many repos plus a year of contributions): the first
page alone could take 3–4s, time out, and degrade the whole card to a
GitHub-less
partial. The read budget is raised to 8s (connect stays short so a dead host still fails fast), and follow-up repository pages now use a lighter repos-only query that doesn't re-fetch the expensive contribution aggregation — bounding multi-page cost so it stays within the serverless limit.
1.0.1 - 2026-06-09
- Cross-platform scoring is now monotonic (
scoreVersion0.2.0) — linking another algorithm judge can no longer lower your tier. Problem Solving now sums solved counts across judges (saturated once) instead of averaging per-platform scores, and Depth takes the best rating (BOJ tier vs LeetCode contest) with summed hard-problem volume, counting only platforms that contribute positive evidence. A freshly created account no longer dilutes an established profile. - Tier crest crown redesigned so the spike count reads as a rank: 3 at Gold, +1 per tier up to 7 at Maru (no spikes below Gold).
- Card text is rendered as vector outlines using bundled Space Grotesk /
JetBrains Mono (both OFL). GitHub renders README SVGs with web fonts disabled,
so text previously fell back to system fonts (inconsistent across macOS /
Windows); outlining bakes the designed fonts into the geometry so the card
looks identical everywhere. Repeated glyphs are de-duplicated via
<defs>/<use>to keep the SVG small.
- Compact layout no longer overlaps the
@handleand thecodemaruwordmark (compact height 256 → 270). - Handle underline now matches the handle width exactly — fixed a scale-rounding bug that mis-sized all outlined text and left a long trailing underline.
- Version is single-sourced from
__version__: the FastAPI app version and the adapterUser-Agentno longer carry a hard-coded0.1.
- Bilingual README (English / 한국어) with a language switcher; the API
reference and fixture/live-mode notes moved to
CONTRIBUTING.md; added theme, compact, and generator preview images.
1.0.0 - 2026-06-05
First public release. codemaru turns a developer's public activity into a self-contained, embeddable SVG summary card for GitHub profile READMEs.
- Summary card — a pure-SVG, self-contained card (no JS, no external resources) embeddable in any GitHub README. Faceted hexagonal emblem with a 마루 crest ornament, calligraphy tier nameplate, top-3 strength medal badges, a fixed 5-axis radar, and a supporting-metric row.
- Tier ladder — eight ranks from Seed to Maru (Seed → Bronze → Silver → Gold → Platinum → Diamond → Master → Maru).
- Themes & layouts —
default,dark, andtransparentthemes, plus acompact(tier-panel-only) layout. - Scoring — five axes (Open Source, Impact, Consistency, Problem Solving,
Depth) combined into an overall score with logarithmic saturation;
confidence is weighted across platforms and caps the tier. Pure, versioned
scoring functions (
SCORE_VERSION). - Data adapters — GitHub (paginated GraphQL), solved.ac (via a
browser-impersonating TLS client to pass Cloudflare), and LeetCode
(unofficial GraphQL). Any platform failure degrades the card to
partialinstead of breaking it, with status-aware caching and a last-good stale fallback during outages. - Hosted generator — a web UI at
codemaru.bnbong.com with a live preview and
copy-paste Markdown / HTML
<picture>/ GitHub Action snippets. - HTTP API —
GET /api/card.svg,GET /api/summary.json, andGET /api/health. Invalid input returns a visible SVG error card with HTTP 200 (so image proxies render it, not a broken image); cards send CDN cache headers and anETag. - GitHub Action —
bnbong/codemaru, a composite action that runs the same scoring/render pipeline in your own repo's CI and commits a static SVG, so the card loads straight from your repository with no dependency on the hosted endpoint. - CLI —
codemaru generate --github <user> --out <path>for local/static card generation (the engine behind the Action). - Deployment — runs on Vercel as a FastAPI ASGI app.
- Docs & project hygiene — bilingual README (English / 한국어), CONTRIBUTING guide, issue/PR templates, CI (ruff, mypy, pytest + coverage), release-drafter, and PR labeler.