From 6f6b1f2238a933cc1b11c7f906a879977dbc88be Mon Sep 17 00:00:00 2001 From: Ashley Childress Date: Sun, 12 Jul 2026 09:37:56 -0400 Subject: [PATCH 01/35] fix(snowflake): track CHRONICLE_CARD's real arity in PIPELINE_VERSION MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET_DDL resolves an exact overload. The hashed signature listed 19 VARCHARs against a 20-parameter function, so every READ_REPO write — which selects PIPELINE_VERSION on both the Cortex and the 'none' path — would fail to compile on a warehouse that has no leftover 19-arg overload lying around. HISTORY_SCOPE was added to the function without updating the hash. Generated-by: Claude Opus 4.8 Signed-off-by: Ashley Childress --- snowflake/schema.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/snowflake/schema.sql b/snowflake/schema.sql index bbf1669..7f3c86d 100644 --- a/snowflake/schema.sql +++ b/snowflake/schema.sql @@ -82,9 +82,13 @@ ALTER TABLE CARDS ADD COLUMN IF NOT EXISTS PIPELINE_VERSION STRING; -- A card is only as current as the prompt and the evidence that produced it. Hashing the -- deployed DDL means the version moves on its own when either changes; a hand-maintained -- constant is a version someone forgets to bump. +-- +-- GET_DDL resolves an exact overload, so this arity must track CHRONICLE_CARD's parameter +-- list in ai_functions.sql. A stale count does not fail at CREATE VIEW — it fails inside +-- READ_REPO, which reads this view on every write. CREATE OR REPLACE VIEW PIPELINE_VERSION AS SELECT LEFT(MD5( - GET_DDL('FUNCTION', 'CHRONICLE_CARD(VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR)') + GET_DDL('FUNCTION', 'CHRONICLE_CARD(VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR,VARCHAR)') || GET_DDL('VIEW', 'CARD_EVIDENCE') ), 12) AS VERSION; From ec5af9a25118b52a6685b872bcba9c3c6d93431d Mon Sep 17 00:00:00 2001 From: Ashley Childress Date: Sun, 12 Jul 2026 09:41:56 -0400 Subject: [PATCH 02/35] fix(config): default PUBLIC_ORIGIN to the host that actually exists commitchronicles.dev is not a domain this project owns; the deployed site is commitchronicles.anchildress1.dev. The fallback only bites when PUBLIC_ORIGIN is unset, but when it does it mints README embeds pointing at someone else's domain. Generated-by: Claude Opus 4.8 Signed-off-by: Ashley Childress --- src/server/config.ts | 2 +- tests/server/config.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/config.ts b/src/server/config.ts index bf32b3a..8b3a9c4 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -91,7 +91,7 @@ export function loadConfig(): Config { return { port: int('PORT', 8080), bucket: required('CARD_BUCKET'), - publicOrigin: origin('PUBLIC_ORIGIN', 'https://commitchronicles.dev'), + publicOrigin: origin('PUBLIC_ORIGIN', 'https://commitchronicles.anchildress1.dev'), dailyGenerationCap: int('DAILY_GENERATION_CAP', 100), generatingTtlMs: int('GENERATING_TTL_SECONDS', 600) * 1000, tasks: loadTasks(), diff --git a/tests/server/config.test.ts b/tests/server/config.test.ts index b6450f4..df79d8e 100644 --- a/tests/server/config.test.ts +++ b/tests/server/config.test.ts @@ -102,7 +102,7 @@ describe('loadConfig', () => { it('defaults the public origin to the site', () => { delete process.env['PUBLIC_ORIGIN']; - expect(loadConfig().publicOrigin).toBe('https://commitchronicles.dev'); + expect(loadConfig().publicOrigin).toBe('https://commitchronicles.anchildress1.dev'); }); it('gives a bare host the scheme it needs to be a link', () => { From fadbb05955d2e9acb57ba181aeda4777bb13ae57 Mon Sep 17 00:00:00 2001 From: Ashley Childress Date: Sun, 12 Jul 2026 09:41:59 -0400 Subject: [PATCH 03/35] docs(readme): rewrite for challenge judges Restructure to the 15-section layout, lead with the Snowflake case, and embed the three live demo cards so the product is visible before any prose is read. Replaces the ASCII architecture sketch with Mermaid (flowchart + sequence) and corrects it against the shipped code: the detector's real thresholds, the nine-key Cortex schema, evidence budgeted in lines rather than commits, the 500-commit ingest cap, and the cost guards that exist as opposed to the ones the old copy implied. Generated-by: Claude Opus 4.8 Signed-off-by: Ashley Childress --- README.md | 408 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 359 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 47a82d3..c4edfde 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,316 @@ # Commit Chronicles -A contribution graph tells you that work happened. It never tells you what happened. +A contribution graph tells you that work happened. It never tells you _what_ happened. -Paste a public GitHub repo. Snowflake reads its commit history, finds the one story -actually hiding in it — the project that went dark for 107 days and came back at 3:32am, -the repo built entirely after midnight whose last commit landed at 3:53 and never got -another — and renders it as a card you can drop into a README. +Paste a public GitHub repo. **Snowflake fetches its own commit history, finds the one story hiding in it with plain SQL, and narrates that single thread with Cortex.** You get a card you can drop into a README. -Plain SQL finds the story. Cortex narrates it, and picks the color it deserves. +[![CI](https://github.com/anchildress1/commit-chronicles/actions/workflows/ci.yml/badge.svg)](https://github.com/anchildress1/commit-chronicles/actions/workflows/ci.yml) [![License: PolyForm Shield 1.0.0](https://img.shields.io/badge/license-PolyForm%20Shield%201.0.0-blue)](LICENSE) [![Conventional Commits](https://img.shields.io/badge/commits-conventional-fe5196)](https://www.conventionalcommits.org/) -```md -[![Commit Chronicles](https://storage.googleapis.com/commit-chronicles-cards/cards/owner/repo/card.svg)](https://commitchronicles.dev/owner/repo) +**Live:** [commitchronicles.anchildress1.dev](https://commitchronicles.anchildress1.dev) · **Prize target:** Best Use of Snowflake + +Three real repos, three storylines the detector found unaided — a **binge**, a **nocturne**, and a **relapse**. Every dot, timestamp, and quoted commit below is real: + +[![save-the-sun](https://storage.googleapis.com/commit-chronicles-cards/cards/anchildress1/save-the-sun/card.svg)](https://commitchronicles.anchildress1.dev/anchildress1/save-the-sun) + +[![carbon-trace](https://storage.googleapis.com/commit-chronicles-cards/cards/anchildress1/carbon-trace/card.svg)](https://commitchronicles.anchildress1.dev/anchildress1/carbon-trace) + +[![legacy-smelter](https://storage.googleapis.com/commit-chronicles-cards/cards/anchildress1/legacy-smelter/card.svg)](https://commitchronicles.anchildress1.dev/anchildress1/legacy-smelter) + +--- + +## Table of Contents + +- [About](#about) +- [Features](#features) +- [Tech Stack](#tech-stack) +- [Architecture](#architecture) +- [The Snowflake case](#the-snowflake-case) +- [Project Structure](#project-structure) +- [Getting Started](#getting-started) +- [Configuration](#configuration) +- [Security](#security) +- [How to Contribute](#how-to-contribute) +- [What's Next](#whats-next) +- [License](#license) +- [Acknowledgements](#acknowledgements) +- [Author](#author) + +--- + +## About + +Buried in a repo's commit history there is usually exactly one story worth telling. A project that went dark for 107 days and came back at 3:32am. A repo built entirely after midnight whose last commit landed at 3:53 and never got another. A week where every commit was a revert. + +Commit Chronicles finds that story with SQL, narrates it with Cortex, and renders it as a 1200×630 SVG sized for a README and a social preview. + +The scope is **one repository**, not a whole profile. A year-in-review across a profile turns to mush. A repo has a clean arc: commits start, cluster, pause, restart, or stop. + +Two rules hold the product together: + +- **Cortex interprets the shape. It never invents the facts.** Every timestamp, count, gap, and quoted message on the card is real and derived from ingested commits. Reading the arc is the product; asserting the author's motivation is not. +- **A repo with no real story says so.** Sparse histories get an honest grey template card, not manufactured drama. + +--- + +## Features + +| Feature | What it does | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| **Repo-first flow** | Enter a public `owner/repo`, submit once. Generation is keyed by the slug. | +| **Durable generation** | Work runs on a Cloud Tasks worker request, so closing the tab doesn't kill the job. Come back later to `/{owner}/{repo}` and re-attach. | +| **Six-storyline SQL detector** | `relapse`, `nocturne`, `binge`, `collapse`, `fight`, `resurrection` — scored deterministically, exactly one winner, plus a `none` fallback. | +| **One Cortex call** | The winning thread's evidence only, never the whole history. Schema-constrained output. | +| **Cortex picks the palette** | The accent hex is a reading of the arc. A repo that died and one that came back and shipped must not wear the same colour. | +| **Copyable README embed** | The card is a public bucket object. Hotlink it from anywhere. | +| **Cost guards by construction** | Daily cap, in-flight dedupe, cached failures, queue-level concurrency ceiling, ingest cap. | + +--- + +## Tech Stack + +| Layer | Choice | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| **Data + AI engine** | **Snowflake** — external access integration, plain-SQL detector views, `AI_COMPLETE` via a hand-written UDF (`claude-sonnet-4-5`) | +| **Deploy tooling** | `snow` CLI — every warehouse object is SQL in this repo | +| **Backend** | Node 24 (ESM), TypeScript strict, [Hono](https://hono.dev) | +| **Frontend** | React 19 + Vite (path-driven SPA, no router library) | +| **Compute** | Cloud Run (scale-to-zero, request-billed) | +| **Queue** | Cloud Tasks (OIDC-signed worker callbacks) | +| **Cache of record** | Public GCS bucket | +| **Tests** | Vitest + Testing Library, Playwright for E2E | + +--- + +## Architecture + +Snowflake does the work. Cloud Run guards the request, calls one stored procedure, turns the returned payload into an SVG, and writes it to the bucket. **It computes no analysis of its own.** + +```mermaid +flowchart TD + accTitle: Commit Chronicles architecture + accDescr: The SPA posts to Cloud Run, which enqueues a Cloud Tasks job. The worker calls one Snowflake procedure that ingests from GitHub, detects a storyline in SQL, and narrates it with Cortex. Cloud Run renders the SVG into a public GCS bucket, which serves the card directly. + + SPA["SPA
React, path-driven"] + API["Cloud Run
POST /api/generate
guard · claim · enqueue"] + Q["Cloud Tasks
max 2 concurrent"] + W["Cloud Run worker
POST /internal/generate
OIDC verified"] + + subgraph SF["Snowflake — CALL READ_REPO(owner, repo)"] + direction TB + ING["ingest
external access → api.github.com"] + DET["detector
plain SQL → score → pick ONE"] + CTX["Cortex
narrate the winner + pick the accent"] + ING --> DET --> CTX + end + + GH[("api.github.com")] + REN["Cloud Run
renders the SVG"] + GCS[("Public GCS bucket
the cache of record")] + README["README embed
· social preview"] + + SPA -->|"repo slug"| API + API --> Q + Q -->|"survives the tab closing"| W + W --> SF + ING <-.->|"the warehouse gets its own data"| GH + SF -->|"card payload (JSON)"| REN + REN -->|"only writer"| GCS + GCS --> README + GCS -->|"GET /api/state polls readiness"| SPA ``` -## How it works 🔧 +**The card's existence in the bucket _is_ the ready state.** There is no Firestore, no status column, no separate database. `readState` checks for `card.json`; if it's there, the repo is ready. + +### The generation path + +```mermaid +sequenceDiagram + accTitle: Generation sequence + accDescr: The browser reads state first and only posts to generate on a miss. Cloud Run claims a quota slot and a generating marker, then enqueues a Cloud Tasks job. The worker calls Snowflake, renders the SVG, and writes it to the bucket. The browser polls state until ready. + + actor U as Browser + participant R as Cloud Run + participant B as GCS bucket + participant Q as Cloud Tasks + participant S as Snowflake + + U->>R: GET /api/state/{owner}/{repo} + R->>B: does card.json exist? + B-->>R: no + R-->>U: unknown + + U->>R: POST /api/generate + R->>B: claim daily quota (cap) + R->>B: claim generating (create-only) + Note over R,B: two racing requests cannot
both start a Cortex call + R->>Q: enqueue (OIDC) + R-->>U: 202 generating + + Q->>R: POST /internal/generate + R->>S: CALL READ_REPO(owner, repo) + S->>S: ingest if cold → detect → Cortex + S-->>R: card payload + + R->>R: render SVG + R->>B: write card.svg → card.json → clear state + Note over R,B: card.json last:
a crash leaves it retryable,
never ready-with-no-card + + loop every 2.5s + U->>R: GET /api/state + end + R-->>U: ready + cardUrl +``` + +**The queue is a cost decision, not plumbing.** Detaching work from the request that started it would need Cloud Run's `--no-cpu-throttling`, which bills instance time instead of request time — you pay for the container to sit there doing nothing. Cloud Tasks calls back _into_ the service, so the pipeline runs inside a request: CPU is billed only while it works, the service still scales to zero, and closing the tab has no effect on a job that is no longer attached to the tab's connection. + +--- + +## The Snowflake case + +This is the part that matters for the prize. **The warehouse is the editor, not a bucket the LLM reads from.** + +### 1. Snowflake reaches out and gets its own data + +An `EXTERNAL ACCESS INTEGRATION` lets a Python stored procedure call `api.github.com` from _inside_ the warehouse. There is no ingestion service, no ETL job, no Cloud Function shovelling JSON. + +| Object | Type | Job | +| ---------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------- | +| `GITHUB_API_RULE` | `NETWORK RULE` (EGRESS) | Let the warehouse out to `api.github.com` | +| `GITHUB_TOKEN` | `SECRET` | The GitHub token, created out-of-band ([setup docs](docs/snowflake-setup.md)) | +| `GITHUB_API_ACCESS` | `EXTERNAL ACCESS INTEGRATION` | Binds the rule to the secret | +| `INGEST_REPO_COMMITS(owner, repo)` | `PROCEDURE` (Python) | Paginates the REST Commits API into `COMMITS`, classifies bot/AI-assisted rows in SQL | + +Ingest caps at **500 commits** by default (hard cap 2000). A longer history sets `windowed`, and the card says so out loud — `last 500 commits · quiet since Feb 25` — because reporting a slice as the repo's whole life is simply false. + +### 2. Plain SQL finds the story — no LLM, no cost + +`detector.sql` is 15 views and zero AI calls. It scores every candidate storyline and keeps exactly one. + +```mermaid +flowchart LR + accTitle: The SQL detector + accDescr: COMMITS_CLEAN feeds six storyline views, each gated on a minimum commit count. Their scores union into STORYLINE_SCORES, and REPO_STORYLINE picks the single highest score, breaking ties by drama rank. CARD_EVIDENCE then selects only the winning thread's commit lines for Cortex. + + C["COMMITS_CLEAN
merges + bots dropped"] + + subgraph SC["STORY_* views — scored, gated, free"] + direction TB + R["relapse
gap ≥ 30d"] + N["nocturne
≥ 50% commits 22:00–04:59"] + B["binge
streak ≥ 7d"] + X["collapse
silent ≥ 90d after a spike"] + F["fight
≥ 4 reverts in 7d"] + S["resurrection
relapse + a release"] + end + + W["REPO_STORYLINE
ORDER BY score DESC, drama_rank"] + E["CARD_EVIDENCE
the winner's lines only"] + AI["CHRONICLE_CARD
the one Cortex call"] + NONE["'none'
grey template card
Cortex never runs
"] + + C --> SC --> W + W -->|"a winner"| E --> AI + W -->|"no storyline clears its floor"| NONE +``` + +Every storyline gates on `MIN_COMMITS = 15` so bot noise can't win, and scoring is deterministic — the same repo always yields the same story. Thresholds live in one `DETECTOR_CONFIG` view: + +```sql +CREATE OR REPLACE VIEW DETECTOR_CONFIG AS SELECT + 15 AS MIN_COMMITS, 30 AS RELAPSE_MIN_GAP_DAYS, + 90 AS ABANDONED_AFTER_DAYS, 7 AS BINGE_MIN_STREAK_DAYS, + 4 AS FIGHT_MIN_COMMITS, 22 AS NIGHT_START_HOUR, + 5 AS NIGHT_END_HOUR, 25 AS EVIDENCE_SHARE_PCT, + 20 AS EVIDENCE_MIN_LINES, 140 AS EVIDENCE_MAX_LINES; +``` + +The winner is picked with one window function: + +```sql +QUALIFY ROW_NUMBER() OVER ( + PARTITION BY f.REPO_OWNER, f.REPO_NAME + ORDER BY s.SCORE DESC NULLS LAST, s.DRAMA_RANK +) = 1 +``` + +**Surveying a whole history produces a report. Picking one story produces an argument.** + +### 3. Cortex narrates _only_ the winning thread + +`CHRONICLE_CARD` is a hand-written SQL UDF wrapping `AI_COMPLETE` — one schema-constrained call that returns the whole card's writing. + +It is fed `CARD_EVIDENCE`: the winning thread's commit lines, budgeted at **25% of the history, floored at 20 lines and capped at 140**. Never the whole repo — that's how you buy an expensive, unfocused paragraph. Squash-merge bodies are exploded into individual lines first, so work hidden inside a merge is still visible. + +The response schema constrains exactly nine keys: + +```json +{ + "kicker": "the death of a side project", + "headline_upright": "Born in daylight. Last touched at", + "headline_accent": "3:53 in the morning", + "headline_trail": ".", + "label_first": "it begins", + "label_pivot": "", + "label_last": "", + "accent": "#e8a04a", + "accent_reason": "amber, for a repo that ran hot and went out" +} +``` + +That is the entire surface area of the writing. **Everything else on the card is composed by the renderer from facts** — the commit count, the status verb, the anchor timestamps, the void-panel gap, the caption. Cortex is never taught to produce a number. + +The output is then _verified in SQL_ before it is stored. A bad accent hex, a digit smuggled into a poetic label, or a kicker that just echoes the storyline name gets rejected with `cortex_rejected` — the card is not written. + +### 4. Cheap by construction + +Detection costs nothing. The LLM sees ~20–140 lines, not twenty thousand. `CHRONICLES_WH` is an XSMALL that auto-suspends after 60 seconds with a 300-second statement timeout. Every Cortex query ID is stored on the card row for cost audit. + +> **Not built with Cortex AI Function Studio — deliberately.** The Studio registers functions through `SNOWFLAKE.CORTEX.CREATE_AI_FUNCTION`, which Snowflake documents as internal and subject to change without notice; its supported entry points are a Snowsight wizard and the Cortex Code CLI, neither of which leaves the function in this repo. It emits an ordinary UDF around `AI_COMPLETE` anyway — so we wrote that ourselves, and you can read it. + +--- + +## Project Structure ```text -SPA → POST /api/generate → Cloud Tasks → /internal/generate - │ - ▼ - Snowflake: CALL READ_REPO(owner, repo) - ├─ ingest external access → api.github.com - ├─ detector plain SQL → score storylines → pick ONE - └─ cortex narrate that one thread + pick the accent - │ - card payload (JSON) - ▼ - Cloud Run renders the SVG → public GCS bucket +snowflake/ # the app. Every object is SQL, deployed with `snow`. + schema.sql # warehouse, tables, COMMITS_CLEAN, PIPELINE_VERSION + ingest_pipeline.sql # network rule, external access integration, ingest proc + detector.sql # 15 views: facts, gaps, the six storylines, the winner, the evidence + ai_functions.sql # CHRONICLE_CARD — the AI_COMPLETE wrapper + read_repo.sql # READ_REPO — the single entry point Cloud Run calls +src/ + server/ + app.ts # routes + generate.ts # claim → enqueue → run → render → write + bucket.ts # GCS: card.svg, card.json, state.json, quota counters + queue.ts # Cloud Tasks + OIDC verification (inline fallback for laptops) + snowflake.ts # the driver wrapper + rerender.ts # CLI: redraw stored cards, no Cortex spend + card/ # SVG renderer — layout, text fitting, formatting + client/ # React SPA: Landing, Loading, Result, Failed + shared/ # slug parsing, error taxonomy (shared by both sides) +docs/ # the spec, the build plan, Snowflake account bootstrap +scripts/ # gcp-bootstrap.sh — one-off GCP resources ``` -- **Snowflake is the app.** It fetches its own data through an external access integration, - finds the story with window functions, and shows Cortex only the winning thread — a - quarter of the material, capped, never the whole history. Squash-merge bodies are split - into line items first, so the work inside a merge is visible. -- **Cloud Run computes nothing.** It guards the request, calls one stored procedure, turns - the returned payload into an SVG, and writes it to the bucket. -- **The bucket is the cache of record.** A card's existence in it _is_ the ready state. - A cached page never re-runs Cortex. -- **Every colour on the card is Cortex's.** A repo that died and one that came back and - shipped must not wear the same accent. +--- -Generation runs through a queue so it survives you closing the tab, and so the service can -still scale to zero — see the architecture section of -[`docs/initial-design-spec.md`](docs/initial-design-spec.md) for why that is a cost -decision rather than a plumbing one. +## Getting Started -## Development +Requires **Node ≥ 24** and, for the warehouse half, the [`snow` CLI](https://docs.snowflake.com/en/developer-guide/snowflake-cli/index). ```bash make install # deps + git hooks +cp .env.example .env make dev # API on :8080, SPA on :5273 make ai-checks # format, lint, typecheck, test, build — the full gate ``` -Without a Cloud Tasks queue configured, generation runs in-process, so a laptop needs no -queue to work. +Without a Cloud Tasks queue configured, generation runs in-process. **A laptop needs no queue.** -Copy `.env.example` to `.env` and fill it in — see -[`docs/snowflake-setup.md`](docs/snowflake-setup.md) for how to mint the Snowflake PAT and -the GitHub token. +See [`docs/snowflake-setup.md`](docs/snowflake-setup.md) for minting the Snowflake PAT (it's role-locked at creation — that trips everyone once) and the GitHub token. -## Deploying +### Deploying ```bash make snowflake-deploy # every warehouse object, in dependency order @@ -68,16 +318,76 @@ make gcp-bootstrap # bucket, image repo, service accounts, secret, queue (o make deploy # build the image, deploy to Cloud Run, prune to 3 revisions ``` -`SNOWFLAKE_PAT` lives in Secret Manager and is mounted at run time. `.env` is for local -development only; nothing in it is baked into an image. +`SNOWFLAKE_PAT` lives in Secret Manager and is mounted at run time. `.env` is local-only; nothing in it is baked into an image. + +--- + +## Configuration + +| Variable | Required | Default | Purpose | +| ------------------------ | -------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CARD_BUCKET` | **yes** | — | The public GCS bucket. Cards, state, quota counters. | +| `SNOWFLAKE_ACCOUNT` | **yes** | — | Account identifier | +| `SNOWFLAKE_USER` | **yes** | — | | +| `SNOWFLAKE_PAT` | **yes** | — | Programmatic access token; role-locked at creation | +| `SNOWFLAKE_WAREHOUSE` | no | `CHRONICLES_WH` | | +| `SNOWFLAKE_DATABASE` | no | `CHRONICLES` | | +| `SNOWFLAKE_SCHEMA` | no | `RAW` | | +| `SNOWFLAKE_ROLE` | no | `ACCOUNTADMIN` | | +| `PORT` | no | `8080` | | +| `PUBLIC_ORIGIN` | no | `https://commitchronicles.anchildress1.dev` | Used to build the page URL in the README embed | +| `DAILY_GENERATION_CAP` | no | `100` | Hard ceiling on live generations per day | +| `GENERATING_TTL_SECONDS` | no | `600` | After this, an in-flight job is presumed dead and re-admitted | +| `TASKS_QUEUE` | no | — | **All-or-nothing.** Set it and `GOOGLE_CLOUD_PROJECT`, `TASKS_LOCATION`, `WORKER_URL`, `TASKS_INVOKER_SA` all become required. Unset ⇒ in-process generation. | +| `GITHUB_TOKEN` | no | — | Only used to mint the Snowflake `SECRET`. **Cloud Run never calls GitHub.** | + +--- + +## Security + +- **Cloud Run is the only writer to the bucket.** Client writes are forbidden. The bucket is public-read because hotlinked cards _are_ the product. +- **`/internal/generate` spends Cortex credits, so it verifies OIDC** — the task's token is checked against the invoker service account's email before anything is billed. No token, no work. +- **Path handling denies by default.** `parseSlug` rejects `..`, enforces GitHub's own owner/repo grammar, and runs on both the client and the server. An unparseable slug never reaches the warehouse. +- **Cortex's accent hex is re-verified before it reaches the SVG** (`safeAccent`), even though the response schema already constrains it. The card is public; the hex is untrusted output. +- **Secrets never land in the repo.** `SNOWFLAKE_PAT` is mounted from Secret Manager; the GitHub token exists only as a Snowflake `SECRET` object. `.env` is local-only and git-ignored. +- **Abuse controls:** daily generation cap counted in the bucket (so it holds across instances), create-only claim so two racing requests can't both bill a Cortex call, cached failures for terminal errors, and a queue `max-concurrent-dispatches=2` ceiling on the warehouse spend rate. + +--- -## Docs +## How to Contribute -- [`docs/initial-design-spec.md`](docs/initial-design-spec.md) — the product spec, the card - contract, and the split between what Cortex writes and what the renderer composes. -- [`docs/build-plan.md`](docs/build-plan.md) — delivery order. -- [`docs/snowflake-setup.md`](docs/snowflake-setup.md) — account bootstrap. +- Branch and PR always. Nothing lands directly on `main`. +- [Conventional Commits](https://www.conventionalcommits.org/), GPG-signed, one logical change per commit. +- AI-authored commits carry a `Generated-by:` footer naming the model that wrote the diff, plus a human `Signed-off-by:`. Commitlint enforces both. +- `make ai-checks` is the gate. Warnings are errors. +- New components and utilities ship with positive, negative, and edge-case tests. + +--- + +## What's Next + +- **Embedded font subset.** GitHub proxies README images through camo, so webfonts don't load in the card. The fix is a base64-embedded Didone subset; today the card falls back through a serif stack. +- **A gallery route.** Cut for the deadline — the landing page ships three example chips instead. +- **Auto-regeneration of stale cards.** `STALE_CARDS` already reports which cards were written by a pipeline version that no longer exists. It reports rather than acts, because acting costs a Cortex call each. + +--- ## License -[Polyform Shield 1.0.0](LICENSE) +[PolyForm Shield 1.0.0](LICENSE). + +Read it, learn from it, fork it, run it yourself. What you can't do is turn around and sell it as a competing product — the license permits everything _except_ using it to compete with me. If you're a developer poking at the SQL, you're fine. If you're a company planning to relabel this as your own commit-storytelling product, you're not. + +--- + +## Acknowledgements + +- Built for the **DEV Weekend Challenge: Passion Edition**, targeting _Best Use of Snowflake_. +- The commit histories on the demo cards are real, and mostly embarrassing. +- GitHub's REST Commits API — used because the Activity Events API stopped carrying commit summaries on 7 Oct 2025, which also gutted every GH-Archive mirror of them. + +--- + +## Author + +**Ashley Childress** — [@anchildress1](https://github.com/anchildress1) From a85901bbe40da924c4b067ad21a33a646453becf Mon Sep 17 00:00:00 2001 From: Ashley Childress Date: Sun, 12 Jul 2026 09:44:49 -0400 Subject: [PATCH 04/35] docs(spec): align the V1 spec with what shipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec had drifted from the warehouse in ways that matter to anyone reading it as the contract: - the nine Cortex keys were presented as the card payload; the payload is a 22-key camelCase object that wraps them - evidence is budgeted in commit *lines* (25%, floor 20, cap 140), not '~20 commits' — squash bodies explode into line items first - the detector's thresholds, tie-break order, and the 'none' path that never reaches Cortex at all were undocumented - Cortex's output is verified in SQL before it is stored, and two labels are overwritten structurally; the old copy implied the prompt was the only guard - status thresholds are literals in read_repo.sql, not DETECTOR_CONFIG entries - the gallery never shipped; three landing-page examples cover it - repo-size rejection does not exist — oversized histories are windowed Adds routes, bucket layout, and the job state machine. ASCII architecture is now Mermaid, alongside a state diagram for the generating/ready/failed cycle. Generated-by: Claude Opus 4.8 Signed-off-by: Ashley Childress --- docs/initial-design-spec.md | 359 ++++++++++++++++++++++++------------ 1 file changed, 243 insertions(+), 116 deletions(-) diff --git a/docs/initial-design-spec.md b/docs/initial-design-spec.md index d440319..a4e786d 100644 --- a/docs/initial-design-spec.md +++ b/docs/initial-design-spec.md @@ -2,6 +2,8 @@ _DEV Weekend Challenge: Passion Edition. Prize target: **Best use of Snowflake**. Due Mon Jul 13, 6:59 AM UTC._ +This document describes the shipped system. Where the original design was cut or changed, it says so rather than describing a product that does not exist. + ## Thesis A contribution graph tells you that work happened. It never tells you what happened. Buried in a repo's commit history is usually exactly one story worth telling — a project that went dark for 107 days and came back at 3:32am, a repo built entirely after midnight whose last commit landed at 3:53 and never got another, a week where every commit was a revert. @@ -19,20 +21,22 @@ Scope is **one repository**, not a whole profile. A profile year-in-review turns - A generated SVG card sized for README and social previews. - A copyable Markdown embed. - A cached public page at `/{owner}/{repo}` and a card served directly from the public GCS bucket. -- A gallery of pre-generated cards for judge-safe demo coverage. +- Three pre-generated example repos on the landing page, for judge-safe demo coverage. + +**Cut:** the gallery route. Three example chips on the landing page cover the same need — a judge who wants a card without waiting for one — at a fraction of the surface area. ## The card 1200×630 SVG, readable as a standalone artifact. - **Product mark** — `Commit Chronicles`. -- **Kicker** naming the genre — `the death of a side project`, `the one that came back`. +- **Kicker** naming the genre — `the death of a side project`, `the one that came back` — prefixed with the repo slug. - **Headline** — Didone serif, in three slots: upright, then an italic accent-colored fragment, then upright again. The italic run may start mid-sentence. - **The arc** — a beeswarm scatter: date across, hour of day down, rotated so night sits at the bottom of the frame. Daylight commits render hollow, night commits solid. Long quiet stretches render as a **void panel** you look straight through. The last commit is a single accent dot. - **Anchor labels** — a poetic tail on the first commit, the pivot, and the last commit. Only the anchors this storyline actually uses get one. -- **Status** — an observed label: `abandoned` (no commits in N days), `shipped 1.0.1`, `active`. +- **Status** — an observed label: `abandoned`, `dormant`, `active`. - **Counts** — commits, span. -- **Attribution** — `read by Snowflake Cortex`. +- **Attribution** — `Read by Snowflake Cortex`. If the card would work equally well as a bar chart, it has failed. @@ -48,123 +52,214 @@ If the card would work equally well as a bar chart, it has failed. ## User flow -1. User enters a repo. -2. Client normalizes the slug; Cloud Run checks the bucket for an existing card. -3. `ready` (the card object exists) → render the cached card immediately. -4. Missing or stale → client calls `POST /api/generate` **once**. -5. Cloud Run marks the job `generating`, then invokes the Snowflake pipeline. +1. User enters a repo. The client normalizes the slug and rejects a malformed one before any request. +2. The client reads `GET /api/state/{owner}/{repo}` **first**. +3. `ready` → render the cached card immediately. `POST /api/generate` is never called. +4. A settled `failed` state with a terminal error code also stops here — retrying it would only re-buy the same failure. +5. Otherwise the client calls `POST /api/generate` **once**. Cloud Run claims a daily-quota slot and a `generating` marker, then enqueues. 6. The user can wait or leave. -7. Snowflake returns the card payload; Cloud Run renders the SVG and writes it to the - bucket (or records `failed`). -8. Returning to `/{owner}/{repo}` finds the existing card and renders it. -9. User copies the card image or the README embed. +7. Cloud Tasks calls `/internal/generate`. Snowflake returns the card payload; Cloud Run renders the SVG and writes it to the bucket (or records `failed`). +8. The client polls state every 2.5s until it settles, and gives up displaying progress after five minutes — the job itself is unaffected. +9. Returning to `/{owner}/{repo}` finds the existing card and renders it. +10. User copies the card image URL or the README embed. The browser tab must not be required for generation to complete. If it is, the app is a loading spinner wearing a trench coat. +### Job state + +State is not a column in a database. **The card's existence in the bucket _is_ the ready state.** + +```mermaid +stateDiagram-v2 + accTitle: Job state + accDescr: A repo starts unknown. Claiming a quota slot and a generating marker moves it to generating. Success writes the card and makes it ready. Failure writes a failed marker, which is terminal for bad-repo errors and retryable for pipeline and Cortex errors. A generating marker older than its TTL is presumed dead and re-admitted. + + [*] --> unknown: no objects in the bucket + unknown --> generating: quota claimed +
create-only marker written + generating --> ready: card.svg then card.json written,
state.json cleared + generating --> failed: state.json records the error code + generating --> generating: marker older than
GENERATING_TTL_SECONDS
presumed dead, re-admitted + failed --> generating: retryable code only + failed --> [*]: terminal code —
cached so it cannot be
retried into a bill + ready --> [*]: served from the bucket forever +``` + +`readState` checks for `card.json` first, so a card always outranks a stale `state.json` beside it. The accent hex rides on the object's custom metadata, so polling never downloads the payload. + +**Terminal error codes** (cached, no retry offered): `repo_not_found`, `repo_private`, `repo_empty`, `invalid_repo_slug`, `no_commits`. +**Retryable:** `cortex_empty`, `cortex_rejected`, `pipeline_error`. + ## Product surface (ready state) -- Repo address, normalized as `commitchronicles.dev/{owner}/{repo}`. -- Large card preview. -- Primary action: **Copy card image**. +- Repo address, normalized as `commitchronicles.anchildress1.dev/{owner}/{repo}`. +- Large card preview, loaded straight from the bucket. +- Primary action: **Copy image URL**. - Secondary action: **Copy README embed**. ```md - [![Commit Chronicles](https://storage.googleapis.com/commit-chronicles-cards/cards/{owner}/{repo}/card.svg)](https://commitchronicles.dev/{owner}/{repo}) + [![Commit Chronicles](https://storage.googleapis.com/commit-chronicles-cards/cards/{owner}/{repo}/card.svg)](https://commitchronicles.anchildress1.dev/{owner}/{repo}) ``` - Link to read another repo. +The whole page takes its accent colour from the card's — Cortex picks the palette for the site, too. + ## Data source The GitHub REST **Commits API** — `/repos/{owner}/{repo}/commits`. Public repos only. **Do not use the Activity Events API.** GitHub stripped commit summaries and counts from `PushEvent` payloads on 7 Oct 2025, which also guts every GH-Archive mirror of it. Commit text now survives only in the main REST API. -Ingest: commit message, SHA, authored timestamp, author login when public. Cap commits per repo. Filter obvious bot noise before anything expensive runs. +Ingest: commit message, SHA, authored timestamp, author login when public. **Cap: 500 commits** per repo by default, hard-capped at 2000. Bot and AI-assisted commits are classified at ingest and filtered before anything expensive runs. + +A history longer than the cap sets `windowed`, and the card admits it — the header reads `last 500 commits · quiet since Feb 25` rather than presenting a slice as the repo's whole life. The Snowflake Marketplace archive is not used in V1. This product is about one submitted repo and its Cortex reading, not whole-population percentile math. ## Architecture -```text -Cloud Run (static SPA + /api/generate — no analysis logic) - │ POST /api/generate → guards, quota, mark `generating`, enqueue - ▼ -Cloud Tasks (commit-chronicles-gen) - │ POST /internal/generate (OIDC-signed) - ▼ -Cloud Run worker → CALL READ_REPO(owner, repo) - ▼ -Snowflake - ├─ ingest proc external access → api.github.com → COMMITS - ├─ detector plain SQL → score storylines → pick ONE - └─ cortex narrate that thread + choose the palette - │ - │ ◄── card payload (JSON) - ▼ -Cloud Run templates the SVG → writes gs://…/{owner}/{repo}/card.svg - ▼ -Public GCS bucket (serving cards and storing state; the card's existence is ready) +```mermaid +flowchart TD + accTitle: Commit Chronicles architecture + accDescr: Cloud Run guards the generate request, claims quota, and enqueues to Cloud Tasks. Cloud Tasks calls back into Cloud Run with an OIDC token. That worker calls one Snowflake procedure, which ingests from GitHub through an external access integration, detects a storyline in SQL, and narrates it with Cortex. Cloud Run renders the returned payload to SVG and writes it to a public GCS bucket, which serves the card directly to READMEs. + + SPA["SPA
no analysis logic"] + API["Cloud Run
POST /api/generate
guards · quota · claim · enqueue"] + Q["Cloud Tasks
commit-chronicles-gen
max-concurrent-dispatches 2
"] + W["Cloud Run worker
POST /internal/generate
OIDC-verified"] + + subgraph SF["Snowflake — CALL READ_REPO(owner, repo)"] + direction TB + ING["ingest
external access integration"] + DET["detector
plain SQL → score → pick ONE"] + CTX["Cortex
narrate that thread + choose the palette"] + ING --> DET --> CTX + end + + GH[("api.github.com")] + REN["Cloud Run
templates the SVG"] + GCS[("Public GCS bucket
cache of record; the card's
existence is the ready state
")] + + SPA --> API --> Q --> W --> SF + ING <-.-> GH + SF -->|"card payload (JSON)"| REN + REN -->|"only writer"| GCS + GCS -->|"GET /api/state"| SPA ``` -**Generation goes through a queue, and that is a cost decision.** The work has to outlive -the browser tab. Detaching it from the request that started it would need Cloud Run's -`--no-cpu-throttling`, which bills instance time instead of request time — you pay for the -container to sit there doing nothing. Cloud Tasks calls back into the service, so the -pipeline runs _inside_ a request: CPU is billed only while it is working, the service still -scales to zero, and closing the tab has no effect on a job that is no longer attached to -the tab's connection. The queue's `max-concurrent-dispatches` is the real ceiling on the -Cortex spend rate — at most two repos can be in the warehouse at once, whatever the front -page is doing. +**Generation goes through a queue, and that is a cost decision.** The work has to outlive the browser tab. Detaching it from the request that started it would need Cloud Run's `--no-cpu-throttling`, which bills instance time instead of request time — you pay for the container to sit there doing nothing. Cloud Tasks calls back into the service, so the pipeline runs _inside_ a request: CPU is billed only while it is working, the service still scales to zero, and closing the tab has no effect on a job that is no longer attached to the tab's connection. The queue's `max-concurrent-dispatches=2` is the real ceiling on the Cortex spend rate — at most two repos can be in the warehouse at once, whatever the front page is doing. -`/internal/generate` is on a public service, so it verifies the task's OIDC token against -the invoker service account before it spends anything. No token, no work. +`/internal/generate` is on a public service, so it verifies the task's OIDC token against the invoker service account before it spends anything. No token, no work. - **Snowflake** reaches GitHub itself via an external access integration, finds the story in SQL, and narrates it with Cortex. The ingest layer is a stored procedure, not a service. - **Cloud Run** owns the routes, calls one Snowflake proc, and turns the returned payload into an SVG. It fetches no commit data and computes no analysis. -- **The GCS bucket** is the cache of record and serves cards directly. Cloud Run serves the - SPA and `/api/state`; a cached render never calls Snowflake or GitHub. +- **The GCS bucket** is the cache of record and serves cards directly. Cloud Run serves the SPA and `/api/state`; a cached render never calls Snowflake or GitHub. **Rendering lives in Cloud Run, not Snowflake.** Templating an SVG string is a chore, not a demonstration of a data warehouse — and doing it in-warehouse would drag a `STORAGE INTEGRATION`, an external stage, and a Snowflake-minted service-account IAM grant onto the critical path to buy nothing. The Snowflake case rests on the ingest, the detector, and the Cortex call. Cloud Run writes to the bucket with ordinary GCP credentials. Snowflake cannot serve an anonymous HTTP request — SPCS "public" endpoints are RBAC-gated and hand a browser a login page. The public GCS bucket serves the card; Cloud Run serves the page and API. -## Snowflake objects +### Routes + +| Route | Guard | Job | +| ------------------------------- | ----- | -------------------------------------------------------------------- | +| `GET /healthz` | — | Liveness | +| `POST /api/generate` | — | Claim and enqueue. `202` accepted, `200` attach, `429` quota spent | +| `POST /internal/generate` | OIDC | The worker. Calls Snowflake, renders, writes. **Spends the credit.** | +| `POST /internal/rerender` | OIDC | Redraws one stored card with the current renderer. No Cortex spend. | +| `GET /api/state/{owner}/{repo}` | — | Job state. `no-store`. The only thing the client polls. | +| `GET /` · `GET /{owner}/{repo}` | — | The SPA shell | + +There is deliberately **no card route**. The bucket is public and serves the SVG itself. + +### Bucket layout + +| Object | Cache-Control | Job | +| --------------------------------- | ---------------------- | ---------------------------------------------------- | +| `cards/{owner}/{repo}/card.svg` | `public, max-age=3600` | The product | +| `cards/{owner}/{repo}/card.json` | `public, max-age=300` | The payload. **Its existence is the ready state.** | +| `cards/{owner}/{repo}/state.json` | `no-store` | `generating` or `failed`. Deleted on success. | +| `meta/quota/{YYYY-MM-DD}.json` | `no-store` | The daily counter, so the cap holds across instances | -| object | job | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `GITHUB_API_RULE` (EGRESS, `api.github.com`) | let the warehouse out | -| `GITHUB_TOKEN` (`SECRET`) | GitHub token | -| `GITHUB_API_ACCESS` (`EXTERNAL ACCESS INTEGRATION`) | binds rule + secret | -| `COMMITS` | owner, repo, sha, subject, body, authored_at, bot/AI flags | -| `COMMITS_CLEAN` (view) | drops merges and bots; derives date + hour parts | -| `PROC INGEST_REPO_COMMITS(owner, repo)` | Python + external access → Commits API → `COMMITS` | -| detector views | gaps, streaks, night share, storyline scores → `REPO_STORYLINE` | -| `CARD_EVIDENCE` (view) | the winning thread's ~20 commits — the only thing Cortex sees | -| `CHRONICLE_CARD` (UDF) | hand-written wrapper around `AI_COMPLETE`; one structured call → the whole card | -| `CARDS` | the generated card payloads, plus the Cortex query id for cost audit | -| `PROC READ_REPO(owner, repo)` | the one entry point: ingests on a cold repo, then detector → `CHRONICLE_CARD` → structured card payload | +Writes are ordered `card.svg` → `card.json` → clear `state.json`. A crash between the first two leaves the repo retryable rather than ready-with-no-card. + +## Snowflake objects Every object is SQL in the repo, deployed with the `snow` CLI. An object created by clicking in a UI does not exist. +| object | job | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `CHRONICLES_WH` | XSMALL, auto-suspend 60s, 300s statement timeout | +| `GITHUB_API_RULE` (EGRESS, `api.github.com`) | let the warehouse out | +| `GITHUB_TOKEN` (`SECRET`) | GitHub token. Created out-of-band — see `docs/snowflake-setup.md`, never in a tracked file | +| `GITHUB_API_ACCESS` (`EXTERNAL ACCESS INTEGRATION`) | binds rule + secret | +| `INGEST_REPO_COMMITS(owner, repo)` | Python proc + external access → Commits API → `COMMITS` | +| `INGEST_STAGE` | transient staging table; an owner's-rights proc cannot create a temp table | +| `COMMITS` | owner, repo, sha, subject, body, authored_at, bot/AI flags | +| `REPO_INGEST` | per-repo ingest metadata: `WINDOWED`, `COMMIT_CAP`, `INGESTED_AT` | +| `COMMITS_CLEAN` (view) | drops merges and bots; derives date + hour parts | +| `DETECTOR_CONFIG` (view) | every threshold the detector uses, in one place | +| detector views | facts, gaps, six `STORY_*` views, scores → `REPO_STORYLINE` | +| `COMMIT_LINES` (view) | explodes squash-merge bodies into line items; drops trailers and stub lines | +| `CARD_EVIDENCE` (view) | the winning thread's lines — the only thing Cortex sees | +| `CHRONICLE_CARD` (UDF) | hand-written wrapper around `AI_COMPLETE`; one structured call → the whole card | +| `CARD_PLOT` (view) | the scatter array. Never sent to a model | +| `CARDS` | the generated card payloads, plus the Cortex query id for cost audit | +| `PIPELINE_VERSION` (view) | MD5 of the deployed prompt + evidence DDL. Moves on its own when either changes | +| `STALE_CARDS` (view) | cards written by a pipeline version that no longer exists. **Reports; does not act** | +| `CARD_PAYLOAD` (view) | an existing card, in the shape `READ_REPO` returns. The zero-cost read path | +| `REFRESH_CARD_DATA()` (proc) | re-derives `FACTS` and `PLOT` without re-billing Cortex | +| `READ_REPO(owner, repo)` (proc) | the one entry point: ingest if cold → detector → `CHRONICLE_CARD` → verify → payload | + ## The detector (plain SQL, no LLM) This is the core of the product and the core of the Snowflake case. Score every candidate storyline deterministically; keep the highest. Surveying a repo's whole history produces a report. Picking one story produces an argument. -| storyline | signal | -| ---------------- | ---------------------------------------------------------------------- | -| **relapse** | `LAG` over commit dates — quiet ≥ N days, then resumed | -| **nocturne** | share of commits after 22:00 | -| **binge** | longest consecutive-day streak, or the heaviest single night | -| **collapse** | a spike, then permanent silence | -| **fight** | a cluster of reverts/hotfixes in a short window (regex — no AI needed) | -| **resurrection** | quiet, resumed, and shipped a release | +Every storyline gates on `MIN_COMMITS = 15`, so bot noise cannot win. + +| storyline | signal | gate | +| ---------------- | ------------------------------------------------------ | -------------------------------------------------------------- | +| **relapse** | `LAG` over commit dates — quiet, then resumed | gap ≥ 30 days | +| **nocturne** | share of commits in the night window (22:00–04:59 UTC) | ≥ 50% of commits | +| **binge** | longest consecutive-active-day streak | streak ≥ 7 days | +| **collapse** | a spike, then permanent silence | ≥ 90 days silent **and** ≥ 15% of commits in the final 30 days | +| **fight** | revert/hotfix cluster (regex on the subject — no AI) | ≥ 4 such commits in a 7-day window | +| **resurrection** | a relapse that also shipped a release after the pivot | inherits relapse's gate, `+15` score | +| **none** | nothing cleared its floor | the fallback | + +Thresholds live in one view, so tuning the detector is a one-line diff: + +```sql +CREATE OR REPLACE VIEW DETECTOR_CONFIG AS SELECT + 15 AS MIN_COMMITS, 30 AS RELAPSE_MIN_GAP_DAYS, + 90 AS ABANDONED_AFTER_DAYS, 7 AS BINGE_MIN_STREAK_DAYS, + 4 AS FIGHT_MIN_COMMITS, 22 AS NIGHT_START_HOUR, + 5 AS NIGHT_END_HOUR, 25 AS EVIDENCE_SHARE_PCT, + 20 AS EVIDENCE_MIN_LINES, 140 AS EVIDENCE_MAX_LINES; +``` -Apply floors — a storyline needs a minimum number of real commits so bot noise can't win. Scoring is deterministic: the same repo always yields the same story. +The winner is one window function — highest score, ties broken by how dramatic the storyline is (`resurrection` > `collapse` > `relapse` > `nocturne` > `fight` > `binge`): + +```sql +QUALIFY ROW_NUMBER() OVER ( + PARTITION BY f.REPO_OWNER, f.REPO_NAME + ORDER BY s.SCORE DESC NULLS LAST, s.DRAMA_RANK +) = 1 +``` + +Scoring is deterministic: the same repo always yields the same story. + +**`none` never reaches Cortex.** It writes a hard-coded grey template card (`#6b7280`, `MODEL = 'none'`) and costs nothing. A history with nothing to say gets told so. Cheap, explainable, and it means Cortex is only ever pointed at the part that matters. ## Cortex -One call, fed **only the winning storyline's evidence**: ~10–20 real commit messages plus the computed facts. Never the whole history — that's how you buy an expensive, unfocused paragraph. +One call, fed **only the winning storyline's evidence**. Never the whole history — that's how you buy an expensive, unfocused paragraph. + +**The evidence budget is measured in lines, not commits**: 25% of the winning thread's commit lines, floored at 20 and capped at 140, plus the first 5 and last 8 lines regardless. `COMMIT_LINES` explodes squash-merge bodies into separate items first, so work buried inside a merge is still visible; trailers and stub lines are dropped. + +Model: `claude-sonnet-4-5` via `AI_COMPLETE`, `temperature 0.4`, `max_tokens 2048`. + +The response schema constrains exactly nine keys: ```json { @@ -180,7 +275,7 @@ One call, fed **only the winning storyline's evidence**: ~10–20 real commit me } ``` -Nine keys. That is the whole surface area of the writing on the card. +Nine keys. That is the whole surface area of the writing on the card — **not** the card payload, which is a 22-key camelCase object wrapping these nine alongside the facts, the plot, the storyline, the query id, and the pipeline version. **The italic run is a fragment, not a clause.** The renderer sets `headline_upright` upright, `headline_accent` italic and in the accent colour, `headline_trail` upright again. The italic can start mid-sentence — that's the design. Punctuation may live in `accent` or `trail`; put it where the typography looks right. @@ -198,37 +293,58 @@ The palette is **muted neons** — bright but not fluorescent, saturated but not Cortex may drift slightly off these anchors — the constraint is the family, not the exact swatch. No greys, no browns, no fluorescents, no deep saturated primaries. `none`-storyline cards use grey (`#6b7280`) deliberately; that colour is reserved for "no story to tell". -**Not every storyline uses every anchor.** The detector picks the storyline first and only the anchors that storyline actually uses get labelled. `label_pivot` is empty for a collapse (no separate pivot from the last commit). `label_last` is empty unless the repo is still active — for abandoned or dormant repos the renderer prints `"last commit ·