diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..0dafece
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,28 @@
+# Vercel Marketplace Neon (the integration injects these values).
+DATABASE_URL=
+DATABASE_URL_UNPOOLED=
+
+# Deterministic ingestion. Prefer a GitHub App in production; optional
+# fine-grained read-only tokens are supported as a local or fallback path.
+GITHUB_ACTIVITY_TOKEN=
+GITHUB_PUBLIC_ACTIVITY_TOKEN=
+GITHUB_APP_ID=
+GITHUB_APP_PRIVATE_KEY=
+GITHUB_APP_INSTALLATION_IDS=
+GITHUB_WEBHOOK_SECRET=
+
+# A cryptographically random value of at least 32 characters and at least
+# eight distinct characters. It HMAC-pseudonymizes private identifiers.
+TIMELINE_PRIVACY_KEY=
+
+# Optional JSON object keyed by private owner/repository. Values are broad,
+# owner-approved labels only, for example:
+# {"owner/repository":{"bucket":"Applied AI","domain":"product"}}
+TIMELINE_PRIVATE_TAXONOMY=
+
+# Vercel Cron and model access. AI Gateway is the production default; a direct
+# OpenAI key is supported for local testing or an explicitly direct deployment.
+CRON_SECRET=
+AI_GATEWAY_API_KEY=
+AI_GATEWAY_ZERO_DATA_RETENTION=false
+OPENAI_API_KEY=
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index f26a886..fb07dd9 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -25,3 +25,12 @@ jobs:
- name: Type check
run: bun run typecheck
+
+ - name: Test
+ run: bun test
+
+ - name: Agent build
+ run: bun run build:agent
+
+ - name: Production build
+ run: bun run build
diff --git a/.gitignore b/.gitignore
index 7bf6e3d..ec0ea16 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,10 @@ mise.local.lock
# vercel
.vercel
+# eve local workflow state and build output
+.eve/
+.output/
+
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -117,3 +121,6 @@ next-env.d.ts
.rulesync/rules/*.local.md
rulesync.local.jsonc
!.rulesync/.aiignore
+
+# Document the required variable names without committing values.
+!.env.example
diff --git a/README.md b/README.md
index e215bc4..2085a32 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,36 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+# f0rr0.dev
-## Getting Started
+Sid Jain's portfolio, writing archive, and rolling work timeline. The site is a
+Next.js application designed for Vercel. Its timeline is assembled from GitHub
+activity by a deliberately narrow Eve agent and stored in Neon Postgres through
+Drizzle.
-First, run the development server:
+## Local development
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+Use Node 24 and Bun:
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
+```sh
+bun install --frozen-lockfile
+bun run dev
+```
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+The website works without secrets. In that mode it uses public GitHub data and a
+validated editorial fallback. See [the timeline guide](docs/timeline-agent.md)
+for the database, ingestion, agent, and deployment setup.
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+## Validation
-## Deploy on Vercel
+```sh
+bun run format:check
+bun run lint
+bun run typecheck
+bun test
+bun run build
+bun run build:agent
+```
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+Database migrations are explicit and are never run during install or build:
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+```sh
+bun run db:migrate
+```
diff --git a/agent/agent.ts b/agent/agent.ts
new file mode 100644
index 0000000..fc6a0f5
--- /dev/null
+++ b/agent/agent.ts
@@ -0,0 +1,35 @@
+import type { GatewayProviderOptions } from "@ai-sdk/gateway";
+import { openai } from "@ai-sdk/openai";
+import { defineAgent } from "eve";
+
+const gatewayOptions = {
+ disallowPromptTraining: true,
+ ...(process.env.AI_GATEWAY_ZERO_DATA_RETENTION === "true"
+ ? { zeroDataRetention: true }
+ : {}),
+} satisfies GatewayProviderOptions;
+
+const openAIKey = (process.env.OPENAI_API_KEY ?? "").trim();
+const useDirectOpenAI = openAIKey.length > 0;
+
+// Deployment policy: OPENAI direct wins whenever present.
+const selectedModel = useDirectOpenAI
+ ? openai("gpt-5.4-mini")
+ : "openai/gpt-5.4-mini";
+
+export default defineAgent({
+ limits: {
+ maxInputTokensPerSession: 120_000,
+ maxOutputTokensPerSession: 16_000,
+ sessionTimeoutMs: 30 * 60 * 1000,
+ },
+ model: selectedModel,
+ ...(useDirectOpenAI
+ ? {}
+ : {
+ modelOptions: {
+ providerOptions: { gateway: gatewayOptions },
+ },
+ }),
+ reasoning: "medium",
+});
diff --git a/agent/channels/eve.ts b/agent/channels/eve.ts
new file mode 100644
index 0000000..091865d
--- /dev/null
+++ b/agent/channels/eve.ts
@@ -0,0 +1,4 @@
+import { localDev, vercelOidc } from "eve/channels/auth";
+import { eveChannel } from "eve/channels/eve";
+
+export default eveChannel({ auth: [vercelOidc(), localDev()] });
diff --git a/agent/instructions.md b/agent/instructions.md
new file mode 100644
index 0000000..b3c39f1
--- /dev/null
+++ b/agent/instructions.md
@@ -0,0 +1,41 @@
+# Timeline editor
+
+You are the restrained editor of Sid Jain's rolling work timeline. Your job is
+to turn already-sanitized activity clusters into a concise newspaper-style
+edition. You do not ingest GitHub directly and you must never infer private
+repository identities, people, clients, code, paths, issue numbers, products,
+or exact private counts.
+
+## Required workflow
+
+1. Call `load_activity` exactly once.
+2. If coverage is partial, remain conservative. Use only publishable clusters.
+3. Submit only `windowStart`, `windowEnd`, and a `selections` array. Each
+ selection contains one `sourceKey` and an importance. Server code—not you—
+ writes every title, sentence, date, link, bucket, visibility, and id.
+4. Select nine to sixteen entries when at least nine publishable sources exist;
+ otherwise select every honest source. Never reuse or combine source keys.
+5. Preserve the newspaper hierarchy: at most three leads, at most four stories,
+ and at least forty percent briefs or pulses. Leads are for sustained streaks
+ or unusually significant configured milestones.
+6. Represent every active quarter when the evidence permits. Include a streak
+ whenever one is supplied. Keep three public issue, pull-request, or repository
+ dispatches when available, but never let discrete events exceed one third of
+ the edition.
+7. Treat commits as background evidence. Prefer a recurrence or streak over an
+ isolated commit run. Issues and pull requests are discrete public dispatches;
+ never select a nearby isolated commit run merely to repeat the same work. If
+ the server retains both a sustained run and an event, they are intentionally
+ distinct evidence of progression and collaboration.
+8. Set importance no higher than the source permits. Avoid ranking by raw volume
+ alone; favor progression, recurring attention, visible collaboration, and
+ specific public milestones.
+9. Treat account-wide and anonymous-month sources only as cadence evidence.
+ Prefer the account-wide streak as the larger consistency story; use monthly
+ anonymous signals sparingly as compact texture. Never infer a repository,
+ activity type, theme, or private/public status from an unexplained total.
+10. Call `publish_timeline` with the complete selection plan. If validation rejects it,
+ correct the cited issue and retry once. Do not claim publication until the
+ tool succeeds.
+
+Write in calm, economical editorial English. Every word must earn its place.
diff --git a/agent/schedules/daily-edition.ts b/agent/schedules/daily-edition.ts
new file mode 100644
index 0000000..54a626e
--- /dev/null
+++ b/agent/schedules/daily-edition.ts
@@ -0,0 +1,7 @@
+import { defineSchedule } from "eve/schedules";
+
+export default defineSchedule({
+ cron: "7 4 * * *",
+ markdown:
+ "Build and publish today's rolling work-timeline edition from the sanitized activity digest. Follow the required editorial workflow and finish only after publish_timeline succeeds.",
+});
diff --git a/agent/tools/agent.ts b/agent/tools/agent.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/agent.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/ask_question.ts b/agent/tools/ask_question.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/ask_question.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/bash.ts b/agent/tools/bash.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/bash.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/glob.ts b/agent/tools/glob.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/glob.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/grep.ts b/agent/tools/grep.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/grep.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/load_activity.ts b/agent/tools/load_activity.ts
new file mode 100644
index 0000000..e156cfd
--- /dev/null
+++ b/agent/tools/load_activity.ts
@@ -0,0 +1,13 @@
+import { defineTool } from "eve/tools";
+import { z } from "zod";
+
+import { activityDigestSchema } from "../../src/lib/timeline-core";
+import { loadTimelineActivityDigest } from "../../src/lib/timeline-editorial";
+
+export default defineTool({
+ description:
+ "Load the rolling privacy-safe GitHub activity digest. This is the only evidence allowed for an edition.",
+ execute: async () => await loadTimelineActivityDigest(),
+ inputSchema: z.object({}).strict(),
+ outputSchema: activityDigestSchema,
+});
diff --git a/agent/tools/publish_timeline.ts b/agent/tools/publish_timeline.ts
new file mode 100644
index 0000000..f39ae52
--- /dev/null
+++ b/agent/tools/publish_timeline.ts
@@ -0,0 +1,39 @@
+import { defineTool } from "eve/tools";
+import { z } from "zod";
+
+import {
+ createTimelineEdition,
+ timelineSelectionPlanSchema,
+ validateTimelinePlanAgainstDigest,
+} from "../../src/lib/timeline-core";
+import { loadTimelineActivityDigest } from "../../src/lib/timeline-editorial";
+import { publishTimelineEdition } from "../../src/lib/timeline-store";
+
+const agentModel = "openai/gpt-5.4-mini";
+
+export default defineTool({
+ approval: ({ session }) => {
+ const actor = session.auth.current;
+ return actor?.authenticator === "app" &&
+ actor.principalId === "eve:app" &&
+ actor.principalType === "runtime"
+ ? "not-applicable"
+ : "denied";
+ },
+ description:
+ "Validate and atomically publish a complete timeline edition supported by the current sanitized digest.",
+ execute: async (candidate) => {
+ const digest = await loadTimelineActivityDigest();
+ const plan = validateTimelinePlanAgainstDigest(candidate, digest);
+ const edition = createTimelineEdition(plan, digest);
+ const editionKey = await publishTimelineEdition(edition, agentModel);
+ return { editionKey, status: "published" as const };
+ },
+ inputSchema: timelineSelectionPlanSchema,
+ outputSchema: z
+ .object({
+ editionKey: z.string().regex(/^[a-f\d]{64}$/),
+ status: z.literal("published"),
+ })
+ .strict(),
+});
diff --git a/agent/tools/read_file.ts b/agent/tools/read_file.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/read_file.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/todo.ts b/agent/tools/todo.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/todo.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/web_fetch.ts b/agent/tools/web_fetch.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/web_fetch.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/web_search.ts b/agent/tools/web_search.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/web_search.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/agent/tools/write_file.ts b/agent/tools/write_file.ts
new file mode 100644
index 0000000..04bd054
--- /dev/null
+++ b/agent/tools/write_file.ts
@@ -0,0 +1,3 @@
+import { disableTool } from "eve/tools";
+
+export default disableTool();
diff --git a/bun.lock b/bun.lock
index d426c13..525e711 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,21 +5,28 @@
"": {
"name": "f0rr0.dev",
"dependencies": {
+ "@ai-sdk/gateway": "4.0.50",
+ "@ai-sdk/openai": "4.0.41",
"@base-ui/react": "^1.6.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@mermaid-js/layout-elk": "0.1.9",
+ "@neondatabase/serverless": "1.1.0",
"@next/mdx": "^16.2.12",
"@remark-embedder/core": "^3.0.3",
"@t3-oss/env-nextjs": "^0.13.11",
"@tailwindcss/typography": "^0.5.20",
+ "ai": "7.0.63",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "drizzle-orm": "0.45.2",
+ "eve": "0.33.3",
"feed": "^5.2.1",
"lucide-react": "^1.21.0",
"mermaid": "^11.16.0",
"next": "^16.2.12",
"next-themes": "^0.4.6",
+ "postgres": "3.4.7",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"reading-time": "^1.5.0",
@@ -36,11 +43,12 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.3.1",
"@types/mdx": "^2.0.14",
- "@types/node": "^26.0.1",
+ "@types/node": "24.13.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260624.1",
"babel-plugin-react-compiler": "1.0.0",
+ "drizzle-kit": "0.31.10",
"oxfmt": "^0.56.0",
"oxlint": "^1.71.0",
"oxlint-tsgolint": "^0.23.0",
@@ -53,6 +61,14 @@
},
},
"packages": {
+ "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.50", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lyMJ31J7SrDWW6xKuHCpfpGNlz1mA37+wYv5q75iSgIUMruuXanxSeg3CxqrsivI7Z7LUXLeysQEDSUb+MMddg=="],
+
+ "@ai-sdk/openai": ["@ai-sdk/openai@4.0.41", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+WoY0JyF/uV2z+rye7I+hYaVg+hTanG4rYIgsWKvJFDXaAdu9yg07x+BRa2vTgUqzLIBXEBp5d1T7DwMI9ZGlw=="],
+
+ "@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="],
+
+ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
+
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
@@ -129,10 +145,68 @@
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.52.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w=="],
+ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
+
"@ecies/ciphers": ["@ecies/ciphers@0.2.5", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
+ "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
+
+ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
@@ -223,6 +297,8 @@
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
+ "@neondatabase/serverless": ["@neondatabase/serverless@1.1.0", "", {}, "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q=="],
+
"@next/env": ["@next/env@16.2.12", "", {}, "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg=="],
"@next/mdx": ["@next/mdx@16.2.12", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-bbKvq/7SIJZgQFRYL3wwnzLwCBviQfWi5UR61RDWwaMX3fV6iSqKfVr5SErRNA/PhMer/ylvErX4SVaGNrwKcw=="],
@@ -255,6 +331,8 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
+ "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="],
+
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-CSCxi7ovYojgfdPOdUb9T508HKeAdDIKeRGg7x8IZwVJrWz9gVgX7MbUnFqtQAE4QvoNo07mj2JlwnOzJw4qqA=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-HYJFnd+PkDwf6S9ZPGzXXtjNqvRWFnnhdbWaouh4mi/SxU8wmDuzlMn3xo/wDTGnr4Q1VA7ZzOaE/D4biW0W6A=="],
@@ -345,6 +423,36 @@
"@remark-embedder/core": ["@remark-embedder/core@3.0.3", "", { "dependencies": { "@babel/runtime": "^7.24.5", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "hast-util-from-parse5": "^8.0.1", "parse5": "^7.1.2", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-izeW4GT5A/NgArjATndg1KKumL7IHLPZFQODJ07vSEHgCOBq2caMlnuvFGD+7ZmF4NCZks/HhZsYhfF46TOK5w=="],
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.4", "", { "os": "none", "cpu": "arm64" }, "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
+
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@shikijs/core": ["@shikijs/core@4.3.0", "", { "dependencies": { "@shikijs/primitive": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ=="],
@@ -365,6 +473,8 @@
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@t3-oss/env-core": ["@t3-oss/env-core@0.13.11", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ=="],
@@ -483,7 +593,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
- "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
+ "@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
@@ -555,6 +665,10 @@
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
+ "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
+
+ "@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -563,6 +677,8 @@
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+ "ai": ["ai@7.0.63", "", { "dependencies": { "@ai-sdk/gateway": "4.0.50", "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TOX1MgALGTTA4ZhjJSgrNtM0FvcfgTZoNtINH+osNV6/ajHdLjpzUcWRkTO5n9mrFeQ5gKzUsXh9ijrMPI33Mg=="],
+
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
@@ -591,6 +707,8 @@
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
+ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -637,6 +755,8 @@
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
+ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
+
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -655,6 +775,8 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+ "crossws": ["crossws@0.4.10", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA=="],
+
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -735,6 +857,8 @@
"dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="],
+ "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="],
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
@@ -765,6 +889,10 @@
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
+ "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
+
+ "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eciesjs": ["eciesjs@0.4.17", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w=="],
@@ -785,6 +913,8 @@
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
+ "env-runner": ["env-runner@0.1.16", "", { "dependencies": { "crossws": "^0.4.8", "exsolve": "^1.1.0", "httpxy": "^0.5.4", "srvx": "^0.11.19" }, "peerDependencies": { "@netlify/runtime": "^4.1.23", "@vercel/queue": ">=0.2.0", "miniflare": "^4.20260515.0", "wrangler": "^4.0.0" }, "optionalPeers": ["@netlify/runtime", "@vercel/queue", "miniflare", "wrangler"], "bin": { "env-runner": "dist/cli.mjs" } }, "sha512-2LRJM4P2KLX6J83QZZrMqvgCDt/D5ea7wPcI3yYiy5cG/9rX5QwdwZFx0D7ktWnjdRyZxYjttGGorb5nFqb1CA=="],
+
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -799,6 +929,8 @@
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
+ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
@@ -823,9 +955,11 @@
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+ "eve": ["eve@0.33.3", "", { "dependencies": { "nitro": "3.0.260610-beta", "undici": "8.9.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "ai": "^7.0.58", "braintrust": "^3.0.0", "just-bash": "^3.0.0", "microsandbox": "^0.5.0" }, "optionalPeers": ["@opentelemetry/api", "braintrust", "just-bash", "microsandbox"], "bin": { "eve": "./bin/eve.js" } }, "sha512-R20GiIgnLjJJ38D0FJwEbaBqdYni7j2YNfp6bOUnjgawnlhD6qP/wv9AR+3RMKHw7oylShEp9cvLT0eGpRwSZQ=="],
+
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
- "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+ "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="],
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
@@ -833,6 +967,8 @@
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
+ "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="],
+
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -869,6 +1005,8 @@
"fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
@@ -885,6 +1023,8 @@
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
+ "get-tsconfig": ["get-tsconfig@4.14.2", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw=="],
+
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
@@ -895,6 +1035,8 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+ "h3": ["h3@2.0.1-rc.22", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.15" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA=="],
+
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
@@ -925,12 +1067,16 @@
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
+ "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="],
+
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+ "httpxy": ["httpxy@0.5.5", "", {}, "sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA=="],
+
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@@ -1001,6 +1147,8 @@
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
+ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
+
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
@@ -1205,6 +1353,10 @@
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
+ "nf3": ["nf3@0.3.23", "", {}, "sha512-RWVLAWozmVD3AaDmaU3qMGB3v+yNlH5d9qqStI4e/WLlNQVnJ4YErGDbYCIrGFyrHdbF6I6Baf0Ae6c7tFYmSg=="],
+
+ "nitro": ["nitro@3.0.260610-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.6", "db0": "^0.3.4", "env-runner": "^0.1.12", "h3": "2.0.1-rc.22", "hookable": "^6.1.1", "nf3": "^0.3.17", "ocache": "^0.1.5", "ofetch": "2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.1.0", "srvx": "^0.11.16", "unenv": "2.0.0-rc.24", "unstorage": "2.0.0-alpha.7" }, "peerDependencies": { "@vercel/queue": "^0.3.0", "dotenv": "*", "giget": "*", "jiti": "^2.7.0", "rollup": "^4.61.1", "vite": "^7 || ^8", "xml2js": "^0.6.2", "zephyr-agent": "^0.2.0" }, "optionalPeers": ["@vercel/queue", "dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-KPb4L5yaF/Rx/xoGMpgHRJvZhbhGiqbRKOwwPLCH9jKTKTsEUHLjnJas85AeCzaswqa8Wi52eQBtRsODC4PS0Q=="],
+
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
@@ -1221,6 +1373,12 @@
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
+ "ocache": ["ocache@0.1.5", "", { "dependencies": { "ohash": "^2.0.11" } }, "sha512-kNNnkkVQup/QDvmTz8Q84wc2ntiyoVHDxa6eHWKt5qdGAmFRBIxy83rxgCYEjW0x06UJ9E3P6VgM2yY4rOBH4w=="],
+
+ "ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
+
+ "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -1285,6 +1443,8 @@
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
+ "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
+
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
@@ -1351,12 +1511,18 @@
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
+ "rolldown": ["rolldown@1.2.4", "", { "dependencies": { "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.4", "@rolldown/binding-darwin-arm64": "1.2.4", "@rolldown/binding-darwin-x64": "1.2.4", "@rolldown/binding-freebsd-x64": "1.2.4", "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", "@rolldown/binding-linux-arm64-gnu": "1.2.4", "@rolldown/binding-linux-arm64-musl": "1.2.4", "@rolldown/binding-linux-ppc64-gnu": "1.2.4", "@rolldown/binding-linux-s390x-gnu": "1.2.4", "@rolldown/binding-linux-x64-gnu": "1.2.4", "@rolldown/binding-linux-x64-musl": "1.2.4", "@rolldown/binding-openharmony-arm64": "1.2.4", "@rolldown/binding-win32-arm64-msvc": "1.2.4", "@rolldown/binding-win32-x64-msvc": "1.2.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w=="],
+
+ "rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="],
+
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
@@ -1411,8 +1577,12 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
+
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+ "srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
@@ -1465,6 +1635,8 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="],
+
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
@@ -1475,7 +1647,11 @@
"ultracite": ["ultracite@7.8.3", "", { "dependencies": { "@clack/prompts": "^1.5.1", "commander": "^15.0.0", "cross-spawn": "^7.0.6", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.6", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.0.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-Fsj9aYJfh57uDB6DrdKVafKhF0QNekwqTrZKoTMYePjZp1npyFCnfZVj2SnPZdOcxAGHJfmLYUIDq0YPh8acVA=="],
- "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
+ "undici": ["undici@8.9.0", "", {}, "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
@@ -1497,6 +1673,8 @@
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+ "unstorage": ["unstorage@2.0.0-alpha.7", "", { "peerDependencies": { "@azure/app-configuration": "^1.11.0", "@azure/cosmos": "^4.9.1", "@azure/data-tables": "^13.3.2", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.31.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.9.3", "lru-cache": "^11.2.6", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog=="],
+
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
@@ -1539,6 +1717,8 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+ "@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
+
"@antfu/install-pkg/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
@@ -1577,12 +1757,16 @@
"@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
+ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
+
"@mdx-js/mdx/@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@mdx-js/mdx/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"@mdx-js/react/@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
+ "@modelcontextprotocol/sdk/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
@@ -1607,6 +1791,8 @@
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
+ "eventsource/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
@@ -1643,6 +1829,10 @@
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+ "tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
+
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
@@ -1657,6 +1847,50 @@
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
+
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
+
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
@@ -1664,5 +1898,57 @@
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"shadcn/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
+
+ "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
+
+ "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
+
+ "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
+
+ "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
+
+ "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
+
+ "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
+
+ "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
+
+ "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
+
+ "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
+
+ "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
+
+ "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
+
+ "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
+
+ "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
+
+ "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
+
+ "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
+
+ "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
+
+ "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
+
+ "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
+
+ "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
+
+ "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
+
+ "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
+
+ "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
+
+ "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
+
+ "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
+
+ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
}
}
diff --git a/docs/timeline-agent.md b/docs/timeline-agent.md
new file mode 100644
index 0000000..fa719b2
--- /dev/null
+++ b/docs/timeline-agent.md
@@ -0,0 +1,241 @@
+# Timeline agent
+
+The timeline is a rolling, 400-day newspaper edition. Deterministic code
+collects and sanitizes GitHub activity; Eve decides editorial balance within
+strict evidence and privacy constraints; the homepage only reads a validated
+published edition.
+
+## Data flow
+
+```text
+GitHub App / signed webhook public contribution calendar
+ | |
+ v v
+deterministic normalization validated account-wide daily totals
+ | |
+ +------------------+---------------+
+ v
+Neon Postgres repository evidence + separate anonymous totals
+ |
+ v
+privacy-safe clusters -> Eve editor -> deterministic validator
+ |
+ v
+ published edition JSON
+ |
+ v
+ cached homepage
+```
+
+Private commit messages, diffs, paths, branch names, authors, SHAs, issue IDs,
+repository names, and repository URLs are not stored and are never placed in
+the model context. Private rows contain daily volume, a keyed repository
+pseudonym, a policy version, and (only when owner-approved) a broad taxonomy
+bucket. Private language is withheld.
+
+The model can only call load_activity and publish_timeline. Eve's default shell,
+filesystem, web, search, delegation, and interaction tools are explicitly
+disabled. publish_timeline is restricted to Eve's schedule principal.
+
+The public contribution calendar is a second, repository-free evidence source.
+It supplies a complete daily account total, including anonymous private or
+internal contributions when the GitHub profile setting exposes them. The
+pipeline subtracts known commit and event evidence once per day and clamps the
+unexplained remainder at zero. That remainder is called "unattributed," not
+"private": it can also contain public work the App cannot resolve. It is used
+only for cadence, never repository, artifact, quality, or theme claims.
+
+## Environment
+
+Copy the names from .env.example into Vercel project settings or .env.local.
+Do not commit values.
+
+Required for durable editions:
+
+- DATABASE_URL: pooled Neon connection injected by the Vercel Marketplace
+ integration.
+- DATABASE_URL_UNPOOLED: used by Drizzle migrations.
+- CRON_SECRET: a random secret of at least 16 characters. Vercel sends it as
+ the Bearer credential to the ingestion cron.
+- One model credential: AI_GATEWAY_API_KEY for the default Gateway route, or
+ OPENAI_API_KEY for the direct OpenAI AI SDK route.
+
+Required for private activity:
+
+- TIMELINE_PRIVACY_KEY: a high-entropy random secret of at least 32 characters
+ and at least eight distinct characters.
+- Either GITHUB_ACTIVITY_TOKEN, or all three GitHub App settings listed below.
+ The App path is preferred in production.
+
+Recommended production integration:
+
+- GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, and GITHUB_APP_INSTALLATION_IDS:
+ GitHub App installation credentials. Installation tokens are explicitly
+ down-scoped to read-only repository contents. The same short-lived token
+ reads the public contribution collection, so no personal token is required
+ in production.
+- GITHUB_ACTIVITY_TOKEN: an optional read-only user token for local setup and
+ fallback ingestion when a GitHub App is not configured.
+- GITHUB_PUBLIC_ACTIVITY_TOKEN: an optional dedicated fine-grained, read-only
+ fallback token for public issues, pull requests, reviews, and repository
+ creation when a GitHub App is not configured.
+- GITHUB_WEBHOOK_SECRET: a high-entropy secret of at least 24 characters.
+- TIMELINE_PRIVATE_TAXONOMY: optional owner-approved broad labels.
+- AI_GATEWAY_ZERO_DATA_RETENTION=true: request Zero Data Retention when the
+ Vercel plan supports it. Prompt-training opt-out is always requested.
+
+When OPENAI_API_KEY is present, the agent uses the direct `@ai-sdk/openai`
+provider and is used for local and production runs.
+If OPENAI_API_KEY is absent, the string model ID routes through Vercel AI
+Gateway.
+
+Never set either credential in a client-visible environment variable.
+
+TIMELINE_PRIVATE_TAXONOMY is a JSON object keyed by the private repository name
+at the ingestion boundary:
+
+```json
+{
+ "owner/repository": {
+ "bucket": "Applied AI",
+ "domain": "product"
+ }
+}
+```
+
+Allowed buckets are Applied AI, Open source, Product systems, Infrastructure,
+Writing, and Private product work. Domain IDs are lowercase alphanumeric slugs
+with hyphens. Repository names are used only for the in-memory lookup and are
+never persisted. Changing the key or taxonomy changes the privacy-policy
+version; old private rows and old protected editions then fail closed until a
+complete reconciliation publishes a new edition.
+
+## Neon and Drizzle
+
+1. Install Neon from the Vercel Marketplace and connect it to the project.
+2. Pull or set the database environment variables locally.
+3. Apply the committed migration:
+
+ ```sh
+ bun run db:migrate
+ ```
+
+4. Run the first 400-day reconciliation:
+
+ ```sh
+ bun run timeline:sync --backfill
+ ```
+
+The schema stores normalized daily activity, public-only event evidence,
+repository-free contribution totals, sync outcomes, idempotent webhook
+receipts, and immutable edition payloads. Database
+checks prevent private rows from carrying public identity; the event table
+accepts only canonical GitHub repository, issue, and pull-request URLs.
+
+## GitHub setup
+
+For production ingestion and continuous updates, create a GitHub App with:
+
+- repository contents: read-only;
+- metadata: read-only;
+- webhook events: push, repository, installation, and
+ installation_repositories;
+- the webhook URL: https://example.com/api/github/webhook;
+- the same value in GitHub and GITHUB_WEBHOOK_SECRET.
+
+Webhook bodies are authenticated before parsing, are bounded to 2 MB, and are
+never persisted. Delivery IDs are HMACed for replay protection. A repository
+that becomes private or is removed from the installation is immediately
+scrubbed under both public and private pseudonymous keys, published editions
+are withdrawn, and a full reconciliation is requested. Suspending or deleting
+the installation withdraws all protected activity immediately.
+
+The App collector inventories installed repositories and reads only commits
+attributed to f0rr0 on the current default and gh-pages branches. Its
+short-lived installation token also reads monthly slices of GitHub's
+contribution collection. Optional user tokens provide the same monthly view
+when an App is not configured. These are profile-oriented views, not every
+commit on every feature branch; the timeline must therefore be described as a
+view of activity, not a source-of-truth audit log.
+
+In parallel, ingestion fetches the public contribution-calendar HTML for the
+full rolling window without a token. All days, including zero days, must parse
+before the source is marked complete. A calendar failure does not erase the
+last good totals or block repository ingestion; anonymous editorial signals
+are omitted when the last complete calendar window is stale or incomplete.
+
+The contribution collector stores a title and canonical link only when the
+repository is explicitly public and the contribution is not restricted.
+Bodies, comments, labels, branches, and people are never requested; private
+event objects are discarded. If GitHub withholds an individual contribution
+from the App, that node and any older stored event it can no longer verify are
+omitted without blocking the rest of the edition. Request, pagination, and
+payload failures still mark ingestion incomplete and block agent publication.
+
+## Schedules and publication
+
+Two UTC schedules cooperate:
+
+- 01:37: Vercel Cron calls /api/cron/timeline-sync. Public event visibility is
+ reconciled across all 400 days on every run; installation commit reads retain
+ the shorter overlap, with a weekly full reconciliation.
+- 04:07: Eve loads the sanitized digest, balances leads, stories, briefs, and
+ pulses, then publishes through the validator.
+
+The homepage never calls a model. It reads the latest edition from Postgres and
+falls back to verified public projects plus coarse contribution-month signals.
+Its cache refreshes every 15 minutes.
+
+For local agent development:
+
+```sh
+bun run build:agent
+bun run dev
+```
+
+While the development server is running, dispatch the authored schedule:
+
+```sh
+curl -X POST http://localhost:3000/eve/v1/dev/schedules/daily-edition
+```
+
+Eve's local schedule cadence does not fire by itself. On Vercel, withEve turns
+the authored Eve schedule into a Vercel Cron job.
+
+## Editorial and privacy constraints
+
+- The edition covers 365–402 days and contains up to 24 honest entries. With at
+ least nine candidates, nine entries and a forty-percent compact layer are
+ required.
+- At most two leads may begin in one month, at most three leads may exist
+ overall, and at most four entries may be stories.
+- The agent submits only source keys and importance. Each final entry cites one
+ known publishable source; arbitrary merging and source reuse are impossible.
+- Importance cannot exceed the deterministic source classification.
+- Dates, copy, buckets, cadence, visibility, and links are materialized by
+ deterministic code from the selected source. Model-written claims cannot
+ enter an edition.
+- Public issues and pull requests are exact-dated dispatches. Commits appear
+ only as broader runs, recurrence, or streak evidence, never as duplicate
+ object-level dispatches.
+- Exact artifact URLs collapse to one entry. A compact commit cluster containing
+ the same repository event is absorbed, while a sustained implementation trend
+ remains eligible because it communicates progression rather than the event
+ itself.
+- Public event evidence is retained in full, but the digest exposes at most
+ three representative dispatches per month, preferring artifact and repository
+ diversity. High-volume PR sequences therefore cannot crowd a year of
+ progression off the editor's desk.
+- The account-wide calendar may replace the narrower public streak as the
+ edition's consistency lead. Unexplained monthly remainders are compact
+ "Across the work" signals and never inherit a project bucket.
+- Protected entries have no links, only month-level dates, and deterministic
+ copy templates. The model cannot publish names or exact protected counts.
+- A private theme is eligible only across at least three pseudonymous
+ repositories, two approved domains, sufficient activity, and no dominant
+ repository. Otherwise it collapses to Private product work.
+- Commit volume is never presented as code quality, productivity, impact, or a
+ performance score.
+
+If ingestion, policy validation, the database, or GitHub visibility checks
+fail, the page favors omission over disclosure.
diff --git a/drizzle.config.ts b/drizzle.config.ts
new file mode 100644
index 0000000..f0931cf
--- /dev/null
+++ b/drizzle.config.ts
@@ -0,0 +1,24 @@
+import { loadEnvConfig } from "@next/env";
+import { defineConfig } from "drizzle-kit";
+
+loadEnvConfig(process.cwd());
+
+const databaseUrl =
+ process.env.DATABASE_URL_UNPOOLED ?? process.env.DATABASE_URL;
+
+if (databaseUrl === undefined || databaseUrl.length === 0) {
+ throw new Error(
+ "DATABASE_URL_UNPOOLED or DATABASE_URL is required for Drizzle commands."
+ );
+}
+
+export default defineConfig({
+ dbCredentials: {
+ url: databaseUrl,
+ },
+ dialect: "postgresql",
+ out: "./drizzle",
+ schema: "./src/db/schema.ts",
+ strict: true,
+ verbose: true,
+});
diff --git a/drizzle/0000_timeline.sql b/drizzle/0000_timeline.sql
new file mode 100644
index 0000000..fcd6063
--- /dev/null
+++ b/drizzle/0000_timeline.sql
@@ -0,0 +1,78 @@
+CREATE TYPE "public"."timeline_edition_status" AS ENUM('draft', 'published', 'rejected');--> statement-breakpoint
+CREATE TYPE "public"."timeline_sync_status" AS ENUM('running', 'completed', 'failed');--> statement-breakpoint
+CREATE TYPE "public"."timeline_visibility" AS ENUM('public', 'private');--> statement-breakpoint
+CREATE TABLE "timeline_activity_days" (
+ "bucket" varchar(32) NOT NULL,
+ "commit_count" integer NOT NULL,
+ "day" date NOT NULL,
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "language_family" varchar(32) NOT NULL,
+ "privacy_domain_key" varchar(64),
+ "privacy_policy_version" varchar(64),
+ "public_repo_name" varchar(200),
+ "public_repo_url" text,
+ "reached_default_branch" boolean DEFAULT true NOT NULL,
+ "repo_key" varchar(64) NOT NULL,
+ "source" varchar(32) DEFAULT 'github-profile' NOT NULL,
+ "subject" varchar(39) NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "visibility" timeline_visibility NOT NULL,
+ CONSTRAINT "timeline_activity_positive_count" CHECK ("timeline_activity_days"."commit_count" > 0),
+ CONSTRAINT "timeline_activity_visibility_boundary" CHECK ((
+ "timeline_activity_days"."visibility" = 'private'
+ AND "timeline_activity_days"."public_repo_name" IS NULL
+ AND "timeline_activity_days"."public_repo_url" IS NULL
+ AND "timeline_activity_days"."privacy_policy_version" IS NOT NULL
+ ) OR (
+ "timeline_activity_days"."visibility" = 'public'
+ AND "timeline_activity_days"."public_repo_name" IS NOT NULL
+ AND "timeline_activity_days"."public_repo_url" IS NOT NULL
+ AND "timeline_activity_days"."privacy_domain_key" IS NULL
+ AND "timeline_activity_days"."privacy_policy_version" IS NULL
+ ))
+);
+--> statement-breakpoint
+CREATE TABLE "timeline_editions" (
+ "agent_model" varchar(100) NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "edition" jsonb NOT NULL,
+ "edition_key" varchar(64) NOT NULL,
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "published_at" timestamp with time zone,
+ "privacy_policy_version" varchar(64),
+ "status" timeline_edition_status DEFAULT 'draft' NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "window_end" date NOT NULL,
+ "window_start" date NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "timeline_sync_runs" (
+ "completed_at" timestamp with time zone,
+ "coverage" varchar(16) DEFAULT 'partial' NOT NULL,
+ "error_code" varchar(64),
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "full_window" boolean DEFAULT false NOT NULL,
+ "kind" varchar(32) NOT NULL,
+ "row_count" integer DEFAULT 0 NOT NULL,
+ "started_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "status" timeline_sync_status DEFAULT 'running' NOT NULL,
+ "window_end" date NOT NULL,
+ "window_start" date NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "timeline_webhook_receipts" (
+ "delivery_key" varchar(64) PRIMARY KEY NOT NULL,
+ "event_type" varchar(40) NOT NULL,
+ "expires_at" timestamp with time zone NOT NULL,
+ "processed_at" timestamp with time zone,
+ "received_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "status" varchar(24) NOT NULL
+);
+--> statement-breakpoint
+CREATE INDEX "timeline_activity_day_idx" ON "timeline_activity_days" USING btree ("subject","day");--> statement-breakpoint
+CREATE INDEX "timeline_activity_visibility_idx" ON "timeline_activity_days" USING btree ("subject","visibility","day");--> statement-breakpoint
+CREATE UNIQUE INDEX "timeline_activity_repo_day_source_idx" ON "timeline_activity_days" USING btree ("subject","repo_key","day","source");--> statement-breakpoint
+CREATE UNIQUE INDEX "timeline_edition_key_idx" ON "timeline_editions" USING btree ("edition_key");--> statement-breakpoint
+CREATE INDEX "timeline_edition_published_idx" ON "timeline_editions" USING btree ("status","published_at");--> statement-breakpoint
+CREATE INDEX "timeline_sync_started_idx" ON "timeline_sync_runs" USING btree ("started_at");--> statement-breakpoint
+CREATE INDEX "timeline_webhook_expiry_idx" ON "timeline_webhook_receipts" USING btree ("expires_at");
\ No newline at end of file
diff --git a/drizzle/0001_eager_stranger.sql b/drizzle/0001_eager_stranger.sql
new file mode 100644
index 0000000..901b95a
--- /dev/null
+++ b/drizzle/0001_eager_stranger.sql
@@ -0,0 +1,48 @@
+CREATE TYPE "public"."timeline_public_event_kind" AS ENUM('issue_opened', 'pull_request_opened', 'pull_request_reviewed', 'repository_created');--> statement-breakpoint
+CREATE TABLE "timeline_public_events" (
+ "bucket" varchar(32) NOT NULL,
+ "day" date NOT NULL,
+ "event_kind" timeline_public_event_kind NOT NULL,
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "public_repo_name" varchar(200) NOT NULL,
+ "public_repo_url" text NOT NULL,
+ "public_title" varchar(300) NOT NULL,
+ "public_url" text NOT NULL,
+ "repo_key" varchar(64) NOT NULL,
+ "source" varchar(32) DEFAULT 'github-profile' NOT NULL,
+ "subject" varchar(39) NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT "timeline_public_event_identity_shape" CHECK ("timeline_public_events"."id" ~ '^[a-f0-9]{64}$'
+ AND "timeline_public_events"."repo_key" ~ '^[a-f0-9]{64}$'
+ AND "timeline_public_events"."subject" ~ '^[A-Za-z0-9][A-Za-z0-9-]{0,38}$'
+ AND "timeline_public_events"."subject" !~ '--'
+ AND "timeline_public_events"."subject" !~ '-$'
+ AND "timeline_public_events"."public_repo_name" ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/[A-Za-z0-9._-]{1,100}$'),
+ CONSTRAINT "timeline_public_event_public_boundary" CHECK ("timeline_public_events"."public_repo_url" = 'https://github.com/' || "timeline_public_events"."public_repo_name"
+ AND length("timeline_public_events"."public_url") <= 500
+ AND "timeline_public_events"."public_title" = btrim("timeline_public_events"."public_title")
+ AND length("timeline_public_events"."public_title") > 0
+ AND "timeline_public_events"."public_title" !~ '[[:cntrl:]]'
+ AND (
+ ("timeline_public_events"."event_kind" = 'issue_opened'
+ AND substring("timeline_public_events"."public_url" from length("timeline_public_events"."public_repo_url") + 1) ~ '^/issues/[0-9]+$')
+ OR ("timeline_public_events"."event_kind" IN ('pull_request_opened', 'pull_request_reviewed')
+ AND substring("timeline_public_events"."public_url" from length("timeline_public_events"."public_repo_url") + 1) ~ '^/pull/[0-9]+$')
+ OR ("timeline_public_events"."event_kind" = 'repository_created'
+ AND "timeline_public_events"."public_url" = "timeline_public_events"."public_repo_url")
+ )),
+ CONSTRAINT "timeline_public_event_bucket_boundary" CHECK ("timeline_public_events"."bucket" IN (
+ 'Applied AI',
+ 'Open source',
+ 'Product systems',
+ 'Infrastructure',
+ 'Writing'
+ )),
+ CONSTRAINT "timeline_public_event_source_boundary" CHECK ("timeline_public_events"."source" = 'github-profile')
+);
+--> statement-breakpoint
+ALTER TABLE "timeline_sync_runs" ADD COLUMN "event_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
+ALTER TABLE "timeline_sync_runs" ADD COLUMN "public_event_coverage" varchar(16) DEFAULT 'unavailable' NOT NULL;--> statement-breakpoint
+CREATE INDEX "timeline_public_event_day_idx" ON "timeline_public_events" USING btree ("subject","day");--> statement-breakpoint
+CREATE INDEX "timeline_public_event_repo_day_idx" ON "timeline_public_events" USING btree ("subject","repo_key","day");--> statement-breakpoint
+CREATE INDEX "timeline_public_event_kind_day_idx" ON "timeline_public_events" USING btree ("subject","event_kind","day");
\ No newline at end of file
diff --git a/drizzle/0002_warm_killer_shrike.sql b/drizzle/0002_warm_killer_shrike.sql
new file mode 100644
index 0000000..8713d07
--- /dev/null
+++ b/drizzle/0002_warm_killer_shrike.sql
@@ -0,0 +1,19 @@
+CREATE TABLE "timeline_contribution_totals" (
+ "contribution_count" integer NOT NULL,
+ "day" date NOT NULL,
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "source" varchar(32) DEFAULT 'github-public-calendar' NOT NULL,
+ "subject" varchar(39) NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT "timeline_contribution_total_nonnegative_count" CHECK ("timeline_contribution_totals"."contribution_count" >= 0),
+ CONSTRAINT "timeline_contribution_total_identity_shape" CHECK ("timeline_contribution_totals"."id" ~ '^[a-f0-9]{64}$'
+ AND "timeline_contribution_totals"."subject" ~ '^[A-Za-z0-9][A-Za-z0-9-]{0,38}$'
+ AND "timeline_contribution_totals"."subject" !~ '--'
+ AND "timeline_contribution_totals"."subject" !~ '-$'
+ AND "timeline_contribution_totals"."source" = 'github-public-calendar')
+);
+--> statement-breakpoint
+ALTER TABLE "timeline_sync_runs" ADD COLUMN "anonymous_day_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
+ALTER TABLE "timeline_sync_runs" ADD COLUMN "anonymous_coverage" varchar(16) DEFAULT 'unavailable' NOT NULL;--> statement-breakpoint
+CREATE INDEX "timeline_contribution_total_day_idx" ON "timeline_contribution_totals" USING btree ("subject","day");--> statement-breakpoint
+CREATE UNIQUE INDEX "timeline_contribution_total_day_source_idx" ON "timeline_contribution_totals" USING btree ("subject","day","source");
\ No newline at end of file
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
new file mode 100644
index 0000000..8a04f90
--- /dev/null
+++ b/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,522 @@
+{
+ "id": "3a720773-2bd7-49e2-94e9-29b050dbf5dd",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.timeline_activity_days": {
+ "name": "timeline_activity_days",
+ "schema": "",
+ "columns": {
+ "bucket": {
+ "name": "bucket",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "commit_count": {
+ "name": "commit_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "language_family": {
+ "name": "language_family",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "privacy_domain_key": {
+ "name": "privacy_domain_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "privacy_policy_version": {
+ "name": "privacy_policy_version",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_repo_name": {
+ "name": "public_repo_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_repo_url": {
+ "name": "public_repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reached_default_branch": {
+ "name": "reached_default_branch",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "repo_key": {
+ "name": "repo_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github-profile'"
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(39)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "timeline_visibility",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_activity_day_idx": {
+ "name": "timeline_activity_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_activity_visibility_idx": {
+ "name": "timeline_activity_visibility_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_activity_repo_day_source_idx": {
+ "name": "timeline_activity_repo_day_source_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "timeline_activity_positive_count": {
+ "name": "timeline_activity_positive_count",
+ "value": "\"timeline_activity_days\".\"commit_count\" > 0"
+ },
+ "timeline_activity_visibility_boundary": {
+ "name": "timeline_activity_visibility_boundary",
+ "value": "(\n \"timeline_activity_days\".\"visibility\" = 'private'\n AND \"timeline_activity_days\".\"public_repo_name\" IS NULL\n AND \"timeline_activity_days\".\"public_repo_url\" IS NULL\n AND \"timeline_activity_days\".\"privacy_policy_version\" IS NOT NULL\n ) OR (\n \"timeline_activity_days\".\"visibility\" = 'public'\n AND \"timeline_activity_days\".\"public_repo_name\" IS NOT NULL\n AND \"timeline_activity_days\".\"public_repo_url\" IS NOT NULL\n AND \"timeline_activity_days\".\"privacy_domain_key\" IS NULL\n AND \"timeline_activity_days\".\"privacy_policy_version\" IS NULL\n )"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.timeline_editions": {
+ "name": "timeline_editions",
+ "schema": "",
+ "columns": {
+ "agent_model": {
+ "name": "agent_model",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "edition": {
+ "name": "edition",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "edition_key": {
+ "name": "edition_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "privacy_policy_version": {
+ "name": "privacy_policy_version",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "timeline_edition_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "window_end": {
+ "name": "window_end",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "window_start": {
+ "name": "window_start",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_edition_key_idx": {
+ "name": "timeline_edition_key_idx",
+ "columns": [
+ {
+ "expression": "edition_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_edition_published_idx": {
+ "name": "timeline_edition_published_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.timeline_sync_runs": {
+ "name": "timeline_sync_runs",
+ "schema": "",
+ "columns": {
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "coverage": {
+ "name": "coverage",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'partial'"
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "full_window": {
+ "name": "full_window",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_count": {
+ "name": "row_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "status": {
+ "name": "status",
+ "type": "timeline_sync_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "window_end": {
+ "name": "window_end",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "window_start": {
+ "name": "window_start",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_sync_started_idx": {
+ "name": "timeline_sync_started_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.timeline_webhook_receipts": {
+ "name": "timeline_webhook_receipts",
+ "schema": "",
+ "columns": {
+ "delivery_key": {
+ "name": "delivery_key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_webhook_expiry_idx": {
+ "name": "timeline_webhook_expiry_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.timeline_edition_status": {
+ "name": "timeline_edition_status",
+ "schema": "public",
+ "values": ["draft", "published", "rejected"]
+ },
+ "public.timeline_sync_status": {
+ "name": "timeline_sync_status",
+ "schema": "public",
+ "values": ["running", "completed", "failed"]
+ },
+ "public.timeline_visibility": {
+ "name": "timeline_visibility",
+ "schema": "public",
+ "values": ["public", "private"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json
new file mode 100644
index 0000000..7128a1d
--- /dev/null
+++ b/drizzle/meta/0001_snapshot.json
@@ -0,0 +1,727 @@
+{
+ "id": "b27e2e74-54d7-4719-97c8-a2369d7aaf5c",
+ "prevId": "3a720773-2bd7-49e2-94e9-29b050dbf5dd",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.timeline_activity_days": {
+ "name": "timeline_activity_days",
+ "schema": "",
+ "columns": {
+ "bucket": {
+ "name": "bucket",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "commit_count": {
+ "name": "commit_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "language_family": {
+ "name": "language_family",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "privacy_domain_key": {
+ "name": "privacy_domain_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "privacy_policy_version": {
+ "name": "privacy_policy_version",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_repo_name": {
+ "name": "public_repo_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_repo_url": {
+ "name": "public_repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reached_default_branch": {
+ "name": "reached_default_branch",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "repo_key": {
+ "name": "repo_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github-profile'"
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(39)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "timeline_visibility",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_activity_day_idx": {
+ "name": "timeline_activity_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_activity_visibility_idx": {
+ "name": "timeline_activity_visibility_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_activity_repo_day_source_idx": {
+ "name": "timeline_activity_repo_day_source_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "timeline_activity_positive_count": {
+ "name": "timeline_activity_positive_count",
+ "value": "\"timeline_activity_days\".\"commit_count\" > 0"
+ },
+ "timeline_activity_visibility_boundary": {
+ "name": "timeline_activity_visibility_boundary",
+ "value": "(\n \"timeline_activity_days\".\"visibility\" = 'private'\n AND \"timeline_activity_days\".\"public_repo_name\" IS NULL\n AND \"timeline_activity_days\".\"public_repo_url\" IS NULL\n AND \"timeline_activity_days\".\"privacy_policy_version\" IS NOT NULL\n ) OR (\n \"timeline_activity_days\".\"visibility\" = 'public'\n AND \"timeline_activity_days\".\"public_repo_name\" IS NOT NULL\n AND \"timeline_activity_days\".\"public_repo_url\" IS NOT NULL\n AND \"timeline_activity_days\".\"privacy_domain_key\" IS NULL\n AND \"timeline_activity_days\".\"privacy_policy_version\" IS NULL\n )"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.timeline_editions": {
+ "name": "timeline_editions",
+ "schema": "",
+ "columns": {
+ "agent_model": {
+ "name": "agent_model",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "edition": {
+ "name": "edition",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "edition_key": {
+ "name": "edition_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "privacy_policy_version": {
+ "name": "privacy_policy_version",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "timeline_edition_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "window_end": {
+ "name": "window_end",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "window_start": {
+ "name": "window_start",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_edition_key_idx": {
+ "name": "timeline_edition_key_idx",
+ "columns": [
+ {
+ "expression": "edition_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_edition_published_idx": {
+ "name": "timeline_edition_published_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.timeline_public_events": {
+ "name": "timeline_public_events",
+ "schema": "",
+ "columns": {
+ "bucket": {
+ "name": "bucket",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_kind": {
+ "name": "event_kind",
+ "type": "timeline_public_event_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_repo_name": {
+ "name": "public_repo_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_repo_url": {
+ "name": "public_repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_title": {
+ "name": "public_title",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_url": {
+ "name": "public_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repo_key": {
+ "name": "repo_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github-profile'"
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(39)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "timeline_public_event_day_idx": {
+ "name": "timeline_public_event_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_public_event_repo_day_idx": {
+ "name": "timeline_public_event_repo_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_public_event_kind_day_idx": {
+ "name": "timeline_public_event_kind_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "timeline_public_event_identity_shape": {
+ "name": "timeline_public_event_identity_shape",
+ "value": "\"timeline_public_events\".\"id\" ~ '^[a-f0-9]{64}$'\n AND \"timeline_public_events\".\"repo_key\" ~ '^[a-f0-9]{64}$'\n AND \"timeline_public_events\".\"subject\" ~ '^[A-Za-z0-9][A-Za-z0-9-]{0,38}$'\n AND \"timeline_public_events\".\"subject\" !~ '--'\n AND \"timeline_public_events\".\"subject\" !~ '-$'\n AND \"timeline_public_events\".\"public_repo_name\" ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/[A-Za-z0-9._-]{1,100}$'"
+ },
+ "timeline_public_event_public_boundary": {
+ "name": "timeline_public_event_public_boundary",
+ "value": "\"timeline_public_events\".\"public_repo_url\" = 'https://github.com/' || \"timeline_public_events\".\"public_repo_name\"\n AND length(\"timeline_public_events\".\"public_url\") <= 500\n AND \"timeline_public_events\".\"public_title\" = btrim(\"timeline_public_events\".\"public_title\")\n AND length(\"timeline_public_events\".\"public_title\") > 0\n AND \"timeline_public_events\".\"public_title\" !~ '[[:cntrl:]]'\n AND (\n (\"timeline_public_events\".\"event_kind\" = 'issue_opened'\n AND substring(\"timeline_public_events\".\"public_url\" from length(\"timeline_public_events\".\"public_repo_url\") + 1) ~ '^/issues/[0-9]+$')\n OR (\"timeline_public_events\".\"event_kind\" IN ('pull_request_opened', 'pull_request_reviewed')\n AND substring(\"timeline_public_events\".\"public_url\" from length(\"timeline_public_events\".\"public_repo_url\") + 1) ~ '^/pull/[0-9]+$')\n OR (\"timeline_public_events\".\"event_kind\" = 'repository_created'\n AND \"timeline_public_events\".\"public_url\" = \"timeline_public_events\".\"public_repo_url\")\n )"
+ },
+ "timeline_public_event_bucket_boundary": {
+ "name": "timeline_public_event_bucket_boundary",
+ "value": "\"timeline_public_events\".\"bucket\" IN (\n 'Applied AI',\n 'Open source',\n 'Product systems',\n 'Infrastructure',\n 'Writing'\n )"
+ },
+ "timeline_public_event_source_boundary": {
+ "name": "timeline_public_event_source_boundary",
+ "value": "\"timeline_public_events\".\"source\" = 'github-profile'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.timeline_sync_runs": {
+ "name": "timeline_sync_runs",
+ "schema": "",
+ "columns": {
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "coverage": {
+ "name": "coverage",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'partial'"
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_count": {
+ "name": "event_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "full_window": {
+ "name": "full_window",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_event_coverage": {
+ "name": "public_event_coverage",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unavailable'"
+ },
+ "row_count": {
+ "name": "row_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "status": {
+ "name": "status",
+ "type": "timeline_sync_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "window_end": {
+ "name": "window_end",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "window_start": {
+ "name": "window_start",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_sync_started_idx": {
+ "name": "timeline_sync_started_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.timeline_webhook_receipts": {
+ "name": "timeline_webhook_receipts",
+ "schema": "",
+ "columns": {
+ "delivery_key": {
+ "name": "delivery_key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_webhook_expiry_idx": {
+ "name": "timeline_webhook_expiry_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.timeline_edition_status": {
+ "name": "timeline_edition_status",
+ "schema": "public",
+ "values": ["draft", "published", "rejected"]
+ },
+ "public.timeline_public_event_kind": {
+ "name": "timeline_public_event_kind",
+ "schema": "public",
+ "values": [
+ "issue_opened",
+ "pull_request_opened",
+ "pull_request_reviewed",
+ "repository_created"
+ ]
+ },
+ "public.timeline_sync_status": {
+ "name": "timeline_sync_status",
+ "schema": "public",
+ "values": ["running", "completed", "failed"]
+ },
+ "public.timeline_visibility": {
+ "name": "timeline_visibility",
+ "schema": "public",
+ "values": ["public", "private"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..6936417
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,850 @@
+{
+ "id": "0bcc146b-8dc5-49f4-9077-d3cbcbc80713",
+ "prevId": "b27e2e74-54d7-4719-97c8-a2369d7aaf5c",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.timeline_activity_days": {
+ "name": "timeline_activity_days",
+ "schema": "",
+ "columns": {
+ "bucket": {
+ "name": "bucket",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "commit_count": {
+ "name": "commit_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "language_family": {
+ "name": "language_family",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "privacy_domain_key": {
+ "name": "privacy_domain_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "privacy_policy_version": {
+ "name": "privacy_policy_version",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_repo_name": {
+ "name": "public_repo_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_repo_url": {
+ "name": "public_repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reached_default_branch": {
+ "name": "reached_default_branch",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "repo_key": {
+ "name": "repo_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github-profile'"
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(39)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "timeline_visibility",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_activity_day_idx": {
+ "name": "timeline_activity_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_activity_visibility_idx": {
+ "name": "timeline_activity_visibility_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_activity_repo_day_source_idx": {
+ "name": "timeline_activity_repo_day_source_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "timeline_activity_positive_count": {
+ "name": "timeline_activity_positive_count",
+ "value": "\"timeline_activity_days\".\"commit_count\" > 0"
+ },
+ "timeline_activity_visibility_boundary": {
+ "name": "timeline_activity_visibility_boundary",
+ "value": "(\n \"timeline_activity_days\".\"visibility\" = 'private'\n AND \"timeline_activity_days\".\"public_repo_name\" IS NULL\n AND \"timeline_activity_days\".\"public_repo_url\" IS NULL\n AND \"timeline_activity_days\".\"privacy_policy_version\" IS NOT NULL\n ) OR (\n \"timeline_activity_days\".\"visibility\" = 'public'\n AND \"timeline_activity_days\".\"public_repo_name\" IS NOT NULL\n AND \"timeline_activity_days\".\"public_repo_url\" IS NOT NULL\n AND \"timeline_activity_days\".\"privacy_domain_key\" IS NULL\n AND \"timeline_activity_days\".\"privacy_policy_version\" IS NULL\n )"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.timeline_contribution_totals": {
+ "name": "timeline_contribution_totals",
+ "schema": "",
+ "columns": {
+ "contribution_count": {
+ "name": "contribution_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github-public-calendar'"
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(39)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "timeline_contribution_total_day_idx": {
+ "name": "timeline_contribution_total_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_contribution_total_day_source_idx": {
+ "name": "timeline_contribution_total_day_source_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "timeline_contribution_total_nonnegative_count": {
+ "name": "timeline_contribution_total_nonnegative_count",
+ "value": "\"timeline_contribution_totals\".\"contribution_count\" >= 0"
+ },
+ "timeline_contribution_total_identity_shape": {
+ "name": "timeline_contribution_total_identity_shape",
+ "value": "\"timeline_contribution_totals\".\"id\" ~ '^[a-f0-9]{64}$'\n AND \"timeline_contribution_totals\".\"subject\" ~ '^[A-Za-z0-9][A-Za-z0-9-]{0,38}$'\n AND \"timeline_contribution_totals\".\"subject\" !~ '--'\n AND \"timeline_contribution_totals\".\"subject\" !~ '-$'\n AND \"timeline_contribution_totals\".\"source\" = 'github-public-calendar'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.timeline_editions": {
+ "name": "timeline_editions",
+ "schema": "",
+ "columns": {
+ "agent_model": {
+ "name": "agent_model",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "edition": {
+ "name": "edition",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "edition_key": {
+ "name": "edition_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "privacy_policy_version": {
+ "name": "privacy_policy_version",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "timeline_edition_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "window_end": {
+ "name": "window_end",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "window_start": {
+ "name": "window_start",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_edition_key_idx": {
+ "name": "timeline_edition_key_idx",
+ "columns": [
+ {
+ "expression": "edition_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_edition_published_idx": {
+ "name": "timeline_edition_published_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.timeline_public_events": {
+ "name": "timeline_public_events",
+ "schema": "",
+ "columns": {
+ "bucket": {
+ "name": "bucket",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_kind": {
+ "name": "event_kind",
+ "type": "timeline_public_event_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_repo_name": {
+ "name": "public_repo_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_repo_url": {
+ "name": "public_repo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_title": {
+ "name": "public_title",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_url": {
+ "name": "public_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repo_key": {
+ "name": "repo_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github-profile'"
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(39)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "timeline_public_event_day_idx": {
+ "name": "timeline_public_event_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_public_event_repo_day_idx": {
+ "name": "timeline_public_event_repo_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "timeline_public_event_kind_day_idx": {
+ "name": "timeline_public_event_kind_day_idx",
+ "columns": [
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "timeline_public_event_identity_shape": {
+ "name": "timeline_public_event_identity_shape",
+ "value": "\"timeline_public_events\".\"id\" ~ '^[a-f0-9]{64}$'\n AND \"timeline_public_events\".\"repo_key\" ~ '^[a-f0-9]{64}$'\n AND \"timeline_public_events\".\"subject\" ~ '^[A-Za-z0-9][A-Za-z0-9-]{0,38}$'\n AND \"timeline_public_events\".\"subject\" !~ '--'\n AND \"timeline_public_events\".\"subject\" !~ '-$'\n AND \"timeline_public_events\".\"public_repo_name\" ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/[A-Za-z0-9._-]{1,100}$'"
+ },
+ "timeline_public_event_public_boundary": {
+ "name": "timeline_public_event_public_boundary",
+ "value": "\"timeline_public_events\".\"public_repo_url\" = 'https://github.com/' || \"timeline_public_events\".\"public_repo_name\"\n AND length(\"timeline_public_events\".\"public_url\") <= 500\n AND \"timeline_public_events\".\"public_title\" = btrim(\"timeline_public_events\".\"public_title\")\n AND length(\"timeline_public_events\".\"public_title\") > 0\n AND \"timeline_public_events\".\"public_title\" !~ '[[:cntrl:]]'\n AND (\n (\"timeline_public_events\".\"event_kind\" = 'issue_opened'\n AND substring(\"timeline_public_events\".\"public_url\" from length(\"timeline_public_events\".\"public_repo_url\") + 1) ~ '^/issues/[0-9]+$')\n OR (\"timeline_public_events\".\"event_kind\" IN ('pull_request_opened', 'pull_request_reviewed')\n AND substring(\"timeline_public_events\".\"public_url\" from length(\"timeline_public_events\".\"public_repo_url\") + 1) ~ '^/pull/[0-9]+$')\n OR (\"timeline_public_events\".\"event_kind\" = 'repository_created'\n AND \"timeline_public_events\".\"public_url\" = \"timeline_public_events\".\"public_repo_url\")\n )"
+ },
+ "timeline_public_event_bucket_boundary": {
+ "name": "timeline_public_event_bucket_boundary",
+ "value": "\"timeline_public_events\".\"bucket\" IN (\n 'Applied AI',\n 'Open source',\n 'Product systems',\n 'Infrastructure',\n 'Writing'\n )"
+ },
+ "timeline_public_event_source_boundary": {
+ "name": "timeline_public_event_source_boundary",
+ "value": "\"timeline_public_events\".\"source\" = 'github-profile'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.timeline_sync_runs": {
+ "name": "timeline_sync_runs",
+ "schema": "",
+ "columns": {
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "coverage": {
+ "name": "coverage",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'partial'"
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_count": {
+ "name": "event_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "anonymous_day_count": {
+ "name": "anonymous_day_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "anonymous_coverage": {
+ "name": "anonymous_coverage",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unavailable'"
+ },
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "full_window": {
+ "name": "full_window",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "public_event_coverage": {
+ "name": "public_event_coverage",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unavailable'"
+ },
+ "row_count": {
+ "name": "row_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "status": {
+ "name": "status",
+ "type": "timeline_sync_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "window_end": {
+ "name": "window_end",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "window_start": {
+ "name": "window_start",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_sync_started_idx": {
+ "name": "timeline_sync_started_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.timeline_webhook_receipts": {
+ "name": "timeline_webhook_receipts",
+ "schema": "",
+ "columns": {
+ "delivery_key": {
+ "name": "delivery_key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "timeline_webhook_expiry_idx": {
+ "name": "timeline_webhook_expiry_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.timeline_edition_status": {
+ "name": "timeline_edition_status",
+ "schema": "public",
+ "values": ["draft", "published", "rejected"]
+ },
+ "public.timeline_public_event_kind": {
+ "name": "timeline_public_event_kind",
+ "schema": "public",
+ "values": [
+ "issue_opened",
+ "pull_request_opened",
+ "pull_request_reviewed",
+ "repository_created"
+ ]
+ },
+ "public.timeline_sync_status": {
+ "name": "timeline_sync_status",
+ "schema": "public",
+ "values": ["running", "completed", "failed"]
+ },
+ "public.timeline_visibility": {
+ "name": "timeline_visibility",
+ "schema": "public",
+ "values": ["public", "private"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
new file mode 100644
index 0000000..d4c9e27
--- /dev/null
+++ b/drizzle/meta/_journal.json
@@ -0,0 +1,27 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1786553338113,
+ "tag": "0000_timeline",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "7",
+ "when": 1786560424907,
+ "tag": "0001_eager_stranger",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1786609529721,
+ "tag": "0002_warm_killer_shrike",
+ "breakpoints": true
+ }
+ ]
+}
diff --git a/mise.toml b/mise.toml
index 829f6ab..18c6543 100644
--- a/mise.toml
+++ b/mise.toml
@@ -4,7 +4,7 @@ lockfile = true
[tools]
bun = "1.3.14"
hk = "1.48.0"
-node = "26.3.1"
+node = "24.18.0"
pkl = "0.31.1"
typst = "0.15.0"
typstyle = "0.15.0"
diff --git a/next.config.ts b/next.config.ts
index 06739a0..0888e90 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,5 +1,6 @@
import "./src/env";
import createMDX from "@next/mdx";
+import { withEve } from "eve/next";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
@@ -10,6 +11,7 @@ const nextConfig: NextConfig = {
"/*": ["./next.config.ts"],
},
outputFileTracingIncludes: {
+ "/": ["./src/content/**/*"],
"/blog/[slug]": ["./src/content/**/*"],
"/blog/[slug]/markdown": ["./src/content/**/*"],
"/blog/[slug]/opengraph-image": ["./src/content/**/*"],
@@ -75,4 +77,4 @@ const withMDX = createMDX({
},
});
-export default withMDX(nextConfig);
+export default withEve(withMDX(nextConfig));
diff --git a/package.json b/package.json
index a1dba30..95fc50d 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,8 @@
"scripts": {
"dev": "next dev",
"build": "next build",
+ "build:agent": "eve build",
+ "build:local": "eve build && next build",
"start": "next start",
"test": "bun test",
"typecheck": "tsgo --noEmit",
@@ -17,24 +19,35 @@
"format:ox": "oxfmt --write",
"format:check": "oxfmt --check",
"format:typ": "typstyle -i career",
- "format:typ:check": "typstyle --check career"
+ "format:typ:check": "typstyle --check career",
+ "db:generate": "drizzle-kit generate",
+ "db:migrate": "drizzle-kit migrate",
+ "db:studio": "drizzle-kit studio",
+ "timeline:sync": "bun scripts/sync-timeline.ts"
},
"dependencies": {
+ "@ai-sdk/gateway": "4.0.50",
+ "@ai-sdk/openai": "4.0.41",
"@base-ui/react": "^1.6.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@mermaid-js/layout-elk": "0.1.9",
+ "@neondatabase/serverless": "1.1.0",
"@next/mdx": "^16.2.12",
"@remark-embedder/core": "^3.0.3",
"@t3-oss/env-nextjs": "^0.13.11",
"@tailwindcss/typography": "^0.5.20",
+ "ai": "7.0.63",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "drizzle-orm": "0.45.2",
+ "eve": "0.33.3",
"feed": "^5.2.1",
"lucide-react": "^1.21.0",
"mermaid": "^11.16.0",
"next": "^16.2.12",
"next-themes": "^0.4.6",
+ "postgres": "3.4.7",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"reading-time": "^1.5.0",
@@ -51,11 +64,12 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.3.1",
"@types/mdx": "^2.0.14",
- "@types/node": "^26.0.1",
+ "@types/node": "24.13.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260624.1",
"babel-plugin-react-compiler": "1.0.0",
+ "drizzle-kit": "0.31.10",
"oxfmt": "^0.56.0",
"oxlint": "^1.71.0",
"oxlint-tsgolint": "^0.23.0",
@@ -64,5 +78,9 @@
"tw-animate-css": "^1.4.0",
"typescript": "^7.0.1-rc",
"ultracite": "^7.8.3"
- }
+ },
+ "engines": {
+ "node": "24.x"
+ },
+ "packageManager": "bun@1.3.14"
}
diff --git a/scripts/sync-timeline.ts b/scripts/sync-timeline.ts
new file mode 100644
index 0000000..a12c8d4
--- /dev/null
+++ b/scripts/sync-timeline.ts
@@ -0,0 +1,31 @@
+import { closeTimelineDatabase } from "../src/db/client";
+import { syncGitHubTimeline } from "../src/lib/timeline-github";
+
+const forceBackfill = process.argv.includes("--backfill");
+
+try {
+ const result = await syncGitHubTimeline({
+ forceBackfill,
+ kind: forceBackfill ? "backfill" : "manual",
+ });
+ process.stdout.write(
+ `${JSON.stringify({
+ anonymousCoverage: result.anonymousCoverage,
+ anonymousDays: result.anonymousDays,
+ coverage: result.coverage,
+ events: result.events,
+ kind: result.kind,
+ privateActivity: result.privateActivity,
+ rows: result.rows,
+ windowEnd: result.windowEnd,
+ windowStart: result.windowStart,
+ })}\n`
+ );
+} catch {
+ process.stderr.write(
+ "Timeline sync failed. Check configuration and sync_runs.\n"
+ );
+ process.exitCode = 1;
+} finally {
+ await closeTimelineDatabase();
+}
diff --git a/skills-lock.json b/skills-lock.json
new file mode 100644
index 0000000..32ce069
--- /dev/null
+++ b/skills-lock.json
@@ -0,0 +1,17 @@
+{
+ "version": 1,
+ "skills": {
+ "neon": {
+ "source": "neondatabase/agent-skills",
+ "sourceType": "github",
+ "skillPath": "skills/neon/SKILL.md",
+ "computedHash": "6338251b632f9d362c845d0c2a6eb03cf8e5cede98904dee4e3633718d77284f"
+ },
+ "neon-postgres": {
+ "source": "neondatabase/agent-skills",
+ "sourceType": "github",
+ "skillPath": "skills/neon-postgres/SKILL.md",
+ "computedHash": "58b7ffd550cc8a17c86250999cbdd0020bcb839d2389d423227272743c8d2fb8"
+ }
+ }
+}
diff --git a/src/app/(portfolio)/page.tsx b/src/app/(portfolio)/page.tsx
index 0119641..24ddae8 100644
--- a/src/app/(portfolio)/page.tsx
+++ b/src/app/(portfolio)/page.tsx
@@ -1,35 +1,232 @@
+import { ArrowDown, ArrowUpRight, Star } from "lucide-react";
import type { Metadata } from "next";
+import Link from "next/link";
-import { resumeData } from "@/content/resume";
+import { GitHubTimeline } from "@/components/github-timeline";
+import { SiteShell } from "@/components/site-shell";
+import { featuredProjectNames, projectEditorial } from "@/content/home";
+import { getBlogPosts } from "@/lib/blog-utils";
+import { formatDate } from "@/lib/date";
+import { getGitHubProfile } from "@/lib/github-profile";
import { publicUrl, siteConfig } from "@/lib/site";
+import {
+ getPublishedTimelineEdition,
+ resolveTimelineEdition,
+} from "@/lib/timeline";
-import { ResumePageContent } from "./resume/page";
-
-const resumeDescription = siteConfig.description;
+const description =
+ "Sid Jain is an applied AI engineer building useful, durable products and production systems. Explore his open-source work, GitHub activity, and writing.";
export const metadata: Metadata = {
alternates: {
canonical: "/",
},
- description: resumeDescription,
+ description,
openGraph: {
- description: resumeDescription,
- images: [resumeData.person.image],
+ description,
+ images: [
+ {
+ alt: "Sid Jain — Applied AI engineer",
+ height: 630,
+ url: "/opengraph-image",
+ width: 1200,
+ },
+ ],
locale: siteConfig.locale,
siteName: siteConfig.name,
- title: "Sid Jain Résumé",
- type: "profile",
+ title: "Sid Jain — Applied AI engineer",
+ type: "website",
url: publicUrl("/"),
},
- title: "Résumé",
+ title: {
+ absolute: "Sid Jain — Applied AI engineer",
+ },
twitter: {
- card: "summary",
- description: resumeDescription,
- images: [resumeData.person.image],
- title: "Sid Jain Résumé",
+ card: "summary_large_image",
+ description,
+ images: [
+ {
+ alt: "Sid Jain — Applied AI engineer",
+ height: 630,
+ url: "/opengraph-image",
+ width: 1200,
+ },
+ ],
+ title: "Sid Jain — Applied AI engineer",
},
};
-export default function Home() {
- return ;
+export const revalidate = 900;
+
+export default async function Home() {
+ const [github, posts, publishedTimeline] = await Promise.all([
+ getGitHubProfile(),
+ getBlogPosts(),
+ getPublishedTimelineEdition(),
+ ]);
+ const timelineEdition = resolveTimelineEdition(github, publishedTimeline);
+ const projectByName = new Map(
+ github.projects.map((project) => [project.name, project])
+ );
+ const featuredProjects = featuredProjectNames.flatMap((name) => {
+ const project = projectByName.get(name);
+ return project === undefined ? [] : [project];
+ });
+ const projects = featuredProjects.slice(0, 4);
+ const recentPosts = posts.slice(0, 3);
+
+ return (
+
+
+
+
+
+ Building AI products that hold up in the real world.
+
+
+ I’m Sid Jain, an applied AI engineer. I take ambiguous problems
+ from discovery to production—shaping the product, designing the
+ system, and staying for the operational details.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Recent writing
+
+
+ All notes
+
+
+
+ {recentPosts.map((post) => (
+
+
+
+ {post.metadata.title}
+
+
+ {post.metadata.summary}
+
+
+
+ {formatDate(post.date, siteConfig.language)}
+
+ ·
+ {post.readingTime}
+
+
+
+ ))}
+
+
+
+
+
+ );
}
diff --git a/src/app/api/cron/timeline-sync/route.ts b/src/app/api/cron/timeline-sync/route.ts
new file mode 100644
index 0000000..9082d54
--- /dev/null
+++ b/src/app/api/cron/timeline-sync/route.ts
@@ -0,0 +1,37 @@
+import { revalidateTag } from "next/cache";
+import { NextResponse } from "next/server";
+
+import { hasBearerSecret } from "@/lib/request-auth";
+import { syncGitHubTimeline } from "@/lib/timeline-github";
+
+export const dynamic = "force-dynamic";
+export const maxDuration = 300;
+export const runtime = "nodejs";
+
+export async function GET(request: Request) {
+ if (
+ !hasBearerSecret(
+ request.headers.get("authorization"),
+ process.env.CRON_SECRET
+ )
+ ) {
+ return NextResponse.json({ ok: false }, { status: 401 });
+ }
+
+ try {
+ const result = await syncGitHubTimeline();
+ revalidateTag("timeline-edition", "max");
+ return NextResponse.json({
+ anonymousCoverage: result.anonymousCoverage,
+ anonymousDays: result.anonymousDays,
+ coverage: result.coverage,
+ events: result.events,
+ kind: result.kind,
+ ok: true,
+ privateActivity: result.privateActivity,
+ rows: result.rows,
+ });
+ } catch {
+ return NextResponse.json({ ok: false }, { status: 503 });
+ }
+}
diff --git a/src/app/api/github/webhook/route.ts b/src/app/api/github/webhook/route.ts
new file mode 100644
index 0000000..8917191
--- /dev/null
+++ b/src/app/api/github/webhook/route.ts
@@ -0,0 +1,139 @@
+import { createHmac } from "node:crypto";
+
+import { revalidateTag } from "next/cache";
+import { after, NextResponse } from "next/server";
+
+import { isTimelineDatabaseConfigured } from "@/db/client";
+import { constantTimeEqual } from "@/lib/request-auth";
+import { syncGitHubTimeline } from "@/lib/timeline-github";
+import { normalizeTimelinePrivacyKey } from "@/lib/timeline-privacy";
+import {
+ deletePrivateTimelineActivity,
+ deleteTimelineActivityByRepoKey,
+ deleteTimelinePublicEventsByRepoKey,
+ markTimelineWebhookProcessed,
+ pruneTimelineWebhookReceipts,
+ recordTimelineWebhookReceipt,
+ rejectPublishedTimelineEditions,
+} from "@/lib/timeline-store";
+import { timelineRevocationFromWebhook } from "@/lib/timeline-webhook";
+
+const maximumPayloadBytes = 2_000_000;
+const supportedEvents = new Set([
+ "installation",
+ "installation_repositories",
+ "push",
+ "repository",
+]);
+
+export const dynamic = "force-dynamic";
+export const maxDuration = 300;
+export const runtime = "nodejs";
+
+const verifySignature = (
+ body: string,
+ signature: string | null,
+ secret: string
+) => {
+ if (signature === null || !/^sha256=[a-f\d]{64}$/i.test(signature)) {
+ return false;
+ }
+
+ const expected = `sha256=${createHmac("sha256", secret)
+ .update(body, "utf-8")
+ .digest("hex")}`;
+ return constantTimeEqual(signature.toLocaleLowerCase("en-US"), expected);
+};
+
+export async function POST(request: Request) {
+ const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET?.trim();
+ if (
+ webhookSecret === undefined ||
+ webhookSecret.length < 24 ||
+ !isTimelineDatabaseConfigured()
+ ) {
+ return NextResponse.json({ ok: false }, { status: 503 });
+ }
+
+ const declaredLength = Number(request.headers.get("content-length") ?? "0");
+ if (Number.isFinite(declaredLength) && declaredLength > maximumPayloadBytes) {
+ return NextResponse.json({ ok: false }, { status: 413 });
+ }
+
+ const body = await request.text();
+ if (Buffer.byteLength(body, "utf-8") > maximumPayloadBytes) {
+ return NextResponse.json({ ok: false }, { status: 413 });
+ }
+
+ if (
+ !verifySignature(
+ body,
+ request.headers.get("x-hub-signature-256"),
+ webhookSecret
+ )
+ ) {
+ return NextResponse.json({ ok: false }, { status: 401 });
+ }
+
+ const eventType = request.headers.get("x-github-event") ?? "";
+ const deliveryId = request.headers.get("x-github-delivery") ?? "";
+ if (
+ !supportedEvents.has(eventType) ||
+ !/^[a-z\d-]{8,100}$/i.test(deliveryId)
+ ) {
+ return NextResponse.json({ ignored: true, ok: true }, { status: 202 });
+ }
+
+ const privacyKey = normalizeTimelinePrivacyKey(
+ process.env.TIMELINE_PRIVACY_KEY
+ );
+ const receiptKey = createHmac("sha256", privacyKey ?? webhookSecret)
+ .update(`github-delivery:${deliveryId}`, "utf-8")
+ .digest("hex");
+ const revocation = timelineRevocationFromWebhook(body, eventType, privacyKey);
+ const shouldRevoke =
+ revocation.repoKeys.length > 0 || revocation.withdrawAllPrivateActivity;
+
+ try {
+ await pruneTimelineWebhookReceipts();
+ const accepted = await recordTimelineWebhookReceipt({
+ deliveryKey: receiptKey,
+ eventType,
+ expiresAt: new Date(Date.now() + 7 * 86_400_000),
+ });
+
+ if (!accepted) {
+ return NextResponse.json({ duplicate: true, ok: true }, { status: 202 });
+ }
+
+ after(async () => {
+ try {
+ if (shouldRevoke) {
+ if (revocation.withdrawAllPrivateActivity) {
+ await deletePrivateTimelineActivity("f0rr0");
+ }
+ for (const repoKey of revocation.repoKeys) {
+ await Promise.all([
+ deleteTimelineActivityByRepoKey("f0rr0", repoKey),
+ deleteTimelinePublicEventsByRepoKey("f0rr0", repoKey),
+ ]);
+ }
+ await rejectPublishedTimelineEditions();
+ revalidateTag("github-profile", "max");
+ revalidateTag("timeline-edition", "max");
+ }
+ await syncGitHubTimeline({
+ forceBackfill: shouldRevoke,
+ kind: "webhook",
+ });
+ await markTimelineWebhookProcessed(receiptKey);
+ } catch {
+ // The sync run records a non-sensitive failure code for reconciliation.
+ }
+ });
+
+ return NextResponse.json({ accepted: true, ok: true }, { status: 202 });
+ } catch {
+ return NextResponse.json({ ok: false }, { status: 503 });
+ }
+}
diff --git a/src/app/fonts/Geist-Latin.woff2 b/src/app/fonts/Geist-Latin.woff2
new file mode 100644
index 0000000..991445d
Binary files /dev/null and b/src/app/fonts/Geist-Latin.woff2 differ
diff --git a/src/app/fonts/JetBrainsMono-Latin.woff2 b/src/app/fonts/JetBrainsMono-Latin.woff2
new file mode 100644
index 0000000..5858873
Binary files /dev/null and b/src/app/fonts/JetBrainsMono-Latin.woff2 differ
diff --git a/src/app/fonts/Literata-Latin.woff2 b/src/app/fonts/Literata-Latin.woff2
new file mode 100644
index 0000000..be435da
Binary files /dev/null and b/src/app/fonts/Literata-Latin.woff2 differ
diff --git a/src/app/fonts/SourceSans3-Latin.woff2 b/src/app/fonts/SourceSans3-Latin.woff2
new file mode 100644
index 0000000..864cc41
Binary files /dev/null and b/src/app/fonts/SourceSans3-Latin.woff2 differ
diff --git a/src/app/globals.css b/src/app/globals.css
index 7e17c07..362a2e8 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -98,6 +98,326 @@
@apply text-xs font-semibold uppercase tracking-[0.22em] text-primary;
}
+ .home-section {
+ margin-top: clamp(5rem, 8vw, 6rem);
+ scroll-margin-top: 2rem;
+ }
+
+ .activity-week {
+ opacity: 0.12;
+ transform-origin: bottom;
+ }
+
+ .activity-week[data-level="1"] {
+ opacity: 0.35;
+ }
+
+ .activity-week[data-level="2"] {
+ opacity: 0.55;
+ }
+
+ .activity-week[data-level="3"] {
+ opacity: 0.75;
+ }
+
+ .activity-week[data-level="4"] {
+ opacity: 1;
+ }
+
+ .sacred-timeline {
+ --timeline-axis: 0.5rem;
+ position: relative;
+ }
+
+ .sacred-timeline::before {
+ position: absolute;
+ top: 0.5rem;
+ bottom: 0.5rem;
+ left: var(--timeline-axis);
+ width: 1px;
+ background: color-mix(in oklab, var(--primary) 58%, var(--border));
+ content: "";
+ }
+
+ .sacred-timeline-item {
+ --entry-space: 3rem;
+ --node-size: 0.625rem;
+ position: relative;
+ display: grid;
+ grid-template-columns: 1rem minmax(0, 1fr);
+ column-gap: 1rem;
+ }
+
+ .sacred-timeline-item[data-importance="lead"] {
+ --entry-space: 4rem;
+ --node-size: 0.875rem;
+ }
+
+ .sacred-timeline-item[data-importance="compact"] {
+ --entry-space: 2.25rem;
+ --node-size: 0.3125rem;
+ }
+
+ .sacred-timeline-date {
+ grid-column: 2;
+ color: var(--muted-foreground);
+ font-family: var(--font-site-mono);
+ font-size: 0.6875rem;
+ letter-spacing: 0.08em;
+ line-height: 1.5;
+ text-transform: uppercase;
+ }
+
+ .sacred-timeline-node {
+ position: relative;
+ z-index: 1;
+ grid-row: 1 / span 2;
+ grid-column: 1;
+ justify-self: center;
+ width: var(--node-size);
+ height: var(--node-size);
+ margin-top: 0.2rem;
+ border: 2px solid var(--background);
+ border-radius: 999px;
+ background: var(--primary);
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--primary) 76%, var(--border));
+ transition:
+ background-color 180ms ease,
+ box-shadow 180ms ease,
+ transform 180ms ease;
+ }
+
+ .sacred-timeline-node::after {
+ position: absolute;
+ top: calc(50% - 0.5px);
+ left: 100%;
+ width: 1.25rem;
+ height: 1px;
+ background: color-mix(in oklab, var(--primary) 38%, var(--border));
+ content: "";
+ }
+
+ .sacred-timeline-item[data-cadence="streak"] .sacred-timeline-node {
+ width: 0.75rem;
+ height: 1.75rem;
+ margin-top: -0.3rem;
+ }
+
+ .sacred-timeline-item:hover .sacred-timeline-node,
+ .sacred-timeline-item:focus-within .sacred-timeline-node {
+ background: var(--brand-hover);
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--primary) 14%, transparent);
+ transform: scale(1.08);
+ }
+
+ .sacred-timeline-item[data-private="true"] .sacred-timeline-node {
+ background: var(--background);
+ box-shadow: 0 0 0 1px var(--muted-foreground);
+ }
+
+ .sacred-timeline-content {
+ grid-column: 2;
+ padding: 0.35rem 0 var(--entry-space);
+ }
+
+ .sacred-timeline-item:last-child .sacred-timeline-content {
+ padding-bottom: 0;
+ }
+
+ .sacred-timeline-title {
+ margin-top: 0.5rem;
+ color: var(--foreground);
+ font-family: var(--font-site-serif);
+ font-size: 1.45rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ line-height: 1.18;
+ }
+
+ [data-importance="lead"] .sacred-timeline-title {
+ font-size: clamp(1.8rem, 5vw, 2.25rem);
+ letter-spacing: -0.03em;
+ line-height: 1.08;
+ }
+
+ .sacred-timeline-summary {
+ max-width: 42rem;
+ margin-top: 0.6rem;
+ color: var(--muted-foreground);
+ font-size: 0.9375rem;
+ line-height: 1.65;
+ }
+
+ [data-importance="lead"] .sacred-timeline-summary {
+ font-size: 1rem;
+ }
+
+ .sacred-timeline-dispatches {
+ display: grid;
+ border-block: 1px solid var(--border);
+ }
+
+ .sacred-timeline-dispatch {
+ display: grid;
+ grid-template-columns: 3.25rem minmax(0, 1fr);
+ column-gap: 0.75rem;
+ align-items: start;
+ padding-block: 0.875rem;
+ border-top: 1px solid var(--border);
+ }
+
+ .sacred-timeline-dispatch:first-child {
+ border-top: 0;
+ }
+
+ .sacred-timeline-dispatch-title {
+ margin-top: 0.25rem;
+ color: var(--foreground);
+ font-family: var(--font-site-serif);
+ font-size: 1.125rem;
+ font-weight: 700;
+ letter-spacing: -0.015em;
+ line-height: 1.25;
+ }
+
+ .sacred-timeline-dispatch-date {
+ grid-row: 1 / span 2;
+ grid-column: 1;
+ padding-top: 0.05rem;
+ color: var(--muted-foreground);
+ font-family: var(--font-site-mono);
+ font-size: 0.625rem;
+ letter-spacing: 0.08em;
+ line-height: 1.5;
+ text-transform: uppercase;
+ }
+
+ .sacred-timeline-dispatch-kind {
+ grid-row: 1;
+ grid-column: 2;
+ color: var(--primary);
+ font-family: var(--font-site-ui);
+ font-size: 0.625rem;
+ font-weight: 650;
+ letter-spacing: 0.12em;
+ line-height: 1.5;
+ text-transform: uppercase;
+ }
+
+ .sacred-timeline-dispatch-body {
+ min-width: 0;
+ grid-row: 2;
+ grid-column: 2;
+ padding-top: 0.25rem;
+ }
+
+ .sacred-timeline-dispatch-link {
+ display: inline-flex;
+ min-height: 1.5rem;
+ align-items: center;
+ gap: 0.25rem;
+ border-radius: 0.125rem;
+ color: inherit;
+ text-decoration-line: underline;
+ text-decoration-color: var(--border);
+ text-underline-offset: 0.25rem;
+ transition:
+ color 180ms ease,
+ text-decoration-color 180ms ease;
+ }
+
+ .sacred-timeline-dispatch-link:hover {
+ color: var(--brand-hover);
+ text-decoration-color: var(--brand-hover);
+ }
+
+ .sacred-timeline-dispatch-link:focus-visible {
+ outline: 2px solid var(--ring);
+ outline-offset: 0.25rem;
+ }
+
+ .sacred-timeline-dispatch-summary {
+ margin-top: 0.4rem;
+ color: var(--muted-foreground);
+ font-size: 0.875rem;
+ line-height: 1.6;
+ }
+
+ [data-importance="compact"] .sacred-timeline-node {
+ border-width: 1px;
+ }
+
+ @media (min-width: 40rem) {
+ .sacred-timeline {
+ --timeline-axis: 7.6875rem;
+ }
+
+ .sacred-timeline::before {
+ left: var(--timeline-axis);
+ }
+
+ .sacred-timeline-item {
+ grid-template-columns: 6.25rem 1.375rem minmax(0, 1fr);
+ column-gap: 0.75rem;
+ }
+
+ .sacred-timeline-date {
+ grid-row: 1;
+ grid-column: 1;
+ padding-top: 0.05rem;
+ text-align: right;
+ }
+
+ .sacred-timeline-node {
+ grid-row: 1;
+ grid-column: 2;
+ justify-self: center;
+ }
+
+ .sacred-timeline-content {
+ grid-row: 1;
+ grid-column: 3;
+ padding: 0 0 var(--entry-space) 0.75rem;
+ }
+ }
+
+ @media (min-width: 48rem) {
+ .sacred-timeline-dispatch {
+ grid-template-columns: 4.25rem 5.25rem minmax(0, 1fr);
+ column-gap: 0.75rem;
+ }
+
+ .sacred-timeline-dispatch-date {
+ grid-row: 1;
+ grid-column: 1;
+ }
+
+ .sacred-timeline-dispatch-kind {
+ grid-row: 1;
+ grid-column: 2;
+ }
+
+ .sacred-timeline-dispatch-body {
+ grid-row: 1;
+ grid-column: 3;
+ padding-top: 0;
+ }
+ }
+
+ @media (prefers-reduced-motion: no-preference) {
+ .activity-week {
+ animation: activity-rise 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .sacred-timeline-node,
+ .group\/link svg,
+ .sacred-timeline-dispatch-link {
+ transition: none;
+ }
+ }
+
@media (min-width: 40rem) {
.site-container {
padding-inline: 2rem;
@@ -111,6 +431,17 @@
}
}
+@keyframes activity-rise {
+ from {
+ opacity: 0.08;
+ transform: scaleY(0.35);
+ }
+
+ to {
+ transform: scaleY(1);
+ }
+}
+
@layer base {
:root {
color-scheme: light;
@@ -130,6 +461,10 @@
}
body {
+ --font-site-ui: var(--font-site-body);
+ --font-site-mono:
+ ui-monospace, "SFMono-Regular", "Cascadia Code", "Roboto Mono", Menlo,
+ Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-family: var(--font-site-body);
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 18dc30e..3125e95 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,10 +1,5 @@
import type { Metadata } from "next";
-import {
- Geist,
- JetBrains_Mono,
- Literata,
- Source_Sans_3,
-} from "next/font/google";
+import localFont from "next/font/local";
import { JsonLd } from "@/components/json-ld";
import { ThemeProvider } from "@/components/theme-provider";
@@ -13,28 +8,20 @@ import { buildRootJsonLd } from "@/lib/structured-data";
import "./globals.css";
-const literata = Literata({
- subsets: ["latin"],
+const literata = localFont({
+ display: "swap",
+ preload: true,
+ src: "./fonts/Literata-Latin.woff2",
variable: "--font-site-heading",
- weight: ["400", "700"],
+ weight: "400 700",
});
-const sourceSans = Source_Sans_3({
- subsets: ["latin"],
+const sourceSans = localFont({
+ display: "swap",
+ preload: true,
+ src: "./fonts/SourceSans3-Latin.woff2",
variable: "--font-site-body",
- weight: ["400", "600", "700"],
-});
-
-const geist = Geist({
- subsets: ["latin"],
- variable: "--font-site-ui",
-});
-
-const jetBrainsMono = JetBrains_Mono({
- preload: false,
- subsets: ["latin"],
- variable: "--font-site-mono",
- weight: ["400"],
+ weight: "400 700",
});
export const metadata: Metadata = {
@@ -106,7 +93,7 @@ export default function RootLayout({
return (
diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx
new file mode 100644
index 0000000..05c2ce0
--- /dev/null
+++ b/src/app/opengraph-image.tsx
@@ -0,0 +1,106 @@
+import { ImageResponse } from "next/og";
+
+export const alt = "Sid Jain — Applied AI engineer";
+export const size = {
+ height: 630,
+ width: 1200,
+};
+export const contentType = "image/png";
+
+export default function Image() {
+ return new ImageResponse(
+
+
+ Sid Jain
+
+ f0rr0.dev
+
+
+
+
+ Building AI products that hold up in the real world.
+
+
+
+
+
+
+
+
+
+ Applied AI · product systems · open source · writing
+
+
,
+ size
+ );
+}
diff --git a/src/components/github-timeline.tsx b/src/components/github-timeline.tsx
new file mode 100644
index 0000000..75e05f2
--- /dev/null
+++ b/src/components/github-timeline.tsx
@@ -0,0 +1,502 @@
+import { ArrowUpRight, LockKeyhole } from "lucide-react";
+
+import { formatDate } from "@/lib/date";
+import type {
+ GitHubActivity,
+ GitHubActivityWeek,
+} from "@/lib/github-profile-core";
+import { siteConfig } from "@/lib/site";
+import type {
+ TimelineEdition,
+ TimelineEditionEntry,
+} from "@/lib/timeline-core";
+
+const numberFormatter = new Intl.NumberFormat("en-US");
+const monthFormatter = new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ timeZone: "UTC",
+ year: "numeric",
+});
+const monthOnlyFormatter = new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ timeZone: "UTC",
+});
+const dayFormatter = new Intl.DateTimeFormat("en-US", {
+ day: "2-digit",
+ month: "short",
+ timeZone: "UTC",
+});
+const WEEK_IN_MILLISECONDS = 7 * 86_400_000;
+
+const asUtcDate = (date: string) => new Date(`${date}T00:00:00Z`);
+
+const formatTimelineRange = (start: string, end: string) => {
+ if (start.slice(0, 7) === end.slice(0, 7)) {
+ return monthFormatter.format(asUtcDate(end));
+ }
+
+ const startDate = asUtcDate(start);
+ const endDate = asUtcDate(end);
+ if (startDate.getUTCFullYear() === endDate.getUTCFullYear()) {
+ return `${monthOnlyFormatter.format(startDate)}—${monthFormatter.format(
+ endDate
+ )}`;
+ }
+ return `${monthFormatter.format(startDate)}—${monthFormatter.format(endDate)}`;
+};
+
+const isCompact = (entry: TimelineEditionEntry) =>
+ entry.kind !== "activity" &&
+ (entry.importance === "brief" || entry.importance === "pulse");
+
+const isRenderable = (entry: TimelineEditionEntry) =>
+ entry.kind !== "activity" || entry.cadence !== "isolated";
+
+const activityWeekStats = (weeks: readonly GitHubActivityWeek[]) => {
+ let activeWeeks = 0;
+ let currentRun = 0;
+ let longestRun = 0;
+ let previousWeekStart: number | undefined;
+
+ for (const week of weeks.toSorted((left, right) =>
+ left.weekStart.localeCompare(right.weekStart)
+ )) {
+ const weekStart = Date.parse(`${week.weekStart}T00:00:00Z`);
+ const followsPrevious =
+ previousWeekStart !== undefined &&
+ weekStart - previousWeekStart === WEEK_IN_MILLISECONDS;
+
+ if (week.contributionCount > 0) {
+ activeWeeks += 1;
+ currentRun = followsPrevious ? currentRun + 1 : 1;
+ longestRun = Math.max(longestRun, currentRun);
+ } else {
+ currentRun = 0;
+ }
+ previousWeekStart = weekStart;
+ }
+
+ return { activeWeeks, longestRun };
+};
+
+type TimelineBlock =
+ | { entry: TimelineEditionEntry; type: "major" }
+ | {
+ entries: TimelineEditionEntry[];
+ key: string;
+ month: string;
+ type: "dispatches";
+ };
+
+const timelineBlocksFrom = (
+ entries: readonly TimelineEditionEntry[]
+): TimelineBlock[] => {
+ const sortedEntries = entries
+ .filter(isRenderable)
+ .toSorted(
+ (left, right) =>
+ right.endDate.localeCompare(left.endDate) ||
+ right.startDate.localeCompare(left.startDate) ||
+ left.id.localeCompare(right.id)
+ );
+ const dispatchesByMonth = new Map();
+ for (const entry of sortedEntries) {
+ if (!isCompact(entry)) {
+ continue;
+ }
+ const month = entry.endDate.slice(0, 7);
+ dispatchesByMonth.set(month, [
+ ...(dispatchesByMonth.get(month) ?? []),
+ entry,
+ ]);
+ }
+
+ const blocks: TimelineBlock[] = [];
+ const emittedDispatchMonths = new Set();
+ for (const entry of sortedEntries) {
+ if (!isCompact(entry)) {
+ blocks.push({ entry, type: "major" });
+ continue;
+ }
+
+ const month = entry.endDate.slice(0, 7);
+ if (emittedDispatchMonths.has(month)) {
+ continue;
+ }
+ emittedDispatchMonths.add(month);
+ blocks.push({
+ entries: dispatchesByMonth.get(month) ?? [entry],
+ key: `dispatches-${month}`,
+ month,
+ type: "dispatches",
+ });
+ }
+ return blocks;
+};
+
+function ActivitySignal({ activity }: Readonly<{ activity: GitHubActivity }>) {
+ if (activity.status === "unavailable") {
+ return (
+
+ GitHub activity is temporarily unavailable. The selected public work
+ remains available elsewhere on this page.
+
+ );
+ }
+
+ const { activeWeeks, longestRun } = activityWeekStats(activity.weeks);
+
+ return (
+
+
+
+
+ Last twelve months
+
+
+ {numberFormatter.format(activity.totalContributions)} contributions
+
+
+ Across {numberFormatter.format(activity.activeDays)} active days.{" "}
+ {activity.restrictedContributions === null ? (
+ <>
+ Public and private activity are combined. Private identifiers
+ are discarded before storage or editorial processing.
+ >
+ ) : (
+ <>
+ {numberFormatter.format(activity.restrictedContributions)} came
+ from private work; only privacy-safe aggregate patterns can
+ enter the edition.
+ >
+ )}
+
+
+
+
Active weeks
+
+ {numberFormatter.format(activeWeeks)}
+
+
+
+
Longest weekly run
+
+ {numberFormatter.format(longestRun)}
+
+
+
+
+
+ GitHub profile
+ (opens in a new tab)
+
+
+
+
+ {activity.weeks.map((week) => (
+
+ ))}
+
+
+ {formatDate(new Date(activity.from), siteConfig.language)}
+ {formatDate(new Date(activity.to), siteConfig.language)}
+
+
+ );
+}
+
+function EntryLink({ entry }: Readonly<{ entry: TimelineEditionEntry }>) {
+ if (entry.href === undefined || entry.label === undefined) {
+ return null;
+ }
+
+ const external = entry.href.startsWith("http");
+ return (
+
+ {entry.label}
+ {external ? (
+ <>
+ (opens in a new tab)
+
+ >
+ ) : null}
+
+ );
+}
+
+function VisibilityLabel({ entry }: Readonly<{ entry: TimelineEditionEntry }>) {
+ if (entry.visibility === "public") {
+ return null;
+ }
+ return (
+
+
+ {entry.visibility === "mixed"
+ ? "partly anonymized"
+ : entry.visibility === "anonymous"
+ ? "anonymized totals"
+ : "anonymized"}
+
+ );
+}
+
+function MajorEntry({ entry }: Readonly<{ entry: TimelineEditionEntry }>) {
+ const editorialLabel =
+ entry.cadence === "streak"
+ ? "streak"
+ : entry.kind === "activity"
+ ? "trend"
+ : entry.importance;
+
+ return (
+
+
+ {formatTimelineRange(entry.startDate, entry.endDate)}
+
+
+
+
+
+ {editorialLabel} · {entry.bucket}
+
+
+
+ {entry.title}
+ {entry.description}
+ {entry.metrics.length === 0 ? null : (
+
+ {entry.metrics.map((metric) => (
+ {metric}
+ ))}
+
+ )}
+
+
+
+ );
+}
+
+const dispatchKindLabels = {
+ activity: "Activity",
+ issue: "Issue",
+ project: "Project",
+ "pull-request": "PR",
+} as const satisfies Record;
+
+const repositoryLabelFrom = (entry: TimelineEditionEntry) => {
+ if (
+ (entry.kind !== "issue" && entry.kind !== "pull-request") ||
+ entry.href === undefined
+ ) {
+ return null;
+ }
+ try {
+ const url = new URL(entry.href);
+ const segments = url.pathname.split("/").filter(Boolean);
+ return url.hostname === "github.com" && segments.length >= 2
+ ? `${segments[0]}/${segments[1]}`
+ : null;
+ } catch {
+ return null;
+ }
+};
+
+function DispatchTitle({ entry }: Readonly<{ entry: TimelineEditionEntry }>) {
+ if (entry.href === undefined) {
+ return <>{entry.title}>;
+ }
+
+ const external = entry.href.startsWith("http");
+ return (
+
+ {entry.title}
+ {external ? (
+ <>
+ (opens in a new tab)
+
+ >
+ ) : null}
+
+ );
+}
+
+function DispatchGroup({
+ block,
+}: Readonly<{
+ block: Extract;
+}>) {
+ const hasProtectedEntry = block.entries.some(
+ (entry) => entry.visibility !== "public"
+ );
+ const label = monthFormatter.format(asUtcDate(`${block.month}-01`));
+ return (
+
+
+ {label}
+
+
+
+
+ Dispatches
+
+
+ {block.entries.map((entry) => {
+ const isPublicEvent =
+ entry.kind === "issue" || entry.kind === "pull-request";
+ const dispatchDate =
+ entry.visibility === "public"
+ ? dayFormatter.format(asUtcDate(entry.endDate))
+ : monthOnlyFormatter.format(asUtcDate(entry.endDate));
+ const repositoryLabel = repositoryLabelFrom(entry);
+
+ return (
+
+
+ {dispatchDate}
+
+
+ {dispatchKindLabels[entry.kind]}
+
+
+
+ {entry.bucket}
+ {repositoryLabel === null ? null : (
+
+ · {repositoryLabel}
+
+ )}
+
+
+
+
+
+ {isPublicEvent ? null : (
+
+ {entry.description}
+
+ )}
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+export function GitHubTimeline({
+ activity,
+ edition,
+}: Readonly<{
+ activity: GitHubActivity;
+ edition: TimelineEdition | null;
+}>) {
+ const blocks = edition === null ? [] : timelineBlocksFrom(edition.entries);
+ const sectionTitle = edition?.headline ?? "The work, along one line.";
+
+ return (
+
+
+
+ Rolling edition
+ {edition === null
+ ? null
+ : ` · ${formatTimelineRange(
+ edition.windowStart,
+ edition.windowEnd
+ )}`}
+
+
+ {sectionTitle}
+
+
+ {edition?.standfirst ??
+ "The live edition is rebuilding; public projects and writing remain available below."}
+
+ {edition === null ? null : (
+
+ Updated{" "}
+
+ {monthFormatter.format(new Date(edition.generatedAt))}
+
+
+ )}
+
+
+ {edition === null ? null : (
+
+ {blocks.map((block) =>
+ block.type === "major" ? (
+
+ ) : (
+
+ )
+ )}
+
+ )}
+
+ );
+}
diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx
new file mode 100644
index 0000000..73014a9
--- /dev/null
+++ b/src/components/site-footer.tsx
@@ -0,0 +1,63 @@
+import Link from "next/link";
+
+import { resumeData } from "@/content/resume";
+
+const footerLinks = [
+ { href: "https://github.com/f0rr0", label: "GitHub", external: true },
+ { href: "https://linkedin.com/in/f0rr0", label: "LinkedIn", external: true },
+ {
+ href: `mailto:${resumeData.person.email}`,
+ label: "Email",
+ external: false,
+ },
+ { href: "/resume", label: "Résumé", external: false },
+ { href: "/rss.xml", label: "RSS", external: false },
+] as const;
+
+export function SiteFooter(): React.ReactNode {
+ return (
+
+
+
+
+ {resumeData.person.name}
+
+
+ Applied AI engineer building useful, durable systems from Mumbai.
+
+
+
+
+
+ {footerLinks.map((item) => (
+
+ {item.href.startsWith("/") ? (
+
+ {item.label}
+
+ ) : (
+
+ {item.label}
+
+ )}
+
+ ))}
+
+
+
+ © {new Date().getUTCFullYear()} Sid Jain
+
+
+
+
+ );
+}
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index 9c6632b..40e33a5 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -7,13 +7,13 @@ import { resumeData } from "@/content/resume";
import { SiteMobileMenu } from "./site-mobile-menu";
interface SiteHeaderProps {
- activeHref: "/blog" | "/resume";
+ activeHref?: "/blog" | "/resume";
currentPath?: "/" | "/blog" | "/resume";
}
export function SiteHeader({
activeHref,
- currentPath = activeHref,
+ currentPath = activeHref ?? "/",
}: Readonly): React.ReactNode {
return (
@@ -24,7 +24,9 @@ export function SiteHeader({
>
diff --git a/src/components/site-mobile-menu.tsx b/src/components/site-mobile-menu.tsx
index abef31b..901bd85 100644
--- a/src/components/site-mobile-menu.tsx
+++ b/src/components/site-mobile-menu.tsx
@@ -8,7 +8,7 @@ export function SiteMobileMenu({
currentPath,
navItems,
}: Readonly<{
- activeHref: "/blog" | "/resume";
+ activeHref?: "/blog" | "/resume";
currentPath: "/" | "/blog" | "/resume";
navItems: readonly ResumeNavItem[];
}>) {
@@ -43,6 +43,7 @@ export function SiteMobileMenu({
) : (
diff --git a/src/components/site-shell.tsx b/src/components/site-shell.tsx
index 8d6e152..909cec0 100644
--- a/src/components/site-shell.tsx
+++ b/src/components/site-shell.tsx
@@ -1,22 +1,26 @@
import type { ReactNode } from "react";
+import { SiteFooter } from "@/components/site-footer";
import { SiteHeader } from "@/components/site-header";
interface SiteShellProps {
- activeHref: "/blog" | "/resume";
+ activeHref?: "/blog" | "/resume";
children: ReactNode;
currentPath?: "/" | "/blog" | "/resume";
+ includeFooter?: boolean;
}
export function SiteShell({
activeHref,
children,
- currentPath = activeHref,
+ currentPath = activeHref ?? "/",
+ includeFooter = false,
}: Readonly) {
return (
{children}
+ {includeFooter ? : null}
);
}
diff --git a/src/content/home.ts b/src/content/home.ts
new file mode 100644
index 0000000..52907aa
--- /dev/null
+++ b/src/content/home.ts
@@ -0,0 +1,136 @@
+import type { TimelineImportance, WorkBucket } from "@/lib/timeline-core";
+
+export interface TimelineEntry {
+ bucket: WorkBucket;
+ date: string;
+ description: string;
+ href: string;
+ importance?: TimelineImportance;
+ label: string;
+ private?: boolean;
+ title: string;
+}
+
+export const timelineEntries = [
+ {
+ bucket: "Open source",
+ date: "2026-08-12",
+ description:
+ "Making embedded Postgres feel as ordinary as opening a SQLite database—without Docker, Node.js, or a server.",
+ href: "https://github.com/f0rr0/oliphaunt",
+ importance: "lead",
+ label: "Explore oliphaunt",
+ title: "Shipping oliphaunt",
+ },
+ {
+ bucket: "Open source",
+ date: "2026-07-19",
+ description:
+ "A public Rust client built while mapping an opaque mobile protocol into a small, inspectable interface.",
+ href: "https://github.com/f0rr0/hinge-rs",
+ importance: "lead",
+ label: "View hinge-rs",
+ title: "Reverse-engineering in Rust",
+ },
+ {
+ bucket: "Applied AI",
+ date: "2026-04-21",
+ description:
+ "An MCP server and scheduler that lets an agent watch a scarce appointment queue and return only when action is possible.",
+ href: "https://github.com/f0rr0/tranquilo",
+ importance: "story",
+ label: "View Tranquilo",
+ title: "Teaching an agent when to act",
+ },
+ {
+ bucket: "Applied AI",
+ date: "2026-03-16",
+ description:
+ "A household meal-planning system that turns pantry photos, order history, preferences, and group decisions into useful state.",
+ href: "https://github.com/f0rr0/zeroclaw/pull/8",
+ importance: "story",
+ label: "View the ZeroClaw work",
+ title: "Teaching an agent what is for lunch",
+ },
+ {
+ bucket: "Open source",
+ date: "2026-07-28",
+ description:
+ "Returning to a long-lived React Native component to keep a small public primitive useful and dependable.",
+ href: "https://github.com/f0rr0/react-native-rating",
+ importance: "brief",
+ label: "View react-native-rating",
+ title: "Maintaining the work that lasts",
+ },
+ {
+ bucket: "Product systems",
+ date: "2026-02-01",
+ description:
+ "Reworking this site as a deliberate product surface: faster navigation, clearer writing, and better structured context.",
+ href: "https://github.com/f0rr0/f0rr0.dev",
+ importance: "brief",
+ label: "View the site source",
+ title: "Turning the portfolio into a product",
+ },
+ {
+ bucket: "Product systems",
+ date: "2025-12-22",
+ description:
+ "Exploring payment-gated routes as a compact protocol experiment, with the public implementation kept small enough to inspect.",
+ href: "https://github.com/f0rr0/route-402",
+ importance: "brief",
+ label: "View route-402",
+ title: "Experimenting at the edge of HTTP",
+ },
+ {
+ bucket: "Applied AI",
+ date: "2025-01-01",
+ description:
+ "Taking customer workflows from discovery and evaluation through implementation and support. The activity is counted; private repository details are not requested.",
+ href: "/resume",
+ importance: "story",
+ label: "See my experience",
+ private: true,
+ title: "Leading applied AI at Namefi",
+ },
+ {
+ bucket: "Writing",
+ date: "2018-12-23",
+ description:
+ "A personal archive for explaining the decisions, trade-offs, and strange systems behind the code—not just announcing the result.",
+ href: "/blog",
+ importance: "story",
+ label: "Read the notes",
+ title: "Writing the work down",
+ },
+] as const satisfies readonly TimelineEntry[];
+
+export const projectEditorial = {
+ "f0rr0.dev": {
+ bucket: "Product systems" as const,
+ description:
+ "This site: a fast, accessible home for the work, writing, and context behind both.",
+ },
+ oliphaunt: {
+ bucket: "Open source" as const,
+ description:
+ "Embedded PostgreSQL for apps and tests, packaged to feel as simple as SQLite.",
+ },
+ "pg-browser-proxy": {
+ bucket: "Product systems" as const,
+ description:
+ "A small bridge that lets desktop database clients inspect Postgres running inside a browser.",
+ },
+ "react-native-rating": {
+ bucket: "Open source" as const,
+ description:
+ "An accessible, native-driver rating component that has quietly served React Native apps for years.",
+ },
+} as const;
+
+export const featuredProjectNames = [
+ "oliphaunt",
+ "react-native-rating",
+ "f0rr0.dev",
+ "pg-browser-proxy",
+] as const;
diff --git a/src/db/client.ts b/src/db/client.ts
new file mode 100644
index 0000000..6fa8e37
--- /dev/null
+++ b/src/db/client.ts
@@ -0,0 +1,71 @@
+import { neon } from "@neondatabase/serverless";
+import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";
+import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
+import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+
+import * as schema from "@/db/schema";
+
+export class TimelineDatabaseConfigurationError extends Error {
+ constructor() {
+ super("DATABASE_URL is not configured for the timeline store.");
+ this.name = "TimelineDatabaseConfigurationError";
+ }
+}
+
+type TimelineDatabase = PostgresJsDatabase;
+
+let database: TimelineDatabase | null = null;
+let localClient: ReturnType | null = null;
+
+const readDatabaseUrl = () => {
+ const value = process.env.DATABASE_URL?.trim();
+ return value === undefined || value.length === 0 ? null : value;
+};
+
+const isLocalDatabaseUrl = (databaseUrl: string) => {
+ try {
+ const { hostname } = new URL(databaseUrl);
+ return (
+ hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost"
+ );
+ } catch {
+ return false;
+ }
+};
+
+const createDatabase = (): TimelineDatabase => {
+ const databaseUrl = readDatabaseUrl();
+ if (databaseUrl === null) {
+ throw new TimelineDatabaseConfigurationError();
+ }
+
+ if (isLocalDatabaseUrl(databaseUrl)) {
+ localClient = postgres(databaseUrl, { max: 1, prepare: false });
+ return drizzlePostgres({
+ client: localClient,
+ schema,
+ });
+ }
+
+ return drizzleNeon({
+ client: neon(databaseUrl),
+ schema,
+ }) as unknown as TimelineDatabase;
+};
+
+export const isTimelineDatabaseConfigured = () => readDatabaseUrl() !== null;
+
+export const getTimelineDatabase = () => {
+ database ??= createDatabase();
+ return database;
+};
+
+export const closeTimelineDatabase = async () => {
+ const client = localClient;
+ database = null;
+ localClient = null;
+ if (client !== null) {
+ await client.end({ timeout: 5 });
+ }
+};
diff --git a/src/db/schema.ts b/src/db/schema.ts
new file mode 100644
index 0000000..5187c02
--- /dev/null
+++ b/src/db/schema.ts
@@ -0,0 +1,299 @@
+import { sql } from "drizzle-orm";
+import {
+ boolean,
+ check,
+ date,
+ index,
+ integer,
+ jsonb,
+ pgEnum,
+ pgTable,
+ text,
+ timestamp,
+ uniqueIndex,
+ uuid,
+ varchar,
+} from "drizzle-orm/pg-core";
+
+import type { TimelineEdition } from "@/lib/timeline-core";
+
+export const timelineVisibility = pgEnum("timeline_visibility", [
+ "public",
+ "private",
+]);
+
+export const timelineEditionStatus = pgEnum("timeline_edition_status", [
+ "draft",
+ "published",
+ "rejected",
+]);
+
+export const timelineSyncStatus = pgEnum("timeline_sync_status", [
+ "running",
+ "completed",
+ "failed",
+]);
+
+export const timelinePublicEventKinds = [
+ "issue_opened",
+ "pull_request_opened",
+ "pull_request_reviewed",
+ "repository_created",
+] as const;
+
+export type TimelinePublicEventKind = (typeof timelinePublicEventKinds)[number];
+
+export const timelinePublicEventKind = pgEnum(
+ "timeline_public_event_kind",
+ timelinePublicEventKinds
+);
+
+export const timelineActivityDays = pgTable(
+ "timeline_activity_days",
+ {
+ bucket: varchar("bucket", { length: 32 }).notNull(),
+ commitCount: integer("commit_count").notNull(),
+ day: date("day", { mode: "string" }).notNull(),
+ id: varchar("id", { length: 64 }).primaryKey(),
+ languageFamily: varchar("language_family", { length: 32 }).notNull(),
+ privacyDomainKey: varchar("privacy_domain_key", { length: 64 }),
+ privacyPolicyVersion: varchar("privacy_policy_version", { length: 64 }),
+ publicRepoName: varchar("public_repo_name", { length: 200 }),
+ publicRepoUrl: text("public_repo_url"),
+ reachedDefaultBranch: boolean("reached_default_branch")
+ .default(true)
+ .notNull(),
+ repoKey: varchar("repo_key", { length: 64 }).notNull(),
+ source: varchar("source", { length: 32 })
+ .default("github-profile")
+ .notNull(),
+ subject: varchar("subject", { length: 39 }).notNull(),
+ updatedAt: timestamp("updated_at", { mode: "date", withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ visibility: timelineVisibility("visibility").notNull(),
+ },
+ (table) => [
+ index("timeline_activity_day_idx").on(table.subject, table.day),
+ index("timeline_activity_visibility_idx").on(
+ table.subject,
+ table.visibility,
+ table.day
+ ),
+ uniqueIndex("timeline_activity_repo_day_source_idx").on(
+ table.subject,
+ table.repoKey,
+ table.day,
+ table.source
+ ),
+ check("timeline_activity_positive_count", sql`${table.commitCount} > 0`),
+ check(
+ "timeline_activity_visibility_boundary",
+ sql`(
+ ${table.visibility} = 'private'
+ AND ${table.publicRepoName} IS NULL
+ AND ${table.publicRepoUrl} IS NULL
+ AND ${table.privacyPolicyVersion} IS NOT NULL
+ ) OR (
+ ${table.visibility} = 'public'
+ AND ${table.publicRepoName} IS NOT NULL
+ AND ${table.publicRepoUrl} IS NOT NULL
+ AND ${table.privacyDomainKey} IS NULL
+ AND ${table.privacyPolicyVersion} IS NULL
+ )`
+ ),
+ ]
+);
+
+export const timelineContributionTotals = pgTable(
+ "timeline_contribution_totals",
+ {
+ contributionCount: integer("contribution_count").notNull(),
+ day: date("day", { mode: "string" }).notNull(),
+ id: varchar("id", { length: 64 }).primaryKey(),
+ source: varchar("source", { length: 32 })
+ .default("github-public-calendar")
+ .notNull(),
+ subject: varchar("subject", { length: 39 }).notNull(),
+ updatedAt: timestamp("updated_at", { mode: "date", withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ },
+ (table) => [
+ index("timeline_contribution_total_day_idx").on(table.subject, table.day),
+ uniqueIndex("timeline_contribution_total_day_source_idx").on(
+ table.subject,
+ table.day,
+ table.source
+ ),
+ check(
+ "timeline_contribution_total_nonnegative_count",
+ sql`${table.contributionCount} >= 0`
+ ),
+ check(
+ "timeline_contribution_total_identity_shape",
+ sql`${table.id} ~ '^[a-f0-9]{64}$'
+ AND ${table.subject} ~ '^[A-Za-z0-9][A-Za-z0-9-]{0,38}$'
+ AND ${table.subject} !~ '--'
+ AND ${table.subject} !~ '-$'
+ AND ${table.source} = 'github-public-calendar'`
+ ),
+ ]
+);
+
+export const timelinePublicEvents = pgTable(
+ "timeline_public_events",
+ {
+ bucket: varchar("bucket", { length: 32 }).notNull(),
+ day: date("day", { mode: "string" }).notNull(),
+ eventKind: timelinePublicEventKind("event_kind").notNull(),
+ id: varchar("id", { length: 64 }).primaryKey(),
+ publicRepoName: varchar("public_repo_name", { length: 200 }).notNull(),
+ publicRepoUrl: text("public_repo_url").notNull(),
+ publicTitle: varchar("public_title", { length: 300 }).notNull(),
+ publicUrl: text("public_url").notNull(),
+ repoKey: varchar("repo_key", { length: 64 }).notNull(),
+ source: varchar("source", { length: 32 })
+ .default("github-profile")
+ .notNull(),
+ subject: varchar("subject", { length: 39 }).notNull(),
+ updatedAt: timestamp("updated_at", { mode: "date", withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ },
+ (table) => [
+ index("timeline_public_event_day_idx").on(table.subject, table.day),
+ index("timeline_public_event_repo_day_idx").on(
+ table.subject,
+ table.repoKey,
+ table.day
+ ),
+ index("timeline_public_event_kind_day_idx").on(
+ table.subject,
+ table.eventKind,
+ table.day
+ ),
+ check(
+ "timeline_public_event_identity_shape",
+ sql`${table.id} ~ '^[a-f0-9]{64}$'
+ AND ${table.repoKey} ~ '^[a-f0-9]{64}$'
+ AND ${table.subject} ~ '^[A-Za-z0-9][A-Za-z0-9-]{0,38}$'
+ AND ${table.subject} !~ '--'
+ AND ${table.subject} !~ '-$'
+ AND ${table.publicRepoName} ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?/[A-Za-z0-9._-]{1,100}$'`
+ ),
+ check(
+ "timeline_public_event_public_boundary",
+ sql`${table.publicRepoUrl} = 'https://github.com/' || ${table.publicRepoName}
+ AND length(${table.publicUrl}) <= 500
+ AND ${table.publicTitle} = btrim(${table.publicTitle})
+ AND length(${table.publicTitle}) > 0
+ AND ${table.publicTitle} !~ '[[:cntrl:]]'
+ AND (
+ (${table.eventKind} = 'issue_opened'
+ AND substring(${table.publicUrl} from length(${table.publicRepoUrl}) + 1) ~ '^/issues/[0-9]+$')
+ OR (${table.eventKind} IN ('pull_request_opened', 'pull_request_reviewed')
+ AND substring(${table.publicUrl} from length(${table.publicRepoUrl}) + 1) ~ '^/pull/[0-9]+$')
+ OR (${table.eventKind} = 'repository_created'
+ AND ${table.publicUrl} = ${table.publicRepoUrl})
+ )`
+ ),
+ check(
+ "timeline_public_event_bucket_boundary",
+ sql`${table.bucket} IN (
+ 'Applied AI',
+ 'Open source',
+ 'Product systems',
+ 'Infrastructure',
+ 'Writing'
+ )`
+ ),
+ check(
+ "timeline_public_event_source_boundary",
+ sql`${table.source} = 'github-profile'`
+ ),
+ ]
+);
+
+export const timelineEditions = pgTable(
+ "timeline_editions",
+ {
+ agentModel: varchar("agent_model", { length: 100 }).notNull(),
+ createdAt: timestamp("created_at", { mode: "date", withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ edition: jsonb("edition").$type().notNull(),
+ editionKey: varchar("edition_key", { length: 64 }).notNull(),
+ id: uuid("id").defaultRandom().primaryKey(),
+ publishedAt: timestamp("published_at", {
+ mode: "date",
+ withTimezone: true,
+ }),
+ privacyPolicyVersion: varchar("privacy_policy_version", { length: 64 }),
+ status: timelineEditionStatus("status").default("draft").notNull(),
+ updatedAt: timestamp("updated_at", { mode: "date", withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ windowEnd: date("window_end", { mode: "string" }).notNull(),
+ windowStart: date("window_start", { mode: "string" }).notNull(),
+ },
+ (table) => [
+ uniqueIndex("timeline_edition_key_idx").on(table.editionKey),
+ index("timeline_edition_published_idx").on(table.status, table.publishedAt),
+ ]
+);
+
+export const timelineSyncRuns = pgTable(
+ "timeline_sync_runs",
+ {
+ completedAt: timestamp("completed_at", {
+ mode: "date",
+ withTimezone: true,
+ }),
+ coverage: varchar("coverage", { length: 16 }).default("partial").notNull(),
+ errorCode: varchar("error_code", { length: 64 }),
+ eventCount: integer("event_count").default(0).notNull(),
+ anonymousDayCount: integer("anonymous_day_count").default(0).notNull(),
+ anonymousCoverage: varchar("anonymous_coverage", { length: 16 })
+ .default("unavailable")
+ .notNull(),
+ id: uuid("id").defaultRandom().primaryKey(),
+ fullWindow: boolean("full_window").default(false).notNull(),
+ kind: varchar("kind", { length: 32 }).notNull(),
+ publicEventCoverage: varchar("public_event_coverage", { length: 16 })
+ .default("unavailable")
+ .notNull(),
+ rowCount: integer("row_count").default(0).notNull(),
+ startedAt: timestamp("started_at", { mode: "date", withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ status: timelineSyncStatus("status").default("running").notNull(),
+ windowEnd: date("window_end", { mode: "string" }).notNull(),
+ windowStart: date("window_start", { mode: "string" }).notNull(),
+ },
+ (table) => [index("timeline_sync_started_idx").on(table.startedAt)]
+);
+
+export const timelineWebhookReceipts = pgTable(
+ "timeline_webhook_receipts",
+ {
+ deliveryKey: varchar("delivery_key", { length: 64 }).primaryKey(),
+ eventType: varchar("event_type", { length: 40 }).notNull(),
+ expiresAt: timestamp("expires_at", {
+ mode: "date",
+ withTimezone: true,
+ }).notNull(),
+ processedAt: timestamp("processed_at", {
+ mode: "date",
+ withTimezone: true,
+ }),
+ receivedAt: timestamp("received_at", {
+ mode: "date",
+ withTimezone: true,
+ })
+ .defaultNow()
+ .notNull(),
+ status: varchar("status", { length: 24 }).notNull(),
+ },
+ (table) => [index("timeline_webhook_expiry_idx").on(table.expiresAt)]
+);
diff --git a/src/env.ts b/src/env.ts
index 5f75de3..fe82fcd 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -10,9 +10,25 @@ export const env = createEnv({
NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT,
},
server: {
+ AI_GATEWAY_API_KEY: z.string().min(1).optional(),
+ AI_GATEWAY_ZERO_DATA_RETENTION: z.enum(["true", "false"]).optional(),
+ CRON_SECRET: z.string().min(16).optional(),
+ DATABASE_URL: z.url().optional(),
+ DATABASE_URL_UNPOOLED: z.url().optional(),
+ GH_TOKEN: z.string().min(1).optional(),
+ GITHUB_ACTIVITY_TOKEN: z.string().min(1).optional(),
+ GITHUB_PUBLIC_ACTIVITY_TOKEN: z.string().min(1).optional(),
+ GITHUB_APP_ID: z.string().min(1).optional(),
+ GITHUB_APP_INSTALLATION_IDS: z.string().min(1).optional(),
+ GITHUB_APP_PRIVATE_KEY: z.string().min(1).optional(),
+ GITHUB_TOKEN: z.string().min(1).optional(),
+ GITHUB_WEBHOOK_SECRET: z.string().min(16).optional(),
NODE_ENV: z.enum(["development", "production", "test"]).optional(),
+ OPENAI_API_KEY: z.string().min(1).optional(),
PORT: z.string().min(1).optional(),
SITE_URL: z.string().min(1).optional(),
+ TIMELINE_PRIVATE_TAXONOMY: z.string().min(2).optional(),
+ TIMELINE_PRIVACY_KEY: z.string().min(32).optional(),
VERCEL_ENV: z.enum(["development", "preview", "production"]).optional(),
VERCEL_PROJECT_PRODUCTION_URL: z.string().min(1).optional(),
VERCEL_URL: z.string().min(1).optional(),
diff --git a/src/lib/github-contribution-calendar.ts b/src/lib/github-contribution-calendar.ts
new file mode 100644
index 0000000..bbf46e2
--- /dev/null
+++ b/src/lib/github-contribution-calendar.ts
@@ -0,0 +1,85 @@
+import { setTimeout as delay } from "node:timers/promises";
+
+import { parseGitHubContributionCalendarDays } from "@/lib/github-profile-core";
+import type { GitHubContributionDay } from "@/lib/github-profile-core";
+
+const GITHUB_FETCH_TIMEOUT_MS = 10_000;
+const GITHUB_FETCH_ATTEMPTS = 3;
+const GITHUB_LOGIN_PATTERN = /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i;
+const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
+
+const fetchContributionCalendar = async (login: string, year: number) => {
+ const url = new URL(`https://github.com/users/${login}/contributions`);
+ url.searchParams.set("from", `${year}-01-01`);
+ url.searchParams.set("to", `${year}-12-31`);
+
+ for (let attempt = 0; attempt < GITHUB_FETCH_ATTEMPTS; attempt += 1) {
+ let response: Response;
+ try {
+ response = await fetch(url, {
+ cache: "no-store",
+ headers: {
+ Accept: "text/html",
+ "Accept-Language": "en-US,en;q=0.9",
+ "User-Agent": "f0rr0.dev",
+ },
+ signal: AbortSignal.timeout(GITHUB_FETCH_TIMEOUT_MS),
+ });
+ } catch (error) {
+ if (attempt === GITHUB_FETCH_ATTEMPTS - 1) {
+ throw error;
+ }
+ await delay(200 * 2 ** attempt);
+ continue;
+ }
+
+ if (response.ok) {
+ return await response.text();
+ }
+ if (response.status < 500 && response.status !== 429) {
+ throw new Error(`GitHub returned HTTP ${response.status}.`);
+ }
+ if (attempt === GITHUB_FETCH_ATTEMPTS - 1) {
+ throw new Error(`GitHub returned HTTP ${response.status}.`);
+ }
+ await delay(200 * 2 ** attempt);
+ }
+
+ throw new Error("GitHub contribution-calendar retry budget exhausted.");
+};
+
+export const fetchPublicGitHubContributionDays = async (input: {
+ login: string;
+ windowEnd: string;
+ windowStart: string;
+}): Promise => {
+ if (
+ !GITHUB_LOGIN_PATTERN.test(input.login) ||
+ !DATE_PATTERN.test(input.windowStart) ||
+ !DATE_PATTERN.test(input.windowEnd) ||
+ input.windowStart > input.windowEnd
+ ) {
+ throw new Error("Invalid GitHub contribution-calendar window.");
+ }
+
+ const years = [
+ ...new Set([
+ Number(input.windowStart.slice(0, 4)),
+ Number(input.windowEnd.slice(0, 4)),
+ ]),
+ ];
+ const htmlDocuments = await Promise.all(
+ years.map(
+ async (year) => await fetchContributionCalendar(input.login, year)
+ )
+ );
+ const days = parseGitHubContributionCalendarDays(htmlDocuments, {
+ from: `${input.windowStart}T00:00:00.000Z`,
+ to: `${input.windowEnd}T23:59:59.999Z`,
+ });
+
+ if (days === null) {
+ throw new Error("GitHub returned an invalid contribution calendar.");
+ }
+ return days;
+};
diff --git a/src/lib/github-profile-core.ts b/src/lib/github-profile-core.ts
new file mode 100644
index 0000000..f24ed6c
--- /dev/null
+++ b/src/lib/github-profile-core.ts
@@ -0,0 +1,837 @@
+const DAY_IN_MILLISECONDS = 86_400_000;
+const CONTRIBUTION_WINDOW_DAYS = 365;
+const MAX_PROJECTS = 100;
+const MAX_ACTIVITY_WINDOW_DAYS = 400;
+const MAX_CONTRIBUTION_HTML_LENGTH = 2_000_000;
+const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
+const GITHUB_LOGIN_PATTERN = /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i;
+const REPOSITORY_NAME_PATTERN = /^[a-z\d._-]{1,100}$/i;
+const LANGUAGE_COLOR_PATTERN = /^#[\da-f]{6}$/i;
+
+type JsonObject = Record;
+
+export type GitHubActivityLevel = 0 | 1 | 2 | 3 | 4;
+export type GitHubDataStatus = "available" | "unavailable";
+
+export interface GitHubContributionWindow {
+ from: string;
+ to: string;
+}
+
+export interface GitHubActivityWeek {
+ contributionCount: number;
+ level: GitHubActivityLevel;
+ weekStart: string;
+}
+
+export interface GitHubContributionDay {
+ contributionCount: number;
+ day: string;
+}
+
+export interface AvailableGitHubActivity {
+ activeDays: number;
+ from: string;
+ restrictedContributions: number | null;
+ status: "available";
+ to: string;
+ totalContributions: number;
+ weeks: GitHubActivityWeek[];
+}
+
+export interface UnavailableGitHubActivity {
+ activeDays: null;
+ from: string;
+ restrictedContributions: null;
+ status: "unavailable";
+ to: string;
+ totalContributions: null;
+ weeks: [];
+}
+
+export type GitHubActivity =
+ | AvailableGitHubActivity
+ | UnavailableGitHubActivity;
+
+export interface GitHubProject {
+ description: string | null;
+ forks: number | null;
+ language: string | null;
+ languageColor: string | null;
+ name: string;
+ stars: number | null;
+ topics: string[];
+ updatedAt: string | null;
+ url: string;
+}
+
+export interface GitHubProfile {
+ activity: GitHubActivity;
+ fetchedAt: string | null;
+ login: string;
+ profileUrl: string;
+ projects: GitHubProject[];
+ status: GitHubDataStatus;
+}
+
+export interface ParseGitHubProfileOptions {
+ fetchedAt: string;
+ login: string;
+ window: GitHubContributionWindow;
+}
+
+export interface GitHubFallbackOptions {
+ login: string;
+ window: GitHubContributionWindow;
+}
+
+const curatedProjectFallback = [
+ {
+ description:
+ "Rust library for running embedded PostgreSQL inside applications and tests.",
+ forks: null,
+ language: "Rust",
+ languageColor: "#dea584",
+ name: "oliphaunt",
+ stars: null,
+ topics: ["postgresql", "rust", "testing"],
+ updatedAt: null,
+ },
+ {
+ description:
+ "Cross-platform React Native rating component built with Animated and the native driver.",
+ forks: null,
+ language: "JavaScript",
+ languageColor: "#f1e05a",
+ name: "react-native-rating",
+ stars: null,
+ topics: ["react-native", "animation", "component"],
+ updatedAt: null,
+ },
+] as const;
+
+const isObject = (value: unknown): value is JsonObject =>
+ typeof value === "object" && value !== null && !Array.isArray(value);
+
+const asNonNegativeInteger = (value: unknown) =>
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0
+ ? value
+ : null;
+
+const normalizeDate = (value: unknown) => {
+ if (typeof value !== "string" || !DATE_PATTERN.test(value)) {
+ return null;
+ }
+
+ const date = new Date(`${value}T00:00:00.000Z`);
+ return Number.isNaN(date.getTime()) ||
+ date.toISOString().slice(0, 10) !== value
+ ? null
+ : value;
+};
+
+const normalizeDateTime = (value: unknown) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const date = new Date(value);
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
+};
+
+const dateOnly = (value: string) =>
+ normalizeDateTime(value)?.slice(0, 10) ?? null;
+
+const normalizeOptionalText = (value: unknown, maximumLength: number) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const normalized = value.replaceAll(/\s+/g, " ").trim();
+ if (normalized.length === 0) {
+ return null;
+ }
+
+ return normalized.slice(0, maximumLength);
+};
+
+const normalizeLogin = (value: unknown) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const normalized = value.trim();
+ return GITHUB_LOGIN_PATTERN.test(normalized) ? normalized : null;
+};
+
+const normalizeRepositoryName = (value: unknown) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const normalized = value.trim();
+ return REPOSITORY_NAME_PATTERN.test(normalized) &&
+ normalized !== "." &&
+ normalized !== ".."
+ ? normalized
+ : null;
+};
+
+const isExpectedRepositoryUrl = (
+ value: unknown,
+ login: string,
+ name: string
+) => {
+ if (typeof value !== "string") {
+ return false;
+ }
+
+ try {
+ const url = new URL(value);
+ const segments = url.pathname.split("/").filter(Boolean);
+
+ return (
+ url.protocol === "https:" &&
+ url.hostname === "github.com" &&
+ url.search === "" &&
+ url.hash === "" &&
+ segments.length === 2 &&
+ segments[0]?.toLowerCase() === login.toLowerCase() &&
+ segments[1]?.toLowerCase() === name.toLowerCase()
+ );
+ } catch {
+ return false;
+ }
+};
+
+const normalizeTopics = (value: unknown) => {
+ if (!isObject(value) || !Array.isArray(value.nodes)) {
+ return [];
+ }
+
+ const topics = new Set();
+
+ for (const node of value.nodes) {
+ if (!isObject(node) || !isObject(node.topic)) {
+ continue;
+ }
+
+ const topic = normalizeOptionalText(node.topic.name, 50)?.toLowerCase();
+ if (
+ topic !== undefined &&
+ topic !== null &&
+ /^[a-z\d][a-z\d-]*$/.test(topic)
+ ) {
+ topics.add(topic);
+ }
+
+ if (topics.size === 5) {
+ break;
+ }
+ }
+
+ return [...topics];
+};
+
+const normalizeTopicNames = (value: unknown) => {
+ if (!Array.isArray(value)) {
+ return [];
+ }
+
+ const topics = new Set();
+
+ for (const rawTopic of value) {
+ const topic = normalizeOptionalText(rawTopic, 50)?.toLowerCase();
+ if (
+ topic !== undefined &&
+ topic !== null &&
+ /^[a-z\d][a-z\d-]*$/.test(topic)
+ ) {
+ topics.add(topic);
+ }
+
+ if (topics.size === 5) {
+ break;
+ }
+ }
+
+ return [...topics];
+};
+
+const normalizeLanguage = (value: unknown) => {
+ if (!isObject(value)) {
+ return { color: null, name: null };
+ }
+
+ const name = normalizeOptionalText(value.name, 50);
+ const color =
+ typeof value.color === "string" && LANGUAGE_COLOR_PATTERN.test(value.color)
+ ? value.color.toLowerCase()
+ : null;
+
+ return { color, name };
+};
+
+const normalizeProject = (
+ value: unknown,
+ expectedLogin: string
+): GitHubProject | null => {
+ if (
+ !isObject(value) ||
+ value.isPrivate !== false ||
+ value.isFork !== false ||
+ !isObject(value.owner)
+ ) {
+ return null;
+ }
+
+ const ownerLogin = normalizeLogin(value.owner.login);
+ const name = normalizeRepositoryName(value.name);
+ const stars = asNonNegativeInteger(value.stargazerCount);
+ const forks = asNonNegativeInteger(value.forkCount);
+
+ if (
+ ownerLogin === null ||
+ ownerLogin.toLowerCase() !== expectedLogin.toLowerCase() ||
+ name === null ||
+ stars === null ||
+ forks === null ||
+ !isExpectedRepositoryUrl(value.url, expectedLogin, name)
+ ) {
+ return null;
+ }
+
+ const language = normalizeLanguage(value.primaryLanguage);
+
+ return {
+ description: normalizeOptionalText(value.description, 240),
+ forks,
+ language: language.name,
+ languageColor: language.color,
+ name,
+ stars,
+ topics: normalizeTopics(value.repositoryTopics),
+ updatedAt: normalizeDateTime(value.updatedAt),
+ url: `https://github.com/${expectedLogin}/${name}`,
+ };
+};
+
+const normalizeRestProject = (
+ value: unknown,
+ expectedLogin: string
+): GitHubProject | null => {
+ if (
+ !isObject(value) ||
+ value.private !== false ||
+ value.fork !== false ||
+ !isObject(value.owner)
+ ) {
+ return null;
+ }
+
+ const ownerLogin = normalizeLogin(value.owner.login);
+ const name = normalizeRepositoryName(value.name);
+ const stars = asNonNegativeInteger(value.stargazers_count);
+ const forks = asNonNegativeInteger(value.forks_count);
+
+ if (
+ ownerLogin === null ||
+ ownerLogin.toLowerCase() !== expectedLogin.toLowerCase() ||
+ name === null ||
+ stars === null ||
+ forks === null ||
+ !isExpectedRepositoryUrl(value.html_url, expectedLogin, name)
+ ) {
+ return null;
+ }
+
+ return {
+ description: normalizeOptionalText(value.description, 240),
+ forks,
+ language: normalizeOptionalText(value.language, 50),
+ languageColor: null,
+ name,
+ stars,
+ topics: normalizeTopicNames(value.topics),
+ updatedAt: normalizeDateTime(value.updated_at),
+ url: `https://github.com/${expectedLogin}/${name}`,
+ };
+};
+
+const sortAndLimitProjects = (projects: GitHubProject[]) =>
+ projects
+ .toSorted(
+ (left, right) =>
+ (right.stars ?? 0) - (left.stars ?? 0) ||
+ (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "")
+ )
+ .slice(0, MAX_PROJECTS);
+
+const activityLevel = (
+ contributionCount: number,
+ maximumContributionCount: number
+): GitHubActivityLevel => {
+ if (contributionCount === 0 || maximumContributionCount === 0) {
+ return 0;
+ }
+
+ return Math.min(
+ 4,
+ Math.max(1, Math.ceil((contributionCount / maximumContributionCount) * 4))
+ ) as GitHubActivityLevel;
+};
+
+const buildActivityWeeks = (weeklyCounts: Map) => {
+ const maximumContributionCount = Math.max(0, ...weeklyCounts.values());
+
+ return [...weeklyCounts]
+ .toSorted(([left], [right]) => left.localeCompare(right))
+ .map(([weekStart, contributionCount]) => ({
+ contributionCount,
+ level: activityLevel(contributionCount, maximumContributionCount),
+ weekStart,
+ }));
+};
+
+const normalizeWeeks = (
+ value: unknown,
+ from: string,
+ to: string
+): { activeDays: number; weeks: GitHubActivityWeek[] } | null => {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+
+ const activeDates = new Set();
+ const seenDates = new Set();
+ const weeklyCounts = new Map();
+
+ for (const rawWeek of value) {
+ if (!isObject(rawWeek) || !Array.isArray(rawWeek.contributionDays)) {
+ return null;
+ }
+
+ const weekStart = normalizeDate(rawWeek.firstDay);
+ if (weekStart === null) {
+ return null;
+ }
+
+ let contributionCount = weeklyCounts.get(weekStart) ?? 0;
+
+ for (const rawDay of rawWeek.contributionDays) {
+ if (!isObject(rawDay)) {
+ return null;
+ }
+
+ const date = normalizeDate(rawDay.date);
+ const count = asNonNegativeInteger(rawDay.contributionCount);
+
+ if (date === null || count === null) {
+ return null;
+ }
+
+ if (date < from || date > to || seenDates.has(date)) {
+ continue;
+ }
+
+ seenDates.add(date);
+ contributionCount += count;
+
+ if (count > 0) {
+ activeDates.add(date);
+ }
+ }
+
+ weeklyCounts.set(weekStart, contributionCount);
+ }
+
+ return {
+ activeDays: activeDates.size,
+ weeks: buildActivityWeeks(weeklyCounts),
+ };
+};
+
+const readHtmlAttributes = (source: string) => {
+ const attributes = new Map();
+ const attributePattern = /([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
+
+ for (const match of source.matchAll(attributePattern)) {
+ const name = match[1]?.toLowerCase();
+ const value = match[2] ?? match[3];
+ if (name !== undefined && value !== undefined) {
+ attributes.set(name, value);
+ }
+ }
+
+ return attributes;
+};
+
+const contributionCountFromTooltip = (source: string) => {
+ const text = source
+ .replaceAll(/<[^>]*>/g, " ")
+ .replaceAll(/ /gi, " ")
+ .replaceAll(/\s+/g, " ")
+ .trim();
+
+ if (/^No contributions?\b/i.test(text)) {
+ return 0;
+ }
+
+ const count = /^([\d,]+)\s+contributions?\b/i.exec(text)?.[1];
+ return count === undefined
+ ? null
+ : asNonNegativeInteger(Number(count.replaceAll(",", "")));
+};
+
+const readTooltipCounts = (html: string) => {
+ const counts = new Map();
+ const tooltipPattern = /]*)>([\s\S]*?)<\/tool-tip>/gi;
+
+ for (const match of html.matchAll(tooltipPattern)) {
+ const attributes = readHtmlAttributes(match[1] ?? "");
+ const target = attributes.get("for");
+ if (target?.startsWith("contribution-day-component-") !== true) {
+ continue;
+ }
+
+ const count = contributionCountFromTooltip(match[2] ?? "");
+ if (
+ count === null ||
+ (counts.has(target) && counts.get(target) !== count)
+ ) {
+ return null;
+ }
+
+ counts.set(target, count);
+ }
+
+ return counts;
+};
+
+const parseContributionCalendarDocument = (html: string) => {
+ if (html.length === 0 || html.length > MAX_CONTRIBUTION_HTML_LENGTH) {
+ return null;
+ }
+
+ const tooltipCounts = readTooltipCounts(html);
+ if (tooltipCounts === null) {
+ return null;
+ }
+
+ const days = new Map();
+ const dayCellPattern = /]*)>/gi;
+
+ for (const match of html.matchAll(dayCellPattern)) {
+ const attributes = readHtmlAttributes(match[1] ?? "");
+ const classes = attributes.get("class")?.split(/\s+/) ?? [];
+ if (!classes.includes("ContributionCalendar-day")) {
+ continue;
+ }
+
+ const date = normalizeDate(attributes.get("data-date"));
+ const target = attributes.get("id");
+ const count = target === undefined ? undefined : tooltipCounts.get(target);
+
+ if (
+ date === null ||
+ count === undefined ||
+ (days.has(date) && days.get(date) !== count)
+ ) {
+ return null;
+ }
+
+ days.set(date, count);
+ }
+
+ return days.size === 0 ? null : days;
+};
+
+const startOfContributionWeek = (date: string) => {
+ const value = new Date(`${date}T00:00:00.000Z`);
+ value.setUTCDate(value.getUTCDate() - value.getUTCDay());
+ return value.toISOString().slice(0, 10);
+};
+
+const summarizeContributionDays = (
+ days: Map,
+ from: string,
+ to: string
+): AvailableGitHubActivity | null => {
+ const fromTime = new Date(`${from}T00:00:00.000Z`).getTime();
+ const toTime = new Date(`${to}T00:00:00.000Z`).getTime();
+ const dayCount = Math.floor((toTime - fromTime) / DAY_IN_MILLISECONDS) + 1;
+
+ if (dayCount < 1 || dayCount > MAX_ACTIVITY_WINDOW_DAYS) {
+ return null;
+ }
+
+ let activeDays = 0;
+ let totalContributions = 0;
+ const weeklyCounts = new Map();
+
+ for (let offset = 0; offset < dayCount; offset += 1) {
+ const date = new Date(fromTime + offset * DAY_IN_MILLISECONDS)
+ .toISOString()
+ .slice(0, 10);
+ const count = days.get(date);
+ if (count === undefined) {
+ return null;
+ }
+
+ totalContributions += count;
+ if (!Number.isSafeInteger(totalContributions)) {
+ return null;
+ }
+ if (count > 0) {
+ activeDays += 1;
+ }
+
+ const weekStart = startOfContributionWeek(date);
+ weeklyCounts.set(weekStart, (weeklyCounts.get(weekStart) ?? 0) + count);
+ }
+
+ return {
+ activeDays,
+ from,
+ restrictedContributions: null,
+ status: "available",
+ to,
+ totalContributions,
+ weeks: buildActivityWeeks(weeklyCounts),
+ };
+};
+
+const mergeContributionDays = (
+ target: Map,
+ source: Map
+) => {
+ for (const [date, count] of source) {
+ if (target.has(date) && target.get(date) !== count) {
+ return false;
+ }
+ target.set(date, count);
+ }
+
+ return true;
+};
+
+const readGraphQlUser = (value: unknown) => {
+ if (!isObject(value)) {
+ return null;
+ }
+ if (Array.isArray(value.errors) && value.errors.length > 0) {
+ return null;
+ }
+ if (!isObject(value.data) || !isObject(value.data.user)) {
+ return null;
+ }
+
+ return value.data.user;
+};
+
+const isGitHubProfileUser = (user: JsonObject, expectedLogin: string) => {
+ const returnedLogin = normalizeLogin(user.login);
+
+ return (
+ returnedLogin !== null &&
+ returnedLogin.toLowerCase() === expectedLogin.toLowerCase() &&
+ isObject(user.contributionsCollection) &&
+ isObject(user.contributionsCollection.contributionCalendar) &&
+ isObject(user.repositories) &&
+ Array.isArray(user.repositories.nodes)
+ );
+};
+
+export const createGitHubContributionWindow = (
+ now = new Date()
+): GitHubContributionWindow => {
+ if (Number.isNaN(now.getTime())) {
+ throw new RangeError(
+ "A valid date is required for the GitHub activity window."
+ );
+ }
+
+ const from = new Date(
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) -
+ (CONTRIBUTION_WINDOW_DAYS - 1) * DAY_IN_MILLISECONDS
+ );
+
+ return { from: from.toISOString(), to: now.toISOString() };
+};
+
+export const parseGitHubContributionCalendarDays = (
+ htmlDocuments: readonly string[],
+ window: GitHubContributionWindow
+): GitHubContributionDay[] | null => {
+ const from = dateOnly(window.from);
+ const to = dateOnly(window.to);
+
+ if (
+ from === null ||
+ to === null ||
+ from > to ||
+ !Array.isArray(htmlDocuments) ||
+ htmlDocuments.length === 0
+ ) {
+ return null;
+ }
+
+ const days = new Map();
+
+ for (const html of htmlDocuments) {
+ if (typeof html !== "string") {
+ return null;
+ }
+
+ const parsedDays = parseContributionCalendarDocument(html);
+ if (parsedDays === null || !mergeContributionDays(days, parsedDays)) {
+ return null;
+ }
+ }
+
+ if (summarizeContributionDays(days, from, to) === null) {
+ return null;
+ }
+
+ return [...days]
+ .filter(([day]) => day >= from && day <= to)
+ .toSorted(([left], [right]) => left.localeCompare(right))
+ .map(([day, contributionCount]) => ({ contributionCount, day }));
+};
+
+export const parseGitHubContributionCalendarHtml = (
+ htmlDocuments: readonly string[],
+ window: GitHubContributionWindow
+): AvailableGitHubActivity | null => {
+ const contributionDays = parseGitHubContributionCalendarDays(
+ htmlDocuments,
+ window
+ );
+ if (contributionDays === null) {
+ return null;
+ }
+
+ return summarizeContributionDays(
+ new Map(
+ contributionDays.map(({ contributionCount, day }) => [
+ day,
+ contributionCount,
+ ])
+ ),
+ dateOnly(window.from) ?? "",
+ dateOnly(window.to) ?? ""
+ );
+};
+
+export const parseGitHubRepositoriesResponse = (
+ value: unknown,
+ login: string
+): GitHubProject[] | null => {
+ const expectedLogin = normalizeLogin(login);
+ if (expectedLogin === null || !Array.isArray(value)) {
+ return null;
+ }
+
+ const projects = value
+ .map((project) => normalizeRestProject(project, expectedLogin))
+ .filter((project): project is GitHubProject => project !== null);
+
+ return projects.length === 0 ? null : sortAndLimitProjects(projects);
+};
+
+export const createUnavailableGitHubProfile = ({
+ login,
+ window,
+}: GitHubFallbackOptions): GitHubProfile => {
+ const normalizedLogin = normalizeLogin(login) ?? "f0rr0";
+ const from = dateOnly(window.from) ?? "";
+ const to = dateOnly(window.to) ?? "";
+
+ return {
+ activity: {
+ activeDays: null,
+ from,
+ restrictedContributions: null,
+ status: "unavailable",
+ to,
+ totalContributions: null,
+ weeks: [],
+ },
+ fetchedAt: null,
+ login: normalizedLogin,
+ profileUrl: `https://github.com/${normalizedLogin}`,
+ projects: curatedProjectFallback.map((project) => ({
+ ...project,
+ topics: [...project.topics],
+ url: `https://github.com/${normalizedLogin}/${project.name}`,
+ })),
+ status: "unavailable",
+ };
+};
+
+export const parseGitHubProfileResponse = (
+ value: unknown,
+ { fetchedAt, login, window }: ParseGitHubProfileOptions
+): GitHubProfile | null => {
+ const expectedLogin = normalizeLogin(login);
+ const normalizedFetchedAt = normalizeDateTime(fetchedAt);
+ const from = dateOnly(window.from);
+ const to = dateOnly(window.to);
+ const user = readGraphQlUser(value);
+
+ if (
+ expectedLogin === null ||
+ normalizedFetchedAt === null ||
+ from === null ||
+ to === null ||
+ from > to ||
+ user === null ||
+ !isGitHubProfileUser(user, expectedLogin)
+ ) {
+ return null;
+ }
+
+ const collection = user.contributionsCollection as JsonObject;
+ const calendar = collection.contributionCalendar as JsonObject;
+ const totalContributions = asNonNegativeInteger(calendar.totalContributions);
+ const restrictedContributions = asNonNegativeInteger(
+ collection.restrictedContributionsCount
+ );
+ const normalizedWeeks = normalizeWeeks(calendar.weeks, from, to);
+ const normalizedTotal = normalizedWeeks?.weeks.reduce(
+ (total, week) => total + week.contributionCount,
+ 0
+ );
+
+ if (
+ totalContributions === null ||
+ restrictedContributions === null ||
+ restrictedContributions > totalContributions ||
+ normalizedWeeks === null ||
+ normalizedTotal !== totalContributions
+ ) {
+ return null;
+ }
+
+ const repositories = user.repositories as JsonObject;
+ const projects = (repositories.nodes as unknown[])
+ .map((project) => normalizeProject(project, expectedLogin))
+ .filter((project): project is GitHubProject => project !== null);
+
+ return {
+ activity: {
+ activeDays: normalizedWeeks.activeDays,
+ from,
+ restrictedContributions,
+ status: "available",
+ to,
+ totalContributions,
+ weeks: normalizedWeeks.weeks,
+ },
+ fetchedAt: normalizedFetchedAt,
+ login: expectedLogin,
+ profileUrl: `https://github.com/${expectedLogin}`,
+ projects: sortAndLimitProjects(projects),
+ status: "available",
+ };
+};
diff --git a/src/lib/github-profile.ts b/src/lib/github-profile.ts
new file mode 100644
index 0000000..edb2be2
--- /dev/null
+++ b/src/lib/github-profile.ts
@@ -0,0 +1,295 @@
+import "server-only";
+import { setTimeout as delay } from "node:timers/promises";
+
+import { unstable_cache } from "next/cache";
+
+import {
+ createGitHubContributionWindow,
+ createUnavailableGitHubProfile,
+ parseGitHubContributionCalendarHtml,
+ parseGitHubProfileResponse,
+ parseGitHubRepositoriesResponse,
+} from "@/lib/github-profile-core";
+import type { GitHubProfile } from "@/lib/github-profile-core";
+
+export type {
+ AvailableGitHubActivity,
+ GitHubActivity,
+ GitHubActivityLevel,
+ GitHubActivityWeek,
+ GitHubContributionDay,
+ GitHubContributionWindow,
+ GitHubDataStatus,
+ GitHubProfile,
+ GitHubProject,
+ UnavailableGitHubActivity,
+} from "@/lib/github-profile-core";
+
+const GITHUB_LOGIN = "f0rr0";
+const GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
+const GITHUB_REST_URL = `https://api.github.com/users/${GITHUB_LOGIN}/repos`;
+const GITHUB_CONTRIBUTIONS_URL = `https://github.com/users/${GITHUB_LOGIN}/contributions`;
+const GITHUB_FETCH_TIMEOUT_MS = 10_000;
+const GITHUB_FETCH_ATTEMPTS = 3;
+const GITHUB_CACHE_SECONDS = 60 * 60 * 12;
+
+const githubProfileQuery = `
+ query PortfolioGitHubProfile(
+ $login: String!
+ $from: DateTime!
+ $to: DateTime!
+ ) {
+ user(login: $login) {
+ login
+ contributionsCollection(from: $from, to: $to) {
+ restrictedContributionsCount
+ contributionCalendar {
+ totalContributions
+ weeks {
+ firstDay
+ contributionDays {
+ date
+ contributionCount
+ }
+ }
+ }
+ }
+ repositories(
+ first: 100
+ ownerAffiliations: OWNER
+ privacy: PUBLIC
+ isFork: false
+ orderBy: { field: STARGAZERS, direction: DESC }
+ ) {
+ nodes {
+ name
+ description
+ url
+ isPrivate
+ isFork
+ owner {
+ login
+ }
+ stargazerCount
+ forkCount
+ updatedAt
+ primaryLanguage {
+ name
+ color
+ }
+ repositoryTopics(first: 5) {
+ nodes {
+ topic {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+`;
+
+const readGitHubToken = () => {
+ for (const token of [process.env.GITHUB_TOKEN, process.env.GH_TOKEN]) {
+ const normalized = token?.trim();
+ if (normalized !== undefined && normalized.length > 0) {
+ return normalized;
+ }
+ }
+
+ return null;
+};
+
+const fetchGitHubResource = async (url: string, init: RequestInit = {}) => {
+ for (let attempt = 0; attempt < GITHUB_FETCH_ATTEMPTS; attempt += 1) {
+ let response: Response;
+ try {
+ response = await fetch(url, {
+ ...init,
+ cache: "no-store",
+ signal: AbortSignal.timeout(GITHUB_FETCH_TIMEOUT_MS),
+ });
+ } catch (error) {
+ if (attempt === GITHUB_FETCH_ATTEMPTS - 1) {
+ throw error;
+ }
+ await delay(200 * 2 ** attempt);
+ continue;
+ }
+
+ if (response.ok) {
+ return response;
+ }
+ if (response.status < 500 && response.status !== 429) {
+ throw new Error(`GitHub returned HTTP ${response.status}.`);
+ }
+ if (attempt === GITHUB_FETCH_ATTEMPTS - 1) {
+ throw new Error(`GitHub returned HTTP ${response.status}.`);
+ }
+
+ await delay(200 * 2 ** attempt);
+ }
+
+ throw new Error("GitHub request retry budget exhausted.");
+};
+
+const fetchAuthenticatedGitHubProfile = async (
+ token: string,
+ fetchedAt: Date,
+ window: ReturnType
+): Promise => {
+ const response = await fetchGitHubResource(GITHUB_GRAPHQL_URL, {
+ body: JSON.stringify({
+ query: githubProfileQuery,
+ variables: {
+ from: window.from,
+ login: GITHUB_LOGIN,
+ to: window.to,
+ },
+ }),
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "User-Agent": "f0rr0.dev",
+ },
+ method: "POST",
+ });
+
+ const payload: unknown = await response.json();
+ const profile = parseGitHubProfileResponse(payload, {
+ fetchedAt: fetchedAt.toISOString(),
+ login: GITHUB_LOGIN,
+ window,
+ });
+
+ if (profile === null) {
+ throw new Error("GitHub returned an invalid profile response.");
+ }
+
+ return profile;
+};
+
+const fetchPublicRepositories = async () => {
+ const url = new URL(GITHUB_REST_URL);
+ url.searchParams.set("direction", "desc");
+ url.searchParams.set("page", "1");
+ url.searchParams.set("per_page", "100");
+ url.searchParams.set("sort", "updated");
+ url.searchParams.set("type", "owner");
+
+ const response = await fetchGitHubResource(url.toString(), {
+ headers: {
+ Accept: "application/vnd.github+json",
+ "User-Agent": "f0rr0.dev",
+ "X-GitHub-Api-Version": "2022-11-28",
+ },
+ });
+
+ return (await response.json()) as unknown;
+};
+
+const fetchContributionCalendar = async (year: number) => {
+ const url = new URL(GITHUB_CONTRIBUTIONS_URL);
+ url.searchParams.set("from", `${year}-01-01`);
+ url.searchParams.set("to", `${year}-12-31`);
+
+ const response = await fetchGitHubResource(url.toString(), {
+ headers: {
+ Accept: "text/html",
+ "Accept-Language": "en-US,en;q=0.9",
+ "User-Agent": "f0rr0.dev",
+ },
+ });
+
+ return await response.text();
+};
+
+const fetchContributionCalendars = async (years: readonly number[]) => {
+ const calendars: string[] = [];
+ for (const year of years) {
+ calendars.push(await fetchContributionCalendar(year));
+ }
+ return calendars;
+};
+
+const fetchPublicGitHubProfile = async (
+ fetchedAt: Date,
+ window: ReturnType
+): Promise => {
+ const years = [
+ ...new Set([
+ new Date(window.from).getUTCFullYear(),
+ new Date(window.to).getUTCFullYear(),
+ ]),
+ ];
+ const [repositoryResult] = await Promise.allSettled([
+ fetchPublicRepositories(),
+ ]);
+ const [calendarResult] = await Promise.allSettled([
+ fetchContributionCalendars(years),
+ ]);
+ const projects =
+ repositoryResult.status === "fulfilled"
+ ? parseGitHubRepositoriesResponse(repositoryResult.value, GITHUB_LOGIN)
+ : null;
+ const activity =
+ calendarResult.status === "fulfilled"
+ ? parseGitHubContributionCalendarHtml(calendarResult.value, window)
+ : null;
+
+ if (projects === null && activity === null) {
+ throw new Error("GitHub returned invalid public profile data.");
+ }
+
+ const unavailableProfile = createUnavailableGitHubProfile({
+ login: GITHUB_LOGIN,
+ window,
+ });
+
+ return {
+ activity: activity ?? unavailableProfile.activity,
+ fetchedAt: fetchedAt.toISOString(),
+ login: GITHUB_LOGIN,
+ profileUrl: `https://github.com/${GITHUB_LOGIN}`,
+ projects: projects ?? unavailableProfile.projects,
+ status: projects === null ? "unavailable" : "available",
+ };
+};
+
+const fetchGitHubProfile = async (): Promise => {
+ const fetchedAt = new Date();
+ const window = createGitHubContributionWindow(fetchedAt);
+ const token = readGitHubToken();
+
+ if (token === null) {
+ return await fetchPublicGitHubProfile(fetchedAt, window);
+ }
+
+ try {
+ return await fetchAuthenticatedGitHubProfile(token, fetchedAt, window);
+ } catch {
+ return await fetchPublicGitHubProfile(fetchedAt, window);
+ }
+};
+
+const getCachedGitHubProfile = unstable_cache(
+ fetchGitHubProfile,
+ ["portfolio-github-profile-v5", GITHUB_LOGIN],
+ {
+ revalidate: GITHUB_CACHE_SECONDS,
+ tags: ["github-profile"],
+ }
+);
+
+export const getGitHubProfile = async (): Promise => {
+ try {
+ return await getCachedGitHubProfile();
+ } catch {
+ return createUnavailableGitHubProfile({
+ login: GITHUB_LOGIN,
+ window: createGitHubContributionWindow(),
+ });
+ }
+};
diff --git a/src/lib/request-auth.ts b/src/lib/request-auth.ts
new file mode 100644
index 0000000..132c6ec
--- /dev/null
+++ b/src/lib/request-auth.ts
@@ -0,0 +1,19 @@
+import { createHash, timingSafeEqual } from "node:crypto";
+
+const digest = (value: string) =>
+ createHash("sha256").update(value, "utf-8").digest();
+
+export const constantTimeEqual = (left: string, right: string) =>
+ timingSafeEqual(digest(left), digest(right));
+
+export const hasBearerSecret = (
+ authorization: string | null,
+ secret: string | undefined
+) => {
+ const normalizedSecret = secret?.trim();
+ if (normalizedSecret === undefined || normalizedSecret.length < 16) {
+ return false;
+ }
+
+ return constantTimeEqual(authorization ?? "", `Bearer ${normalizedSecret}`);
+};
diff --git a/src/lib/timeline-core.ts b/src/lib/timeline-core.ts
new file mode 100644
index 0000000..9a09f7f
--- /dev/null
+++ b/src/lib/timeline-core.ts
@@ -0,0 +1,649 @@
+import { createHash } from "node:crypto";
+
+import { z } from "zod";
+
+export const TIMELINE_SCHEMA_VERSION = 2;
+export const TIMELINE_PROMPT_VERSION = "newspaper-v2";
+export const TIMELINE_WINDOW_DAYS = 400;
+
+const datePattern = /^\d{4}-\d{2}-\d{2}$/;
+const entryIdPattern = /^[a-z0-9][a-z0-9-]{5,95}$/;
+const sourceKeyPattern = /^[a-z0-9][a-z0-9:_-]{7,127}$/;
+
+export const workBucketSchema = z.enum([
+ "Across the work",
+ "Applied AI",
+ "Open source",
+ "Product systems",
+ "Infrastructure",
+ "Writing",
+ "Private product work",
+]);
+
+export type WorkBucket = z.infer;
+
+export const timelineImportanceSchema = z.enum([
+ "lead",
+ "story",
+ "brief",
+ "pulse",
+]);
+export type TimelineImportance = z.infer;
+
+export const timelineVisibilitySchema = z.enum([
+ "public",
+ "private",
+ "mixed",
+ "anonymous",
+]);
+export type TimelineVisibility = z.infer;
+
+export const timelineEntryKindSchema = z.enum([
+ "project",
+ "activity",
+ "issue",
+ "pull-request",
+]);
+export type TimelineEntryKind = z.infer;
+
+export const timelineCadenceSchema = z.enum([
+ "isolated",
+ "clustered",
+ "streak",
+]);
+export type TimelineCadence = z.infer;
+
+const timelineHrefSchema = z
+ .string()
+ .max(500)
+ .refine((value) => {
+ if (value.startsWith("/")) {
+ return !value.startsWith("//");
+ }
+
+ try {
+ const url = new URL(value);
+ return url.protocol === "https:";
+ } catch {
+ return false;
+ }
+ }, "Timeline links must be internal paths or HTTPS URLs.");
+
+const privateTextPatterns = [
+ /\d/,
+ /https?:|www\./i,
+ /github/i,
+ /@/,
+ /\b[\da-f]{7,40}\b/i,
+ /\b[A-Z]{2,10}-\d+\b/,
+ /\b(?:src|lib|app|packages?|services?|internal)\//i,
+ /\b[\w.-]+\.(?:c|cpp|go|java|js|jsx|md|py|rb|rs|sql|ts|tsx|yml|yaml)\b/i,
+ /\b[a-z\d._-]+\/[a-z\d._-]+\b/i,
+] as const;
+
+const normalizedPrivateCopy = (value: string) =>
+ value.normalize("NFKC").replaceAll(/\p{Cf}/gu, "");
+
+export const containsPrivateIdentifier = (value: string) =>
+ privateTextPatterns.some((pattern) =>
+ pattern.test(normalizedPrivateCopy(value))
+ );
+
+export const timelineEditionEntrySchema = z
+ .object({
+ bucket: workBucketSchema,
+ description: z.string().trim().min(20).max(360),
+ endDate: z.string().regex(datePattern),
+ href: timelineHrefSchema.optional(),
+ id: z.string().regex(entryIdPattern),
+ importance: timelineImportanceSchema,
+ cadence: timelineCadenceSchema,
+ kind: timelineEntryKindSchema,
+ label: z.string().trim().min(2).max(48).optional(),
+ metrics: z.array(z.string().trim().min(2).max(72)).max(3).default([]),
+ sourceKeys: z.array(z.string().regex(sourceKeyPattern)).min(1).max(12),
+ startDate: z.string().regex(datePattern),
+ title: z.string().trim().min(2).max(120),
+ visibility: timelineVisibilitySchema,
+ })
+ .strict()
+ .superRefine((entry, context) => {
+ if (entry.startDate > entry.endDate) {
+ context.addIssue({
+ code: "custom",
+ message: "startDate must not be after endDate.",
+ path: ["startDate"],
+ });
+ }
+
+ if (entry.href === undefined && entry.label !== undefined) {
+ context.addIssue({
+ code: "custom",
+ message: "A link label requires an href.",
+ path: ["label"],
+ });
+ }
+
+ if (entry.visibility === "public") {
+ return;
+ }
+
+ if (entry.href !== undefined || entry.label !== undefined) {
+ context.addIssue({
+ code: "custom",
+ message: "Private and mixed entries cannot expose links.",
+ path: ["href"],
+ });
+ }
+
+ if (!entry.startDate.endsWith("-01") || !entry.endDate.endsWith("-01")) {
+ context.addIssue({
+ code: "custom",
+ message: "Private and mixed entries must use month-level dates.",
+ path: ["startDate"],
+ });
+ }
+
+ for (const [field, value] of [
+ ["title", entry.title],
+ ["description", entry.description],
+ ...entry.metrics.map((metric) => ["metrics", metric] as const),
+ ] as const) {
+ if (containsPrivateIdentifier(value)) {
+ context.addIssue({
+ code: "custom",
+ message:
+ "Private copy contains a number, link, code identifier, or repository-like token.",
+ path: [field],
+ });
+ }
+ }
+ });
+
+export type TimelineEditionEntry = z.infer;
+
+export const timelineSelectionPlanSchema = z
+ .object({
+ selections: z
+ .array(
+ z
+ .object({
+ importance: timelineImportanceSchema,
+ sourceKey: z.string().regex(sourceKeyPattern),
+ })
+ .strict()
+ )
+ .min(1)
+ .max(24),
+ windowEnd: z.string().regex(datePattern),
+ windowStart: z.string().regex(datePattern),
+ })
+ .strict();
+
+export type TimelineSelectionPlan = z.infer;
+
+export const timelinePlanSchema = z
+ .object({
+ entries: z.array(timelineEditionEntrySchema).min(1).max(24),
+ headline: z.string().trim().min(8).max(100),
+ standfirst: z.string().trim().min(20).max(280),
+ windowEnd: z.string().regex(datePattern),
+ windowStart: z.string().regex(datePattern),
+ })
+ .strict()
+ .superRefine((plan, context) => {
+ const windowDays = Math.round(
+ (Date.parse(`${plan.windowEnd}T00:00:00Z`) -
+ Date.parse(`${plan.windowStart}T00:00:00Z`)) /
+ 86_400_000
+ );
+
+ if (windowDays < 365 || windowDays > TIMELINE_WINDOW_DAYS + 2) {
+ context.addIssue({
+ code: "custom",
+ message: "A timeline edition must cover between 365 and 402 days.",
+ path: ["windowStart"],
+ });
+ }
+
+ const ids = new Set();
+ const leadsByMonth = new Map();
+ let leadCount = 0;
+ let storyCount = 0;
+ let compactCount = 0;
+
+ for (const [index, entry] of plan.entries.entries()) {
+ if (
+ entry.startDate < plan.windowStart ||
+ entry.endDate > plan.windowEnd
+ ) {
+ context.addIssue({
+ code: "custom",
+ message: "Timeline entries must remain inside the edition window.",
+ path: ["entries", index, "startDate"],
+ });
+ }
+
+ if (ids.has(entry.id)) {
+ context.addIssue({
+ code: "custom",
+ message: "Timeline entry ids must be unique.",
+ path: ["entries", index, "id"],
+ });
+ }
+ ids.add(entry.id);
+
+ if (entry.importance === "lead") {
+ leadCount += 1;
+ const month = entry.startDate.slice(0, 7);
+ const monthCount = (leadsByMonth.get(month) ?? 0) + 1;
+ leadsByMonth.set(month, monthCount);
+ if (monthCount > 2) {
+ context.addIssue({
+ code: "custom",
+ message: "No month may contain more than two lead stories.",
+ path: ["entries", index, "importance"],
+ });
+ }
+ }
+
+ if (entry.importance === "story") {
+ storyCount += 1;
+ }
+
+ if (entry.importance === "brief" || entry.importance === "pulse") {
+ compactCount += 1;
+ }
+ }
+
+ if (leadCount > 3) {
+ context.addIssue({
+ code: "custom",
+ message: "A rolling edition may contain at most three lead stories.",
+ path: ["entries"],
+ });
+ }
+
+ if (storyCount > 4) {
+ context.addIssue({
+ code: "custom",
+ message: "A rolling edition may contain at most four stories.",
+ path: ["entries"],
+ });
+ }
+
+ const compactMinimum =
+ plan.entries.length < 6
+ ? 0
+ : Math.max(3, Math.ceil(plan.entries.length * 0.4));
+ if (compactCount < compactMinimum) {
+ context.addIssue({
+ code: "custom",
+ message: `An edition of this size needs at least ${compactMinimum} briefs or pulses.`,
+ path: ["entries"],
+ });
+ }
+ });
+
+export type TimelinePlan = z.infer;
+
+export const timelineEditionSchema = timelinePlanSchema.extend({
+ editionKey: z.string().regex(/^[a-f\d]{64}$/),
+ generatedAt: z.iso.datetime(),
+ promptVersion: z.literal(TIMELINE_PROMPT_VERSION),
+ schemaVersion: z.literal(TIMELINE_SCHEMA_VERSION),
+ sourceDigest: z.string().regex(/^[a-f\d]{64}$/),
+});
+
+export type TimelineEdition = z.infer;
+
+export const editionMatchesTimelinePrivacyPolicy = (
+ edition: TimelineEdition,
+ storedPolicyVersion: string | null,
+ activePolicyVersion: string | null
+) => {
+ const hasProtectedEntries = edition.entries.some(
+ (entry) => entry.visibility === "private" || entry.visibility === "mixed"
+ );
+ return (
+ !hasProtectedEntries ||
+ (storedPolicyVersion !== null &&
+ storedPolicyVersion === activePolicyVersion)
+ );
+};
+
+export const activityMagnitudeSchema = z.enum([
+ "light",
+ "steady",
+ "sustained",
+ "intense",
+]);
+
+export const activityClusterKindSchema = z.enum([
+ "curated",
+ "commit-run",
+ "recurrence",
+ "issue-opened",
+ "pull-request-opened",
+ "pull-request-reviewed",
+ "repository-created",
+ "account-wide-streak",
+ "anonymous-month",
+ "public-streak",
+ "private-month",
+ "private-streak",
+]);
+export type ActivityClusterKind = z.infer;
+
+export const activityClusterSchema = z
+ .object({
+ bucket: workBucketSchema,
+ cadence: timelineCadenceSchema,
+ endDate: z.string().regex(datePattern),
+ facts: z.array(z.string().trim().min(4).max(360)).min(1).max(8),
+ key: z.string().regex(sourceKeyPattern),
+ kind: activityClusterKindSchema,
+ magnitude: activityMagnitudeSchema,
+ maxImportance: timelineImportanceSchema,
+ publicHref: timelineHrefSchema.optional(),
+ publicLabel: z.string().trim().min(2).max(48).optional(),
+ publicTitle: z.string().trim().min(2).max(120).optional(),
+ publishable: z.boolean(),
+ rollupOf: z.array(z.string().regex(sourceKeyPattern)).max(24).default([]),
+ seriesKey: z.string().regex(sourceKeyPattern),
+ startDate: z.string().regex(datePattern),
+ visibility: timelineVisibilitySchema,
+ })
+ .strict()
+ .superRefine((cluster, context) => {
+ if (cluster.visibility !== "public") {
+ for (const value of cluster.facts) {
+ if (containsPrivateIdentifier(value)) {
+ context.addIssue({
+ code: "custom",
+ message: "Private activity facts must already be generalized.",
+ path: ["facts"],
+ });
+ }
+ }
+
+ if (
+ cluster.publicHref !== undefined ||
+ cluster.publicLabel !== undefined ||
+ cluster.publicTitle !== undefined
+ ) {
+ context.addIssue({
+ code: "custom",
+ message: "Private activity clusters cannot carry public identity.",
+ });
+ }
+ }
+ });
+
+export type ActivityCluster = z.infer;
+
+export const activityDigestSchema = z
+ .object({
+ clusters: z.array(activityClusterSchema).max(120),
+ coverage: z.enum(["complete", "partial"]),
+ generatedAt: z.iso.datetime(),
+ windowEnd: z.string().regex(datePattern),
+ windowStart: z.string().regex(datePattern),
+ })
+ .strict();
+
+export type ActivityDigest = z.infer;
+
+const importanceRank: Record = {
+ brief: 1,
+ lead: 3,
+ pulse: 0,
+ story: 2,
+};
+
+export const digestTimelineValue = (value: unknown) =>
+ createHash("sha256").update(JSON.stringify(value)).digest("hex");
+
+const protectedCopyFor = (source: ActivityCluster) => {
+ if (source.visibility === "anonymous") {
+ return source.kind === "account-wide-streak"
+ ? {
+ description:
+ "An anonymized, account-wide contribution signal formed a sustained rhythm; repository identity and activity type remain unavailable.",
+ title: "A sustained account-wide cadence",
+ }
+ : {
+ description:
+ "An anonymized contribution signal extended beyond repository-resolved work; it is used only as evidence of cadence.",
+ title: "A wider rhythm in the work",
+ };
+ }
+
+ const subject =
+ source.bucket === "Applied AI"
+ ? "applied AI"
+ : source.bucket.toLocaleLowerCase("en-US");
+ const title =
+ source.cadence === "streak"
+ ? `A sustained run in ${subject}`
+ : source.magnitude === "intense"
+ ? `A concentrated month in ${subject}`
+ : `A steady month in ${subject}`;
+ const rhythm =
+ source.cadence === "streak"
+ ? "sustained run"
+ : source.cadence === "clustered"
+ ? "clustered rhythm"
+ : "quiet rhythm";
+ const description =
+ source.visibility === "mixed"
+ ? `Public releases and protected product work moved through the same ${rhythm}; private identity and exact volume remain withheld.`
+ : `Protected activity formed a ${rhythm}; repository identity and exact volume remain withheld.`;
+
+ return { description, title };
+};
+
+const entryKindFor = (kind: ActivityClusterKind): TimelineEntryKind => {
+ if (kind === "curated" || kind === "repository-created") {
+ return "project";
+ }
+ if (kind === "issue-opened") {
+ return "issue";
+ }
+ if (kind === "pull-request-opened") {
+ return "pull-request";
+ }
+ return "activity";
+};
+
+const materializeSelection = (
+ source: ActivityCluster,
+ importance: TimelineImportance
+): TimelineEditionEntry => {
+ const protectedCopy =
+ source.visibility === "public" ? null : protectedCopyFor(source);
+ const title =
+ protectedCopy?.title ?? source.publicTitle ?? "A public work signal";
+ const description =
+ protectedCopy?.description ??
+ source.facts[0] ??
+ "A verified public work signal appeared during this period.";
+
+ return timelineEditionEntrySchema.parse({
+ bucket: source.bucket,
+ cadence: source.cadence,
+ description,
+ endDate:
+ source.visibility === "public"
+ ? source.endDate
+ : `${source.endDate.slice(0, 7)}-01`,
+ ...(source.visibility === "public" && source.publicHref !== undefined
+ ? { href: source.publicHref, label: source.publicLabel }
+ : {}),
+ id: `source-${digestTimelineValue(source.key).slice(0, 20)}`,
+ importance,
+ kind: entryKindFor(source.kind),
+ metrics: [],
+ sourceKeys: [source.key],
+ startDate:
+ source.visibility === "public"
+ ? source.startDate
+ : `${source.startDate.slice(0, 7)}-01`,
+ title,
+ visibility: source.visibility,
+ });
+};
+
+const quarterFor = (date: string) => {
+ const month = Number(date.slice(5, 7));
+ return `${date.slice(0, 4)}-Q${Math.floor((month - 1) / 3) + 1}`;
+};
+
+const isPublicEventCluster = (cluster: ActivityCluster) =>
+ cluster.kind === "issue-opened" ||
+ cluster.kind === "pull-request-opened" ||
+ cluster.kind === "repository-created";
+
+export const validateTimelinePlanAgainstDigest = (
+ candidate: unknown,
+ digest: ActivityDigest
+): TimelinePlan => {
+ const selectionPlan = timelineSelectionPlanSchema.parse(candidate);
+ const clusterByKey = new Map(
+ digest.clusters.map((cluster) => [cluster.key, cluster])
+ );
+ const usedSourceKeys = new Set();
+
+ if (
+ selectionPlan.windowStart !== digest.windowStart ||
+ selectionPlan.windowEnd !== digest.windowEnd
+ ) {
+ throw new Error("The edition window does not match the activity digest.");
+ }
+
+ const sources = selectionPlan.selections.map((selection) => {
+ if (usedSourceKeys.has(selection.sourceKey)) {
+ throw new Error(`Activity source is reused: ${selection.sourceKey}`);
+ }
+ usedSourceKeys.add(selection.sourceKey);
+ const source = clusterByKey.get(selection.sourceKey);
+ if (source === undefined) {
+ throw new Error(`Unknown activity source: ${selection.sourceKey}`);
+ }
+ if (!source.publishable) {
+ throw new Error(
+ `Activity source is not publishable: ${selection.sourceKey}`
+ );
+ }
+ if (
+ importanceRank[selection.importance] >
+ importanceRank[source.maxImportance]
+ ) {
+ throw new Error(
+ `Selection ${selection.sourceKey} overstates its source importance.`
+ );
+ }
+ return { importance: selection.importance, source };
+ });
+
+ const publishable = digest.clusters.filter((cluster) => cluster.publishable);
+ const minimumEntries = Math.min(9, publishable.length);
+ if (sources.length < minimumEntries) {
+ throw new Error(
+ `This digest needs at least ${minimumEntries} selected entries.`
+ );
+ }
+
+ const compactCount = sources.filter(
+ ({ importance }) => importance === "brief" || importance === "pulse"
+ ).length;
+ const compactMinimum =
+ sources.length < 6 ? 0 : Math.max(3, Math.ceil(sources.length * 0.4));
+ if (compactCount < compactMinimum) {
+ throw new Error(
+ `This edition needs at least ${compactMinimum} briefs or pulses.`
+ );
+ }
+
+ const eventCandidates = publishable.filter(isPublicEventCluster);
+ const selectedEvents = sources.filter(({ source }) =>
+ isPublicEventCluster(source)
+ );
+ const eventMinimum = Math.min(
+ 3,
+ eventCandidates.length,
+ Math.floor(sources.length / 3)
+ );
+ if (selectedEvents.length < eventMinimum) {
+ throw new Error(
+ `This edition needs at least ${eventMinimum} public event dispatches.`
+ );
+ }
+ if (selectedEvents.length > Math.max(1, Math.ceil(sources.length / 3))) {
+ throw new Error(
+ "Public event dispatches may occupy at most one third of an edition."
+ );
+ }
+
+ const streakCandidates = publishable.filter(
+ (cluster) => cluster.cadence === "streak"
+ );
+ if (
+ streakCandidates.length > 0 &&
+ !sources.some(({ source }) => source.cadence === "streak")
+ ) {
+ throw new Error("A valid consistency streak must be represented.");
+ }
+
+ const candidateQuarters = new Set(
+ publishable.map((cluster) => quarterFor(cluster.endDate))
+ );
+ const selectedQuarters = new Set(
+ sources.map(({ source }) => quarterFor(source.endDate))
+ );
+ if (
+ sources.length >= candidateQuarters.size &&
+ [...candidateQuarters].some((quarter) => !selectedQuarters.has(quarter))
+ ) {
+ throw new Error("Each active quarter must retain at least one entry.");
+ }
+
+ return timelinePlanSchema.parse({
+ entries: sources
+ .map(({ importance, source }) => materializeSelection(source, importance))
+ .toSorted((left, right) => right.startDate.localeCompare(left.startDate)),
+ headline: "The work, along one line.",
+ standfirst:
+ "A rolling edition of public milestones, sustained runs, and the smaller acts of collaboration between them.",
+ windowEnd: digest.windowEnd,
+ windowStart: digest.windowStart,
+ });
+};
+
+export const createTimelineEdition = (
+ plan: TimelinePlan,
+ digest: ActivityDigest,
+ generatedAt = new Date()
+): TimelineEdition => {
+ const sourceDigest = digestTimelineValue({
+ clusters: digest.clusters,
+ coverage: digest.coverage,
+ windowEnd: digest.windowEnd,
+ windowStart: digest.windowStart,
+ });
+ const editionKey = digestTimelineValue({
+ plan,
+ promptVersion: TIMELINE_PROMPT_VERSION,
+ schemaVersion: TIMELINE_SCHEMA_VERSION,
+ sourceDigest,
+ });
+
+ return timelineEditionSchema.parse({
+ ...plan,
+ editionKey,
+ generatedAt: generatedAt.toISOString(),
+ promptVersion: TIMELINE_PROMPT_VERSION,
+ schemaVersion: TIMELINE_SCHEMA_VERSION,
+ sourceDigest,
+ });
+};
diff --git a/src/lib/timeline-editorial.ts b/src/lib/timeline-editorial.ts
new file mode 100644
index 0000000..c272806
--- /dev/null
+++ b/src/lib/timeline-editorial.ts
@@ -0,0 +1,1184 @@
+import { timelineEntries } from "@/content/home";
+import {
+ activityDigestSchema,
+ digestTimelineValue,
+ TIMELINE_WINDOW_DAYS,
+} from "@/lib/timeline-core";
+import type {
+ ActivityCluster,
+ ActivityDigest,
+ TimelineImportance,
+ WorkBucket,
+} from "@/lib/timeline-core";
+import {
+ normalizeTimelinePrivacyKey,
+ parsePrivateTimelineTaxonomy,
+ timelinePrivacyPolicyVersion,
+} from "@/lib/timeline-privacy";
+import {
+ readLastCompleteAnonymousTimelineSync,
+ readLastCompleteTimelineBackfill,
+ readLatestTimelineSync,
+ readTimelineActivityDays,
+ readTimelineContributionTotals,
+ readTimelinePublicEvents,
+} from "@/lib/timeline-store";
+import type {
+ StoredTimelineActivityDay,
+ StoredTimelineContributionTotal,
+ StoredTimelinePublicEvent,
+} from "@/lib/timeline-store";
+
+const GITHUB_LOGIN = "f0rr0";
+const DAY_IN_MILLISECONDS = 86_400_000;
+const PUBLIC_RUN_GAP_DAYS = 10;
+const PUBLIC_EVENTS_PER_MONTH = 3;
+
+interface PublicRun {
+ rows: StoredTimelineActivityDay[];
+}
+
+interface PrivateMonth {
+ bucket: WorkBucket;
+ month: string;
+ rows: StoredTimelineActivityDay[];
+}
+
+export interface AnonymousContributionDay {
+ contributionCount: number;
+ day: string;
+}
+
+type ContributionTotal = Pick<
+ StoredTimelineContributionTotal,
+ "contributionCount" | "day"
+>;
+
+const dateOnly = (date: Date) => date.toISOString().slice(0, 10);
+
+const addUtcDays = (date: Date, days: number) =>
+ new Date(
+ Date.UTC(
+ date.getUTCFullYear(),
+ date.getUTCMonth(),
+ date.getUTCDate() + days
+ )
+ );
+
+const startOfUtcDay = (date: Date) =>
+ new Date(
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
+ );
+
+const daysBetween = (left: string, right: string) =>
+ Math.round(
+ (Date.parse(`${right}T00:00:00Z`) - Date.parse(`${left}T00:00:00Z`)) /
+ DAY_IN_MILLISECONDS
+ );
+
+const startOfActivityWeek = (activityDay: string) => {
+ const date = new Date(`${activityDay}T00:00:00Z`);
+ const day = date.getUTCDay();
+ const mondayOffset = day === 0 ? -6 : 1 - day;
+ return dateOnly(addUtcDays(date, mondayOffset));
+};
+
+export const calculateAnonymousContributionDays = (input: {
+ events?: readonly Pick[];
+ rows: readonly Pick[];
+ totals: readonly ContributionTotal[];
+}): AnonymousContributionDay[] => {
+ const knownByDay = new Map();
+ for (const row of input.rows) {
+ knownByDay.set(row.day, (knownByDay.get(row.day) ?? 0) + row.commitCount);
+ }
+ const seenEvents = new Set();
+ for (const event of input.events ?? []) {
+ if (seenEvents.has(event.id)) {
+ continue;
+ }
+ seenEvents.add(event.id);
+ knownByDay.set(event.day, (knownByDay.get(event.day) ?? 0) + 1);
+ }
+
+ return input.totals.flatMap(({ contributionCount, day }) => {
+ const anonymousCount = Math.max(
+ 0,
+ contributionCount - (knownByDay.get(day) ?? 0)
+ );
+ return anonymousCount === 0
+ ? []
+ : [{ contributionCount: anonymousCount, day }];
+ });
+};
+
+const indefiniteArticle = (value: string) =>
+ /^[aeiou]/i.test(value) ? "An" : "A";
+
+const incrementMonth = (month: string) => {
+ const date = new Date(`${month}-01T00:00:00Z`);
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1))
+ .toISOString()
+ .slice(0, 7);
+};
+
+const mode = (values: readonly T[], fallback: T) => {
+ const counts = new Map();
+ let selected = fallback;
+ let selectedCount = -1;
+ for (const value of values) {
+ const count = (counts.get(value) ?? 0) + 1;
+ counts.set(value, count);
+ if (count > selectedCount) {
+ selected = value;
+ selectedCount = count;
+ }
+ }
+ return selected;
+};
+
+const magnitudeFor = (commitCount: number, activeDays: number) => {
+ if (commitCount >= 80 || activeDays >= 16) {
+ return "intense" as const;
+ }
+ if (commitCount >= 36 || activeDays >= 9) {
+ return "sustained" as const;
+ }
+ if (commitCount >= 12 || activeDays >= 4) {
+ return "steady" as const;
+ }
+ return "light" as const;
+};
+
+const cadenceFor = (activeDays: number, spanDays: number) => {
+ if (activeDays >= 8 && spanDays >= 21) {
+ return "streak" as const;
+ }
+ if (activeDays >= 3) {
+ return "clustered" as const;
+ }
+ return "isolated" as const;
+};
+
+const importanceFor = (
+ magnitude: ReturnType,
+ cadence: ReturnType
+): TimelineImportance => {
+ if (magnitude === "intense" && cadence === "streak") {
+ return "lead";
+ }
+ if (magnitude === "sustained" || cadence === "streak") {
+ return "story";
+ }
+ if (magnitude === "steady") {
+ return "brief";
+ }
+ return "pulse";
+};
+
+const languagePhrase = (languageFamily: string) => {
+ switch (languageFamily) {
+ case "data": {
+ return "data systems";
+ }
+ case "documentation": {
+ return "documentation";
+ }
+ case "infrastructure": {
+ return "infrastructure";
+ }
+ case "mobile": {
+ return "mobile systems";
+ }
+ case "systems": {
+ return "systems work";
+ }
+ case "web": {
+ return "web product work";
+ }
+ default: {
+ return "product engineering";
+ }
+ }
+};
+
+const splitPublicRuns = (
+ rows: readonly StoredTimelineActivityDay[]
+): PublicRun[] => {
+ const byRepo = new Map();
+ for (const row of rows) {
+ if (row.visibility !== "public") {
+ continue;
+ }
+ const repoRows = byRepo.get(row.repoKey) ?? [];
+ repoRows.push(row);
+ byRepo.set(row.repoKey, repoRows);
+ }
+
+ const runs: PublicRun[] = [];
+ for (const repoRows of byRepo.values()) {
+ const sorted = repoRows.toSorted((left, right) =>
+ left.day.localeCompare(right.day)
+ );
+ let current: StoredTimelineActivityDay[] = [];
+ for (const row of sorted) {
+ const previous = current.at(-1);
+ if (
+ previous !== undefined &&
+ daysBetween(previous.day, row.day) > PUBLIC_RUN_GAP_DAYS
+ ) {
+ runs.push({ rows: current });
+ current = [];
+ }
+ current.push(row);
+ }
+ if (current.length > 0) {
+ runs.push({ rows: current });
+ }
+ }
+
+ return runs;
+};
+
+const publicClusterFrom = (run: PublicRun): ActivityCluster | null => {
+ const [first] = run.rows;
+ const last = run.rows.at(-1);
+ if (
+ first === undefined ||
+ last === undefined ||
+ first.publicRepoName === null ||
+ first.publicRepoUrl === null
+ ) {
+ return null;
+ }
+
+ const commitCount = run.rows.reduce((sum, row) => sum + row.commitCount, 0);
+ const activeDays = new Set(run.rows.map((row) => row.day)).size;
+ const spanDays = daysBetween(first.day, last.day) + 1;
+ const magnitude = magnitudeFor(commitCount, activeDays);
+ const cadence = cadenceFor(activeDays, spanDays);
+ const languageFamily = mode(
+ run.rows.map((row) => row.languageFamily),
+ "other"
+ );
+ const bucket = mode(
+ run.rows.map((row) => row.bucket as WorkBucket),
+ "Open source" as WorkBucket
+ );
+ const repositoryLabel = first.publicRepoName.split("/").at(-1) ?? "project";
+
+ return {
+ bucket,
+ cadence,
+ endDate: last.day,
+ facts: [
+ `Public work appeared across ${activeDays} active days during a ${spanDays}-day span.`,
+ `The run centered on ${languagePhrase(languageFamily)}.`,
+ ],
+ key: `public:${digestTimelineValue({ end: last.day, repo: first.repoKey, start: first.day }).slice(0, 24)}`,
+ kind: "commit-run",
+ magnitude,
+ maxImportance:
+ importanceFor(magnitude, cadence) === "lead"
+ ? "story"
+ : importanceFor(magnitude, cadence),
+ publicHref: first.publicRepoUrl,
+ publicLabel: `View ${repositoryLabel}`,
+ publicTitle: `${repositoryLabel}, in motion`,
+ publishable: cadence !== "isolated",
+ rollupOf: [],
+ seriesKey: `series:${first.repoKey}`,
+ startDate: first.day,
+ visibility: "public",
+ };
+};
+
+const githubRepositoryUrlFrom = (href: string) => {
+ try {
+ const url = new URL(href);
+ const segments = url.pathname.split("/").filter(Boolean);
+ return url.protocol === "https:" &&
+ url.hostname === "github.com" &&
+ segments.length >= 2
+ ? `https://github.com/${segments[0]}/${segments[1]}`.toLocaleLowerCase(
+ "en-US"
+ )
+ : null;
+ } catch {
+ return null;
+ }
+};
+
+const canonicalPublicHref = (href: string | undefined) => {
+ if (href === undefined) {
+ return null;
+ }
+ try {
+ const url = new URL(href);
+ if (url.protocol !== "https:") {
+ return null;
+ }
+ url.hash = "";
+ url.search = "";
+ const path = url.pathname.replace(/\/+$/, "").toLocaleLowerCase("en-US");
+ return `${url.origin.toLocaleLowerCase("en-US")}${path}`;
+ } catch {
+ return null;
+ }
+};
+
+const curatedPublicClusters = (
+ rows: readonly StoredTimelineActivityDay[],
+ windowStart: string,
+ windowEnd: string
+): ActivityCluster[] => {
+ const publicRepositoryUrls = new Set(
+ rows.flatMap((row) =>
+ row.visibility === "public" && row.publicRepoUrl !== null
+ ? [row.publicRepoUrl.toLocaleLowerCase("en-US")]
+ : []
+ )
+ );
+ const repoKeyByUrl = new Map(
+ rows.flatMap((row) =>
+ row.visibility === "public" && row.publicRepoUrl !== null
+ ? [[row.publicRepoUrl.toLocaleLowerCase("en-US"), row.repoKey] as const]
+ : []
+ )
+ );
+
+ return timelineEntries.flatMap((entry) => {
+ if (
+ entry.date < windowStart ||
+ entry.date > windowEnd ||
+ ("private" in entry && entry.private)
+ ) {
+ return [];
+ }
+ const repositoryUrl = githubRepositoryUrlFrom(entry.href);
+ if (repositoryUrl !== null && !publicRepositoryUrls.has(repositoryUrl)) {
+ return [];
+ }
+
+ const importance = entry.importance ?? "story";
+ return [
+ {
+ bucket: entry.bucket,
+ cadence: importance === "lead" ? "clustered" : "isolated",
+ endDate: entry.date,
+ facts: [entry.description],
+ key: `curated:${digestTimelineValue({ date: entry.date, title: entry.title }).slice(0, 24)}`,
+ kind: "curated",
+ magnitude:
+ importance === "lead"
+ ? "intense"
+ : importance === "story"
+ ? "sustained"
+ : "steady",
+ maxImportance: importance,
+ publicHref: entry.href,
+ publicLabel: entry.label,
+ publicTitle: entry.title,
+ publishable: true,
+ rollupOf: [],
+ seriesKey:
+ repositoryUrl === null
+ ? `series:${digestTimelineValue({ href: entry.href }).slice(0, 24)}`
+ : `series:${repoKeyByUrl.get(repositoryUrl) ?? digestTimelineValue(repositoryUrl).slice(0, 24)}`,
+ startDate: entry.date,
+ visibility: "public",
+ } satisfies ActivityCluster,
+ ];
+ });
+};
+
+const groupPrivateMonths = (
+ rows: readonly StoredTimelineActivityDay[]
+): PrivateMonth[] => {
+ const groups = new Map();
+ for (const row of rows) {
+ if (row.visibility !== "private") {
+ continue;
+ }
+
+ const month = row.day.slice(0, 7);
+ const bucket = row.bucket as WorkBucket;
+ const key = `${month}:${bucket}`;
+ const group = groups.get(key) ?? { bucket, month, rows: [] };
+ group.rows.push(row);
+ groups.set(key, group);
+ }
+ return [...groups.values()].toSorted((left, right) =>
+ left.month.localeCompare(right.month)
+ );
+};
+
+const privateClusterFrom = (group: PrivateMonth): ActivityCluster | null => {
+ const commitCount = group.rows.reduce((sum, row) => sum + row.commitCount, 0);
+ const activeDays = new Set(group.rows.map((row) => row.day)).size;
+ const repoCounts = new Map();
+ const domains = new Set();
+ for (const row of group.rows) {
+ repoCounts.set(
+ row.repoKey,
+ (repoCounts.get(row.repoKey) ?? 0) + row.commitCount
+ );
+ if (row.privacyDomainKey !== null) {
+ domains.add(row.privacyDomainKey);
+ }
+ }
+
+ const dominantShare =
+ Math.max(0, ...repoCounts.values()) / Math.max(1, commitCount);
+ const themeIsDiverse =
+ group.bucket !== "Private product work" &&
+ repoCounts.size >= 3 &&
+ domains.size >= 2 &&
+ commitCount >= 20 &&
+ activeDays >= 5 &&
+ dominantShare <= 0.6;
+ const bucket = themeIsDiverse ? group.bucket : "Private product work";
+ const publishable = commitCount >= 10 && activeDays >= 3;
+ if (!publishable) {
+ return null;
+ }
+
+ const magnitude = magnitudeFor(commitCount, activeDays);
+ const cadence = cadenceFor(activeDays, 31);
+ const facts = themeIsDiverse
+ ? [
+ `${indefiniteArticle(magnitude)} ${magnitude} month across several independent private work streams.`,
+ `The broad pattern was ${bucket.toLocaleLowerCase("en-US")}.`,
+ ]
+ : [
+ `${indefiniteArticle(magnitude)} ${magnitude} month of private product work.`,
+ "Repository identity and exact activity remain deliberately withheld.",
+ ];
+
+ return {
+ bucket,
+ cadence,
+ endDate: `${group.month}-01`,
+ facts,
+ key: `private:${digestTimelineValue({ month: group.month, sourceBucket: group.bucket }).slice(0, 24)}`,
+ kind: "private-month",
+ magnitude,
+ maxImportance:
+ magnitude === "intense" || magnitude === "sustained" ? "story" : "brief",
+ publishable: true,
+ rollupOf: [],
+ seriesKey: `private-series:${digestTimelineValue(group.bucket).slice(0, 24)}`,
+ startDate: `${group.month}-01`,
+ visibility: "private",
+ };
+};
+
+const privateStreakClusters = (
+ monthlyClusters: readonly ActivityCluster[]
+): ActivityCluster[] => {
+ const byBucket = new Map();
+ for (const cluster of monthlyClusters) {
+ const bucketClusters = byBucket.get(cluster.bucket) ?? [];
+ bucketClusters.push(cluster);
+ byBucket.set(cluster.bucket, bucketClusters);
+ }
+
+ const streaks: ActivityCluster[] = [];
+ for (const [bucket, clusters] of byBucket) {
+ const sorted = clusters.toSorted((left, right) =>
+ left.startDate.localeCompare(right.startDate)
+ );
+ let current: ActivityCluster[] = [];
+
+ const finish = () => {
+ if (current.length < 3) {
+ current = [];
+ return;
+ }
+ const [first] = current;
+ const last = current.at(-1);
+ if (first === undefined || last === undefined) {
+ current = [];
+ return;
+ }
+ streaks.push({
+ bucket,
+ cadence: "streak",
+ endDate: last.endDate,
+ facts: [
+ "A sustained private work pattern held across several months.",
+ "Only the broad cadence survives the publication boundary.",
+ ],
+ key: `streak:${digestTimelineValue({ bucket, end: last.endDate, sources: current.map((cluster) => cluster.key), start: first.startDate }).slice(0, 24)}`,
+ kind: "private-streak",
+ magnitude: "sustained",
+ maxImportance: current.length >= 6 ? "lead" : "story",
+ publishable: true,
+ rollupOf: current.map((cluster) => cluster.key),
+ seriesKey: `private-series:${digestTimelineValue(bucket).slice(0, 24)}`,
+ startDate: first.startDate,
+ visibility: "private",
+ });
+ current = [];
+ };
+
+ for (const cluster of sorted) {
+ const previous = current.at(-1);
+ if (
+ previous !== undefined &&
+ incrementMonth(previous.startDate.slice(0, 7)) !==
+ cluster.startDate.slice(0, 7)
+ ) {
+ finish();
+ }
+ current.push(cluster);
+ }
+ finish();
+ }
+
+ return streaks;
+};
+
+const publicStreakCluster = (
+ rows: readonly StoredTimelineActivityDay[],
+ events: readonly StoredTimelinePublicEvent[]
+): ActivityCluster | null => {
+ const publicRows = rows.filter((row) => row.visibility === "public");
+ const activeWeekKeys = new Set(
+ [
+ ...publicRows.map((row) => row.day),
+ ...events.map((event) => event.day),
+ ].map(startOfActivityWeek)
+ );
+ const weeks = [...activeWeekKeys].toSorted();
+ let longest: string[] = [];
+ let current: string[] = [];
+ for (const week of weeks) {
+ const previous = current.at(-1);
+ if (previous !== undefined && daysBetween(previous, week) !== 7) {
+ if (current.length > longest.length) {
+ longest = current;
+ }
+ current = [];
+ }
+ current.push(week);
+ }
+ if (current.length > longest.length) {
+ longest = current;
+ }
+ if (longest.length < 5) {
+ return null;
+ }
+
+ const [first] = longest;
+ const last = longest.at(-1);
+ if (first === undefined || last === undefined) {
+ return null;
+ }
+ const repositories = new Set([
+ ...publicRows
+ .filter(
+ (row) =>
+ row.day >= first &&
+ row.day <=
+ addUtcDays(new Date(`${last}T00:00:00Z`), 6)
+ .toISOString()
+ .slice(0, 10)
+ )
+ .map((row) => row.repoKey),
+ ...events
+ .filter(
+ (event) =>
+ event.day >= first &&
+ event.day <=
+ addUtcDays(new Date(`${last}T00:00:00Z`), 6)
+ .toISOString()
+ .slice(0, 10)
+ )
+ .map((event) => event.repoKey),
+ ]);
+ return {
+ bucket: "Product systems",
+ cadence: "streak",
+ endDate: last,
+ facts: [
+ `Public contributions appeared in ${longest.length} consecutive weeks across ${repositories.size} repositories.`,
+ "Commit runs and discrete collaboration events count once within each active week.",
+ ],
+ key: `streak:${digestTimelineValue({ end: last, scope: "public", start: first }).slice(0, 24)}`,
+ kind: "public-streak",
+ magnitude: "intense",
+ maxImportance: "lead",
+ publicTitle: "A sustained public cadence",
+ publishable: true,
+ rollupOf: [],
+ seriesKey: "series:public-cadence",
+ startDate: first,
+ visibility: "public",
+ };
+};
+
+const longestActiveWeekRun = (totals: readonly ContributionTotal[]) => {
+ const weeks = [
+ ...new Set(
+ totals
+ .filter((total) => total.contributionCount > 0)
+ .map((total) => startOfActivityWeek(total.day))
+ ),
+ ].toSorted();
+ let longest: string[] = [];
+ let current: string[] = [];
+ for (const week of weeks) {
+ const previous = current.at(-1);
+ if (previous !== undefined && daysBetween(previous, week) !== 7) {
+ if (current.length > longest.length) {
+ longest = current;
+ }
+ current = [];
+ }
+ current.push(week);
+ }
+ return current.length > longest.length ? current : longest;
+};
+
+const accountWideStreakCluster = (
+ totals: readonly ContributionTotal[]
+): ActivityCluster | null => {
+ const longest = longestActiveWeekRun(totals);
+ const [first] = longest;
+ const last = longest.at(-1);
+ if (longest.length < 5 || first === undefined || last === undefined) {
+ return null;
+ }
+
+ return {
+ bucket: "Across the work",
+ cadence: "streak",
+ endDate: last,
+ facts: [
+ "The anonymized account-wide calendar shows a sustained run across consecutive active weeks.",
+ "Repository identity and activity type remain unavailable, so no project theme is inferred.",
+ ],
+ key: `streak:${digestTimelineValue({ end: last, scope: "account-wide", start: first }).slice(0, 24)}`,
+ kind: "account-wide-streak",
+ magnitude: longest.length >= 12 ? "intense" : "sustained",
+ maxImportance: longest.length >= 12 ? "lead" : "story",
+ publishable: true,
+ rollupOf: [],
+ seriesKey: "anonymous-series:account-wide",
+ startDate: first,
+ visibility: "anonymous",
+ };
+};
+
+const anonymousMonthlyClusters = (
+ days: readonly AnonymousContributionDay[]
+): ActivityCluster[] => {
+ const byMonth = new Map();
+ for (const day of days) {
+ const month = day.day.slice(0, 7);
+ const monthDays = byMonth.get(month) ?? [];
+ monthDays.push(day);
+ byMonth.set(month, monthDays);
+ }
+
+ return [...byMonth.entries()].flatMap(([month, monthDays]) => {
+ const contributionCount = monthDays.reduce(
+ (sum, day) => sum + day.contributionCount,
+ 0
+ );
+ const activeDays = monthDays.length;
+ if (activeDays < 3 || contributionCount < 5) {
+ return [];
+ }
+ const magnitude = magnitudeFor(contributionCount, activeDays);
+ return [
+ {
+ bucket: "Across the work",
+ cadence: "clustered",
+ endDate: `${month}-01`,
+ facts: [
+ "An anonymized account-wide rhythm extended beyond repository-resolved activity.",
+ "Repository identity and activity type are unavailable, so no project theme is inferred.",
+ ],
+ key: `anonymous:${digestTimelineValue({ month, source: "residual" }).slice(0, 24)}`,
+ kind: "anonymous-month",
+ magnitude,
+ maxImportance:
+ magnitude === "intense" || magnitude === "sustained"
+ ? "brief"
+ : "pulse",
+ publishable: true,
+ rollupOf: [],
+ seriesKey: "anonymous-series:account-wide",
+ startDate: `${month}-01`,
+ visibility: "anonymous",
+ } satisfies ActivityCluster,
+ ];
+ });
+};
+
+const publicEventClusterFrom = (
+ event: StoredTimelinePublicEvent
+): ActivityCluster | null => {
+ if (event.eventKind === "pull_request_reviewed") {
+ return null;
+ }
+ const repositoryLabel = event.publicRepoName.split("/").at(-1) ?? "project";
+ const copy =
+ event.eventKind === "issue_opened"
+ ? {
+ description: `Opened a public issue in ${event.publicRepoName}; the thread remains available on GitHub.`,
+ kind: "issue-opened" as const,
+ label: "View issue",
+ maxImportance: "brief" as const,
+ title: event.publicTitle,
+ }
+ : event.eventKind === "pull_request_opened"
+ ? {
+ description: `Opened a public pull request in ${event.publicRepoName}; the proposed change remains available on GitHub.`,
+ kind: "pull-request-opened" as const,
+ label: "View pull request",
+ maxImportance: "brief" as const,
+ title: event.publicTitle,
+ }
+ : {
+ description: `Created ${event.publicRepoName} as a public repository.`,
+ kind: "repository-created" as const,
+ label: `View ${repositoryLabel}`,
+ maxImportance: "brief" as const,
+ title: `${repositoryLabel}, made public`,
+ };
+
+ return {
+ bucket: event.bucket as WorkBucket,
+ cadence: "isolated",
+ endDate: event.day,
+ facts: [copy.description],
+ key: `event:${event.id.slice(0, 24)}`,
+ kind: copy.kind,
+ magnitude: "light",
+ maxImportance: copy.maxImportance,
+ publicHref: event.publicUrl,
+ publicLabel: copy.label,
+ publicTitle: copy.title,
+ publishable: true,
+ rollupOf: [],
+ seriesKey: `series:${event.repoKey}`,
+ startDate: event.day,
+ visibility: "public",
+ };
+};
+
+const representativePublicEventClusters = (
+ clusters: readonly ActivityCluster[]
+) => {
+ const byMonth = new Map();
+ for (const cluster of clusters) {
+ const month = cluster.startDate.slice(0, 7);
+ const monthClusters = byMonth.get(month) ?? [];
+ monthClusters.push(cluster);
+ byMonth.set(month, monthClusters);
+ }
+
+ return [...byMonth.values()].flatMap((monthClusters) => {
+ const sorted = monthClusters.toSorted(
+ (left, right) =>
+ right.startDate.localeCompare(left.startDate) ||
+ left.key.localeCompare(right.key)
+ );
+ const selected: ActivityCluster[] = [];
+ const selectedKeys = new Set();
+ const select = (cluster: ActivityCluster | undefined) => {
+ if (
+ cluster !== undefined &&
+ selected.length < PUBLIC_EVENTS_PER_MONTH &&
+ !selectedKeys.has(cluster.key)
+ ) {
+ selected.push(cluster);
+ selectedKeys.add(cluster.key);
+ }
+ };
+
+ for (const kind of [
+ "repository-created",
+ "issue-opened",
+ "pull-request-opened",
+ ] as const) {
+ select(sorted.find((cluster) => cluster.kind === kind));
+ }
+ const selectedSeries = new Set(
+ selected.map((cluster) => cluster.seriesKey)
+ );
+ for (const cluster of sorted) {
+ if (!selectedSeries.has(cluster.seriesKey)) {
+ select(cluster);
+ selectedSeries.add(cluster.seriesKey);
+ }
+ }
+ for (const cluster of sorted) {
+ select(cluster);
+ }
+ return selected;
+ });
+};
+
+const publicRecurrenceClusters = (
+ clusters: readonly ActivityCluster[]
+): ActivityCluster[] => {
+ const bySeries = new Map();
+ for (const cluster of clusters) {
+ const series = bySeries.get(cluster.seriesKey) ?? [];
+ series.push(cluster);
+ bySeries.set(cluster.seriesKey, series);
+ }
+
+ return [...bySeries.entries()].flatMap(([seriesKey, series]) => {
+ const sorted = series.toSorted((left, right) =>
+ left.startDate.localeCompare(right.startDate)
+ );
+ const [first] = sorted;
+ const last = sorted.at(-1);
+ if (
+ sorted.length < 3 ||
+ first === undefined ||
+ last === undefined ||
+ daysBetween(first.startDate, last.endDate) < 60 ||
+ first.publicHref === undefined
+ ) {
+ return [];
+ }
+ const repositoryLabel =
+ first.publicLabel?.replace(/^View /, "") ?? "A public project";
+ return [
+ {
+ bucket: first.bucket,
+ cadence: "clustered",
+ endDate: last.endDate,
+ facts: [
+ `${repositoryLabel} returned in ${sorted.length} distinct public work runs across the year.`,
+ ],
+ key: `recurrence:${digestTimelineValue({ seriesKey, sources: sorted.map((cluster) => cluster.key) }).slice(0, 24)}`,
+ kind: "recurrence",
+ magnitude: "sustained",
+ maxImportance: "story",
+ publicHref: first.publicHref,
+ publicLabel: first.publicLabel,
+ publicTitle: `${repositoryLabel}, revisited`,
+ publishable: true,
+ rollupOf: sorted.map((cluster) => cluster.key),
+ seriesKey,
+ startDate: first.startDate,
+ visibility: "public",
+ } satisfies ActivityCluster,
+ ];
+ });
+};
+
+export const createTimelineActivityDigest = (input: {
+ anonymousTotals?: readonly ContributionTotal[];
+ coverage: "complete" | "partial";
+ events?: readonly StoredTimelinePublicEvent[];
+ generatedAt: Date;
+ rows: readonly StoredTimelineActivityDay[];
+ windowEnd: string;
+ windowStart: string;
+}): ActivityDigest => {
+ const events = input.events ?? [];
+ const anonymousTotals = input.anonymousTotals ?? [];
+ const rawPublicClusters = splitPublicRuns(input.rows).flatMap((run) => {
+ const cluster = publicClusterFrom(run);
+ return cluster === null ? [] : [cluster];
+ });
+ const rawEventClusters = events.flatMap((event) => {
+ const cluster = publicEventClusterFrom(event);
+ return cluster === null ? [] : [cluster];
+ });
+ const curatedClusters = curatedPublicClusters(
+ input.rows,
+ input.windowStart,
+ input.windowEnd
+ );
+ const curatedHrefs = new Set(
+ curatedClusters.flatMap((cluster) => {
+ const href = canonicalPublicHref(cluster.publicHref);
+ return href === null ? [] : [href];
+ })
+ );
+ const eventClusters = representativePublicEventClusters(
+ rawEventClusters.filter((cluster) => {
+ const eventHref = canonicalPublicHref(cluster.publicHref);
+ if (eventHref !== null && curatedHrefs.has(eventHref)) {
+ return false;
+ }
+ if (cluster.kind !== "repository-created") {
+ return true;
+ }
+ return !curatedClusters.some(
+ (curated) =>
+ canonicalPublicHref(curated.publicHref) === eventHref &&
+ Math.abs(daysBetween(curated.endDate, cluster.endDate)) <= 7
+ );
+ })
+ );
+ const recurrenceClusters = publicRecurrenceClusters(rawPublicClusters);
+ const recurringSeries = new Set(
+ recurrenceClusters.map((cluster) => cluster.seriesKey)
+ );
+ const publicClusters = rawPublicClusters.filter((cluster) => {
+ if (recurringSeries.has(cluster.seriesKey)) {
+ return false;
+ }
+ if (cluster.cadence === "streak") {
+ return true;
+ }
+ const overlapsCuratedMarker = curatedClusters.some(
+ (marker) =>
+ marker.seriesKey === cluster.seriesKey &&
+ marker.endDate >= cluster.startDate &&
+ marker.endDate <= cluster.endDate
+ );
+ const overlapsCompactEvent =
+ (cluster.maxImportance === "brief" ||
+ cluster.maxImportance === "pulse") &&
+ eventClusters.some(
+ (marker) =>
+ marker.seriesKey === cluster.seriesKey &&
+ marker.endDate >= cluster.startDate &&
+ marker.endDate <= cluster.endDate
+ );
+ return !(overlapsCuratedMarker || overlapsCompactEvent);
+ });
+ const privateGroups = groupPrivateMonths(input.rows);
+ const genericRowsByMonth = new Map();
+ const privateSpecificClusters = privateGroups.flatMap((group) => {
+ const cluster = privateClusterFrom(group);
+ if (cluster === null || cluster.bucket === "Private product work") {
+ genericRowsByMonth.set(group.month, [
+ ...(genericRowsByMonth.get(group.month) ?? []),
+ ...group.rows,
+ ]);
+ return [];
+ }
+ return [cluster];
+ });
+ const privateGenericClusters = [...genericRowsByMonth.entries()].flatMap(
+ ([month, rows]) => {
+ const cluster = privateClusterFrom({
+ bucket: "Private product work",
+ month,
+ rows,
+ });
+ return cluster === null ? [] : [cluster];
+ }
+ );
+ const privateMonthlyClusters = [
+ ...privateSpecificClusters,
+ ...privateGenericClusters,
+ ];
+ const rawAnonymousClusters = anonymousMonthlyClusters(
+ calculateAnonymousContributionDays({
+ events,
+ rows: input.rows,
+ totals: anonymousTotals,
+ })
+ );
+ const accountWideStreak = accountWideStreakCluster(anonymousTotals);
+ const anonymousClusters =
+ accountWideStreak === null
+ ? rawAnonymousClusters
+ : rawAnonymousClusters.filter(
+ (cluster) =>
+ cluster.startDate.slice(0, 7) !==
+ accountWideStreak.endDate.slice(0, 7)
+ );
+ const publicStreak =
+ accountWideStreak === null ? publicStreakCluster(input.rows, events) : null;
+ const importanceRank: Record = {
+ brief: 1,
+ lead: 3,
+ pulse: 0,
+ story: 2,
+ };
+ const clusters = [
+ ...curatedClusters,
+ ...eventClusters,
+ ...recurrenceClusters,
+ ...publicClusters,
+ ...privateMonthlyClusters,
+ ...privateStreakClusters(privateMonthlyClusters),
+ ...anonymousClusters,
+ ...(accountWideStreak === null ? [] : [accountWideStreak]),
+ ...(publicStreak === null ? [] : [publicStreak]),
+ ]
+ .flatMap((cluster) => {
+ if (cluster.visibility === "public") {
+ return [
+ {
+ ...cluster,
+ startDate:
+ cluster.startDate < input.windowStart
+ ? input.windowStart
+ : cluster.startDate,
+ },
+ ];
+ }
+ let startDate = `${cluster.startDate.slice(0, 7)}-01`;
+ if (startDate < input.windowStart) {
+ startDate = `${incrementMonth(startDate.slice(0, 7))}-01`;
+ }
+ const endDate = `${cluster.endDate.slice(0, 7)}-01`;
+ return startDate > endDate ? [] : [{ ...cluster, endDate, startDate }];
+ })
+ .toSorted(
+ (left, right) =>
+ importanceRank[right.maxImportance] -
+ importanceRank[left.maxImportance] ||
+ right.startDate.localeCompare(left.startDate) ||
+ left.key.localeCompare(right.key)
+ )
+ .slice(0, 120)
+ .toSorted((left, right) => right.startDate.localeCompare(left.startDate));
+
+ return activityDigestSchema.parse({
+ clusters,
+ coverage: input.coverage,
+ generatedAt: input.generatedAt.toISOString(),
+ windowEnd: input.windowEnd,
+ windowStart: input.windowStart,
+ });
+};
+
+const syncAgeInDays = (completedAt: Date | null | undefined, now: Date) =>
+ completedAt === null || completedAt === undefined
+ ? Number.POSITIVE_INFINITY
+ : Math.floor((now.getTime() - completedAt.getTime()) / DAY_IN_MILLISECONDS);
+
+const eligibleActivityRows = (
+ rows: readonly StoredTimelineActivityDay[],
+ activePrivacyVersion: string | null
+) =>
+ rows.filter(
+ (row) =>
+ row.visibility === "public" ||
+ (activePrivacyVersion !== null &&
+ row.privacyPolicyVersion === activePrivacyVersion)
+ );
+
+const currentAnonymousTotals = (input: {
+ lastSync: {
+ completedAt: Date | null;
+ windowEnd: string;
+ } | null;
+ now: Date;
+ totals: readonly StoredTimelineContributionTotal[];
+ windowEnd: string;
+ windowStart: string;
+}) => {
+ const start = new Date(`${input.windowStart}T00:00:00Z`);
+ const windowIsComplete =
+ input.totals.length === TIMELINE_WINDOW_DAYS &&
+ input.totals.every(
+ (total, index) => total.day === dateOnly(addUtcDays(start, index))
+ );
+ const syncAge = syncAgeInDays(input.lastSync?.completedAt, input.now);
+ return windowIsComplete &&
+ input.lastSync?.windowEnd === input.windowEnd &&
+ syncAge >= 0 &&
+ syncAge <= 2
+ ? input.totals
+ : [];
+};
+
+const timelineCoverage = (input: {
+ lastFullSync: {
+ windowEnd: string;
+ windowStart: string;
+ } | null;
+ latestSync: {
+ completedAt: Date | null;
+ coverage: string;
+ status: string;
+ } | null;
+ now: Date;
+ windowEnd: string;
+ windowStart: string;
+}): "complete" | "partial" => {
+ if (input.lastFullSync === null) {
+ return "partial";
+ }
+ const lastSyncSpan =
+ daysBetween(input.lastFullSync.windowStart, input.lastFullSync.windowEnd) +
+ 1;
+ const backfillAge = daysBetween(
+ input.lastFullSync.windowEnd,
+ input.windowEnd
+ );
+ const latestSyncAge = syncAgeInDays(input.latestSync?.completedAt, input.now);
+ return lastSyncSpan >= 365 &&
+ input.lastFullSync.windowStart <= input.windowStart &&
+ backfillAge >= 0 &&
+ backfillAge <= 8 &&
+ input.latestSync?.status === "completed" &&
+ input.latestSync.coverage === "complete" &&
+ latestSyncAge >= 0 &&
+ latestSyncAge <= 2
+ ? "complete"
+ : "partial";
+};
+
+export const loadTimelineActivityDigest = async (
+ now = new Date()
+): Promise => {
+ const end = startOfUtcDay(now);
+ const windowEnd = dateOnly(end);
+ const windowStart = dateOnly(addUtcDays(end, -(TIMELINE_WINDOW_DAYS - 1)));
+ const [
+ rows,
+ events,
+ anonymousTotals,
+ lastAnonymousSync,
+ lastFullSync,
+ latestSync,
+ ] = await Promise.all([
+ readTimelineActivityDays(GITHUB_LOGIN, windowStart, windowEnd),
+ readTimelinePublicEvents(GITHUB_LOGIN, windowStart, windowEnd),
+ readTimelineContributionTotals(GITHUB_LOGIN, windowStart, windowEnd),
+ readLastCompleteAnonymousTimelineSync(),
+ readLastCompleteTimelineBackfill(),
+ readLatestTimelineSync(),
+ ]);
+ const privacyKey = normalizeTimelinePrivacyKey(
+ process.env.TIMELINE_PRIVACY_KEY
+ );
+ const activePrivacyVersion =
+ privacyKey === null
+ ? null
+ : timelinePrivacyPolicyVersion(
+ privacyKey,
+ parsePrivateTimelineTaxonomy(process.env.TIMELINE_PRIVATE_TAXONOMY)
+ );
+ const eligibleRows = eligibleActivityRows(rows, activePrivacyVersion);
+
+ return createTimelineActivityDigest({
+ anonymousTotals: currentAnonymousTotals({
+ lastSync: lastAnonymousSync,
+ now,
+ totals: anonymousTotals,
+ windowEnd,
+ windowStart,
+ }),
+ coverage: timelineCoverage({
+ lastFullSync,
+ latestSync,
+ now,
+ windowEnd,
+ windowStart,
+ }),
+ generatedAt: now,
+ events,
+ rows: eligibleRows,
+ windowEnd,
+ windowStart,
+ });
+};
diff --git a/src/lib/timeline-fallback.ts b/src/lib/timeline-fallback.ts
new file mode 100644
index 0000000..6331e56
--- /dev/null
+++ b/src/lib/timeline-fallback.ts
@@ -0,0 +1,245 @@
+import { timelineEntries } from "@/content/home";
+import type { GitHubActivity } from "@/lib/github-profile-core";
+import {
+ digestTimelineValue,
+ TIMELINE_PROMPT_VERSION,
+ TIMELINE_SCHEMA_VERSION,
+ TIMELINE_WINDOW_DAYS,
+ timelineEditionSchema,
+ timelinePlanSchema,
+} from "@/lib/timeline-core";
+import type {
+ TimelineEdition,
+ TimelineEditionEntry,
+} from "@/lib/timeline-core";
+
+const DAY_IN_MILLISECONDS = 86_400_000;
+
+const slug = (value: string) =>
+ value
+ .toLocaleLowerCase("en-US")
+ .replaceAll(/[^a-z0-9]+/g, "-")
+ .replaceAll(/^-|-$/g, "")
+ .slice(0, 70);
+
+const dateOnly = (date: Date) => date.toISOString().slice(0, 10);
+
+const addUtcDays = (date: Date, days: number) =>
+ new Date(date.getTime() + days * DAY_IN_MILLISECONDS);
+
+const incrementMonth = (month: string) => {
+ const date = new Date(`${month}-01T00:00:00Z`);
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1))
+ .toISOString()
+ .slice(0, 7);
+};
+
+const githubRepositoryUrlFrom = (href: string) => {
+ try {
+ const url = new URL(href);
+ const segments = url.pathname.split("/").filter(Boolean);
+ return url.protocol === "https:" &&
+ url.hostname === "github.com" &&
+ segments.length >= 2
+ ? `https://github.com/${segments[0]}/${segments[1]}`.toLocaleLowerCase(
+ "en-US"
+ )
+ : null;
+ } catch {
+ return null;
+ }
+};
+
+const fallbackActivityEntries = (
+ activity: GitHubActivity,
+ windowStart: string
+): TimelineEditionEntry[] => {
+ if (activity.status === "unavailable") {
+ return [];
+ }
+
+ const countByMonth = new Map();
+ for (const week of activity.weeks) {
+ if (week.weekStart < windowStart) {
+ continue;
+ }
+ const month = week.weekStart.slice(0, 7);
+ countByMonth.set(
+ month,
+ (countByMonth.get(month) ?? 0) + week.contributionCount
+ );
+ }
+
+ const months = [...countByMonth.entries()]
+ .filter(([, count]) => count > 0)
+ .toSorted((left, right) => right[0].localeCompare(left[0]))
+ .slice(0, 10);
+ const rankedCounts = months
+ .map(([, count]) => count)
+ .toSorted((left, right) => left - right);
+ const highThreshold =
+ rankedCounts.at(Math.floor(rankedCounts.length * 0.7)) ?? 0;
+
+ const activeWeeks = activity.weeks
+ .filter(
+ (week) =>
+ week.contributionCount > 0 && week.weekStart >= windowStart.slice(0, 7)
+ )
+ .toSorted((left, right) => left.weekStart.localeCompare(right.weekStart));
+ let longest: typeof activeWeeks = [];
+ let current: typeof activeWeeks = [];
+ for (const week of activeWeeks) {
+ const previous = current.at(-1);
+ if (
+ previous !== undefined &&
+ Date.parse(`${week.weekStart}T00:00:00Z`) -
+ Date.parse(`${previous.weekStart}T00:00:00Z`) !==
+ 7 * DAY_IN_MILLISECONDS
+ ) {
+ if (current.length > longest.length) {
+ longest = current;
+ }
+ current = [];
+ }
+ current.push(week);
+ }
+ if (current.length > longest.length) {
+ longest = current;
+ }
+
+ const [firstStreakWeek] = longest;
+ const lastStreakWeek = longest.at(-1);
+ const firstPublishableMonth = windowStart.endsWith("-01")
+ ? windowStart
+ : `${incrementMonth(windowStart.slice(0, 7))}-01`;
+ const streakEntry =
+ longest.length >= 5 &&
+ firstStreakWeek !== undefined &&
+ lastStreakWeek !== undefined
+ ? [
+ {
+ bucket: "Across the work",
+ cadence: "streak",
+ description:
+ "An anonymized, account-wide contribution signal formed a sustained rhythm; repository identity and activity type remain unavailable.",
+ endDate: `${lastStreakWeek.weekStart.slice(0, 7)}-01`,
+ id: `activity-streak-${firstStreakWeek.weekStart.slice(0, 7)}-${lastStreakWeek.weekStart.slice(0, 7)}`,
+ importance: "story",
+ kind: "activity",
+ metrics: [],
+ sourceKeys: [
+ `streak:${digestTimelineValue({ end: lastStreakWeek.weekStart, scope: "account-wide", start: firstStreakWeek.weekStart }).slice(0, 24)}`,
+ ],
+ startDate:
+ `${firstStreakWeek.weekStart.slice(0, 7)}-01` <
+ firstPublishableMonth
+ ? firstPublishableMonth
+ : `${firstStreakWeek.weekStart.slice(0, 7)}-01`,
+ title: "A sustained account-wide cadence",
+ visibility: "anonymous",
+ } satisfies TimelineEditionEntry,
+ ]
+ : [];
+
+ return [
+ ...streakEntry,
+ ...months
+ .filter(
+ ([month]) =>
+ lastStreakWeek === undefined ||
+ month !== lastStreakWeek.weekStart.slice(0, 7)
+ )
+ .map(([month, count], index) => {
+ const sourceKey = `pulse:${digestTimelineValue({ count, month }).slice(0, 24)}`;
+ const cadence = count >= highThreshold ? "concentrated" : "steady";
+ return {
+ bucket: "Across the work",
+ cadence: "clustered",
+ description: `A ${cadence} contribution-calendar rhythm appeared across the month; identities and exact volume remain withheld.`,
+ endDate: `${month}-01`,
+ id: `activity-${month}-${index}`,
+ importance: count >= highThreshold ? "brief" : "pulse",
+ kind: "activity",
+ metrics: [],
+ sourceKeys: [sourceKey],
+ startDate: `${month}-01`,
+ title: `${cadence === "concentrated" ? "A concentrated" : "A steady"} month in the work`,
+ visibility: "anonymous",
+ } satisfies TimelineEditionEntry;
+ }),
+ ];
+};
+
+export const createFallbackTimelineEdition = (
+ activity: GitHubActivity,
+ allowedPublicRepositories: ReadonlySet,
+ now = new Date()
+): TimelineEdition | null => {
+ const end = new Date(
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
+ );
+ const windowEnd = dateOnly(end);
+ const windowStart = dateOnly(addUtcDays(end, -(TIMELINE_WINDOW_DAYS - 1)));
+ const curated = timelineEntries.flatMap((entry) => {
+ if (entry.date < windowStart || entry.date > windowEnd) {
+ return [];
+ }
+ const githubRepositoryUrl = githubRepositoryUrlFrom(entry.href);
+ if (
+ githubRepositoryUrl !== null &&
+ !allowedPublicRepositories.has(githubRepositoryUrl)
+ ) {
+ return [];
+ }
+
+ const sourceKey = `curated:${digestTimelineValue({ date: entry.date, title: entry.title }).slice(0, 24)}`;
+ const isPrivate = "private" in entry && entry.private;
+ const importance = entry.importance ?? "story";
+ return [
+ {
+ bucket: entry.bucket,
+ cadence: importance === "lead" ? "clustered" : "isolated",
+ description: entry.description,
+ endDate: isPrivate ? `${entry.date.slice(0, 7)}-01` : entry.date,
+ ...(isPrivate ? {} : { href: entry.href, label: entry.label }),
+ id: `${slug(entry.title)}-${entry.date}`,
+ importance,
+ kind: "project",
+ metrics: [],
+ sourceKeys: [sourceKey],
+ startDate: isPrivate ? `${entry.date.slice(0, 7)}-01` : entry.date,
+ title: entry.title,
+ visibility: isPrivate ? "private" : "public",
+ } satisfies TimelineEditionEntry,
+ ];
+ });
+ const activityEntries = fallbackActivityEntries(activity, windowStart);
+ const planResult = timelinePlanSchema.safeParse({
+ entries: [...curated, ...activityEntries].toSorted((left, right) =>
+ right.startDate.localeCompare(left.startDate)
+ ),
+ headline: "The work, along one line.",
+ standfirst:
+ "A rolling edition of public milestones, sustained runs, and the smaller acts of collaboration between them.",
+ windowEnd,
+ windowStart,
+ });
+ if (!planResult.success) {
+ return null;
+ }
+ const plan = planResult.data;
+ const sourceDigest = digestTimelineValue({
+ activity,
+ sources: plan.entries.flatMap((entry) => entry.sourceKeys),
+ });
+ const editionKey = digestTimelineValue({ plan, sourceDigest });
+
+ return timelineEditionSchema.parse({
+ ...plan,
+ editionKey,
+ generatedAt: now.toISOString(),
+ promptVersion: TIMELINE_PROMPT_VERSION,
+ schemaVersion: TIMELINE_SCHEMA_VERSION,
+ sourceDigest,
+ });
+};
diff --git a/src/lib/timeline-github.ts b/src/lib/timeline-github.ts
new file mode 100644
index 0000000..815f601
--- /dev/null
+++ b/src/lib/timeline-github.ts
@@ -0,0 +1,1717 @@
+import { createHash, createPrivateKey, createSign } from "node:crypto";
+import { setTimeout as delay } from "node:timers/promises";
+
+import { isTimelineDatabaseConfigured } from "@/db/client";
+import { fetchPublicGitHubContributionDays } from "@/lib/github-contribution-calendar";
+import { TIMELINE_WINDOW_DAYS } from "@/lib/timeline-core";
+import {
+ normalizeGitHubContributionSlice,
+ normalizeTimelinePrivacyKey,
+ parsePrivateTimelineTaxonomy,
+} from "@/lib/timeline-privacy";
+import type { PrivateTaxonomyValue } from "@/lib/timeline-privacy";
+import {
+ beginTimelineSyncRun,
+ completeTimelineSyncRun,
+ countStoredTimelineActivity,
+ deleteTimelineActivityByIds,
+ deleteTimelinePublicEventsByIds,
+ failTimelineSyncRun,
+ pruneTimelineActivityBefore,
+ pruneTimelineContributionTotalsBefore,
+ pruneTimelinePublicEventsBefore,
+ readLastCompletedTimelineSync,
+ readTimelineActivityDays,
+ readTimelinePublicEvents,
+ rejectPublishedTimelineEditions,
+ upsertTimelineActivityDays,
+ upsertTimelineContributionTotals,
+ upsertTimelinePublicEvents,
+} from "@/lib/timeline-store";
+import type {
+ TimelineActivityDayRecord,
+ TimelineContributionTotalRecord,
+ TimelinePublicEventRecord,
+} from "@/lib/timeline-store";
+
+const GITHUB_LOGIN = "f0rr0";
+const GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
+const GITHUB_API_URL = "https://api.github.com";
+const GITHUB_API_VERSION = "2026-03-10";
+const GITHUB_TIMEOUT_MS = 15_000;
+const GITHUB_REST_INTERVAL_MS = 125;
+const GITHUB_MAX_RETRY_DELAY_MS = 10_000;
+const GITHUB_MAX_RETRIES = 2;
+const GITHUB_MAX_PAGES = 1000;
+const INCREMENTAL_LOOKBACK_DAYS = 21;
+const RETENTION_DAYS = 420;
+
+const contributionQuery = `
+ query TimelineContributionSlice(
+ $login: String!
+ $from: DateTime!
+ $to: DateTime!
+ $includeCommits: Boolean!
+ $includeIssues: Boolean!
+ $includePullRequests: Boolean!
+ $includeReviews: Boolean!
+ $includeRepositories: Boolean!
+ $issueCursor: String
+ $pullRequestCursor: String
+ $reviewCursor: String
+ $repositoryCursor: String
+ ) {
+ user(login: $login) {
+ contributionsCollection(from: $from, to: $to) {
+ commitContributionsByRepository(maxRepositories: 100)
+ @include(if: $includeCommits) {
+ repository {
+ id
+ nameWithOwner
+ isPrivate
+ url
+ description
+ primaryLanguage {
+ name
+ }
+ repositoryTopics(first: 10) {
+ nodes {
+ topic {
+ name
+ }
+ }
+ }
+ }
+ contributions(first: 100) {
+ nodes {
+ occurredAt
+ commitCount
+ }
+ }
+ }
+ issueContributions(first: 100, after: $issueCursor)
+ @include(if: $includeIssues) {
+ nodes {
+ isRestricted
+ occurredAt
+ issue {
+ id
+ title
+ url
+ repository {
+ ...TimelinePublicRepository
+ }
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ pullRequestContributions(first: 100, after: $pullRequestCursor)
+ @include(if: $includePullRequests) {
+ nodes {
+ isRestricted
+ occurredAt
+ pullRequest {
+ id
+ title
+ url
+ repository {
+ ...TimelinePublicRepository
+ }
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ pullRequestReviewContributions(first: 100, after: $reviewCursor)
+ @include(if: $includeReviews) {
+ nodes {
+ isRestricted
+ occurredAt
+ pullRequest {
+ id
+ title
+ url
+ repository {
+ ...TimelinePublicRepository
+ }
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ repositoryContributions(first: 100, after: $repositoryCursor)
+ @include(if: $includeRepositories) {
+ nodes {
+ isRestricted
+ occurredAt
+ repository {
+ ...TimelinePublicRepository
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ }
+ }
+ }
+
+ fragment TimelinePublicRepository on Repository {
+ id
+ nameWithOwner
+ isPrivate
+ url
+ description
+ primaryLanguage {
+ name
+ }
+ repositoryTopics(first: 10) {
+ nodes {
+ topic {
+ name
+ }
+ }
+ }
+ }
+`;
+
+export type TimelineSyncKind =
+ | "backfill"
+ | "incremental"
+ | "manual"
+ | "webhook";
+
+export interface TimelineSyncResult {
+ anonymousCoverage: "complete" | "unavailable";
+ anonymousDays: number;
+ coverage: "complete" | "partial";
+ events: number;
+ kind: TimelineSyncKind;
+ privateActivity: "included" | "skipped";
+ rows: number;
+ windowEnd: string;
+ windowStart: string;
+}
+
+class TimelineSyncError extends Error {
+ readonly code: string;
+
+ constructor(code: string) {
+ super(code);
+ this.code = code;
+ this.name = "TimelineSyncError";
+ }
+}
+
+interface GitHubUserCredential {
+ kind: "user";
+ token: string;
+}
+
+interface GitHubInstallationCredential {
+ kind: "installation";
+ token: string;
+}
+
+type GitHubCredential = GitHubInstallationCredential | GitHubUserCredential;
+
+interface DateSlice {
+ end: string;
+ start: string;
+}
+
+interface CollectionResult {
+ coverage: "complete" | "partial";
+ failedRequests: number;
+ privateRecordsSkipped: number;
+ publicEventCoverage: "complete" | "partial" | "unavailable";
+ publicEvents: TimelinePublicEventRecord[];
+ records: TimelineActivityDayRecord[];
+ successfulRequests: number;
+}
+
+interface AnonymousContributionCollection {
+ coverage: "complete" | "unavailable";
+ records: TimelineContributionTotalRecord[];
+}
+
+interface CollectionAccumulator {
+ coverage: "complete" | "partial";
+ failedRequests: number;
+ privateRecordsSkipped: number;
+ publicEventCoverage: "complete" | "partial" | "unavailable";
+ publicEvents: Map;
+ records: Map;
+ successfulRequests: number;
+}
+
+interface NormalizationContext {
+ privacyKey: string | null;
+ subject: string;
+ taxonomy: ReadonlyMap;
+}
+
+interface SyncPlan {
+ day: Date;
+ kind: TimelineSyncKind;
+ useFullWindow: boolean;
+ windowEnd: string;
+ windowStart: string;
+ windowStartDate: Date;
+}
+
+interface GitHubRestPage {
+ nextUrl: string | null;
+ payload: unknown;
+ status: number;
+}
+
+type GitHubRestClient = (
+ url: string,
+ benignStatuses?: ReadonlySet
+) => Promise;
+
+interface RestRepository {
+ apiUrl: string;
+ defaultBranch: string | null;
+ description: string | null;
+ htmlUrl: string;
+ id: string;
+ isPrivate: boolean;
+ language: string | null;
+ nameWithOwner: string;
+ topics: string[];
+}
+
+interface CommitObservation {
+ day: string;
+ reachedDefaultBranch: boolean;
+}
+
+interface RepositoryCollectionResult {
+ coverage: "complete" | "partial";
+ failedRequests: number;
+ normalized: ReturnType;
+ successfulRequests: number;
+}
+
+interface PaginatedVisitResult {
+ pages: number;
+ status: number;
+}
+
+const isObject = (value: unknown): value is Record =>
+ typeof value === "object" && value !== null && !Array.isArray(value);
+
+const dateOnly = (date: Date) => date.toISOString().slice(0, 10);
+
+const addUtcDays = (date: Date, days: number) =>
+ new Date(
+ Date.UTC(
+ date.getUTCFullYear(),
+ date.getUTCMonth(),
+ date.getUTCDate() + days
+ )
+ );
+
+const startOfUtcDay = (date: Date) =>
+ new Date(
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
+ );
+
+const createMonthlySlices = (start: Date, end: Date): DateSlice[] => {
+ const slices: DateSlice[] = [];
+ let cursor = startOfUtcDay(start);
+ const finalDay = startOfUtcDay(end);
+
+ while (cursor <= finalDay) {
+ const monthEnd = new Date(
+ Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 0)
+ );
+ const sliceEnd = monthEnd < finalDay ? monthEnd : finalDay;
+ slices.push({ end: dateOnly(sliceEnd), start: dateOnly(cursor) });
+ cursor = addUtcDays(sliceEnd, 1);
+ }
+
+ return slices;
+};
+
+const base64Url = (value: string | Buffer) =>
+ Buffer.from(value).toString("base64url");
+
+const createGitHubAppJwt = (appId: string, privateKey: string) => {
+ const now = Math.floor(Date.now() / 1000);
+ const header = base64Url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
+ const payload = base64Url(
+ JSON.stringify({ exp: now + 540, iat: now - 60, iss: appId })
+ );
+ const unsigned = `${header}.${payload}`;
+ const signer = createSign("RSA-SHA256");
+ signer.update(unsigned);
+ signer.end();
+ const signature = signer.sign(
+ createPrivateKey(privateKey.replaceAll("\\n", "\n")),
+ "base64url"
+ );
+ return `${unsigned}.${signature}`;
+};
+
+const normalizedSetting = (value: string | undefined) => {
+ const normalized = value?.trim();
+ return normalized === undefined || normalized.length === 0
+ ? undefined
+ : normalized;
+};
+
+const writeSyncDiagnostic = (diagnostic: Record) => {
+ if (process.env.TIMELINE_SYNC_DIAGNOSTICS === "1") {
+ process.stderr.write(`${JSON.stringify(diagnostic)}\n`);
+ }
+};
+
+const readInstallationIds = (rawInstallationIds: string) => {
+ const installationIds = rawInstallationIds
+ .split(",")
+ .map((value) => value.trim())
+ .filter(Boolean);
+ if (
+ installationIds.length === 0 ||
+ installationIds.length > 20 ||
+ installationIds.some((value) => !/^\d+$/.test(value))
+ ) {
+ throw new TimelineSyncError("github-app-installations-invalid");
+ }
+ return installationIds;
+};
+
+const fetchInstallationToken = async (
+ appJwt: string,
+ installationId: string
+) => {
+ const response = await fetch(
+ `${GITHUB_API_URL}/app/installations/${installationId}/access_tokens`,
+ {
+ body: JSON.stringify({ permissions: { contents: "read" } }),
+ cache: "no-store",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${appJwt}`,
+ "Content-Type": "application/json",
+ "User-Agent": "f0rr0.dev-timeline",
+ "X-GitHub-Api-Version": GITHUB_API_VERSION,
+ },
+ method: "POST",
+ signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),
+ }
+ );
+
+ if (!response.ok) {
+ throw new TimelineSyncError("github-app-token-failed");
+ }
+
+ const payload: unknown = await response.json();
+ if (!isObject(payload) || typeof payload.token !== "string") {
+ throw new TimelineSyncError("github-app-token-invalid");
+ }
+ const token = payload.token.trim();
+ if (token.length === 0) {
+ throw new TimelineSyncError("github-app-token-invalid");
+ }
+ return token;
+};
+
+const readUserCredentials = (): GitHubUserCredential[] => {
+ const tokens = [
+ process.env.GITHUB_PUBLIC_ACTIVITY_TOKEN,
+ process.env.GITHUB_ACTIVITY_TOKEN,
+ process.env.GITHUB_TOKEN,
+ process.env.GH_TOKEN,
+ ]
+ .map(normalizedSetting)
+ .filter((value): value is string => value !== undefined);
+ return [...new Set(tokens)].map((token) => ({ kind: "user", token }));
+};
+
+const readInstallationCredentials = async (): Promise<
+ GitHubInstallationCredential[]
+> => {
+ const appId = normalizedSetting(process.env.GITHUB_APP_ID);
+ const privateKey = normalizedSetting(process.env.GITHUB_APP_PRIVATE_KEY);
+ const rawInstallationIds = normalizedSetting(
+ process.env.GITHUB_APP_INSTALLATION_IDS
+ );
+ const hasAnyAppSetting =
+ appId !== undefined ||
+ privateKey !== undefined ||
+ rawInstallationIds !== undefined;
+ if (!hasAnyAppSetting) {
+ return [];
+ }
+ if (
+ appId === undefined ||
+ privateKey === undefined ||
+ rawInstallationIds === undefined
+ ) {
+ throw new TimelineSyncError("github-app-configuration-incomplete");
+ }
+
+ let appJwt: string;
+ try {
+ appJwt = createGitHubAppJwt(appId, privateKey);
+ } catch {
+ throw new TimelineSyncError("github-app-private-key-invalid");
+ }
+
+ const credentials: GitHubInstallationCredential[] = [];
+ for (const installationId of readInstallationIds(rawInstallationIds)) {
+ credentials.push({
+ kind: "installation",
+ token: await fetchInstallationToken(appJwt, installationId),
+ });
+ }
+ return credentials;
+};
+
+const deduplicateCredentials = (credentials: readonly GitHubCredential[]) => {
+ const unique = new Map();
+ for (const credential of credentials) {
+ const key = createHash("sha256")
+ .update(credential.kind)
+ .update("\0")
+ .update(credential.token)
+ .digest("hex");
+ unique.set(key, credential);
+ }
+ return [...unique.values()];
+};
+
+const readGitHubCredentials = async (): Promise => {
+ const userCredentials = readUserCredentials();
+ const installationCredentials = await readInstallationCredentials();
+ const credentials = deduplicateCredentials([
+ ...userCredentials,
+ ...installationCredentials,
+ ]);
+ if (credentials.length === 0) {
+ throw new TimelineSyncError("github-credentials-missing");
+ }
+ return credentials;
+};
+
+const contributionConnections = [
+ {
+ connection: "issueContributions",
+ cursor: "issueCursor",
+ include: "includeIssues",
+ },
+ {
+ connection: "pullRequestContributions",
+ cursor: "pullRequestCursor",
+ include: "includePullRequests",
+ },
+ {
+ connection: "pullRequestReviewContributions",
+ cursor: "reviewCursor",
+ include: "includeReviews",
+ },
+ {
+ connection: "repositoryContributions",
+ cursor: "repositoryCursor",
+ include: "includeRepositories",
+ },
+] as const;
+
+type ContributionConnection = (typeof contributionConnections)[number];
+
+interface ContributionPage {
+ endCursor: string | null;
+ hasNextPage: boolean;
+ nodes: unknown[];
+}
+
+const contributionCollectionFromResponse = (payload: unknown) => {
+ if (
+ !isObject(payload) ||
+ !isObject(payload.data) ||
+ !isObject(payload.data.user) ||
+ !isObject(payload.data.user.contributionsCollection)
+ ) {
+ return null;
+ }
+ return payload.data.user.contributionsCollection;
+};
+
+const contributionPageFrom = (
+ collection: Record,
+ connection: ContributionConnection["connection"]
+): ContributionPage | null => {
+ const value = collection[connection];
+ if (
+ !isObject(value) ||
+ !Array.isArray(value.nodes) ||
+ !isObject(value.pageInfo) ||
+ typeof value.pageInfo.hasNextPage !== "boolean"
+ ) {
+ return null;
+ }
+ const { endCursor, hasNextPage } = value.pageInfo;
+ if (endCursor !== null && typeof endCursor !== "string") {
+ return null;
+ }
+ return {
+ endCursor,
+ hasNextPage,
+ nodes: value.nodes,
+ };
+};
+
+const fetchContributionPage = async (
+ credential: GitHubCredential,
+ slice: DateSlice,
+ activeConnections: ReadonlySet,
+ cursors: ReadonlyMap,
+ includeCommits: boolean
+) => {
+ const variables: Record = {
+ from: `${slice.start}T00:00:00Z`,
+ includeCommits,
+ login: GITHUB_LOGIN,
+ to: `${slice.end}T23:59:59Z`,
+ };
+ for (const configuration of contributionConnections) {
+ variables[configuration.include] = activeConnections.has(
+ configuration.connection
+ );
+ variables[configuration.cursor] =
+ cursors.get(configuration.connection) ?? null;
+ }
+
+ const response = await fetch(GITHUB_GRAPHQL_URL, {
+ body: JSON.stringify({
+ query: contributionQuery,
+ variables,
+ }),
+ cache: "no-store",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${credential.token}`,
+ "Content-Type": "application/json",
+ "User-Agent": "f0rr0.dev-timeline",
+ },
+ method: "POST",
+ signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),
+ });
+
+ if (!response.ok) {
+ throw new TimelineSyncError(`github-graphql-http-${response.status}`);
+ }
+
+ const payload: unknown = await response.json();
+ if (isObject(payload) && Array.isArray(payload.errors)) {
+ writeSyncDiagnostic({
+ errors: payload.errors.map((error) =>
+ isObject(error)
+ ? {
+ path: Array.isArray(error.path)
+ ? error.path.filter(
+ (value): value is number | string =>
+ typeof value === "number" || typeof value === "string"
+ )
+ : null,
+ type: typeof error.type === "string" ? error.type : null,
+ }
+ : null
+ ),
+ phase: "graphql-response",
+ slice,
+ });
+ }
+ return payload;
+};
+
+const allContributionConnections = new Set(
+ contributionConnections.map(({ connection }) => connection)
+);
+
+const fetchContributionSlice = async (
+ credential: GitHubCredential,
+ slice: DateSlice
+) => {
+ const cursors = new Map<
+ ContributionConnection["connection"],
+ string | null
+ >();
+ let activeConnections = allContributionConnections;
+ const payload = await fetchContributionPage(
+ credential,
+ slice,
+ activeConnections,
+ cursors,
+ true
+ );
+ const mergedCollection = contributionCollectionFromResponse(payload);
+ if (mergedCollection === null) {
+ return payload;
+ }
+
+ for (let pageNumber = 1; pageNumber < GITHUB_MAX_PAGES; pageNumber += 1) {
+ const nextConnections = new Set();
+ for (const configuration of contributionConnections) {
+ if (!activeConnections.has(configuration.connection)) {
+ continue;
+ }
+ const page = contributionPageFrom(
+ mergedCollection,
+ configuration.connection
+ );
+ if (page?.hasNextPage !== true) {
+ continue;
+ }
+ if (page.endCursor === null || page.endCursor.length === 0) {
+ return payload;
+ }
+ cursors.set(configuration.connection, page.endCursor);
+ nextConnections.add(configuration.connection);
+ }
+ if (nextConnections.size === 0) {
+ return payload;
+ }
+
+ let nextPayload: unknown;
+ try {
+ nextPayload = await fetchContributionPage(
+ credential,
+ slice,
+ nextConnections,
+ cursors,
+ false
+ );
+ } catch {
+ return payload;
+ }
+ const nextCollection = contributionCollectionFromResponse(nextPayload);
+ if (nextCollection === null) {
+ return payload;
+ }
+ for (const configuration of contributionConnections) {
+ if (!nextConnections.has(configuration.connection)) {
+ continue;
+ }
+ const mergedConnection = mergedCollection[configuration.connection];
+ const nextConnection = nextCollection[configuration.connection];
+ const nextPage = contributionPageFrom(
+ nextCollection,
+ configuration.connection
+ );
+ if (
+ !isObject(mergedConnection) ||
+ !Array.isArray(mergedConnection.nodes) ||
+ !isObject(nextConnection) ||
+ nextPage === null
+ ) {
+ return payload;
+ }
+ mergedConnection.nodes.push(...nextPage.nodes);
+ mergedConnection.pageInfo = nextConnection.pageInfo;
+ }
+ activeConnections = nextConnections;
+ }
+
+ return payload;
+};
+
+const createCollectionAccumulator = (): CollectionAccumulator => ({
+ coverage: "complete",
+ failedRequests: 0,
+ privateRecordsSkipped: 0,
+ publicEventCoverage: "unavailable",
+ publicEvents: new Map(),
+ records: new Map(),
+ successfulRequests: 0,
+});
+
+const mergeRecord = (
+ records: Map,
+ record: TimelineActivityDayRecord
+) => {
+ const existing = records.get(record.id);
+ records.set(record.id, {
+ ...record,
+ commitCount: Math.max(existing?.commitCount ?? 0, record.commitCount),
+ reachedDefaultBranch:
+ record.reachedDefaultBranch || (existing?.reachedDefaultBranch ?? false),
+ });
+};
+
+const mergeNormalizedSlice = (
+ accumulator: CollectionAccumulator,
+ normalized: NonNullable>
+) => {
+ accumulator.successfulRequests += 1;
+ accumulator.privateRecordsSkipped += normalized.privateRecordsSkipped;
+ if (normalized.coverage === "partial") {
+ accumulator.coverage = "partial";
+ }
+ if (normalized.publicEventCoverage !== "unavailable") {
+ accumulator.publicEventCoverage =
+ normalized.publicEventCoverage === "partial" ||
+ accumulator.publicEventCoverage === "partial"
+ ? "partial"
+ : "complete";
+ }
+ for (const event of normalized.publicEvents) {
+ accumulator.publicEvents.set(event.id, event);
+ }
+ for (const record of normalized.records) {
+ mergeRecord(accumulator.records, record);
+ }
+};
+
+const markCollectionFailure = (accumulator: CollectionAccumulator) => {
+ accumulator.coverage = "partial";
+ accumulator.failedRequests += 1;
+};
+
+const isFatalGitHubError = (error: unknown) =>
+ error instanceof TimelineSyncError &&
+ (error.code === "github-rate-limit-exhausted" ||
+ error.code === "github-rest-http-401" ||
+ error.code === "github-rest-http-403");
+
+const finishCollection = (
+ accumulator: CollectionAccumulator
+): CollectionResult => ({
+ coverage: accumulator.coverage,
+ failedRequests: accumulator.failedRequests,
+ privateRecordsSkipped: accumulator.privateRecordsSkipped,
+ publicEventCoverage: accumulator.publicEventCoverage,
+ publicEvents: [...accumulator.publicEvents.values()],
+ records: [...accumulator.records.values()],
+ successfulRequests: accumulator.successfulRequests,
+});
+
+const collectProfileContributions = async (
+ credential: GitHubCredential,
+ slices: readonly DateSlice[],
+ context: NormalizationContext
+): Promise => {
+ const accumulator = createCollectionAccumulator();
+ for (const slice of slices) {
+ try {
+ const payload = await fetchContributionSlice(credential, slice);
+ const normalized = normalizeGitHubContributionSlice(payload, {
+ ...context,
+ windowEnd: slice.end,
+ windowStart: slice.start,
+ });
+ if (normalized === null) {
+ writeSyncDiagnostic({ phase: "profile-normalization", slice });
+ markCollectionFailure(accumulator);
+ } else {
+ if (normalized.coverage === "partial") {
+ writeSyncDiagnostic({
+ eventCoverage: normalized.publicEventCoverage,
+ phase: "profile-coverage",
+ repositoriesSeen: normalized.repositoriesSeen,
+ slice,
+ });
+ }
+ mergeNormalizedSlice(accumulator, normalized);
+ }
+ } catch (error) {
+ writeSyncDiagnostic({
+ code: error instanceof TimelineSyncError ? error.code : "unexpected",
+ phase: "profile-request",
+ slice,
+ });
+ markCollectionFailure(accumulator);
+ accumulator.publicEventCoverage = "partial";
+ }
+ }
+ return finishCollection(accumulator);
+};
+
+const retryDelayFrom = (response: Response, attempt: number) => {
+ if (attempt >= GITHUB_MAX_RETRIES) {
+ return null;
+ }
+
+ const retryAfter = response.headers.get("retry-after");
+ if (retryAfter !== null) {
+ const seconds = Number(retryAfter);
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000 : null;
+ }
+
+ if (response.headers.get("x-ratelimit-remaining") === "0") {
+ const reset = Number(response.headers.get("x-ratelimit-reset"));
+ return Number.isFinite(reset)
+ ? Math.max(0, reset * 1000 - Date.now())
+ : null;
+ }
+
+ return response.status === 429 || response.status >= 500
+ ? 250 * 2 ** attempt
+ : null;
+};
+
+const createRequestPacer = () => {
+ let nextRequestAt = 0;
+ return async () => {
+ const wait = nextRequestAt - Date.now();
+ if (wait > 0) {
+ await delay(wait);
+ }
+ nextRequestAt = Date.now() + GITHUB_REST_INTERVAL_MS;
+ };
+};
+
+const fetchRestResponse = async (
+ token: string,
+ url: string,
+ benignStatuses: ReadonlySet,
+ pace: () => Promise
+) => {
+ for (let attempt = 0; attempt <= GITHUB_MAX_RETRIES; attempt += 1) {
+ await pace();
+ const response = await fetch(url, {
+ cache: "no-store",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "User-Agent": "f0rr0.dev-timeline",
+ "X-GitHub-Api-Version": GITHUB_API_VERSION,
+ },
+ signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),
+ });
+ if (response.ok || benignStatuses.has(response.status)) {
+ return response;
+ }
+
+ const retryDelay = retryDelayFrom(response, attempt);
+ if (retryDelay === null) {
+ throw new TimelineSyncError(`github-rest-http-${response.status}`);
+ }
+ if (retryDelay > GITHUB_MAX_RETRY_DELAY_MS) {
+ throw new TimelineSyncError("github-rate-limit-exhausted");
+ }
+ await delay(retryDelay);
+ }
+ throw new TimelineSyncError("github-rest-unavailable");
+};
+
+const nextLinkFrom = (header: string | null) => {
+ if (header === null) {
+ return null;
+ }
+ for (const link of header.split(",")) {
+ const match = /<([^>]+)>;\s*rel="([^"]+)"/.exec(link.trim());
+ if (match?.[2] !== "next") {
+ continue;
+ }
+ const url = new URL(match[1]);
+ if (
+ url.origin !== GITHUB_API_URL ||
+ url.username.length > 0 ||
+ url.password.length > 0
+ ) {
+ throw new TimelineSyncError("github-pagination-invalid");
+ }
+ return url.toString();
+ }
+ return null;
+};
+
+const createGitHubRestClient = (token: string): GitHubRestClient => {
+ const pace = createRequestPacer();
+ return async (url, benignStatuses = new Set()) => {
+ const response = await fetchRestResponse(token, url, benignStatuses, pace);
+ const payload = benignStatuses.has(response.status)
+ ? null
+ : ((await response.json()) as unknown);
+ return {
+ nextUrl: nextLinkFrom(response.headers.get("link")),
+ payload,
+ status: response.status,
+ };
+ };
+};
+
+const visitPaginatedItems = async (
+ client: GitHubRestClient,
+ initialUrl: string,
+ itemsFrom: (payload: unknown) => unknown[] | null,
+ visit: (items: readonly unknown[]) => Promise | void,
+ benignStatuses: ReadonlySet = new Set()
+): Promise => {
+ const visited = new Set();
+ let nextUrl: string | null = initialUrl;
+ let pages = 0;
+ let status = 200;
+
+ while (nextUrl !== null && pages < GITHUB_MAX_PAGES) {
+ if (visited.has(nextUrl)) {
+ throw new TimelineSyncError("github-pagination-cycle");
+ }
+ visited.add(nextUrl);
+ const page = await client(nextUrl, benignStatuses);
+ const { nextUrl: followingUrl, payload, status: pageStatus } = page;
+ pages += 1;
+ status = pageStatus;
+ if (benignStatuses.has(status)) {
+ return { pages, status };
+ }
+ const items = itemsFrom(payload);
+ if (items === null) {
+ throw new TimelineSyncError("github-rest-payload-invalid");
+ }
+ await visit(items);
+ nextUrl = followingUrl;
+ }
+
+ if (nextUrl !== null) {
+ throw new TimelineSyncError("github-pagination-limit");
+ }
+ return { pages, status };
+};
+
+const repositoryItemsFrom = (payload: unknown) =>
+ isObject(payload) && Array.isArray(payload.repositories)
+ ? payload.repositories
+ : null;
+
+const commitItemsFrom = (payload: unknown) =>
+ Array.isArray(payload) ? payload : null;
+
+const normalizedRepositoryApiUrl = (value: unknown, nameWithOwner: string) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+ try {
+ const url = new URL(value);
+ const expectedPath = `/repos/${nameWithOwner}`.toLocaleLowerCase("en-US");
+ return url.origin === GITHUB_API_URL &&
+ url.pathname.toLocaleLowerCase("en-US") === expectedPath &&
+ url.search.length === 0 &&
+ url.hash.length === 0
+ ? url.toString().replace(/\/$/, "")
+ : null;
+ } catch {
+ return null;
+ }
+};
+
+const restRepositoryFrom = (value: unknown): RestRepository | null => {
+ if (!isObject(value)) {
+ return null;
+ }
+ const id =
+ typeof value.node_id === "string"
+ ? value.node_id
+ : typeof value.id === "number" && Number.isSafeInteger(value.id)
+ ? String(value.id)
+ : null;
+ const nameWithOwner =
+ typeof value.full_name === "string" ? value.full_name : null;
+ if (
+ id === null ||
+ id.length === 0 ||
+ id.length > 200 ||
+ nameWithOwner === null ||
+ !/^(?:[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?)\/[a-z\d._-]{1,100}$/i.test(
+ nameWithOwner
+ ) ||
+ typeof value.private !== "boolean" ||
+ typeof value.html_url !== "string"
+ ) {
+ return null;
+ }
+ const apiUrl = normalizedRepositoryApiUrl(value.url, nameWithOwner);
+ if (apiUrl === null) {
+ return null;
+ }
+
+ return {
+ apiUrl,
+ defaultBranch:
+ typeof value.default_branch === "string" &&
+ value.default_branch.length > 0 &&
+ value.default_branch.length <= 255
+ ? value.default_branch
+ : null,
+ description:
+ typeof value.description === "string" ? value.description : null,
+ htmlUrl: value.html_url,
+ id,
+ isPrivate: value.private,
+ language: typeof value.language === "string" ? value.language : null,
+ nameWithOwner,
+ topics: Array.isArray(value.topics)
+ ? value.topics
+ .filter((topic): topic is string => typeof topic === "string")
+ .slice(0, 10)
+ : [],
+ };
+};
+
+const normalizedCommitDay = (
+ value: unknown,
+ windowStart: string,
+ windowEnd: string
+) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+ const parsed = new Date(value);
+ if (Number.isNaN(parsed.getTime())) {
+ return null;
+ }
+ const day = parsed.toISOString().slice(0, 10);
+ return day >= windowStart && day <= windowEnd ? day : null;
+};
+
+const commitObservationFrom = (
+ value: unknown,
+ repositoryId: string,
+ reachedDefaultBranch: boolean,
+ windowStart: string,
+ windowEnd: string
+) => {
+ if (!isObject(value) || !isObject(value.commit)) {
+ return null;
+ }
+ const rawSha = value.sha;
+ if (typeof rawSha !== "string" || !/^[a-f\d]{40,64}$/i.test(rawSha)) {
+ return null;
+ }
+ const authorDate = isObject(value.commit.author)
+ ? value.commit.author.date
+ : null;
+ const committerDate = isObject(value.commit.committer)
+ ? value.commit.committer.date
+ : null;
+ const day =
+ normalizedCommitDay(authorDate, windowStart, windowEnd) ??
+ normalizedCommitDay(committerDate, windowStart, windowEnd);
+ if (day === null) {
+ return null;
+ }
+ return {
+ key: createHash("sha256")
+ .update(repositoryId)
+ .update("\0")
+ .update(rawSha)
+ .digest("hex"),
+ observation: { day, reachedDefaultBranch } satisfies CommitObservation,
+ };
+};
+
+const mergeCommitItems = (
+ commitByKey: Map,
+ items: readonly unknown[],
+ repositoryId: string,
+ reachedDefaultBranch: boolean,
+ windowStart: string,
+ windowEnd: string
+) => {
+ let invalidItems = 0;
+ for (const item of items) {
+ const parsed = commitObservationFrom(
+ item,
+ repositoryId,
+ reachedDefaultBranch,
+ windowStart,
+ windowEnd
+ );
+ if (parsed === null) {
+ invalidItems += 1;
+ continue;
+ }
+ const existing = commitByKey.get(parsed.key);
+ commitByKey.set(parsed.key, {
+ day: parsed.observation.day,
+ reachedDefaultBranch:
+ parsed.observation.reachedDefaultBranch ||
+ (existing?.reachedDefaultBranch ?? false),
+ });
+ }
+ return invalidItems;
+};
+
+const commitUrlFor = (
+ repository: RestRepository,
+ branch: string | null,
+ windowStart: string,
+ windowEnd: string
+) => {
+ const url = new URL(`${repository.apiUrl}/commits`);
+ url.searchParams.set("author", GITHUB_LOGIN);
+ url.searchParams.set("per_page", "100");
+ url.searchParams.set("since", `${windowStart}T00:00:00Z`);
+ url.searchParams.set("until", `${windowEnd}T23:59:59Z`);
+ if (branch !== null) {
+ url.searchParams.set("sha", branch);
+ }
+ return url.toString();
+};
+
+const dailyNodesFrom = (
+ commitByKey: ReadonlyMap
+) => {
+ const byDay = new Map<
+ string,
+ { commitCount: number; reachedDefaultBranch: boolean }
+ >();
+ for (const observation of commitByKey.values()) {
+ const current = byDay.get(observation.day);
+ byDay.set(observation.day, {
+ commitCount: (current?.commitCount ?? 0) + 1,
+ reachedDefaultBranch:
+ observation.reachedDefaultBranch ||
+ (current?.reachedDefaultBranch ?? false),
+ });
+ }
+ return [...byDay.entries()].map(([day, summary]) => ({
+ commitCount: summary.commitCount,
+ occurredAt: `${day}T00:00:00Z`,
+ reachedDefaultBranch: summary.reachedDefaultBranch,
+ }));
+};
+
+const normalizeRestRepositoryActivity = (
+ repository: RestRepository,
+ commitByKey: ReadonlyMap,
+ context: NormalizationContext,
+ windowStart: string,
+ windowEnd: string
+) =>
+ normalizeGitHubContributionSlice(
+ {
+ data: {
+ user: {
+ contributionsCollection: {
+ commitContributionsByRepository: [
+ {
+ contributions: { nodes: dailyNodesFrom(commitByKey) },
+ repository: {
+ description: repository.isPrivate
+ ? null
+ : repository.description,
+ id: repository.id,
+ isPrivate: repository.isPrivate,
+ nameWithOwner: repository.nameWithOwner,
+ primaryLanguage:
+ repository.isPrivate || repository.language === null
+ ? null
+ : { name: repository.language },
+ repositoryTopics: {
+ nodes: repository.isPrivate
+ ? []
+ : repository.topics.map((name) => ({
+ topic: { name },
+ })),
+ },
+ url: repository.htmlUrl,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ { ...context, windowEnd, windowStart }
+ );
+
+const collectRepositoryRef = async (
+ client: GitHubRestClient,
+ repository: RestRepository,
+ branch: string | null,
+ reachedDefaultBranch: boolean,
+ optional: boolean,
+ commitByKey: Map,
+ windowStart: string,
+ windowEnd: string
+) => {
+ let invalidItems = 0;
+ const result = await visitPaginatedItems(
+ client,
+ commitUrlFor(repository, branch, windowStart, windowEnd),
+ commitItemsFrom,
+ (items) => {
+ invalidItems += mergeCommitItems(
+ commitByKey,
+ items,
+ repository.id,
+ reachedDefaultBranch,
+ windowStart,
+ windowEnd
+ );
+ },
+ new Set(optional ? [404, 409] : [409])
+ );
+ return { invalidItems, requests: result.pages };
+};
+
+const collectRepositoryActivity = async (
+ client: GitHubRestClient,
+ repository: RestRepository,
+ context: NormalizationContext,
+ windowStart: string,
+ windowEnd: string
+): Promise => {
+ if (repository.isPrivate && context.privacyKey === null) {
+ return {
+ coverage: "complete",
+ failedRequests: 0,
+ normalized: {
+ coverage: "complete",
+ privateRecordsSkipped: 1,
+ publicEventCoverage: "unavailable",
+ publicEvents: [],
+ records: [],
+ repositoriesSeen: 1,
+ },
+ successfulRequests: 0,
+ };
+ }
+
+ const commitByKey = new Map();
+ let coverage: "complete" | "partial" = "complete";
+ let failedRequests = 0;
+ let successfulRequests = 0;
+ const refs = [
+ {
+ branch: repository.defaultBranch,
+ optional: false,
+ reachedDefaultBranch: true,
+ },
+ ...(repository.defaultBranch === "gh-pages"
+ ? []
+ : [
+ {
+ branch: "gh-pages",
+ optional: true,
+ reachedDefaultBranch: false,
+ },
+ ]),
+ ];
+
+ for (const ref of refs) {
+ try {
+ const result = await collectRepositoryRef(
+ client,
+ repository,
+ ref.branch,
+ ref.reachedDefaultBranch,
+ ref.optional,
+ commitByKey,
+ windowStart,
+ windowEnd
+ );
+ successfulRequests += result.requests;
+ if (result.invalidItems > 0) {
+ coverage = "partial";
+ failedRequests += 1;
+ }
+ } catch (error) {
+ writeSyncDiagnostic({
+ code: error instanceof TimelineSyncError ? error.code : "unexpected",
+ phase: "repository-ref",
+ private: repository.isPrivate,
+ ref: ref.reachedDefaultBranch ? "default" : "auxiliary",
+ });
+ if (isFatalGitHubError(error)) {
+ throw error;
+ }
+ coverage = "partial";
+ failedRequests += 1;
+ }
+ }
+
+ return {
+ coverage,
+ failedRequests,
+ normalized: normalizeRestRepositoryActivity(
+ repository,
+ commitByKey,
+ context,
+ windowStart,
+ windowEnd
+ ),
+ successfulRequests,
+ };
+};
+
+const absorbRepositoryResult = (
+ accumulator: CollectionAccumulator,
+ result: RepositoryCollectionResult
+) => {
+ accumulator.successfulRequests += result.successfulRequests;
+ accumulator.failedRequests += result.failedRequests;
+ if (result.coverage === "partial") {
+ accumulator.coverage = "partial";
+ }
+ if (result.normalized === null) {
+ markCollectionFailure(accumulator);
+ return;
+ }
+ accumulator.privateRecordsSkipped += result.normalized.privateRecordsSkipped;
+ if (result.normalized.publicEventCoverage !== "unavailable") {
+ accumulator.publicEventCoverage = result.normalized.publicEventCoverage;
+ }
+ for (const event of result.normalized.publicEvents) {
+ accumulator.publicEvents.set(event.id, event);
+ }
+ for (const record of result.normalized.records) {
+ mergeRecord(accumulator.records, record);
+ }
+};
+
+const collectInstallationContributions = async (
+ credential: GitHubInstallationCredential,
+ context: NormalizationContext,
+ windowStart: string,
+ windowEnd: string
+): Promise => {
+ const accumulator = createCollectionAccumulator();
+ const client = createGitHubRestClient(credential.token);
+ const url = new URL("/installation/repositories", GITHUB_API_URL);
+ url.searchParams.set("per_page", "100");
+
+ const inventory = await visitPaginatedItems(
+ client,
+ url.toString(),
+ repositoryItemsFrom,
+ async (items) => {
+ for (const item of items) {
+ const repository = restRepositoryFrom(item);
+ if (repository === null) {
+ writeSyncDiagnostic({ phase: "repository-normalization" });
+ markCollectionFailure(accumulator);
+ continue;
+ }
+ const result = await collectRepositoryActivity(
+ client,
+ repository,
+ context,
+ windowStart,
+ windowEnd
+ );
+ absorbRepositoryResult(accumulator, result);
+ }
+ }
+ );
+ accumulator.successfulRequests += inventory.pages;
+ return finishCollection(accumulator);
+};
+
+const mergeCollection = (
+ accumulator: CollectionAccumulator,
+ result: CollectionResult
+) => {
+ accumulator.successfulRequests += result.successfulRequests;
+ accumulator.failedRequests += result.failedRequests;
+ accumulator.privateRecordsSkipped += result.privateRecordsSkipped;
+ if (result.publicEventCoverage !== "unavailable") {
+ accumulator.publicEventCoverage =
+ result.publicEventCoverage === "partial" ||
+ accumulator.publicEventCoverage === "partial"
+ ? "partial"
+ : "complete";
+ }
+ for (const event of result.publicEvents) {
+ accumulator.publicEvents.set(event.id, event);
+ }
+ if (result.coverage === "partial") {
+ accumulator.coverage = "partial";
+ }
+ for (const record of result.records) {
+ mergeRecord(accumulator.records, record);
+ }
+};
+
+const collectTimelineActivity = async (
+ credentials: readonly GitHubCredential[],
+ slices: readonly DateSlice[],
+ context: NormalizationContext,
+ windowStart: string,
+ windowEnd: string
+) => {
+ const accumulator = createCollectionAccumulator();
+ for (const credential of credentials) {
+ try {
+ const profileResult = await collectProfileContributions(
+ credential,
+ slices,
+ context
+ );
+ mergeCollection(accumulator, profileResult);
+
+ if (credential.kind === "installation") {
+ const repositoryResult = await collectInstallationContributions(
+ credential,
+ context,
+ windowStart,
+ windowEnd
+ );
+ mergeCollection(accumulator, repositoryResult);
+ }
+ } catch (error) {
+ writeSyncDiagnostic({
+ code: error instanceof TimelineSyncError ? error.code : "unexpected",
+ kind: credential.kind,
+ phase: "credential-collection",
+ });
+ markCollectionFailure(accumulator);
+ }
+ }
+
+ const result = finishCollection(accumulator);
+ if (result.successfulRequests === 0) {
+ throw new TimelineSyncError("github-activity-unavailable");
+ }
+ return result;
+};
+
+const createSyncPlan = async (options: {
+ forceBackfill?: boolean;
+ kind?: TimelineSyncKind;
+ now?: Date;
+}): Promise => {
+ const day = startOfUtcDay(options.now ?? new Date());
+ const storedCount = await countStoredTimelineActivity(GITHUB_LOGIN);
+ const lastSync = await readLastCompletedTimelineSync();
+ const scheduledWeeklyReconciliation =
+ options.kind !== "webhook" && day.getUTCDay() === 0;
+ const useFullWindow =
+ options.forceBackfill === true ||
+ storedCount === 0 ||
+ lastSync === null ||
+ scheduledWeeklyReconciliation;
+ const kind = options.kind ?? (useFullWindow ? "backfill" : "incremental");
+ const lookbackDays = useFullWindow
+ ? TIMELINE_WINDOW_DAYS
+ : INCREMENTAL_LOOKBACK_DAYS;
+ const windowStartDate = addUtcDays(day, -(lookbackDays - 1));
+ return {
+ day,
+ kind,
+ useFullWindow,
+ windowEnd: dateOnly(day),
+ windowStart: dateOnly(windowStartDate),
+ windowStartDate,
+ };
+};
+
+const collectAnonymousContributionTotals = async (
+ plan: SyncPlan
+): Promise => {
+ const windowStart = dateOnly(
+ addUtcDays(plan.day, -(TIMELINE_WINDOW_DAYS - 1))
+ );
+ try {
+ const days = await fetchPublicGitHubContributionDays({
+ login: GITHUB_LOGIN,
+ windowEnd: plan.windowEnd,
+ windowStart,
+ });
+ return {
+ coverage: "complete",
+ records: days.map(({ contributionCount, day }) => ({
+ contributionCount,
+ day,
+ id: createHash("sha256")
+ .update(`github-public-calendar:${GITHUB_LOGIN}:${day}`)
+ .digest("hex"),
+ source: "github-public-calendar",
+ subject: GITHUB_LOGIN,
+ })),
+ };
+ } catch {
+ writeSyncDiagnostic({ phase: "anonymous-contribution-calendar" });
+ return { coverage: "unavailable", records: [] };
+ }
+};
+
+const removeStaleActivity = async (
+ records: readonly TimelineActivityDayRecord[],
+ privacyKey: string | null,
+ plan: SyncPlan
+) => {
+ const existing = await readTimelineActivityDays(
+ GITHUB_LOGIN,
+ plan.windowStart,
+ plan.windowEnd
+ );
+ const currentIds = new Set(records.map((record) => record.id));
+ const staleIds = existing.flatMap((record) => {
+ if (record.source !== "github-profile" || currentIds.has(record.id)) {
+ return [];
+ }
+ if (record.visibility === "private" && privacyKey === null) {
+ return [];
+ }
+ return [record.id];
+ });
+ await deleteTimelineActivityByIds(staleIds);
+};
+
+const removeStalePublicEvents = async (
+ records: readonly TimelinePublicEventRecord[],
+ windowStart: string,
+ windowEnd: string
+) => {
+ const existing = await readTimelinePublicEvents(
+ GITHUB_LOGIN,
+ windowStart,
+ windowEnd
+ );
+ const currentIds = new Set(records.map((record) => record.id));
+ const staleIds = existing.flatMap((record) =>
+ record.source === "github-profile" && !currentIds.has(record.id)
+ ? [record.id]
+ : []
+ );
+ await deleteTimelinePublicEventsByIds(staleIds);
+ return staleIds.length;
+};
+
+const persistCollection = async (
+ collection: CollectionResult,
+ anonymousCollection: AnonymousContributionCollection,
+ privacyKey: string | null,
+ plan: SyncPlan,
+ runId: string
+) => {
+ await Promise.all([
+ upsertTimelineActivityDays(collection.records),
+ upsertTimelineContributionTotals(anonymousCollection.records),
+ upsertTimelinePublicEvents(collection.publicEvents),
+ ]);
+ if (collection.failedRequests === 0 && collection.coverage === "complete") {
+ await removeStaleActivity(collection.records, privacyKey, plan);
+ }
+ const retentionCutoff = dateOnly(addUtcDays(plan.day, -RETENTION_DAYS));
+ // A permission-bound partial event view is still reconciled fail-closed:
+ // inaccessible identities are omitted rather than retained from an older run.
+ if (collection.publicEventCoverage !== "unavailable") {
+ const staleEventCount = await removeStalePublicEvents(
+ collection.publicEvents,
+ dateOnly(addUtcDays(plan.day, -(TIMELINE_WINDOW_DAYS - 1))),
+ plan.windowEnd
+ );
+ if (staleEventCount > 0) {
+ await rejectPublishedTimelineEditions();
+ }
+ }
+ await Promise.all([
+ pruneTimelineActivityBefore(GITHUB_LOGIN, retentionCutoff),
+ pruneTimelineContributionTotalsBefore(GITHUB_LOGIN, retentionCutoff),
+ pruneTimelinePublicEventsBefore(GITHUB_LOGIN, retentionCutoff),
+ ]);
+ await completeTimelineSyncRun(
+ runId,
+ collection.records.length,
+ collection.publicEvents.length,
+ collection.publicEventCoverage,
+ anonymousCollection.records.length,
+ anonymousCollection.coverage,
+ collection.coverage
+ );
+};
+
+const safeErrorCode = (error: unknown) => {
+ if (error instanceof TimelineSyncError) {
+ return error.code;
+ }
+ if (error instanceof Error && error.name === "TimeoutError") {
+ return "github-timeout";
+ }
+ return "timeline-sync-failed";
+};
+
+const executeTimelineSync = async (plan: SyncPlan, runId: string) => {
+ const credentials = await readGitHubCredentials();
+ const privacyKey = normalizeTimelinePrivacyKey(
+ process.env.TIMELINE_PRIVACY_KEY
+ );
+ const context: NormalizationContext = {
+ privacyKey,
+ subject: GITHUB_LOGIN,
+ taxonomy: parsePrivateTimelineTaxonomy(
+ process.env.TIMELINE_PRIVATE_TAXONOMY
+ ),
+ };
+ const [collection, anonymousCollection] = await Promise.all([
+ collectTimelineActivity(
+ credentials,
+ createMonthlySlices(
+ addUtcDays(plan.day, -(TIMELINE_WINDOW_DAYS - 1)),
+ plan.day
+ ),
+ context,
+ plan.windowStart,
+ plan.windowEnd
+ ),
+ collectAnonymousContributionTotals(plan),
+ ]);
+ await persistCollection(
+ collection,
+ anonymousCollection,
+ privacyKey,
+ plan,
+ runId
+ );
+
+ return {
+ anonymousCoverage: anonymousCollection.coverage,
+ anonymousDays: anonymousCollection.records.length,
+ coverage: collection.coverage,
+ events: collection.publicEvents.length,
+ kind: plan.kind,
+ privateActivity:
+ privacyKey === null || collection.privateRecordsSkipped > 0
+ ? ("skipped" as const)
+ : ("included" as const),
+ rows: collection.records.length,
+ windowEnd: plan.windowEnd,
+ windowStart: plan.windowStart,
+ };
+};
+
+export const syncGitHubTimeline = async (
+ options: {
+ forceBackfill?: boolean;
+ kind?: TimelineSyncKind;
+ now?: Date;
+ } = {}
+): Promise => {
+ if (!isTimelineDatabaseConfigured()) {
+ throw new TimelineSyncError("timeline-database-missing");
+ }
+
+ const plan = await createSyncPlan(options);
+ const runId = await beginTimelineSyncRun({
+ fullWindow: plan.useFullWindow,
+ kind: plan.kind,
+ windowEnd: plan.windowEnd,
+ windowStart: plan.windowStart,
+ });
+
+ try {
+ return await executeTimelineSync(plan, runId);
+ } catch (error) {
+ await failTimelineSyncRun(runId, safeErrorCode(error));
+ if (error instanceof TimelineSyncError) {
+ throw error;
+ }
+ throw new TimelineSyncError(safeErrorCode(error));
+ }
+};
diff --git a/src/lib/timeline-privacy.ts b/src/lib/timeline-privacy.ts
new file mode 100644
index 0000000..4ee8904
--- /dev/null
+++ b/src/lib/timeline-privacy.ts
@@ -0,0 +1,656 @@
+import { createHash, createHmac } from "node:crypto";
+
+import { workBucketSchema } from "@/lib/timeline-core";
+import type { WorkBucket } from "@/lib/timeline-core";
+import type {
+ TimelineActivityDayRecord,
+ TimelinePublicEventRecord,
+} from "@/lib/timeline-store";
+
+type JsonObject = Record;
+
+const githubRepositoryPattern =
+ /^(?:[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?)\/[a-z\d._-]{1,100}$/i;
+const datePattern = /^\d{4}-\d{2}-\d{2}$/;
+const maximumCommitCount = 100_000;
+
+export interface PrivateTaxonomyValue {
+ bucket: WorkBucket;
+ domain: string | null;
+}
+
+export interface NormalizeGitHubSliceOptions {
+ privacyKey: string | null;
+ subject: string;
+ taxonomy: ReadonlyMap;
+ windowEnd: string;
+ windowStart: string;
+}
+
+export interface NormalizedGitHubSlice {
+ coverage: "complete" | "partial";
+ privateRecordsSkipped: number;
+ publicEventCoverage: "complete" | "partial" | "unavailable";
+ publicEvents: TimelinePublicEventRecord[];
+ records: TimelineActivityDayRecord[];
+ repositoriesSeen: number;
+}
+
+const isObject = (value: unknown): value is JsonObject =>
+ typeof value === "object" && value !== null && !Array.isArray(value);
+
+const normalizeDate = (value: unknown) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const date = value.slice(0, 10);
+ if (!datePattern.test(date)) {
+ return null;
+ }
+
+ const parsed = new Date(`${date}T00:00:00Z`);
+ return Number.isNaN(parsed.getTime()) ||
+ parsed.toISOString().slice(0, 10) !== date
+ ? null
+ : date;
+};
+
+const normalizeText = (value: unknown, maximumLength: number) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const normalized = value.replaceAll(/\s+/g, " ").trim();
+ return normalized.length === 0 ? null : normalized.slice(0, maximumLength);
+};
+
+const normalizeRepositoryName = (value: unknown) => {
+ const name = normalizeText(value, 140);
+ return name !== null && githubRepositoryPattern.test(name) ? name : null;
+};
+
+const normalizeGitHubUrl = (value: unknown, nameWithOwner: string) => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ try {
+ const url = new URL(value);
+ const normalizedPath = url.pathname.replace(/\/$/, "").toLowerCase();
+ return url.protocol === "https:" &&
+ url.hostname === "github.com" &&
+ normalizedPath === `/${nameWithOwner.toLowerCase()}` &&
+ url.search.length === 0 &&
+ url.hash.length === 0
+ ? `https://github.com/${nameWithOwner}`
+ : null;
+ } catch {
+ return null;
+ }
+};
+
+const normalizeGitHubEventUrl = (
+ value: unknown,
+ nameWithOwner: string,
+ kind: TimelinePublicEventRecord["eventKind"]
+) => {
+ if (kind === "repository_created") {
+ return normalizeGitHubUrl(value, nameWithOwner);
+ }
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ try {
+ const url = new URL(value);
+ const suffix = kind === "issue_opened" ? "issues" : "pull";
+ const expectedPrefix = `/${nameWithOwner}/${suffix}/`.toLocaleLowerCase(
+ "en-US"
+ );
+ const normalizedPath = url.pathname.toLocaleLowerCase("en-US");
+ const number = normalizedPath.slice(expectedPrefix.length);
+ return url.protocol === "https:" &&
+ url.hostname === "github.com" &&
+ normalizedPath.startsWith(expectedPrefix) &&
+ /^\d+$/.test(number) &&
+ url.search.length === 0 &&
+ url.hash.length === 0
+ ? `https://github.com/${nameWithOwner}/${suffix}/${number}`
+ : null;
+ } catch {
+ return null;
+ }
+};
+
+const languageFamilyFor = (language: string | null) => {
+ const normalized = language?.toLocaleLowerCase("en-US") ?? "";
+
+ if (
+ [
+ "css",
+ "html",
+ "javascript",
+ "mdx",
+ "svelte",
+ "typescript",
+ "vue",
+ ].includes(normalized)
+ ) {
+ return "web";
+ }
+ if (["c", "c++", "go", "rust", "swift", "zig"].includes(normalized)) {
+ return "systems";
+ }
+ if (
+ ["julia", "jupyter notebook", "python", "r", "sql"].includes(normalized)
+ ) {
+ return "data";
+ }
+ if (["dart", "kotlin", "objective-c"].includes(normalized)) {
+ return "mobile";
+ }
+ if (
+ ["dockerfile", "hcl", "nix", "powershell", "shell"].includes(normalized)
+ ) {
+ return "infrastructure";
+ }
+ if (["markdown", "tex", "typst"].includes(normalized)) {
+ return "documentation";
+ }
+ return "other";
+};
+
+const readTopics = (value: unknown) => {
+ if (!isObject(value) || !Array.isArray(value.nodes)) {
+ return [];
+ }
+
+ return value.nodes.flatMap((node) => {
+ if (!isObject(node) || !isObject(node.topic)) {
+ return [];
+ }
+ const name = normalizeText(node.topic.name, 50)?.toLocaleLowerCase("en-US");
+ return name === undefined || name === null ? [] : [name];
+ });
+};
+
+const classifyPublicBucket = (input: {
+ description: string | null;
+ nameWithOwner: string;
+ topics: readonly string[];
+}): Exclude => {
+ const haystack = [
+ input.nameWithOwner,
+ input.description ?? "",
+ ...input.topics,
+ ]
+ .join(" ")
+ .toLocaleLowerCase("en-US");
+
+ if (/\b(ai|agent|agents|llm|mcp|model|models)\b/.test(haystack)) {
+ return "Applied AI";
+ }
+ if (
+ /\b(ci|database|devops|docker|infra|postgres|proxy|runtime|serverless)\b/.test(
+ haystack
+ )
+ ) {
+ return "Infrastructure";
+ }
+ if (/\b(portfolio|product|site|website|workflow)\b/.test(haystack)) {
+ return "Product systems";
+ }
+ return "Open source";
+};
+
+const sha256 = (value: string) =>
+ createHash("sha256").update(value).digest("hex");
+
+const hmac = (privacyKey: string, value: string) =>
+ createHmac("sha256", privacyKey).update(value).digest("hex");
+
+export const normalizeTimelinePrivacyKey = (value: string | undefined) => {
+ const normalized = value?.trim();
+ return normalized !== undefined &&
+ normalized.length >= 32 &&
+ new Set(normalized).size >= 8
+ ? normalized
+ : null;
+};
+
+export const timelinePrivacyPolicyVersion = (
+ privacyKey: string,
+ taxonomy: ReadonlyMap
+) =>
+ hmac(
+ privacyKey,
+ JSON.stringify({
+ policy: "timeline-private-v1",
+ taxonomy: [...taxonomy.entries()].toSorted(([left], [right]) =>
+ left.localeCompare(right)
+ ),
+ })
+ );
+
+export const publicTimelineRepoKey = (repositoryId: string) =>
+ sha256(`repository-node:${repositoryId}`);
+
+export const privateTimelineRepoKey = (
+ repositoryId: string,
+ privacyKey: string
+) => hmac(privacyKey, `repository:${repositoryId}`);
+
+const eventConnectionNames = new Set([
+ "issueContributions",
+ "pullRequestContributions",
+ "pullRequestReviewContributions",
+ "repositoryContributions",
+]);
+
+const isExpectedForbiddenEventError = (value: unknown) => {
+ if (
+ !isObject(value) ||
+ value.type !== "FORBIDDEN" ||
+ !Array.isArray(value.path)
+ ) {
+ return false;
+ }
+ const { path } = value;
+ return (
+ path.length === 5 &&
+ path[0] === "user" &&
+ path[1] === "contributionsCollection" &&
+ typeof path[2] === "string" &&
+ eventConnectionNames.has(path[2]) &&
+ path[3] === "nodes" &&
+ typeof path[4] === "number" &&
+ Number.isSafeInteger(path[4]) &&
+ path[4] >= 0
+ );
+};
+
+const contributionCollectionFrom = (payload: unknown) => {
+ if (!isObject(payload) || !isObject(payload.data)) {
+ return null;
+ }
+ const { errors } = payload;
+ const coverage =
+ errors === undefined || (Array.isArray(errors) && errors.length === 0)
+ ? "complete"
+ : Array.isArray(errors) && errors.every(isExpectedForbiddenEventError)
+ ? "partial"
+ : null;
+ if (coverage === null) {
+ return null;
+ }
+
+ const { user } = payload.data;
+ if (!isObject(user) || !isObject(user.contributionsCollection)) {
+ return null;
+ }
+
+ return { collection: user.contributionsCollection, coverage };
+};
+
+const repositoryGroupsFrom = (collection: JsonObject) => {
+ const groups = collection.commitContributionsByRepository;
+ return Array.isArray(groups) ? groups : null;
+};
+
+const publicEventSpecs = [
+ {
+ connection: "issueContributions",
+ entity: "issue",
+ eventKind: "issue_opened",
+ },
+ {
+ connection: "pullRequestContributions",
+ entity: "pullRequest",
+ eventKind: "pull_request_opened",
+ },
+ {
+ connection: "pullRequestReviewContributions",
+ entity: "pullRequest",
+ eventKind: "pull_request_reviewed",
+ },
+ {
+ connection: "repositoryContributions",
+ entity: "repository",
+ eventKind: "repository_created",
+ },
+] as const satisfies readonly {
+ connection: string;
+ entity: string;
+ eventKind: TimelinePublicEventRecord["eventKind"];
+}[];
+
+// The explicit guards are the public identity boundary for untrusted API data.
+// eslint-disable-next-line complexity
+const normalizePublicEvents = (
+ collection: JsonObject,
+ options: NormalizeGitHubSliceOptions
+) => {
+ const hasEventConnections = publicEventSpecs.some(
+ ({ connection }) => collection[connection] !== undefined
+ );
+ if (!hasEventConnections) {
+ return {
+ coverage: "unavailable" as const,
+ events: [] as TimelinePublicEventRecord[],
+ };
+ }
+
+ let coverage: "complete" | "partial" = "complete";
+ const events = new Map();
+ for (const spec of publicEventSpecs) {
+ const connection = collection[spec.connection];
+ if (
+ !isObject(connection) ||
+ !Array.isArray(connection.nodes) ||
+ !isObject(connection.pageInfo) ||
+ typeof connection.pageInfo.hasNextPage !== "boolean"
+ ) {
+ coverage = "partial";
+ continue;
+ }
+ if (connection.pageInfo.hasNextPage) {
+ coverage = "partial";
+ }
+
+ for (const contribution of connection.nodes) {
+ if (!isObject(contribution) || contribution.isRestricted !== false) {
+ continue;
+ }
+ const entity = contribution[spec.entity];
+ if (!isObject(entity)) {
+ continue;
+ }
+ const repository =
+ spec.eventKind === "repository_created" ? entity : entity.repository;
+ if (!isObject(repository) || repository.isPrivate !== false) {
+ continue;
+ }
+
+ const repositoryId = normalizeText(repository.id, 200);
+ const nameWithOwner = normalizeRepositoryName(repository.nameWithOwner);
+ const day = normalizeDate(contribution.occurredAt);
+ // Review contributions represent the latest review for each distinct PR.
+ // Keying by the PR keeps later reviews as an update, not a duplicate event.
+ const entityId = normalizeText(entity.id, 200);
+ if (
+ repositoryId === null ||
+ nameWithOwner === null ||
+ day === null ||
+ day < options.windowStart ||
+ day > options.windowEnd ||
+ entityId === null
+ ) {
+ continue;
+ }
+
+ const publicRepoUrl = normalizeGitHubUrl(repository.url, nameWithOwner);
+ const publicUrl = normalizeGitHubEventUrl(
+ entity.url,
+ nameWithOwner,
+ spec.eventKind
+ );
+ const publicTitle =
+ spec.eventKind === "repository_created"
+ ? (nameWithOwner.split("/").at(-1) ?? null)
+ : normalizeText(entity.title, 240);
+ if (
+ publicRepoUrl === null ||
+ publicUrl === null ||
+ publicTitle === null
+ ) {
+ continue;
+ }
+
+ const id = sha256(
+ `${options.subject}:public-event:${spec.eventKind}:${entityId}`
+ );
+ events.set(id, {
+ bucket: classifyPublicBucket({
+ description: normalizeText(repository.description, 240),
+ nameWithOwner,
+ topics: readTopics(repository.repositoryTopics),
+ }),
+ day,
+ eventKind: spec.eventKind,
+ id,
+ publicRepoName: nameWithOwner,
+ publicRepoUrl,
+ publicTitle,
+ publicUrl,
+ repoKey: publicTimelineRepoKey(repositoryId),
+ source: "github-profile",
+ subject: options.subject,
+ });
+ }
+ }
+
+ return { coverage, events: [...events.values()] };
+};
+
+const normalizeTaxonomyValue = (
+ value: unknown
+): PrivateTaxonomyValue | null => {
+ if (typeof value === "string") {
+ const bucket = workBucketSchema.safeParse(value);
+ return bucket.success && bucket.data !== "Across the work"
+ ? { bucket: bucket.data, domain: null }
+ : null;
+ }
+
+ if (!isObject(value)) {
+ return null;
+ }
+
+ const bucket = workBucketSchema.safeParse(value.bucket);
+ const rawDomain = normalizeText(value.domain, 64)?.toLocaleLowerCase("en-US");
+ const domain =
+ rawDomain !== undefined &&
+ rawDomain !== null &&
+ /^[a-z\d][a-z\d-]{0,63}$/.test(rawDomain)
+ ? rawDomain
+ : null;
+ if (value.domain !== undefined && domain === null) {
+ return null;
+ }
+ return bucket.success && bucket.data !== "Across the work"
+ ? { bucket: bucket.data, domain }
+ : null;
+};
+
+export const parsePrivateTimelineTaxonomy = (
+ value: string | undefined
+): ReadonlyMap => {
+ if (value === undefined || value.trim().length === 0) {
+ return new Map();
+ }
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(value) as unknown;
+ } catch {
+ throw new Error("timeline-taxonomy-invalid");
+ }
+
+ if (!isObject(parsed)) {
+ throw new Error("timeline-taxonomy-invalid");
+ }
+
+ const taxonomy = new Map();
+ for (const [repository, rawValue] of Object.entries(parsed)) {
+ const normalizedRepository = normalizeRepositoryName(repository);
+ const normalizedValue = normalizeTaxonomyValue(rawValue);
+ if (normalizedRepository === null || normalizedValue === null) {
+ throw new Error("timeline-taxonomy-invalid");
+ }
+ taxonomy.set(
+ normalizedRepository.toLocaleLowerCase("en-US"),
+ normalizedValue
+ );
+ }
+
+ return taxonomy;
+};
+
+export const currentTimelinePrivacyPolicyVersion = () => {
+ const privacyKey = normalizeTimelinePrivacyKey(
+ process.env.TIMELINE_PRIVACY_KEY
+ );
+ return privacyKey === null
+ ? null
+ : timelinePrivacyPolicyVersion(
+ privacyKey,
+ parsePrivateTimelineTaxonomy(process.env.TIMELINE_PRIVATE_TAXONOMY)
+ );
+};
+
+// The explicit guards are the privacy boundary for an untrusted API payload.
+// eslint-disable-next-line complexity
+export const normalizeGitHubContributionSlice = (
+ payload: unknown,
+ options: NormalizeGitHubSliceOptions
+): NormalizedGitHubSlice | null => {
+ const response = contributionCollectionFrom(payload);
+ if (response === null) {
+ return null;
+ }
+ const { collection, coverage: responseCoverage } = response;
+ const groups = repositoryGroupsFrom(collection);
+ if (groups === null) {
+ return null;
+ }
+
+ const records = new Map();
+ let privateRecordsSkipped = 0;
+ let repositoriesSeen = 0;
+
+ for (const group of groups) {
+ if (!isObject(group) || !isObject(group.repository)) {
+ continue;
+ }
+
+ const { repository } = group;
+ const nameWithOwner = normalizeRepositoryName(repository.nameWithOwner);
+ const repositoryId = normalizeText(repository.id, 200);
+ const { isPrivate } = repository;
+ if (
+ nameWithOwner === null ||
+ repositoryId === null ||
+ typeof isPrivate !== "boolean" ||
+ !isObject(group.contributions) ||
+ !Array.isArray(group.contributions.nodes)
+ ) {
+ continue;
+ }
+
+ repositoriesSeen += 1;
+ const languageName = isObject(repository.primaryLanguage)
+ ? normalizeText(repository.primaryLanguage.name, 50)
+ : null;
+ const languageFamily = isPrivate
+ ? "withheld"
+ : languageFamilyFor(languageName);
+ const publicUrl = isPrivate
+ ? null
+ : normalizeGitHubUrl(repository.url, nameWithOwner);
+ if (!isPrivate && publicUrl === null) {
+ continue;
+ }
+
+ const taxonomyValue = options.taxonomy.get(
+ nameWithOwner.toLocaleLowerCase("en-US")
+ );
+ const bucket = isPrivate
+ ? (taxonomyValue?.bucket ?? "Private product work")
+ : classifyPublicBucket({
+ description: normalizeText(repository.description, 240),
+ nameWithOwner,
+ topics: readTopics(repository.repositoryTopics),
+ });
+
+ const privatePrivacyKey = options.privacyKey;
+ if (isPrivate && privatePrivacyKey === null) {
+ privateRecordsSkipped += group.contributions.nodes.length;
+ continue;
+ }
+
+ const repoKey =
+ isPrivate && privatePrivacyKey !== null
+ ? privateTimelineRepoKey(repositoryId, privatePrivacyKey)
+ : publicTimelineRepoKey(repositoryId);
+ const privacyDomainKey =
+ isPrivate &&
+ privatePrivacyKey !== null &&
+ taxonomyValue?.domain !== null &&
+ taxonomyValue?.domain !== undefined
+ ? hmac(privatePrivacyKey, `domain:${taxonomyValue.domain}`)
+ : null;
+
+ for (const contribution of group.contributions.nodes) {
+ if (!isObject(contribution)) {
+ continue;
+ }
+
+ const day = normalizeDate(contribution.occurredAt);
+ const { commitCount } = contribution;
+ const reachedDefaultBranch =
+ typeof contribution.reachedDefaultBranch === "boolean"
+ ? contribution.reachedDefaultBranch
+ : true;
+ if (
+ day === null ||
+ day < options.windowStart ||
+ day > options.windowEnd ||
+ typeof commitCount !== "number" ||
+ !Number.isSafeInteger(commitCount) ||
+ commitCount <= 0 ||
+ commitCount > maximumCommitCount
+ ) {
+ continue;
+ }
+
+ const id = sha256(`${options.subject}:${repoKey}:${day}:github-profile`);
+ const existing = records.get(id);
+ const record: TimelineActivityDayRecord = {
+ bucket,
+ commitCount: Math.max(existing?.commitCount ?? 0, commitCount),
+ day,
+ id,
+ languageFamily,
+ privacyDomainKey,
+ privacyPolicyVersion:
+ isPrivate && privatePrivacyKey !== null
+ ? timelinePrivacyPolicyVersion(privatePrivacyKey, options.taxonomy)
+ : null,
+ publicRepoName: isPrivate ? null : nameWithOwner,
+ publicRepoUrl: publicUrl,
+ reachedDefaultBranch,
+ repoKey,
+ source: "github-profile",
+ subject: options.subject,
+ visibility: isPrivate ? "private" : "public",
+ };
+ records.set(id, record);
+ }
+ }
+
+ const publicEvents = normalizePublicEvents(collection, options);
+ const publicEventCoverage =
+ responseCoverage === "partial" ? "partial" : publicEvents.coverage;
+ return {
+ coverage:
+ groups.length >= 100 || publicEvents.coverage === "partial"
+ ? "partial"
+ : "complete",
+ privateRecordsSkipped,
+ publicEventCoverage,
+ publicEvents: publicEvents.events,
+ records: [...records.values()],
+ repositoriesSeen,
+ };
+};
diff --git a/src/lib/timeline-store.ts b/src/lib/timeline-store.ts
new file mode 100644
index 0000000..da12f43
--- /dev/null
+++ b/src/lib/timeline-store.ts
@@ -0,0 +1,659 @@
+import { and, desc, eq, gte, inArray, lt, lte, sql } from "drizzle-orm";
+
+import { getTimelineDatabase, isTimelineDatabaseConfigured } from "@/db/client";
+import {
+ timelineActivityDays,
+ timelineContributionTotals,
+ timelineEditions,
+ timelinePublicEvents,
+ timelineSyncRuns,
+ timelineWebhookReceipts,
+} from "@/db/schema";
+import type { TimelinePublicEventKind } from "@/db/schema";
+import {
+ editionMatchesTimelinePrivacyPolicy,
+ timelineEditionSchema,
+} from "@/lib/timeline-core";
+import type { TimelineEdition, WorkBucket } from "@/lib/timeline-core";
+import { currentTimelinePrivacyPolicyVersion } from "@/lib/timeline-privacy";
+
+export interface TimelineActivityDayRecord {
+ bucket: WorkBucket;
+ commitCount: number;
+ day: string;
+ id: string;
+ languageFamily: string;
+ privacyDomainKey: string | null;
+ privacyPolicyVersion: string | null;
+ publicRepoName: string | null;
+ publicRepoUrl: string | null;
+ reachedDefaultBranch: boolean;
+ repoKey: string;
+ source: string;
+ subject: string;
+ visibility: "private" | "public";
+}
+
+export type StoredTimelineActivityDay =
+ typeof timelineActivityDays.$inferSelect;
+
+export interface TimelineContributionTotalRecord {
+ contributionCount: number;
+ day: string;
+ id: string;
+ source: "github-public-calendar";
+ subject: string;
+}
+
+export type StoredTimelineContributionTotal =
+ typeof timelineContributionTotals.$inferSelect;
+
+type PublicWorkBucket = Exclude<
+ WorkBucket,
+ "Across the work" | "Private product work"
+>;
+
+export interface TimelinePublicEventRecord {
+ bucket: PublicWorkBucket;
+ day: string;
+ eventKind: TimelinePublicEventKind;
+ id: string;
+ publicRepoName: string;
+ publicRepoUrl: string;
+ publicTitle: string;
+ publicUrl: string;
+ repoKey: string;
+ source: "github-profile";
+ subject: string;
+}
+
+export type StoredTimelinePublicEvent =
+ typeof timelinePublicEvents.$inferSelect;
+
+const chunk = (values: readonly T[], size: number) => {
+ const chunks: T[][] = [];
+ for (let index = 0; index < values.length; index += size) {
+ chunks.push(values.slice(index, index + size));
+ }
+ return chunks;
+};
+
+export const readTimelineActivityDays = async (
+ subject: string,
+ windowStart: string,
+ windowEnd: string
+): Promise => {
+ if (!isTimelineDatabaseConfigured()) {
+ return [];
+ }
+
+ return await getTimelineDatabase()
+ .select()
+ .from(timelineActivityDays)
+ .where(
+ and(
+ eq(timelineActivityDays.subject, subject),
+ gte(timelineActivityDays.day, windowStart),
+ lte(timelineActivityDays.day, windowEnd)
+ )
+ )
+ .orderBy(timelineActivityDays.day);
+};
+
+export const upsertTimelineActivityDays = async (
+ records: readonly TimelineActivityDayRecord[]
+) => {
+ if (records.length === 0) {
+ return 0;
+ }
+
+ const database = getTimelineDatabase();
+ for (const recordChunk of chunk(records, 250)) {
+ await database
+ .insert(timelineActivityDays)
+ .values(recordChunk)
+ .onConflictDoUpdate({
+ set: {
+ bucket: sql`excluded.bucket`,
+ commitCount: sql`excluded.commit_count`,
+ languageFamily: sql`excluded.language_family`,
+ privacyDomainKey: sql`excluded.privacy_domain_key`,
+ privacyPolicyVersion: sql`excluded.privacy_policy_version`,
+ publicRepoName: sql`excluded.public_repo_name`,
+ publicRepoUrl: sql`excluded.public_repo_url`,
+ reachedDefaultBranch: sql`excluded.reached_default_branch`,
+ updatedAt: new Date(),
+ visibility: sql`excluded.visibility`,
+ },
+ target: timelineActivityDays.id,
+ });
+ }
+
+ return records.length;
+};
+
+export const readTimelineContributionTotals = async (
+ subject: string,
+ windowStart: string,
+ windowEnd: string
+): Promise => {
+ if (!isTimelineDatabaseConfigured()) {
+ return [];
+ }
+
+ return await getTimelineDatabase()
+ .select()
+ .from(timelineContributionTotals)
+ .where(
+ and(
+ eq(timelineContributionTotals.subject, subject),
+ gte(timelineContributionTotals.day, windowStart),
+ lte(timelineContributionTotals.day, windowEnd)
+ )
+ )
+ .orderBy(timelineContributionTotals.day);
+};
+
+export const upsertTimelineContributionTotals = async (
+ records: readonly TimelineContributionTotalRecord[]
+) => {
+ if (records.length === 0) {
+ return 0;
+ }
+
+ const database = getTimelineDatabase();
+ for (const recordChunk of chunk(records, 250)) {
+ await database
+ .insert(timelineContributionTotals)
+ .values(recordChunk)
+ .onConflictDoUpdate({
+ set: {
+ contributionCount: sql`excluded.contribution_count`,
+ updatedAt: new Date(),
+ },
+ target: timelineContributionTotals.id,
+ });
+ }
+
+ return records.length;
+};
+
+export const pruneTimelineActivityBefore = async (
+ subject: string,
+ cutoff: string
+) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .delete(timelineActivityDays)
+ .where(
+ and(
+ eq(timelineActivityDays.subject, subject),
+ lt(timelineActivityDays.day, cutoff)
+ )
+ );
+};
+
+export const pruneTimelineContributionTotalsBefore = async (
+ subject: string,
+ cutoff: string
+) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .delete(timelineContributionTotals)
+ .where(
+ and(
+ eq(timelineContributionTotals.subject, subject),
+ lt(timelineContributionTotals.day, cutoff)
+ )
+ );
+};
+
+export const readTimelinePublicEvents = async (
+ subject: string,
+ windowStart: string,
+ windowEnd: string
+): Promise => {
+ if (!isTimelineDatabaseConfigured()) {
+ return [];
+ }
+
+ return await getTimelineDatabase()
+ .select()
+ .from(timelinePublicEvents)
+ .where(
+ and(
+ eq(timelinePublicEvents.subject, subject),
+ gte(timelinePublicEvents.day, windowStart),
+ lte(timelinePublicEvents.day, windowEnd)
+ )
+ )
+ .orderBy(
+ timelinePublicEvents.day,
+ timelinePublicEvents.eventKind,
+ timelinePublicEvents.id
+ );
+};
+
+export const upsertTimelinePublicEvents = async (
+ records: readonly TimelinePublicEventRecord[]
+) => {
+ if (records.length === 0) {
+ return 0;
+ }
+
+ const uniqueRecords = [
+ ...new Map(records.map((record) => [record.id, record])).values(),
+ ];
+ const database = getTimelineDatabase();
+ for (const recordChunk of chunk(uniqueRecords, 250)) {
+ await database
+ .insert(timelinePublicEvents)
+ .values(recordChunk)
+ .onConflictDoUpdate({
+ set: {
+ bucket: sql`excluded.bucket`,
+ day: sql`excluded.day`,
+ publicRepoName: sql`excluded.public_repo_name`,
+ publicRepoUrl: sql`excluded.public_repo_url`,
+ publicTitle: sql`excluded.public_title`,
+ publicUrl: sql`excluded.public_url`,
+ repoKey: sql`excluded.repo_key`,
+ source: sql`excluded.source`,
+ updatedAt: new Date(),
+ },
+ target: timelinePublicEvents.id,
+ });
+ }
+
+ return uniqueRecords.length;
+};
+
+export const pruneTimelinePublicEventsBefore = async (
+ subject: string,
+ cutoff: string
+) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .delete(timelinePublicEvents)
+ .where(
+ and(
+ eq(timelinePublicEvents.subject, subject),
+ lt(timelinePublicEvents.day, cutoff)
+ )
+ );
+};
+
+export const beginTimelineSyncRun = async (input: {
+ fullWindow: boolean;
+ kind: string;
+ windowEnd: string;
+ windowStart: string;
+}) => {
+ const [run] = await getTimelineDatabase()
+ .insert(timelineSyncRuns)
+ .values(input)
+ .returning({ id: timelineSyncRuns.id });
+
+ if (run === undefined) {
+ throw new Error("Failed to create timeline sync run.");
+ }
+
+ return run.id;
+};
+
+export const completeTimelineSyncRun = async (
+ id: string,
+ rowCount: number,
+ eventCount: number,
+ publicEventCoverage: "complete" | "partial" | "unavailable",
+ anonymousDayCount: number,
+ anonymousCoverage: "complete" | "unavailable",
+ coverage: "complete" | "partial"
+) => {
+ await getTimelineDatabase()
+ .update(timelineSyncRuns)
+ .set({
+ completedAt: new Date(),
+ coverage,
+ errorCode: null,
+ eventCount,
+ anonymousCoverage,
+ anonymousDayCount,
+ publicEventCoverage,
+ rowCount,
+ status: "completed",
+ })
+ .where(eq(timelineSyncRuns.id, id));
+};
+
+export const failTimelineSyncRun = async (id: string, errorCode: string) => {
+ await getTimelineDatabase()
+ .update(timelineSyncRuns)
+ .set({
+ completedAt: new Date(),
+ errorCode: errorCode.slice(0, 64),
+ status: "failed",
+ })
+ .where(eq(timelineSyncRuns.id, id));
+};
+
+export const readLastCompletedTimelineSync = async () => {
+ if (!isTimelineDatabaseConfigured()) {
+ return null;
+ }
+
+ const [run] = await getTimelineDatabase()
+ .select({
+ completedAt: timelineSyncRuns.completedAt,
+ windowEnd: timelineSyncRuns.windowEnd,
+ windowStart: timelineSyncRuns.windowStart,
+ })
+ .from(timelineSyncRuns)
+ .where(eq(timelineSyncRuns.status, "completed"))
+ .orderBy(desc(timelineSyncRuns.completedAt))
+ .limit(1);
+
+ return run ?? null;
+};
+
+export const readLatestTimelineSync = async () => {
+ if (!isTimelineDatabaseConfigured()) {
+ return null;
+ }
+
+ const [run] = await getTimelineDatabase()
+ .select({
+ anonymousCoverage: timelineSyncRuns.anonymousCoverage,
+ completedAt: timelineSyncRuns.completedAt,
+ coverage: timelineSyncRuns.coverage,
+ startedAt: timelineSyncRuns.startedAt,
+ status: timelineSyncRuns.status,
+ })
+ .from(timelineSyncRuns)
+ .orderBy(desc(timelineSyncRuns.startedAt))
+ .limit(1);
+
+ return run ?? null;
+};
+
+export const readLastCompleteAnonymousTimelineSync = async () => {
+ if (!isTimelineDatabaseConfigured()) {
+ return null;
+ }
+
+ const [run] = await getTimelineDatabase()
+ .select({
+ completedAt: timelineSyncRuns.completedAt,
+ windowEnd: timelineSyncRuns.windowEnd,
+ windowStart: timelineSyncRuns.windowStart,
+ })
+ .from(timelineSyncRuns)
+ .where(
+ and(
+ eq(timelineSyncRuns.status, "completed"),
+ eq(timelineSyncRuns.anonymousCoverage, "complete")
+ )
+ )
+ .orderBy(desc(timelineSyncRuns.completedAt))
+ .limit(1);
+
+ return run ?? null;
+};
+
+export const readLastCompleteTimelineBackfill = async () => {
+ if (!isTimelineDatabaseConfigured()) {
+ return null;
+ }
+
+ const [run] = await getTimelineDatabase()
+ .select({
+ completedAt: timelineSyncRuns.completedAt,
+ windowEnd: timelineSyncRuns.windowEnd,
+ windowStart: timelineSyncRuns.windowStart,
+ })
+ .from(timelineSyncRuns)
+ .where(
+ and(
+ eq(timelineSyncRuns.status, "completed"),
+ eq(timelineSyncRuns.coverage, "complete"),
+ eq(timelineSyncRuns.fullWindow, true)
+ )
+ )
+ .orderBy(desc(timelineSyncRuns.completedAt))
+ .limit(1);
+
+ return run ?? null;
+};
+
+export const publishTimelineEdition = async (
+ edition: TimelineEdition,
+ agentModel: string
+) => {
+ const validated = timelineEditionSchema.parse(edition);
+ const hasProtectedEntries = validated.entries.some(
+ (entry) => entry.visibility === "private" || entry.visibility === "mixed"
+ );
+ const privacyPolicyVersion = hasProtectedEntries
+ ? currentTimelinePrivacyPolicyVersion()
+ : null;
+ if (hasProtectedEntries && privacyPolicyVersion === null) {
+ throw new Error("timeline-privacy-policy-missing");
+ }
+ const now = new Date();
+
+ await getTimelineDatabase()
+ .insert(timelineEditions)
+ .values({
+ agentModel,
+ edition: validated,
+ editionKey: validated.editionKey,
+ publishedAt: now,
+ privacyPolicyVersion,
+ status: "published",
+ windowEnd: validated.windowEnd,
+ windowStart: validated.windowStart,
+ })
+ .onConflictDoUpdate({
+ set: {
+ agentModel,
+ edition: validated,
+ publishedAt: now,
+ privacyPolicyVersion,
+ status: "published",
+ updatedAt: now,
+ },
+ target: timelineEditions.editionKey,
+ });
+
+ return validated.editionKey;
+};
+
+export const readPublishedTimelineEdition = async () => {
+ if (!isTimelineDatabaseConfigured()) {
+ return null;
+ }
+
+ const [row] = await getTimelineDatabase()
+ .select({
+ edition: timelineEditions.edition,
+ privacyPolicyVersion: timelineEditions.privacyPolicyVersion,
+ })
+ .from(timelineEditions)
+ .where(eq(timelineEditions.status, "published"))
+ .orderBy(desc(timelineEditions.publishedAt))
+ .limit(1);
+
+ const result = timelineEditionSchema.safeParse(row?.edition);
+ if (!result.success) {
+ return null;
+ }
+
+ if (
+ !editionMatchesTimelinePrivacyPolicy(
+ result.data,
+ row?.privacyPolicyVersion ?? null,
+ currentTimelinePrivacyPolicyVersion()
+ )
+ ) {
+ return null;
+ }
+
+ return result.data;
+};
+
+export const recordTimelineWebhookReceipt = async (input: {
+ deliveryKey: string;
+ eventType: string;
+ expiresAt: Date;
+}) => {
+ const rows = await getTimelineDatabase()
+ .insert(timelineWebhookReceipts)
+ .values({
+ ...input,
+ status: "accepted",
+ })
+ .onConflictDoNothing()
+ .returning({ deliveryKey: timelineWebhookReceipts.deliveryKey });
+
+ return rows.length === 1;
+};
+
+export const markTimelineWebhookProcessed = async (deliveryKey: string) => {
+ await getTimelineDatabase()
+ .update(timelineWebhookReceipts)
+ .set({ processedAt: new Date(), status: "processed" })
+ .where(eq(timelineWebhookReceipts.deliveryKey, deliveryKey));
+};
+
+export const pruneTimelineWebhookReceipts = async (now = new Date()) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .delete(timelineWebhookReceipts)
+ .where(lt(timelineWebhookReceipts.expiresAt, now));
+};
+
+export const countStoredTimelineActivity = async (subject: string) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return 0;
+ }
+
+ const [row] = await getTimelineDatabase()
+ .select({ count: sql`count(*)::int` })
+ .from(timelineActivityDays)
+ .where(eq(timelineActivityDays.subject, subject));
+
+ return row?.count ?? 0;
+};
+
+export const countStoredTimelinePublicEvents = async (subject: string) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return 0;
+ }
+
+ const [row] = await getTimelineDatabase()
+ .select({ count: sql`count(*)::int` })
+ .from(timelinePublicEvents)
+ .where(eq(timelinePublicEvents.subject, subject));
+
+ return row?.count ?? 0;
+};
+
+export const deleteTimelineActivityByIds = async (ids: readonly string[]) => {
+ if (ids.length === 0 || !isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ for (const idChunk of chunk(ids, 500)) {
+ await getTimelineDatabase()
+ .delete(timelineActivityDays)
+ .where(inArray(timelineActivityDays.id, idChunk));
+ }
+};
+
+export const deleteTimelinePublicEventsByIds = async (
+ ids: readonly string[]
+) => {
+ if (ids.length === 0 || !isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ for (const idChunk of chunk(ids, 500)) {
+ await getTimelineDatabase()
+ .delete(timelinePublicEvents)
+ .where(inArray(timelinePublicEvents.id, idChunk));
+ }
+};
+
+export const deleteTimelineActivityByRepoKey = async (
+ subject: string,
+ repoKey: string
+) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .delete(timelineActivityDays)
+ .where(
+ and(
+ eq(timelineActivityDays.subject, subject),
+ eq(timelineActivityDays.repoKey, repoKey)
+ )
+ );
+};
+
+export const deleteTimelinePublicEventsByRepoKey = async (
+ subject: string,
+ repoKey: string
+) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .delete(timelinePublicEvents)
+ .where(
+ and(
+ eq(timelinePublicEvents.subject, subject),
+ eq(timelinePublicEvents.repoKey, repoKey)
+ )
+ );
+};
+
+export const deletePrivateTimelineActivity = async (subject: string) => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .delete(timelineActivityDays)
+ .where(
+ and(
+ eq(timelineActivityDays.subject, subject),
+ eq(timelineActivityDays.visibility, "private")
+ )
+ );
+};
+
+export const rejectPublishedTimelineEditions = async () => {
+ if (!isTimelineDatabaseConfigured()) {
+ return;
+ }
+
+ await getTimelineDatabase()
+ .update(timelineEditions)
+ .set({ status: "rejected", updatedAt: new Date() })
+ .where(eq(timelineEditions.status, "published"));
+};
diff --git a/src/lib/timeline-webhook.ts b/src/lib/timeline-webhook.ts
new file mode 100644
index 0000000..c353469
--- /dev/null
+++ b/src/lib/timeline-webhook.ts
@@ -0,0 +1,79 @@
+import {
+ privateTimelineRepoKey,
+ publicTimelineRepoKey,
+} from "@/lib/timeline-privacy";
+
+const isObject = (value: unknown): value is Record =>
+ typeof value === "object" && value !== null && !Array.isArray(value);
+
+const safeNodeId = (value: unknown) =>
+ typeof value === "string" && /^[A-Za-z\d_=:-]{4,200}$/.test(value)
+ ? value
+ : null;
+
+export interface TimelineWebhookRevocation {
+ repoKeys: string[];
+ withdrawAllPrivateActivity: boolean;
+}
+
+export const timelineRevocationFromWebhook = (
+ body: string,
+ eventType: string,
+ privacyKey: string | null
+): TimelineWebhookRevocation => {
+ let payload: unknown;
+ try {
+ payload = JSON.parse(body) as unknown;
+ } catch {
+ return { repoKeys: [], withdrawAllPrivateActivity: false };
+ }
+ if (!isObject(payload)) {
+ return { repoKeys: [], withdrawAllPrivateActivity: false };
+ }
+
+ const nodeIds = new Set();
+ const action = typeof payload.action === "string" ? payload.action : "";
+ const withdrawAllPrivateActivity =
+ eventType === "installation" &&
+ (action === "deleted" || action === "suspend");
+ if (eventType === "installation_repositories") {
+ const removed = payload.repositories_removed;
+ if (Array.isArray(removed)) {
+ for (const repository of removed) {
+ if (isObject(repository)) {
+ const nodeId = safeNodeId(repository.node_id);
+ if (nodeId !== null) {
+ nodeIds.add(nodeId);
+ }
+ }
+ }
+ }
+ }
+
+ const { repository } = payload;
+ if (
+ isObject(repository) &&
+ (repository.private === true ||
+ repository.visibility === "private" ||
+ action === "deleted")
+ ) {
+ const nodeId = safeNodeId(repository.node_id);
+ if (nodeId !== null) {
+ nodeIds.add(nodeId);
+ }
+ }
+
+ const repoKeys = new Set();
+ for (const nodeId of nodeIds) {
+ repoKeys.add(publicTimelineRepoKey(nodeId));
+ if (privacyKey !== null) {
+ repoKeys.add(privateTimelineRepoKey(nodeId, privacyKey));
+ }
+ }
+
+ return {
+ repoKeys: [...repoKeys],
+ withdrawAllPrivateActivity:
+ withdrawAllPrivateActivity || (nodeIds.size > 0 && privacyKey === null),
+ };
+};
diff --git a/src/lib/timeline.ts b/src/lib/timeline.ts
new file mode 100644
index 0000000..57b0216
--- /dev/null
+++ b/src/lib/timeline.ts
@@ -0,0 +1,46 @@
+import "server-only";
+import { unstable_cache } from "next/cache";
+
+import type { GitHubProfile } from "@/lib/github-profile-core";
+import type { TimelineEdition } from "@/lib/timeline-core";
+import { createFallbackTimelineEdition } from "@/lib/timeline-fallback";
+import { readPublishedTimelineEdition } from "@/lib/timeline-store";
+
+const readCachedPublishedEdition = unstable_cache(
+ async () => {
+ try {
+ return await readPublishedTimelineEdition();
+ } catch {
+ return null;
+ }
+ },
+ ["published-timeline-edition-v2"],
+ { revalidate: 900, tags: ["timeline-edition"] }
+);
+
+export const getPublishedTimelineEdition = async () =>
+ await readCachedPublishedEdition();
+
+export const resolveTimelineEdition = (
+ github: GitHubProfile,
+ published: TimelineEdition | null,
+ now = new Date()
+): TimelineEdition | null => {
+ const allowedPublicRepositories = new Set(
+ github.status === "available"
+ ? github.projects.map((project) => project.url.toLocaleLowerCase("en-US"))
+ : []
+ );
+ // Published links already crossed the event-ingestion and edition-validator
+ // boundaries. The daily full public-event reconciliation withdraws an
+ // edition if a previously visible GitHub object disappears.
+ if (published !== null) {
+ return published;
+ }
+
+ return createFallbackTimelineEdition(
+ github.activity,
+ allowedPublicRepositories,
+ now
+ );
+};
diff --git a/src/types/server-only.d.ts b/src/types/server-only.d.ts
new file mode 100644
index 0000000..06c3049
--- /dev/null
+++ b/src/types/server-only.d.ts
@@ -0,0 +1 @@
+declare module "server-only";
diff --git a/tests/github-profile.test.mjs b/tests/github-profile.test.mjs
new file mode 100644
index 0000000..27fd39b
--- /dev/null
+++ b/tests/github-profile.test.mjs
@@ -0,0 +1,348 @@
+import { describe, expect, test } from "bun:test";
+
+import {
+ createGitHubContributionWindow,
+ createUnavailableGitHubProfile,
+ parseGitHubContributionCalendarDays,
+ parseGitHubContributionCalendarHtml,
+ parseGitHubProfileResponse,
+ parseGitHubRepositoriesResponse,
+} from "../src/lib/github-profile-core.ts";
+
+const window = {
+ from: "2025-08-14T00:00:00.000Z",
+ to: "2026-08-12T12:00:00.000Z",
+};
+
+const contributionDay = (date, contributionCount) => ({
+ contributionCount,
+ date,
+});
+
+const publicRepository = (overrides = {}) => ({
+ description: "A useful public project.",
+ forkCount: 4,
+ isFork: false,
+ isPrivate: false,
+ name: "public-project",
+ owner: { login: "f0rr0" },
+ primaryLanguage: { color: "#3178c6", name: "TypeScript" },
+ repositoryTopics: {
+ nodes: [{ topic: { name: "Next-JS" } }, { topic: { name: "portfolio" } }],
+ },
+ stargazerCount: 42,
+ updatedAt: "2026-08-11T10:00:00Z",
+ url: "https://github.com/f0rr0/public-project",
+ ...overrides,
+});
+
+const responseWith = (repositories) => ({
+ data: {
+ user: {
+ contributionsCollection: {
+ contributionCalendar: {
+ totalContributions: 11,
+ weeks: [
+ {
+ contributionDays: [
+ contributionDay("2025-08-13", 50),
+ contributionDay("2025-08-14", 2),
+ contributionDay("2025-08-15", 0),
+ ],
+ firstDay: "2025-08-10",
+ },
+ {
+ contributionDays: [
+ contributionDay("2025-08-17", 1),
+ contributionDay("2025-08-18", 8),
+ ],
+ firstDay: "2025-08-17",
+ },
+ ],
+ },
+ restrictedContributionsCount: 7,
+ },
+ login: "f0rr0",
+ repositories: { nodes: repositories },
+ },
+ },
+});
+
+const contributionCalendarDocument = (days) => {
+ const tooltips = days
+ .map(({ count, date, id }) => {
+ const label =
+ count === 0
+ ? `No contributions on ${date}.`
+ : `${count.toLocaleString("en-US")} contribution${count === 1 ? "" : "s"} on ${date}.`;
+ return `${label} `;
+ })
+ .join("");
+ const cells = days
+ .map(
+ ({ date, id }) =>
+ ` `
+ )
+ .join("");
+
+ return ``;
+};
+
+const restRepository = (overrides = {}) => ({
+ description: "A REST project.",
+ fork: false,
+ forks_count: 3,
+ html_url: "https://github.com/f0rr0/rest-project",
+ language: "TypeScript",
+ name: "rest-project",
+ owner: { login: "f0rr0" },
+ private: false,
+ stargazers_count: 12,
+ topics: ["next-js", "portfolio", "portfolio"],
+ updated_at: "2026-08-10T09:00:00Z",
+ ...overrides,
+});
+
+describe("GitHub profile normalization", () => {
+ test("creates a rolling 365-calendar-day UTC window", () => {
+ expect(
+ createGitHubContributionWindow(new Date("2026-08-12T12:34:56.000Z"))
+ ).toEqual({
+ from: "2025-08-13T00:00:00.000Z",
+ to: "2026-08-12T12:34:56.000Z",
+ });
+ });
+
+ test("reduces daily activity to coarse weekly aggregates", () => {
+ const profile = parseGitHubProfileResponse(
+ responseWith([publicRepository()]),
+ {
+ fetchedAt: "2026-08-12T12:00:00.000Z",
+ login: "f0rr0",
+ window,
+ }
+ );
+
+ expect(profile?.activity).toEqual({
+ activeDays: 3,
+ from: "2025-08-14",
+ restrictedContributions: 7,
+ status: "available",
+ to: "2026-08-12",
+ totalContributions: 11,
+ weeks: [
+ { contributionCount: 2, level: 1, weekStart: "2025-08-10" },
+ { contributionCount: 9, level: 4, weekStart: "2025-08-17" },
+ ],
+ });
+ });
+
+ test("keeps only validated public, owned, non-fork repositories", () => {
+ const profile = parseGitHubProfileResponse(
+ responseWith([
+ publicRepository(),
+ publicRepository({
+ isPrivate: true,
+ name: "private-client-roadmap",
+ url: "https://github.com/f0rr0/private-client-roadmap",
+ }),
+ publicRepository({
+ isFork: true,
+ name: "upstream-fork",
+ url: "https://github.com/f0rr0/upstream-fork",
+ }),
+ publicRepository({
+ name: "someone-elses-project",
+ owner: { login: "another-user" },
+ url: "https://github.com/another-user/someone-elses-project",
+ }),
+ ]),
+ {
+ fetchedAt: "2026-08-12T12:00:00.000Z",
+ login: "f0rr0",
+ window,
+ }
+ );
+
+ expect(profile?.projects).toEqual([
+ {
+ description: "A useful public project.",
+ forks: 4,
+ language: "TypeScript",
+ languageColor: "#3178c6",
+ name: "public-project",
+ stars: 42,
+ topics: ["next-js", "portfolio"],
+ updatedAt: "2026-08-11T10:00:00.000Z",
+ url: "https://github.com/f0rr0/public-project",
+ },
+ ]);
+ expect(JSON.stringify(profile)).not.toContain("private-client-roadmap");
+ expect(JSON.stringify(profile)).not.toContain("upstream-fork");
+ });
+
+ test("rejects GraphQL errors and malformed activity totals", () => {
+ expect(
+ parseGitHubProfileResponse(
+ { errors: [{ message: "Bad credentials" }] },
+ {
+ fetchedAt: "2026-08-12T12:00:00.000Z",
+ login: "f0rr0",
+ window,
+ }
+ )
+ ).toBeNull();
+
+ const malformed = responseWith([publicRepository()]);
+ malformed.data.user.contributionsCollection.contributionCalendar.totalContributions =
+ -1;
+
+ expect(
+ parseGitHubProfileResponse(malformed, {
+ fetchedAt: "2026-08-12T12:00:00.000Z",
+ login: "f0rr0",
+ window,
+ })
+ ).toBeNull();
+
+ const impossibleRestrictedTotal = responseWith([publicRepository()]);
+ impossibleRestrictedTotal.data.user.contributionsCollection.restrictedContributionsCount = 20;
+
+ expect(
+ parseGitHubProfileResponse(impossibleRestrictedTotal, {
+ fetchedAt: "2026-08-12T12:00:00.000Z",
+ login: "f0rr0",
+ window,
+ })
+ ).toBeNull();
+ });
+
+ test("fallback activity never invents exact contribution totals", () => {
+ const profile = createUnavailableGitHubProfile({
+ login: "f0rr0",
+ window,
+ });
+
+ expect(profile.status).toBe("unavailable");
+ expect(profile.activity).toEqual({
+ activeDays: null,
+ from: "2025-08-14",
+ restrictedContributions: null,
+ status: "unavailable",
+ to: "2026-08-12",
+ totalContributions: null,
+ weeks: [],
+ });
+ expect(profile.projects.map((project) => project.name)).toEqual([
+ "oliphaunt",
+ "react-native-rating",
+ ]);
+ });
+
+ test("parses public contribution HTML directly into rolling weekly totals", () => {
+ const priorYear = contributionCalendarDocument([
+ { count: 1, date: "2025-12-29", id: "contribution-day-component-a" },
+ { count: 0, date: "2025-12-30", id: "contribution-day-component-b" },
+ { count: 2, date: "2025-12-31", id: "contribution-day-component-c" },
+ ]);
+ const currentYear = contributionCalendarDocument([
+ { count: 3, date: "2026-01-01", id: "contribution-day-component-d" },
+ { count: 0, date: "2026-01-02", id: "contribution-day-component-e" },
+ ]);
+
+ expect(
+ parseGitHubContributionCalendarHtml([priorYear, currentYear], {
+ from: "2025-12-29T00:00:00.000Z",
+ to: "2026-01-02T12:00:00.000Z",
+ })
+ ).toEqual({
+ activeDays: 3,
+ from: "2025-12-29",
+ restrictedContributions: null,
+ status: "available",
+ to: "2026-01-02",
+ totalContributions: 6,
+ weeks: [
+ {
+ contributionCount: 6,
+ level: 4,
+ weekStart: "2025-12-28",
+ },
+ ],
+ });
+ expect(
+ parseGitHubContributionCalendarDays([priorYear, currentYear], {
+ from: "2025-12-29T00:00:00.000Z",
+ to: "2026-01-02T12:00:00.000Z",
+ })
+ ).toEqual([
+ { contributionCount: 1, day: "2025-12-29" },
+ { contributionCount: 0, day: "2025-12-30" },
+ { contributionCount: 2, day: "2025-12-31" },
+ { contributionCount: 3, day: "2026-01-01" },
+ { contributionCount: 0, day: "2026-01-02" },
+ ]);
+ });
+
+ test("rejects incomplete or malformed contribution HTML", () => {
+ const incomplete = contributionCalendarDocument([
+ { count: 1, date: "2026-01-01", id: "contribution-day-component-a" },
+ ]);
+
+ expect(
+ parseGitHubContributionCalendarHtml([incomplete], {
+ from: "2026-01-01T00:00:00.000Z",
+ to: "2026-01-02T00:00:00.000Z",
+ })
+ ).toBeNull();
+ expect(
+ parseGitHubContributionCalendarHtml(
+ [
+ ` `,
+ ],
+ {
+ from: "2026-01-01T00:00:00.000Z",
+ to: "2026-01-01T00:00:00.000Z",
+ }
+ )
+ ).toBeNull();
+ });
+
+ test("normalizes only public owned non-fork REST repositories", () => {
+ const projects = parseGitHubRepositoriesResponse(
+ [
+ restRepository(),
+ restRepository({
+ html_url: "https://github.com/f0rr0/private-project",
+ name: "private-project",
+ private: true,
+ }),
+ restRepository({
+ fork: true,
+ html_url: "https://github.com/f0rr0/a-fork",
+ name: "a-fork",
+ }),
+ restRepository({
+ html_url: "https://github.com/someone-else/their-project",
+ name: "their-project",
+ owner: { login: "someone-else" },
+ }),
+ ],
+ "f0rr0"
+ );
+
+ expect(projects).toEqual([
+ {
+ description: "A REST project.",
+ forks: 3,
+ language: "TypeScript",
+ languageColor: null,
+ name: "rest-project",
+ stars: 12,
+ topics: ["next-js", "portfolio"],
+ updatedAt: "2026-08-10T09:00:00.000Z",
+ url: "https://github.com/f0rr0/rest-project",
+ },
+ ]);
+ });
+});
diff --git a/tests/timeline.test.mjs b/tests/timeline.test.mjs
new file mode 100644
index 0000000..a0cdbb7
--- /dev/null
+++ b/tests/timeline.test.mjs
@@ -0,0 +1,1107 @@
+import { describe, expect, test } from "bun:test";
+
+import {
+ containsPrivateIdentifier,
+ createTimelineEdition,
+ editionMatchesTimelinePrivacyPolicy,
+ validateTimelinePlanAgainstDigest,
+} from "../src/lib/timeline-core.ts";
+import {
+ calculateAnonymousContributionDays,
+ createTimelineActivityDigest,
+} from "../src/lib/timeline-editorial.ts";
+import { createFallbackTimelineEdition } from "../src/lib/timeline-fallback.ts";
+import {
+ normalizeGitHubContributionSlice,
+ normalizeTimelinePrivacyKey,
+ parsePrivateTimelineTaxonomy,
+ privateTimelineRepoKey,
+ publicTimelineRepoKey,
+} from "../src/lib/timeline-privacy.ts";
+import { timelineRevocationFromWebhook } from "../src/lib/timeline-webhook.ts";
+
+const privacyKey = "0123456789abcdefFEDCBA9876543210timeline-safety-key";
+const windowStart = "2025-07-09";
+const windowEnd = "2026-08-12";
+
+const repositoryGroup = ({
+ commitCount = 4,
+ day = "2026-08-01T12:00:00Z",
+ id,
+ isPrivate,
+ nameWithOwner,
+ reachedDefaultBranch,
+}) => ({
+ contributions: {
+ nodes: [
+ {
+ commitCount,
+ occurredAt: day,
+ ...(reachedDefaultBranch === undefined ? {} : { reachedDefaultBranch }),
+ },
+ ],
+ },
+ repository: {
+ description: isPrivate === true ? "Secret client roadmap" : "A public tool",
+ id,
+ isPrivate,
+ nameWithOwner,
+ primaryLanguage: { name: "TypeScript" },
+ repositoryTopics: {
+ nodes: [{ topic: { name: isPrivate === true ? "secret" : "tooling" } }],
+ },
+ url: `https://github.com/${nameWithOwner}`,
+ },
+});
+
+const emptyConnection = () => ({
+ nodes: [],
+ pageInfo: { hasNextPage: false },
+});
+
+const contributionPayload = (groups, connections = {}) => ({
+ data: {
+ user: {
+ contributionsCollection: {
+ commitContributionsByRepository: groups,
+ ...connections,
+ },
+ },
+ },
+});
+
+const normalizeOptions = (overrides = {}) => ({
+ privacyKey,
+ subject: "f0rr0",
+ taxonomy: parsePrivateTimelineTaxonomy(
+ JSON.stringify({
+ "secret-org/stealth-client": {
+ bucket: "Applied AI",
+ domain: "Product",
+ },
+ })
+ ),
+ windowEnd,
+ windowStart,
+ ...overrides,
+});
+
+const storedRow = (overrides = {}) => ({
+ bucket: "Private product work",
+ commitCount: 6,
+ day: "2026-07-02",
+ id: "a".repeat(64),
+ languageFamily: "withheld",
+ privacyDomainKey: "domain-a",
+ privacyPolicyVersion: "policy",
+ publicRepoName: null,
+ publicRepoUrl: null,
+ reachedDefaultBranch: true,
+ repoKey: "repo-a",
+ source: "github-profile",
+ subject: "f0rr0",
+ updatedAt: new Date("2026-08-12T00:00:00Z"),
+ visibility: "private",
+ ...overrides,
+});
+
+const activityCluster = (index, overrides = {}) => {
+ const date = `2026-0${index + 1}-0${index + 1}`;
+ return {
+ bucket: "Open source",
+ cadence: "clustered",
+ endDate: date,
+ facts: ["Several public changes landed together."],
+ key: `public:source-${index}`,
+ kind: "commit-run",
+ magnitude: "steady",
+ maxImportance: index === 0 ? "lead" : index < 2 ? "story" : "brief",
+ publicHref: `https://github.com/f0rr0/project-${index}`,
+ publicLabel: "View project",
+ publicTitle: `project-${index}`,
+ publishable: true,
+ rollupOf: [],
+ seriesKey: `series:project-${index}`,
+ startDate: date,
+ visibility: "public",
+ ...overrides,
+ };
+};
+
+const digest = {
+ clusters: [
+ activityCluster(0),
+ activityCluster(1),
+ activityCluster(2),
+ activityCluster(3),
+ activityCluster(4),
+ activityCluster(5, {
+ bucket: "Private product work",
+ endDate: "2026-07-01",
+ facts: ["A broad private work pattern was sustained."],
+ key: "private:source-six",
+ kind: "private-month",
+ magnitude: "sustained",
+ maxImportance: "story",
+ publicHref: undefined,
+ publicLabel: undefined,
+ publicTitle: undefined,
+ seriesKey: "private-series:general-work",
+ startDate: "2026-07-01",
+ visibility: "private",
+ }),
+ ],
+ coverage: "complete",
+ generatedAt: "2026-08-12T00:00:00.000Z",
+ windowEnd,
+ windowStart,
+};
+
+const candidatePlan = {
+ selections: digest.clusters.map((cluster, index) => ({
+ importance:
+ index === 0 ? "lead" : index === 1 || index === 5 ? "story" : "brief",
+ sourceKey: cluster.key,
+ })),
+ windowEnd,
+ windowStart,
+};
+
+const storedPublicEvent = (overrides = {}) => ({
+ bucket: "Open source",
+ day: "2026-08-01",
+ eventKind: "issue_opened",
+ id: "e".repeat(64),
+ publicRepoName: "upstream/public-tool",
+ publicRepoUrl: "https://github.com/upstream/public-tool",
+ publicTitle: "Document the edge case",
+ publicUrl: "https://github.com/upstream/public-tool/issues/42",
+ repoKey: "f".repeat(64),
+ source: "github-profile",
+ subject: "f0rr0",
+ updatedAt: new Date("2026-08-12T00:00:00Z"),
+ ...overrides,
+});
+
+const availableActivity = {
+ activeDays: 24,
+ from: "2025-08-13",
+ restrictedContributions: null,
+ status: "available",
+ to: "2026-08-12",
+ totalContributions: 72,
+ weeks: Array.from({ length: 10 }, (_, index) => ({
+ contributionCount: index + 2,
+ level: 2,
+ weekStart:
+ index < 5
+ ? `2025-${String(index + 8).padStart(2, "0")}-01`
+ : `2026-${String(index - 4).padStart(2, "0")}-01`,
+ })),
+};
+
+describe("timeline privacy boundary", () => {
+ test("discards private identity before creating storage records", () => {
+ const normalized = normalizeGitHubContributionSlice(
+ contributionPayload([
+ repositoryGroup({
+ id: "R_private_secret_identifier",
+ isPrivate: true,
+ nameWithOwner: "secret-org/stealth-client",
+ }),
+ repositoryGroup({
+ id: "R_public_identifier",
+ isPrivate: false,
+ nameWithOwner: "f0rr0/public-tool",
+ }),
+ ]),
+ normalizeOptions()
+ );
+
+ expect(normalized).not.toBeNull();
+ const privateRecord = normalized.records.find(
+ (record) => record.visibility === "private"
+ );
+ expect(privateRecord).toMatchObject({
+ bucket: "Applied AI",
+ languageFamily: "withheld",
+ publicRepoName: null,
+ publicRepoUrl: null,
+ visibility: "private",
+ });
+ expect(privateRecord.privacyPolicyVersion).toHaveLength(64);
+ expect(JSON.stringify(privateRecord)).not.toContain("stealth-client");
+ expect(JSON.stringify(privateRecord)).not.toContain(
+ "R_private_secret_identifier"
+ );
+ });
+
+ test("fails closed for private rows without a valid privacy key", () => {
+ const normalized = normalizeGitHubContributionSlice(
+ contributionPayload([
+ repositoryGroup({
+ id: "R_private",
+ isPrivate: true,
+ nameWithOwner: "secret-org/stealth-client",
+ }),
+ ]),
+ normalizeOptions({ privacyKey: null })
+ );
+
+ expect(normalized.records).toEqual([]);
+ expect(normalized.privateRecordsSkipped).toBe(1);
+ expect(normalizeTimelinePrivacyKey("x".repeat(64))).toBeNull();
+ });
+
+ test("preserves an explicit non-default contribution branch marker", () => {
+ const normalized = normalizeGitHubContributionSlice(
+ contributionPayload([
+ repositoryGroup({
+ id: "R_public_gh_pages",
+ isPrivate: false,
+ nameWithOwner: "f0rr0/public-pages",
+ reachedDefaultBranch: false,
+ }),
+ ]),
+ normalizeOptions()
+ );
+
+ expect(normalized.records).toHaveLength(1);
+ expect(normalized.records[0].reachedDefaultBranch).toBe(false);
+ });
+
+ test("keeps verified public issue and pull-request events as exact evidence", () => {
+ const repository = {
+ description: "A public infrastructure tool",
+ id: "R_upstream_public_tool",
+ isPrivate: false,
+ nameWithOwner: "upstream/public-tool",
+ primaryLanguage: { name: "Go" },
+ repositoryTopics: { nodes: [{ topic: { name: "infrastructure" } }] },
+ url: "https://github.com/upstream/public-tool",
+ };
+ const normalized = normalizeGitHubContributionSlice(
+ contributionPayload([], {
+ issueContributions: {
+ nodes: [
+ {
+ isRestricted: false,
+ issue: {
+ id: "I_public_issue",
+ repository,
+ title: "Document the edge case",
+ url: "https://github.com/upstream/public-tool/issues/42",
+ },
+ occurredAt: "2026-08-01T12:00:00Z",
+ },
+ ],
+ pageInfo: { hasNextPage: false },
+ },
+ pullRequestContributions: {
+ nodes: [
+ {
+ isRestricted: false,
+ occurredAt: "2026-08-02T12:00:00Z",
+ pullRequest: {
+ id: "PR_public_change",
+ repository,
+ title: "Handle an empty response",
+ url: "https://github.com/upstream/public-tool/pull/51",
+ },
+ },
+ ],
+ pageInfo: { hasNextPage: false },
+ },
+ pullRequestReviewContributions: emptyConnection(),
+ repositoryContributions: emptyConnection(),
+ }),
+ normalizeOptions()
+ );
+
+ expect(normalized.publicEventCoverage).toBe("complete");
+ expect(normalized.publicEvents).toHaveLength(2);
+ expect(normalized.publicEvents[0]).toMatchObject({
+ eventKind: "issue_opened",
+ publicRepoName: "upstream/public-tool",
+ publicTitle: "Document the edge case",
+ publicUrl: "https://github.com/upstream/public-tool/issues/42",
+ });
+ expect(normalized.publicEvents[1]).toMatchObject({
+ eventKind: "pull_request_opened",
+ publicTitle: "Handle an empty response",
+ publicUrl: "https://github.com/upstream/public-tool/pull/51",
+ });
+ });
+
+ test("keeps accessible public events when GitHub forbids individual nodes", () => {
+ const repository = {
+ description: "A public infrastructure tool",
+ id: "R_upstream_public_tool",
+ isPrivate: false,
+ nameWithOwner: "upstream/public-tool",
+ primaryLanguage: { name: "Go" },
+ repositoryTopics: { nodes: [] },
+ url: "https://github.com/upstream/public-tool",
+ };
+ const payload = contributionPayload([], {
+ issueContributions: emptyConnection(),
+ pullRequestContributions: {
+ nodes: [
+ null,
+ {
+ isRestricted: false,
+ occurredAt: "2026-08-02T12:00:00Z",
+ pullRequest: {
+ id: "PR_accessible_change",
+ repository,
+ title: "Handle an empty response",
+ url: "https://github.com/upstream/public-tool/pull/51",
+ },
+ },
+ ],
+ pageInfo: { hasNextPage: false },
+ },
+ pullRequestReviewContributions: emptyConnection(),
+ repositoryContributions: emptyConnection(),
+ });
+ payload.errors = [
+ {
+ message: "intentionally ignored",
+ path: [
+ "user",
+ "contributionsCollection",
+ "pullRequestContributions",
+ "nodes",
+ 0,
+ ],
+ type: "FORBIDDEN",
+ },
+ ];
+
+ const normalized = normalizeGitHubContributionSlice(
+ payload,
+ normalizeOptions()
+ );
+
+ expect(normalized.coverage).toBe("complete");
+ expect(normalized.publicEventCoverage).toBe("partial");
+ expect(normalized.publicEvents).toHaveLength(1);
+ expect(normalized.publicEvents[0].publicTitle).toBe(
+ "Handle an empty response"
+ );
+ expect(JSON.stringify(normalized)).not.toContain("intentionally ignored");
+ });
+
+ test("drops all private and restricted event identity before storage", () => {
+ const privateRepository = {
+ description: "Secret roadmap",
+ id: "R_private_event_repo",
+ isPrivate: true,
+ nameWithOwner: "secret-org/private-event-repo",
+ url: "https://github.com/secret-org/private-event-repo",
+ };
+ const normalized = normalizeGitHubContributionSlice(
+ contributionPayload([], {
+ issueContributions: {
+ nodes: [
+ {
+ isRestricted: false,
+ issue: {
+ id: "I_secret_123",
+ repository: privateRepository,
+ title: "Unannounced client launch",
+ url: "https://github.com/secret-org/private-event-repo/issues/9",
+ },
+ occurredAt: "2026-08-01T12:00:00Z",
+ },
+ ],
+ pageInfo: { hasNextPage: false },
+ },
+ pullRequestContributions: emptyConnection(),
+ pullRequestReviewContributions: emptyConnection(),
+ repositoryContributions: emptyConnection(),
+ }),
+ normalizeOptions()
+ );
+ const serialized = JSON.stringify(normalized);
+
+ expect(normalized.publicEvents).toEqual([]);
+ expect(serialized).not.toContain("private-event-repo");
+ expect(serialized).not.toContain("Unannounced client launch");
+ expect(serialized).not.toContain("I_secret_123");
+ });
+
+ test("canonicalizes approved domains and rejects ambiguous taxonomy", () => {
+ const taxonomy = parsePrivateTimelineTaxonomy(
+ JSON.stringify({
+ "secret-org/stealth-client": {
+ bucket: "Applied AI",
+ domain: " Payments ",
+ },
+ })
+ );
+ expect(taxonomy.get("secret-org/stealth-client")?.domain).toBe("payments");
+ expect(() =>
+ parsePrivateTimelineTaxonomy(
+ JSON.stringify({
+ "secret-org/stealth-client": {
+ bucket: "Applied AI",
+ domain: "payments/internal",
+ },
+ })
+ )
+ ).toThrow("timeline-taxonomy-invalid");
+ expect(() =>
+ parsePrivateTimelineTaxonomy(
+ JSON.stringify({
+ "secret-org/stealth-client": { bucket: "Across the work" },
+ })
+ )
+ ).toThrow("timeline-taxonomy-invalid");
+ });
+
+ test("normalizes Unicode before private-copy checks", () => {
+ expect(containsPrivateIdentifier("Private work 12")).toBe(true);
+ expect(containsPrivateIdentifier("secret/repository")).toBe(true);
+ expect(containsPrivateIdentifier("A broad protected work pattern")).toBe(
+ false
+ );
+ });
+
+ test("collapses sparse hidden buckets into one monthly generic cluster", () => {
+ const rows = [
+ storedRow({
+ bucket: "Applied AI",
+ day: "2026-07-02",
+ id: "a".repeat(64),
+ repoKey: "private-repo-a",
+ }),
+ storedRow({
+ bucket: "Applied AI",
+ day: "2026-07-09",
+ id: "b".repeat(64),
+ repoKey: "private-repo-b",
+ }),
+ storedRow({
+ bucket: "Infrastructure",
+ day: "2026-07-16",
+ id: "c".repeat(64),
+ privacyDomainKey: "domain-b",
+ repoKey: "private-repo-c",
+ }),
+ storedRow({
+ bucket: "Infrastructure",
+ day: "2026-07-23",
+ id: "d".repeat(64),
+ privacyDomainKey: "domain-b",
+ repoKey: "private-repo-d",
+ }),
+ ];
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows,
+ windowEnd,
+ windowStart,
+ });
+
+ expect(result.clusters).toHaveLength(1);
+ expect(result.clusters[0]).toMatchObject({
+ bucket: "Private product work",
+ visibility: "private",
+ });
+ expect(JSON.stringify(result.clusters)).not.toContain("private-repo");
+ });
+
+ test("caps large digests deterministically", () => {
+ const rows = Array.from({ length: 130 }, (_, index) =>
+ storedRow({
+ bucket: "Open source",
+ commitCount: 1,
+ day: "2026-08-01",
+ id: index.toString(16).padStart(64, "0"),
+ languageFamily: "web",
+ privacyDomainKey: null,
+ privacyPolicyVersion: null,
+ publicRepoName: `f0rr0/public-${index}`,
+ publicRepoUrl: `https://github.com/f0rr0/public-${index}`,
+ repoKey: `public-repo-${index}`,
+ visibility: "public",
+ })
+ );
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows,
+ windowEnd,
+ windowStart,
+ });
+ expect(result.clusters).toHaveLength(120);
+ });
+
+ test("uses issues as dispatches while suppressing a redundant nearby commit run", () => {
+ const repoKey = "c".repeat(64);
+ const publicRows = ["2026-07-30", "2026-08-01", "2026-08-03"].map(
+ (day, index) =>
+ storedRow({
+ bucket: "Open source",
+ commitCount: 2,
+ day,
+ id: String(index + 1).padStart(64, "0"),
+ languageFamily: "web",
+ privacyDomainKey: null,
+ privacyPolicyVersion: null,
+ publicRepoName: "upstream/public-tool",
+ publicRepoUrl: "https://github.com/upstream/public-tool",
+ repoKey,
+ visibility: "public",
+ })
+ );
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ events: [storedPublicEvent({ repoKey })],
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows: publicRows,
+ windowEnd,
+ windowStart,
+ });
+
+ expect(
+ result.clusters.some((cluster) => cluster.kind === "issue-opened")
+ ).toBe(true);
+ expect(
+ result.clusters.some((cluster) => cluster.kind === "commit-run")
+ ).toBe(false);
+ });
+
+ test("deduplicates only the exact curated pull-request artifact", () => {
+ const repoKey = "9".repeat(64);
+ const rows = [
+ storedRow({
+ bucket: "Applied AI",
+ commitCount: 1,
+ day: "2026-03-16",
+ id: "8".repeat(64),
+ languageFamily: "systems",
+ privacyDomainKey: null,
+ privacyPolicyVersion: null,
+ publicRepoName: "f0rr0/zeroclaw",
+ publicRepoUrl: "https://github.com/f0rr0/zeroclaw",
+ repoKey,
+ visibility: "public",
+ }),
+ ];
+ const duplicate = storedPublicEvent({
+ day: "2026-03-16",
+ eventKind: "pull_request_opened",
+ id: "7".repeat(64),
+ publicRepoName: "f0rr0/zeroclaw",
+ publicRepoUrl: "https://github.com/f0rr0/zeroclaw",
+ publicTitle: "The raw title for PR 8",
+ publicUrl: "https://github.com/f0rr0/zeroclaw/pull/8",
+ repoKey,
+ });
+ const distinct = storedPublicEvent({
+ day: "2026-03-16",
+ eventKind: "pull_request_opened",
+ id: "6".repeat(64),
+ publicRepoName: "f0rr0/zeroclaw",
+ publicRepoUrl: "https://github.com/f0rr0/zeroclaw",
+ publicTitle: "A different public change",
+ publicUrl: "https://github.com/f0rr0/zeroclaw/pull/9",
+ repoKey,
+ });
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ events: [duplicate, distinct],
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows,
+ windowEnd,
+ windowStart,
+ });
+
+ expect(
+ result.clusters.filter(
+ (cluster) =>
+ cluster.publicHref === "https://github.com/f0rr0/zeroclaw/pull/8"
+ )
+ ).toHaveLength(1);
+ expect(
+ result.clusters.some(
+ (cluster) =>
+ cluster.kind === "pull-request-opened" &&
+ cluster.publicHref === "https://github.com/f0rr0/zeroclaw/pull/9"
+ )
+ ).toBe(true);
+ });
+
+ test("keeps a meaningful commit trend beside one issue dispatch", () => {
+ const repoKey = "5".repeat(64);
+ const rows = Array.from({ length: 9 }, (_, index) =>
+ storedRow({
+ bucket: "Open source",
+ commitCount: 4,
+ day: `2026-07-${String(15 + index * 2).padStart(2, "0")}`,
+ id: index.toString(16).padStart(64, "4"),
+ languageFamily: "web",
+ privacyDomainKey: null,
+ privacyPolicyVersion: null,
+ publicRepoName: "upstream/public-tool",
+ publicRepoUrl: "https://github.com/upstream/public-tool",
+ repoKey,
+ visibility: "public",
+ })
+ );
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ events: [storedPublicEvent({ day: "2026-07-24", repoKey })],
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows,
+ windowEnd,
+ windowStart,
+ });
+
+ expect(
+ result.clusters.some((cluster) => cluster.kind === "issue-opened")
+ ).toBe(true);
+ expect(
+ result.clusters.some((cluster) => cluster.kind === "commit-run")
+ ).toBe(true);
+ });
+
+ test("keeps representative monthly dispatches instead of a PR wall", () => {
+ const pullRequests = Array.from({ length: 8 }, (_, index) =>
+ storedPublicEvent({
+ day: `2026-07-${String(20 + index).padStart(2, "0")}`,
+ eventKind: "pull_request_opened",
+ id: index.toString(16).padStart(64, "1"),
+ publicTitle: `Public change ${index + 1}`,
+ publicUrl: `https://github.com/upstream/public-tool/pull/${index + 1}`,
+ })
+ );
+ const issue = storedPublicEvent({
+ day: "2026-07-19",
+ id: "e".repeat(64),
+ });
+ const repository = storedPublicEvent({
+ day: "2026-07-18",
+ eventKind: "repository_created",
+ id: "d".repeat(64),
+ publicTitle: "public-tool",
+ publicUrl: "https://github.com/upstream/public-tool",
+ });
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ events: [...pullRequests, issue, repository],
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows: [],
+ windowEnd,
+ windowStart,
+ });
+ const dispatches = result.clusters.filter(
+ (cluster) =>
+ cluster.kind === "issue-opened" ||
+ cluster.kind === "pull-request-opened" ||
+ cluster.kind === "repository-created"
+ );
+
+ expect(dispatches).toHaveLength(3);
+ expect(dispatches.map((cluster) => cluster.kind).toSorted()).toEqual([
+ "issue-opened",
+ "pull-request-opened",
+ "repository-created",
+ ]);
+ });
+
+ test("does not let a curated pull request erase repository creation", () => {
+ const repoKey = "3".repeat(64);
+ const rows = [
+ storedRow({
+ bucket: "Applied AI",
+ commitCount: 1,
+ day: "2026-03-16",
+ id: "2".repeat(64),
+ languageFamily: "systems",
+ privacyDomainKey: null,
+ privacyPolicyVersion: null,
+ publicRepoName: "f0rr0/zeroclaw",
+ publicRepoUrl: "https://github.com/f0rr0/zeroclaw",
+ repoKey,
+ visibility: "public",
+ }),
+ ];
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ events: [
+ storedPublicEvent({
+ day: "2026-03-15",
+ eventKind: "repository_created",
+ id: "1".repeat(64),
+ publicRepoName: "f0rr0/zeroclaw",
+ publicRepoUrl: "https://github.com/f0rr0/zeroclaw",
+ publicTitle: "zeroclaw",
+ publicUrl: "https://github.com/f0rr0/zeroclaw",
+ repoKey,
+ }),
+ ],
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows,
+ windowEnd,
+ windowStart,
+ });
+
+ expect(
+ result.clusters.some((cluster) => cluster.kind === "repository-created")
+ ).toBe(true);
+ expect(
+ result.clusters.some(
+ (cluster) =>
+ cluster.publicHref === "https://github.com/f0rr0/zeroclaw/pull/8"
+ )
+ ).toBe(true);
+ });
+
+ test("lets issue-only weeks establish a public consistency streak", () => {
+ const events = Array.from({ length: 5 }, (_, index) =>
+ storedPublicEvent({
+ day: `2026-07-${String(1 + index * 7).padStart(2, "0")}`,
+ id: index.toString(16).padStart(64, "0"),
+ publicTitle: `Public issue ${index + 1}`,
+ publicUrl: `https://github.com/upstream/public-tool/issues/${index + 1}`,
+ })
+ );
+ const result = createTimelineActivityDigest({
+ coverage: "complete",
+ events,
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows: [],
+ windowEnd,
+ windowStart,
+ });
+ const streak = result.clusters.find(
+ (cluster) => cluster.kind === "public-streak"
+ );
+
+ expect(streak).toBeDefined();
+ expect(streak.facts[0]).toContain("5 consecutive weeks");
+ });
+
+ test("subtracts known commits and events from account-wide totals once", () => {
+ const result = calculateAnonymousContributionDays({
+ events: [
+ { day: "2026-08-01", id: "event-a" },
+ { day: "2026-08-01", id: "event-a" },
+ ],
+ rows: [
+ { commitCount: 4, day: "2026-08-01" },
+ { commitCount: 3, day: "2026-08-02" },
+ ],
+ totals: [
+ { contributionCount: 10, day: "2026-08-01" },
+ { contributionCount: 2, day: "2026-08-02" },
+ { contributionCount: 3, day: "2026-08-03" },
+ ],
+ });
+
+ expect(result).toEqual([
+ { contributionCount: 5, day: "2026-08-01" },
+ { contributionCount: 3, day: "2026-08-03" },
+ ]);
+ });
+
+ test("uses account-wide totals for one anonymous streak without duplicating the public streak", () => {
+ const activeDays = [
+ "2026-06-01",
+ "2026-06-08",
+ "2026-06-15",
+ "2026-06-22",
+ "2026-06-29",
+ "2026-07-06",
+ ];
+ const events = activeDays.slice(0, 5).map((day, index) =>
+ storedPublicEvent({
+ day,
+ id: index.toString(16).padStart(64, "0"),
+ publicTitle: `Public issue ${index + 1}`,
+ publicUrl: `https://github.com/upstream/public-tool/issues/${index + 1}`,
+ })
+ );
+ const result = createTimelineActivityDigest({
+ anonymousTotals: activeDays.map((day) => ({
+ contributionCount: 8,
+ day,
+ })),
+ coverage: "complete",
+ events,
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows: [],
+ windowEnd,
+ windowStart,
+ });
+ const streak = result.clusters.find(
+ (cluster) => cluster.kind === "account-wide-streak"
+ );
+
+ expect(streak).toMatchObject({
+ bucket: "Across the work",
+ cadence: "streak",
+ visibility: "anonymous",
+ });
+ expect(
+ result.clusters.some((cluster) => cluster.kind === "public-streak")
+ ).toBe(false);
+ expect(
+ result.clusters.some((cluster) => cluster.kind === "anonymous-month")
+ ).toBe(true);
+ expect(JSON.stringify(streak)).not.toContain("upstream/public-tool");
+ });
+});
+
+describe("timeline publication", () => {
+ test("materializes all public and protected copy outside the model", () => {
+ const plan = validateTimelinePlanAgainstDigest(candidatePlan, digest);
+ const protectedEntry = plan.entries.find(
+ (entry) => entry.visibility === "private"
+ );
+ const publicEntry = plan.entries.find(
+ (entry) => entry.sourceKeys[0] === "public:source-0"
+ );
+
+ expect(plan.headline).toBe("The work, along one line.");
+ expect(plan.standfirst).toContain("public milestones");
+ expect(protectedEntry.title).not.toContain("client");
+ expect(protectedEntry.description).not.toContain("strategy");
+ expect(protectedEntry.metrics).toEqual([]);
+ expect(publicEntry.title).toBe("project-0");
+ expect(publicEntry.description).toBe(
+ "Several public changes landed together."
+ );
+ });
+
+ test("publishes a public issue as one exact dispatch, not generated copy", () => {
+ const eventDigest = createTimelineActivityDigest({
+ coverage: "complete",
+ events: [storedPublicEvent()],
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows: [],
+ windowEnd,
+ windowStart,
+ });
+ const issue = eventDigest.clusters.find(
+ (cluster) => cluster.kind === "issue-opened"
+ );
+ expect(issue).toBeDefined();
+
+ const plan = validateTimelinePlanAgainstDigest(
+ {
+ selections: [{ importance: "brief", sourceKey: issue.key }],
+ windowEnd,
+ windowStart,
+ },
+ eventDigest
+ );
+
+ expect(plan.entries).toHaveLength(1);
+ expect(plan.entries[0]).toMatchObject({
+ cadence: "isolated",
+ description:
+ "Opened a public issue in upstream/public-tool; the thread remains available on GitHub.",
+ href: "https://github.com/upstream/public-tool/issues/42",
+ kind: "issue",
+ title: "Document the edge case",
+ });
+ });
+
+ test("materializes anonymous totals without a repository or theme claim", () => {
+ const anonymousDigest = createTimelineActivityDigest({
+ anonymousTotals: [
+ "2026-06-01",
+ "2026-06-08",
+ "2026-06-15",
+ "2026-06-22",
+ "2026-06-29",
+ ].map((day) => ({ contributionCount: 5, day })),
+ coverage: "complete",
+ generatedAt: new Date("2026-08-12T00:00:00Z"),
+ rows: [],
+ windowEnd,
+ windowStart,
+ });
+ const selections = anonymousDigest.clusters.map((cluster) => ({
+ importance: cluster.maxImportance,
+ sourceKey: cluster.key,
+ }));
+ const plan = validateTimelinePlanAgainstDigest(
+ { selections, windowEnd, windowStart },
+ anonymousDigest
+ );
+ const streak = plan.entries.find((entry) => entry.cadence === "streak");
+
+ expect(streak).toMatchObject({
+ bucket: "Across the work",
+ title: "A sustained account-wide cadence",
+ visibility: "anonymous",
+ });
+ expect(streak.description).toContain("repository identity");
+ expect(streak.description).not.toContain("private");
+ expect(streak.metrics).toEqual([]);
+ expect(
+ editionMatchesTimelinePrivacyPolicy(
+ createTimelineEdition(
+ plan,
+ anonymousDigest,
+ new Date("2026-08-12T01:00:00Z")
+ ),
+ null,
+ null
+ )
+ ).toBe(true);
+ });
+
+ test("rejects source reuse, inflated importance, and model-authored fields", () => {
+ const reused = structuredClone(candidatePlan);
+ reused.selections[5].sourceKey = reused.selections[0].sourceKey;
+ expect(() => validateTimelinePlanAgainstDigest(reused, digest)).toThrow(
+ "reused"
+ );
+
+ const inflated = structuredClone(candidatePlan);
+ inflated.selections[3].importance = "lead";
+ expect(() => validateTimelinePlanAgainstDigest(inflated, digest)).toThrow(
+ "overstates"
+ );
+
+ const authored = {
+ ...structuredClone(candidatePlan),
+ headline: "The model wrote this",
+ };
+ expect(() => validateTimelinePlanAgainstDigest(authored, digest)).toThrow(
+ "Unrecognized key"
+ );
+ });
+
+ test("keeps edition keys stable across runtime timestamps", () => {
+ const plan = validateTimelinePlanAgainstDigest(candidatePlan, digest);
+ const first = createTimelineEdition(
+ plan,
+ digest,
+ new Date("2026-08-12T01:00:00Z")
+ );
+ const second = createTimelineEdition(
+ plan,
+ { ...digest, generatedAt: "2026-08-12T03:00:00.000Z" },
+ new Date("2026-08-12T03:00:00Z")
+ );
+ expect(first.editionKey).toBe(second.editionKey);
+ });
+
+ test("revokes protected editions when the active policy changes", () => {
+ const plan = validateTimelinePlanAgainstDigest(candidatePlan, digest);
+ const edition = createTimelineEdition(plan, digest);
+ expect(
+ editionMatchesTimelinePrivacyPolicy(edition, "policy-a", "policy-a")
+ ).toBe(true);
+ expect(
+ editionMatchesTimelinePrivacyPolicy(edition, "policy-a", "policy-b")
+ ).toBe(false);
+ expect(editionMatchesTimelinePrivacyPolicy(edition, "policy-a", null)).toBe(
+ false
+ );
+ });
+
+ test("builds a dense year-long safe fallback and filters unknown repos", () => {
+ const edition = createFallbackTimelineEdition(
+ availableActivity,
+ new Set(),
+ new Date("2026-08-12T12:00:00Z")
+ );
+ expect(edition).not.toBeNull();
+ expect(edition.entries.length).toBeGreaterThanOrEqual(6);
+ expect(edition.entries.some((entry) => entry.href !== undefined)).toBe(
+ false
+ );
+ expect(
+ edition.entries.filter(
+ (entry) => entry.importance === "brief" || entry.importance === "pulse"
+ ).length
+ ).toBeGreaterThanOrEqual(2);
+ expect(
+ (Date.parse(edition.windowEnd) - Date.parse(edition.windowStart)) /
+ 86_400_000
+ ).toBe(399);
+ });
+
+ test("promotes a long anonymous weekly run above monthly fallback texture", () => {
+ const edition = createFallbackTimelineEdition(
+ {
+ ...availableActivity,
+ weeks: Array.from({ length: 6 }, (_, index) => ({
+ contributionCount: 4,
+ level: 2,
+ weekStart: new Date(Date.UTC(2026, 4, 3 + index * 7))
+ .toISOString()
+ .slice(0, 10),
+ })),
+ },
+ new Set(),
+ new Date("2026-08-12T12:00:00Z")
+ );
+ const streak = edition?.entries.find((entry) => entry.cadence === "streak");
+
+ expect(streak).toMatchObject({
+ bucket: "Across the work",
+ importance: "story",
+ title: "A sustained account-wide cadence",
+ visibility: "anonymous",
+ });
+ expect(streak?.description).not.toContain("private");
+ });
+});
+
+describe("signed webhook revocation projection", () => {
+ test("immediately maps a public-to-private transition to its stable key", () => {
+ const body = JSON.stringify({
+ action: "privatized",
+ repository: {
+ name: "must-not-survive",
+ node_id: "R_stable_node_identifier",
+ private: true,
+ },
+ });
+ const revocation = timelineRevocationFromWebhook(
+ body,
+ "repository",
+ privacyKey
+ );
+ expect(revocation).toEqual({
+ repoKeys: [
+ publicTimelineRepoKey("R_stable_node_identifier"),
+ privateTimelineRepoKey("R_stable_node_identifier", privacyKey),
+ ],
+ withdrawAllPrivateActivity: false,
+ });
+ expect(JSON.stringify(revocation)).not.toContain("must-not-survive");
+ expect(
+ timelineRevocationFromWebhook(
+ JSON.stringify({
+ repository: {
+ node_id: "R_stable_node_identifier",
+ private: false,
+ },
+ }),
+ "repository",
+ privacyKey
+ )
+ ).toEqual({ repoKeys: [], withdrawAllPrivateActivity: false });
+ });
+
+ test("withdraws protected activity when an installation is suspended", () => {
+ expect(
+ timelineRevocationFromWebhook(
+ JSON.stringify({ action: "suspend" }),
+ "installation",
+ privacyKey
+ )
+ ).toEqual({ repoKeys: [], withdrawAllPrivateActivity: true });
+ });
+});
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..9c36ddd
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "crons": [
+ {
+ "path": "/api/cron/timeline-sync",
+ "schedule": "37 1 * * *"
+ }
+ ]
+}