Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6f6b1f2
fix(snowflake): track CHRONICLE_CARD's real arity in PIPELINE_VERSION
anchildress1 Jul 12, 2026
ec5af9a
fix(config): default PUBLIC_ORIGIN to the host that actually exists
anchildress1 Jul 12, 2026
fadbb05
docs(readme): rewrite for challenge judges
anchildress1 Jul 12, 2026
a85901b
docs(spec): align the V1 spec with what shipped
anchildress1 Jul 12, 2026
3b84994
docs(plan): reconcile the build plan with what was actually delivered
anchildress1 Jul 12, 2026
115521b
docs(agents): stop claiming oversized repos are rejected
anchildress1 Jul 12, 2026
6cf4b49
docs: record why the Cortex call runs at temperature 0.4
anchildress1 Jul 12, 2026
4840d48
fix(deploy): read config from .env in the script, not the caller
anchildress1 Jul 12, 2026
bc1c86f
fix(deploy): load named config keys, never source .env wholesale
anchildress1 Jul 12, 2026
6534e9f
fix(docs): strip HTML from Mermaid labels so GitHub renders them
anchildress1 Jul 12, 2026
f2fc44c
docs(spec): draw the user flow instead of numbering it
anchildress1 Jul 12, 2026
c5fd239
fix(deploy): survive absent .env keys, and stop halving the quota cap
anchildress1 Jul 12, 2026
72db46a
docs: cut the prose that was not doing any work
anchildress1 Jul 12, 2026
72eca1b
docs(readme): add the social preview banner under the badges
anchildress1 Jul 12, 2026
d5b69ed
docs(readme): add stack, AI, and provenance badges plus the judging s…
anchildress1 Jul 12, 2026
4d19162
feat(landing): swap the third example to forem/forem
anchildress1 Jul 12, 2026
08af43f
docs(readme): show the whole warehouse pipeline in the architecture d…
anchildress1 Jul 12, 2026
ffb42ba
docs(readme): one branch in the architecture diagram
anchildress1 Jul 12, 2026
cb9dbf7
feat(client): sticky footer with icon socials, and an embed-safe layout
anchildress1 Jul 12, 2026
6203fc8
fix(detector): make the evidence array order deterministic
anchildress1 Jul 12, 2026
4deab0d
docs(license): supply the Required Notice the licence asks for
anchildress1 Jul 12, 2026
aa66a30
fix(client): pin the footer to the viewport, not to the bottom of 28v…
anchildress1 Jul 12, 2026
b466f35
test: exclude the App shell from coverage
anchildress1 Jul 12, 2026
b637108
fix(client): use the real brand marks, not paths I made up
anchildress1 Jul 12, 2026
90aed4a
refactor(client): drop the licence link from the footer
anchildress1 Jul 12, 2026
3ce244b
fix(client): stop reserving space for a footer that was already in flow
anchildress1 Jul 12, 2026
805ff24
fix(client): copy still works when the Clipboard API is not there
anchildress1 Jul 12, 2026
8137a96
feat(card): ship the card as a PNG, because dev.to will not serve an SVG
anchildress1 Jul 12, 2026
8b5318f
Revert "fix(client): copy still works when the Clipboard API is not t…
anchildress1 Jul 12, 2026
26ea146
fix(deps): record every resvg platform binary in the lock file
anchildress1 Jul 12, 2026
9e6b984
fix(cortex): keep statistics out of the headline
anchildress1 Jul 12, 2026
e8cfae3
feat(card): print the colour reason on the card
anchildress1 Jul 12, 2026
fe87bca
fix(client): make the field a label instead of a clickable div
anchildress1 Jul 12, 2026
c2ff4f7
fix(card): size the card from the space it actually has, and the reas…
anchildress1 Jul 12, 2026
baf15bc
fix(detector): one card, one story
anchildress1 Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ for the full product spec and `docs/build-plan.md` for the delivery order.
repo. It emits an ordinary UDF around `AI_COMPLETE` anyway, so we write that ourselves.

- **Backend**: Cloud Run — `/api/generate`, plus serving `/{owner}/{repo}` and
`/{owner}/{repo}/card.svg`. It renders the SVG from Snowflake's card payload and
`/{owner}/{repo}/card.png`. It renders the SVG from Snowflake's card payload and
writes it to the bucket. It computes no analysis of its own.
- **Cache of record**: a public GCS bucket. The card's existence in the bucket _is_
the ready state.
Expand Down Expand Up @@ -105,8 +105,9 @@ rescaffold; if you find a reference to either, it is stale — fix it.
- A repo with no real story says so. Sparse histories get an honest template card, not
manufactured drama.
- Cost guards are mandatory: cap commits per repo, cap daily live generations, cache
failed states, reject private/missing/oversized repos, and keep gallery cards
pre-generated.
failed states, reject private/missing repos, and keep the landing-page example cards
pre-generated. An oversized history is windowed to its newest slice, not rejected —
and the card says so rather than passing a slice off as the whole life.

## Documentation

Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
# The image ships no fonts, and the card names its families by hand. Without these the PNG
# rasterizes with the type missing — and the type is the card.
COPY assets ./assets

USER node
EXPOSE 8080
Expand Down
6 changes: 6 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

Copyright (c) 2026 Ashley Childress

Required Notice: Copyright Ashley Childress (https://anchildress1.dev)

Licensor: Ashley Childress <https://github.com/anchildress1>

Software: Commit Chronicles

## Acceptance

In order to get any license under these terms, you must agree
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ gcp-bootstrap: ## Create the GCP resources deploy.sh expects (idempotent, one-of
./scripts/gcp-bootstrap.sh

deploy: ## Build the image and deploy to Cloud Run
set -a; [ -f .env ] && . ./.env; set +a; ./deploy.sh
./deploy.sh

clean: ## Remove build output
rm -rf dist coverage playwright-report test-results .vite
423 changes: 374 additions & 49 deletions README.md

Large diffs are not rendered by default.

Binary file added assets/fonts/BodoniModa-0.ttf
Binary file not shown.
Binary file added assets/fonts/BodoniModa-1.ttf
Binary file not shown.
Binary file added assets/fonts/BodoniModa-2.ttf
Binary file not shown.
Binary file added assets/fonts/BodoniModa-3.ttf
Binary file not shown.
Binary file added assets/fonts/BodoniModa-4.ttf
Binary file not shown.
Binary file added assets/fonts/HankenGrotesk-0.ttf
Binary file not shown.
Binary file added assets/fonts/HankenGrotesk-1.ttf
Binary file not shown.
Binary file added assets/fonts/HankenGrotesk-2.ttf
Binary file not shown.
Binary file added assets/fonts/SpaceMono-0.ttf
Binary file not shown.
Binary file added assets/fonts/SpaceMono-1.ttf
Binary file not shown.
22 changes: 20 additions & 2 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@
# The project resources this expects (bucket, Artifact Registry repo, service accounts,
# Secret Manager entry, Cloud Tasks queue) are created once by `make gcp-bootstrap`.
#
# Secrets never appear here. SNOWFLAKE_PAT is mounted from Secret Manager at run time;
# .env is for local development only and is not read by this script.
# Config comes from .env; SNOWFLAKE_PAT comes from Secret Manager and is mounted at run
# time. No secret value is ever passed on a gcloud command line, where it would land in
# shell history and the audit log.

set -euo pipefail

# Read here rather than in the caller, so the script behaves the same run bare or run by
# make. Named keys only — sourcing .env would pull SNOWFLAKE_PAT and GITHUB_TOKEN into the
# environment of a process that shells out to gcloud, which is exactly what Secret Manager
# exists to prevent. An already-exported value wins, so CI can override with no file present.
#
# Every key here is optional at *this* layer: a missing one falls through to the defaults
# below, or to the ${VAR:?} guards that make the genuinely-required ones fatal. Hence the
# `|| true` — under `set -e`, a grep that matches nothing would otherwise kill the deploy.
CONFIG_KEYS=(SNOWFLAKE_ACCOUNT SNOWFLAKE_USER CARD_BUCKET PUBLIC_ORIGIN DAILY_GENERATION_CAP)
if [[ -f .env ]]; then
for key in "${CONFIG_KEYS[@]}"; do
[[ -n ${!key:-} ]] && continue
value="$(grep -E "^${key}=" .env | tail -1 | cut -d= -f2- || true)"
[[ -n ${value} ]] && export "${key}=${value}"
done
fi

PROJECT="${PROJECT:-anchildress1}"
REGION="${REGION:-us-east1}"
SERVICE="${SERVICE:-commit-chronicles}"
Expand Down
153 changes: 33 additions & 120 deletions docs/build-plan.md
Original file line number Diff line number Diff line change
@@ -1,136 +1,49 @@
# Commit Chronicles — Build Plan

## TL;DR
Delivery order and what became of it. The product contract — architecture, the card, the detector, the Cortex call, the renderer/Cortex ownership split — lives in [`initial-design-spec.md`](initial-design-spec.md) and is not repeated here.

- **What:** a shareable OG card that tells the one real story hiding in a developer's commit history. Drops into a README, embeds in a post.
- **Stack:** Snowflake does the work. A public **GCS bucket** holds the card. **Cloud Run** is a thin display shell — it serves the embeddable page and pushes the button.
- **Built with:** `snow` CLI (SQL objects scripted in-repo) + Snowflake MCP (so Claude writes and runs the DDL).
- **Prize:** Best Use of Snowflake — the app _is_ Snowflake.
- **Due:** Mon Jul 13, 06:59 UTC.

## Architecture

```
Cloud Run (static SPA + API, no analysis)
│ POST /api/generate → guard, claim, enqueue
Cloud Tasks
│ OIDC POST /internal/generate
Cloud Run worker ────────────────────────────────────────┐
Snowflake
│ ├─ ingest proc external access → api.github.com → COMMITS
│ ├─ detector plain SQL: gaps, streaks, hours → pick ONE thread
│ └─ cortex narrates it + picks the palette (hex + mood)
│ │
│ ◄── card payload (JSON) ────────────────────────────┘
Cloud Run renders the SVG → writes it to the bucket
public GCS bucket ──► README <img> · dev.to embed
```

Snowflake gets its own data, finds the story, and reads it. Cloud Run serves the page and
API, queues durable work, calls one proc in the worker request, and turns the payload into
an SVG. **The card in the bucket is the state** — if the file exists, it is ready. The
browser polls `/api/state/{owner}/{repo}`; it never calls the bucket for state.

**Rendering is Cloud Run's job, not Snowflake's.** Templating an SVG proves nothing about a warehouse, and doing it in-warehouse would put a `STORAGE INTEGRATION`, an external stage, and a service-account IAM grant on the critical path for zero narrative gain. Cloud Run has to exist anyway; it writes to GCS with ordinary credentials.

## Tooling

- **Snowflake MCP (self-hosted, `Snowflake-Labs/mcp`)** — Cortex plus object management and SQL orchestration. This is what lets Claude create the network rule, secret, integration, and procedures.
- **Snowflake connector (directory)** — Cortex Agents / Cortex Search. Retrieval only; good for querying, not for DDL.
- **`snow` CLI** — every object lives as SQL in the repo and deploys with one command. Reproducible, reviewable, and it's what the writeup screenshots.

## Snowflake objects (the whole app)

| object | job | status |
| --------------------------------------------------- | ---------------------------------------------------------- | ----------- |
| `GITHUB_API_RULE` (EGRESS, `api.github.com`) | let the warehouse out | ✅ deployed |
| `GITHUB_TOKEN` (`SECRET`) | GitHub token | ✅ deployed |
| `GITHUB_API_ACCESS` (`EXTERNAL ACCESS INTEGRATION`) | binds rule + secret | ✅ deployed |
| `COMMITS` | owner, repo, sha, subject, body, authored_at, bot/AI flags | ✅ deployed |
| `PROC INGEST_REPO_COMMITS(owner, repo)` | Python + external access → REST Commits API → `COMMITS` | ✅ deployed |
| `COMMITS_CLEAN` (view) | drops merges + bots, derives date/hour parts | ✅ deployed |
| detector views | gaps, streaks, night share, drama scores → one winner | ✅ scripted |
| `CARD_EVIDENCE` (view) | the winning thread's ~20 commits — all Cortex ever sees | ✅ scripted |
| `PROC READ_REPO(owner, repo)` | ingest if cold → detector → Cortex → card payload JSON | ✅ deployed |
- **Deploy:** `make snowflake-deploy` — every object is SQL in the repo. An object created by clicking in a UI does not exist.

## Snowflake objects

| object | status |
| --------------------------------------------------- | ---------- |
| `GITHUB_API_RULE` (EGRESS, `api.github.com`) | ✅ shipped |
| `GITHUB_TOKEN` (`SECRET`) — created out-of-band | ✅ shipped |
| `GITHUB_API_ACCESS` (`EXTERNAL ACCESS INTEGRATION`) | ✅ shipped |
| `COMMITS` · `REPO_INGEST` · `INGEST_STAGE` | ✅ shipped |
| `PROC INGEST_REPO_COMMITS(owner, repo)` | ✅ shipped |
| `COMMITS_CLEAN` · `DETECTOR_CONFIG` (views) | ✅ shipped |
| detector views (15) → `REPO_STORYLINE` | ✅ shipped |
| `COMMIT_LINES` · `CARD_EVIDENCE` (views) | ✅ shipped |
| `CHRONICLE_CARD` (UDF) | ✅ shipped |
| `CARD_PLOT` (view) · `CARDS` | ✅ shipped |
| `PIPELINE_VERSION` · `STALE_CARDS` (views) | ✅ shipped |
| `CARD_PAYLOAD` (view) · `REFRESH_CARD_DATA()` | ✅ shipped |
| `PROC READ_REPO(owner, repo)` | ✅ shipped |

No `STORAGE INTEGRATION` and no external stage — Cloud Run owns the bucket.

## Stage 1 — the detector (plain SQL, no LLM, free)

Score candidate threads, pick the single most dramatic **true** one:

- **relapse** — `LAG` over commit dates: silent ≥N days, then resumed. _(rai-lint: 107 days dark, back at 3:32am)_
- **nocturne** — share of commits after 22:00. _(my-hermantic-agent: 85%)_
- **binge** — longest consecutive-day streak, or the heaviest single night.
- **collapse** — a spike, then permanent silence. _(the 3:53am ending)_
- **fight** — a revert/hotfix cluster (regex; no AI needed).
- **resurrection** — dead, returned, **and shipped a release**.

Floors (minimum real commits) keep bots and noise from winning. Deterministic — same handle, same story.

## Stage 2 — Cortex (one small call)

Feed **only the winning thread's evidence** — ~10–20 real commit messages plus the computed facts. It returns:

```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 thing that burned out"
}
```

Nine keys, all the writing on the card. The italic run sits inside sentence 2 — `headline_upright` is upright, `headline_accent` is italic and in the accent colour, `headline_trail` is upright again. Empty labels for the anchors this storyline doesn't use (a collapse has no separate pivot; a non-active repo doesn't get a poetic label on the last commit).

**Cortex picks the palette.** One `accent` hex paints every accent-coloured element on the card — kicker slug, italic headline fragment, last-commit dot, arrow, void-panel rule, attribution bullet. A project that died and one that shipped must not wear the same colour.

**Renderer owns the facts.** Kicker slug prefix, header meta (`59 COMMITS · QUIET SINCE FEB 25`), first/last-commit anchor prefixes, void-panel text, caption, author handle — all composed from `FACTS`, `STATUS`, `PIVOT_AT`, and the `PLOT` array. See the Ownership section in `docs/initial-design-spec.md` for the full split.

## Stage 3 — the card

Cloud Run templates an SVG (1200×630) from the Cortex JSON plus the plotted commits, and writes it to the public GCS bucket.

**Design:**

- Didone headline in three slots — upright, italic accent fragment, upright · mono kicker naming the genre · status label (`abandoned` / `shipped 1.0.1`).
- **The arc is the card:** beeswarm scatter, date across and hour down, night at the bottom. Daylight commits hollow, night commits solid. The dead stretch is a **void panel** you look through. The final commit is one accent dot.
- Poetic tails pin to the anchors the storyline uses. The headline **interprets** — that's the point.

## Cost

Detection is free SQL. Cortex sees ~20 commits, not thousands. Cloud Run scales to zero. Storage is a bucket. Ballpark: **lunch money, once.**

## Timeline
## Timeline (as delivered)

- **Fri night — THE GATE.** MCP + `snow` CLI connected. Prove two things: external access can reach `api.github.com` from inside a proc, and Cortex AISQL runs in your region. Nothing else matters until both are green.
- **Sat AM** — `ingest_commits` → `COMMITS_RAW` for one handle.
- **Sat PM** — detector SQL. Confirm it picks `my-hermantic-agent` (nocturne/collapse) and `rai-lint` (resurrection) unaided.
- **Sun AM** — Cortex call (kicker, headline, anchor labels, accent) via `READ_REPO`; Cloud Run renders the SVG → GCS.
- **Sun PM** — Cloud Run shell + `/api/generate`; confirm the card renders in a real README and the page embeds in a dev.to draft.
- **Sun night** — writeup: the detector SQL, the Cortex call, the `snow` deploy. Cards are the demo.
- **Fri night — THE GATE.** Two things proven before anything else was allowed to matter: external access can reach `api.github.com` from inside a proc, and Cortex AISQL runs in the region.
- **Sat AM** — ingest → `COMMITS`.
- **Sat PM** — detector SQL. It picked the intended storylines unaided.
- **Sun AM** — Cortex call via `READ_REPO`; Cloud Run renders the SVG → GCS.
- **Sun PM** — Cloud Run shell + `/api/generate`; card confirmed rendering in a real README.
- **Sun night** — writeup.
- **Mon early** — submit with buffer.

## Open items

- **Font rendering in the SVG** — GitHub proxies README images through camo, so webfonts won't load. Fix is a base64-embedded subset. Deferred by decision.
- **Anonymous serving** — Snowflake will not answer an anonymous HTTP request (SPCS "public" endpoints are RBAC-gated; a browser gets a login page). The public GCS bucket is the answer.
- **Async generation** — Cloud Tasks owns the durable worker request. The page polls
`/api/state`, so closing the tab does not affect the job.
- ~~**Font rendering**~~ **Closed.** The card is rasterized to PNG with the fonts vendored, so nothing depends on a proxy fetching a webfont. dev.to forced this — its image proxy will not serve an SVG.
- **Stale cards** — `STALE_CARDS` reports which cards a dead pipeline version wrote; it does not act, because acting costs a Cortex call each. `make cards-rerender` redraws stored cards for free — only a prompt change needs real spend.

## Cut first if behind
## Cut

1. Extra story types (ship `relapse` + `nocturne` only).
2. The gallery page.
1. **The gallery page.** Three example chips on the landing page cover the same need.
2. Story types beyond the six — none were needed.

**Never cut:** external-access ingest · the detector SQL · the Cortex call including the palette pick · the SVG card · the writeup showing the SQL.
**Never cut, and wasn't:** external-access ingest · the detector SQL · the Cortex call including the palette pick · the SVG card.
Loading