Every commit count, churn figure and timeline date on this site is measured by a bespoke CLI,
validated against a schema and written to a manifest the build reads. None of it is typed by
hand.
the build, in depth
The stack, briefly
For anyone who wants the headline before the internals.
Framework
SvelteKit 2 on Svelte 5, written with runes throughout.
Language
TypeScript in strict mode. No any; unknown when honest.
Build
Vite, prerendered to static HTML, deployed on Vercel (nodejs22.x).
Content
Typed TypeScript objects. No CMS, no markdown, no database.
Metrics
Drift: a decoupled git-metrics engine with a JSON Schema output contract, running on Bun.
Colour
Reasonable Colors, behind semantic tokens, with a no-flash dark theme.
The content is code, not a database
Every project is a typed object. The type system refuses to let me describe the work
incorrectly.
A team project that does not say what I actually did fails the build. The Contribution type discriminates on role, so any team-project entry must
carry either 'lead' or 'collaborator' before it will compile.
Flip for the code →
Once the shape is enforced at the type level, I write the editorial content myself.
Cross-links are checked at build time: the prerender throws on dangling slugs and the
test suite asserts every target resolves before the build runs.
interface Collaboration { team: string; // who the work was built with employer?: string; // the org Jason was employed by client?: string; // the end client, when distinct}interface SoloContribution { role: 'solo'; collaboration: Collaboration;}interface TeamContribution { role: 'lead' | 'collaborator'; collaboration: Collaboration; /** * Specific verified contributions. Optional: manifest-derived team * projects may have role inferred from commit share but no authored * note yet. Present once editorially authored. */ contributionNote?: string;}export type Contribution = SoloContribution | TeamContribution;
src/lib/data/types.ts: Contribution discriminated union
/** * Previously a hand-maintained string-literal union. Now a plain string: * slugs are discovered dynamically and cannot be enumerated in a closed union. * * Type safety is preserved at build time through two mechanisms: * 1. themes.ts throws during prerender when a relationship target is * not in the project registry (the build fails on dangling links). * 2. data.test.ts asserts that every relationship target is a known slug * (the test suite fails on typos before the build runs). */export type ProjectSlug = string;export interface ProjectRelationship { kind: 'extracted-from' | 'powers' | 'related'; target: ProjectSlug; note?: string;}
src/lib/data/types.ts: ProjectSlug and cross-link safety
Almost everything else is derived
The map, the timeline, the engine threads and the adoption chart are all computed from
one registry at build time.
The "libraries from the inside out" thread on the home page is a clear example. Nothing
declares those pairings by hand. A library says it powers an application; the
derivation walks the graph and finds every such pair, so the story stays true to the data
rather than to my memory of it.
Flip for the code →
One dataset in, every page out. The whole derivation runs at build time, so there is
nothing to hydrate and no runtime to fall over.
export function getEngineThreads(): EngineThread[] { const projectBySlug = new Map(projects.map((p) => [p.slug, p])); const threads: EngineThread[] = []; for (const project of projects) { for (const rel of project.relationships) { if (rel.kind !== 'powers') continue; const consumer = projectBySlug.get(rel.target); if (!consumer) continue; threads.push({ library: project, consumer, note: rel.note ?? '' }); } } return threads;}
src/lib/data/threads.ts: engine-thread derivation
One dataset in, every page out
The whole build pipeline, from real git history to prerendered HTML.
Source reposReal git history across every project I have shipped.
→
sources.jsonCommit counts, churn and dates, synced by the Drift engine.
→
projects/*.tsHand-authored typed objects, with the live metrics merged in.
→
queries.tsA pure query and filter layer over the registry.
→
Derived viewsgraph, threads, adoption and themes, all computed at build time.
→
Prerendered HTMLStatic routes, OG cards and the sitemap. No server at runtime.
One dataset in, every page out. The whole pipeline runs at build time.
Static, deterministic, dependency-light
The project map is laid out with a force simulation, but a deterministic one: identical
input gives identical output, so the prerendered SVG never drifts between builds. Each
social card is procedurally generated from four project dimensions: the kind sets the colour
scheme, the curated language tags drive a row of branded glyphs, the runtime selects a
background geometry tiled across the canvas, and the data model picks the typeface for the
project name. Satori lays it out as SVG; resvg rasterises it to PNG at build time. Identical
inputs produce an identical card, so the prerendered result never drifts between builds. The
syntax highlighting is baked in at build time by Shiki; the browser receives finished HTML.
A page either built correctly or it did not.
Drift
bespoke tooling
Commit counts and churn figures are the easiest things on a portfolio to quietly inflate. So
I do not write them. A bespoke CLI measures them against a schema that decides what it is
allowed to say.
Architecture
Drift Enginescripts/check-drift.jsFramework-agnostic. Fingerprints repos, owns the data files and the overlay contract, knows nothing about Svelte.
↓
Schema contractscripts/sources.schema.jsonJSON Schema draft-07, additionalProperties: false. The Engine validates every record before writing.
↓
Drift Frameworksrc/lib/data/Build-time registry. Reads files as static JSON, assembles typed Project objects for the site.
Two things with a contract between them. The Engine measures; the Framework presents.
Drift started as a single script that did everything: walked the repos, measured them, wrote
the manifest and understood how the site would render every figure. Measurement was tangled
with presentation, so neither could move without the other.
It is now two things with a contract between them. The Drift Engine (scripts/check-drift.js) is a framework-agnostic Bun script: it fingerprints repos, owns the four data files and
the overlay contract, and knows nothing about Svelte. The Drift Framework (src/lib/data/) is build-time SvelteKit code: it reads those files as static JSON imports and assembles
the typed Project objects the site is built from. Copying just the Engine into another
repo measures today, once its manifest is seeded by hand; making that copy stand alone is the
next milestone, and a published package comes after that.
Drift Enginescripts/check-drift.jsFramework-agnostic. Fingerprints repos, owns the data files and the overlay contract, knows nothing about Svelte.
→
Schema contractscripts/sources.schema.jsonJSON Schema draft-07, additionalProperties: false. The Engine validates every record before writing.
→
Drift Frameworksrc/lib/data/Build-time registry. Reads files as static JSON, assembles typed Project objects for the site.
The Engine owns measurement; the Framework owns presentation. The schema is the seam.
Before
check-drift.js
fingerprint repos
write manifest
render output
know the site's data shape
→
After
engine
fingerprint repos
write manifest
schema
framework
assemble Projects
render output
The contract
JSON Schema draft-07 with additionalProperties: false. A violation throws and
writes nothing.
Between the Engine and the Framework sits sources.schema.json: a JSON Schema
draft-07 definition with additionalProperties: false. The Engine validates
every assembled record against it before writing anything. A violation is a programming
error in the Engine, not a user-data problem, so the response is blunt: throw, write
nothing. A half-correct manifest never reaches disk.
This makes adding a new metric a deliberate three-step act. Declare the property in the
schema. Add it to the SyncedSource interface in types.ts. Return
it from getFingerprint in the Engine. Miss one and the build tells you, either
at bun run check or when the Engine throws on its next sync. The boundary is not
a convention I am trusting myself to respect; it is enforced.
// Validate the fully-assembled manifest against the engine's public schema// before the single sanctioned write. A violation is a programming error:// throw and write nothing (fail-closed).const violations = validateManifest(manifest);if (violations.length > 0) { for (const v of violations) { process.stderr.write(`drift: schema violation: ${v}\n`); } throw new Error( `sources.json failed validation (${violations.length}); nothing written.` );}writeJson(sourcesPath, manifest); // the only write to sources.json
scripts/check-drift.js: validation gate
Measurement
Every figure is measured against the canonical commit, not the working tree, via a bounded
concurrent worker pool.
For each repo, getFingerprint fans a set of independent git calls out via Promise.all against the resolved default branch, not whatever happens to be
checked out locally. defaultBranch resolves origin/HEAD, then main, then master, falling back to bare HEAD only
when none of those exist. The resolved ref is recorded as measuredRef in the
manifest, excluded from drift comparisons via DRIFT_SKIP_FIELDS, so a branch
rename never registers as drift.
Lines of code and languages are read straight from git blobs via git cat-file --batch, not the working tree, so the measurement is always
against the canonical commit. Repos run concurrently across a bounded worker pool (cpus().length slots). A HEAD-plus-TTL cache, keyed on the measured commit's SHA and gitignored, means an unchanged
repo is not re-scanned. drift sync and --no-cache bypass it.
Per repo, the fingerprint covers: commit counts on two axes (mine versus all authors,
lifetime versus trailing four weeks); line churn on the same axes; lines of code; languages
by file count; first and last commit dates; and the runtime, framework and database inferred
from manifest files.
src/lib/data/sources.json: one entry, every field a measurement
The staging pipeline
In-flight work surfaces on the site before it merges, via a self-healing three-tier
precedence chain.
Work that is still on an unmerged branch has no entry in sources.json yet, but
it can still surface on the site. A committed in-progress.json holds
provisional metrics for in-flight projects: the branch name, a promotion pipeline (ordered
merge targets), a visibility flag ('public' surfaces on the site; 'local' stays in the CLI), and per-field tracked values with their baseOnMain counterpart for context.
The Framework's withSyncedMetrics applies a three-tier precedence across every
metric field. Manual overrides win; real synced figures come next; provisional values from in-progress.json are the floor. Once a branch lands and drift sync picks up real numbers, the synced value naturally shadows the provisional
one. Promotion is self-healing: no stale figures leak through.
// Precedence: override > synced > provisional.// prov(field) returns the in-progress tracked value, or undefined.const prov = (field: keyof ProjectMetrics) => provisional?.tracked?.[field]?.value;commitsMe: ov?.commitsMe?.value ?? synced?.commitsMe ?? prov('commitsMe'),linesAny: ov?.linesAny?.value ?? synced?.linesAny ?? prov('linesAny'),// ...every metric field follows the same three-tier chain// Scope stays honest: commitsAny is always all-authors and commitsMe is// always Jason, whatever the project's role. The role-keyed figure the// page actually shows is a separate field, so nothing reading a scoped// fact silently gets the other scope's number.commitsHeadline: isSolo ? synced?.commitsAny : synced?.commitsMe,commitsHeadlineScope: isSolo ? 'any' : 'me',
src/lib/data/index.ts: metric precedence chain
The verbs
Each write verb touches exactly one file. Read-only verbs touch nothing at all.
Verb
Does
Writes
report
Field-level drift for repos whose HEAD moved. --full diffs all; --check exits non-zero; --json for scripts.
nothing
snapshot
Every current metric for every repo, one card per project.
nothing
sync
The one sanctioned write to the manifest. Backfills all resolvable repos, bypasses the cache, schema-gated.
sources.json
keep / keep-all
Refresh the baseline behind a manual override without discarding the override value.
overrides.json
hide
Drop a repo from the public site.
excluded.json
promote
Graduate in-flight work off an unmerged branch into the staging pipeline.
in-progress.json
author
Scaffold projects/<slug>.ts from a commented template if absent, then open in $EDITOR.
projects/<slug>.ts
flag
flag <slug> --pin | --hide. Set a curation flag in the overlay via TypeScript compiler-API splice.
projects/<slug>.ts
audit
Editorial-depth scoring (Full / Partial / Thin) across all overlays. Recomputes from live files.
nothing
init
Scaffold the per-machine config files. Interactive prompts when a TTY is present.
config files
help
Per-verb help, rendered in gum-formatted markdown.
nothing
Every figure you see on this site is a measurement from that manifest, validated against a
schema.
Credits
Type
Source Serif 4 for display, IBM Plex Sans for prose, JetBrains Mono for the apparatus; the
same three set the OG cards, where a project's data model picks the name's face.