Files
leetcode/apps/api/README.md
T

118 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# srs-api
Cloudflare Worker running the spaced-repetition system: daily digest email,
one-tap logging, `/done` webhook, Saturday review issues, live SVG charts,
and the `/api/stats` feed for the docs progress page.
Live at `https://srs-api.prdlk.workers.dev`.
## Direction of truth
- **D1 owns SRS state** — stages, review dates, attempt history, gate
scores, boost flags. Nothing else is authoritative.
- **GitHub issues own the catalog** — topics, problems, `set:*`/`diff:*`
labels, milestones. `catalog.ts` reconciles them into D1 nightly (and via
`/admin/reconcile`); it recomputes from scratch, never invents rows, and
never overwrites SRS-owned columns (`stage`, `next_review`).
- **The repo owns the schedule** — `data/schedule.json`, human-edited,
bundled at deploy. Git history is its audit log.
- The GitHub Project mirror writes `Target Date` and nothing else: it is
best-effort, so failures log warnings and never block a D1 write, and topic
rows are never written. SRS stage and first-attempt result stay in D1 only —
the charts, digest and `/api/stats` read them from there, so mirroring them
into Project single-selects bought only drift. Do not re-add them.
`Set`/`Difficulty` are a projection of the issue labels, reconciled by
`bun run sync-project-fields` at the repo root, not by this Worker.
## The ladder
`new → +3 → +7 → retired`; pass advances, fail resets to `+3`. A problem's
first-ever log enters at `+3` regardless of result. Stage names the NEXT
review's interval, and the two rungs are the same numbers as the `work/3` and
`work/7` buckets `bun run pick` scaffolds re-solves into.
**Sunday is never booked.** Topics run MonFri, the gate is Saturday, and 3/7
are chosen so a solve returns on a working day: only a Thursday solve's `+3`
would land on the rest day, and `workingDay()` in `src/srs.ts` slides it to
Monday. Every scheduled date in the Worker — ladder reviews, levelled
overflow, deferred-Hard release dates — is minted by that one function, so the
rest day cannot be booked and then swallowed by the digest's Sunday branch.
Reviews may slip later, never earlier: pulling one back to Saturday would
shorten the interval it exists to test. `src/srs.test.ts` sweeps the whole
campaign calendar to prove it.
All dates are ET calendar strings anchored at noon UTC (`src/srs.ts`) — never
raw `Date` math.
Blind drills draw only from topics **already learned**: scheduled in an
earlier week (the current week's optional pool is reserved for Saturday's
gate) and with at least one core problem on the ladder — a skipped learning
day never feeds drills just because its calendar week lapsed.
## Routes
| Route | Auth | Purpose |
|---|---|---|
| `GET /log?p&r&d&sig` | HMAC (`LINK_KEY`) | one-tap pass/fail; idempotent per (problem, date, kind); links expire after 3 days |
| `POST /webhook/github` | HMAC (`WEBHOOK_SECRET`) | `/done <n> pass\|fail` comments (owner only, any issue); `review`-issue close → gate scoring |
| `GET /chart/{progress,ladder,heatmap}.svg`, `GET /badge/gate.svg` | public | hand-rolled SVGs, `max-age=300` (GitHub Camo's freshness floor) |
| `GET /api/stats` | public, CORS-pinned to the docs origin | one JSON document for `/progress` |
| `POST /admin/solved` | `Authorization: Bearer <LINK_KEY>` | `{"lc":[…]}` — solutions committed under `work/`; logs each *first* solve (`source='commit'`), skips anything past stage `new`, takes `?dry=1&date=` |
| `POST /admin/{digest,review,reconcile}` | `Authorization: Bearer <LINK_KEY>` | manual triggers; `digest` takes `?dry=1&force=1&date=` |
## Crons (DST-proof)
Each event has two UTC crons; code fires only when the computed ET hour
matches (`etHour` in `src/srs.ts`, unit-checked in `src/srs.test.ts`):
- `0 12,13 * * *` → 8 AM ET: catalog reconcile, then the digest.
- `0 4,5 * * 6` → midnight ET Saturday: create `Review — Week N` (so the
8 AM digest can link to it).
## Secrets & bindings
`wrangler secret put``GH_PAT` (Issues + Projects RW), `WEBHOOK_SECRET`
(matches the repo webhook), `LINK_KEY` (signs one-tap links, gates admin
routes). Bindings in `wrangler.jsonc`: `DB` (D1 `srs`), `EMAIL`
(`send_email`, restricted to the verified destination). Sender domain
`prdlk.com` is onboarded to Email Sending.
Two Actions workflows push into D1 with the `SRS_ADMIN_KEY` repo secret:
`sync-d1.yml` sends curriculum issue edits to `POST /admin/reconcile`
(the morning cron is the backstop), and `close-solved.yml` sends the whole
implemented `work/` set to `POST /admin/solved` on every push to `main`.
That second call is what keeps a committed solution from being invisible
here: closing its issue is not a state change the Worker can see — the
catalog reconcile ignores issue state, and only `logAttempt()` moves the
ladder. `close-solved` still owns the issue close itself (it knows the files
and the commit), so this path only writes D1 and mirrors Project fields.
## Local dev
```sh
cp .dev.vars.example .dev.vars # or fill GH_PAT/WEBHOOK_SECRET/LINK_KEY
bun install
bunx wrangler d1 migrations apply srs --local
bun run dev # wrangler dev on :8787, local D1
curl -X POST -H "Authorization: Bearer $LINK_KEY" \
"localhost:8787/admin/digest?dry=1&date=2026-08-31" # prints HTML, sends nothing
bun test src # DST guards, date math, the rest-day sweep
```
`?date=` on admin routes is the `SRS_TODAY` equivalent. Migrations are
append-only and applied in order; `0002_ladder_3_7.sql` is the +2/+5/+10 →
`+3`/`+7` rebuild, which also lifts every date that was sitting on a Sunday.
The one-shot importer for the retired `.github/srs/srs.json` is gone — that
state no longer exists, and git history is its record.
## Deploy
```sh
bun run deploy # from apps/api/, or `bun run api:deploy` at the repo root
```
CI deploys on every push to `main` (`deploy.yml`, gated on `bun run api:test`,
using the `CLOUDFLARE_API_TOKEN`/`CLOUDFLARE_ACCOUNT_ID` repo secrets); the
commands above are for out-of-band deploys. After changing bindings, rerun
`bunx wrangler types`.