mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
Migrate SRS to Cloudflare Worker: D1 state, email digest, review issues, live charts
This commit is contained in:
@@ -2,27 +2,31 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
LeetCode interview-prep campaign (8 weeks, 2026-08-17 → Oct 11) run as a repo: solution files under `work/`, a Blume docs site under `docs/`, and GitHub Actions automation that reconciles issues, publishes docs, and drives a spaced-repetition system (SRS). Issues model the curriculum: ~24 `topic` issues own ~161 `problem` sub-issues (`set:core|optional|deferred`, `diff:*`); milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
|
||||
LeetCode interview-prep campaign (8 weeks, 2026-08-17 → Oct 11) run as a repo: solution files under `work/`, a Blume docs site under `docs/`, GitHub Actions that reconcile issues and publish docs, and a Cloudflare Worker (`api/`) that runs the spaced-repetition system (SRS) — daily digest email, one-tap logging, weekly review issues, live charts. Issues model the curriculum: ~24 `topic` issues own ~161 `problem` sub-issues (`set:core|optional|deferred`, `diff:*`); milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
|
||||
|
||||
## Architecture & Data Flow
|
||||
|
||||
Two subsystems, one direction of truth:
|
||||
Three owners, one direction of truth:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
work[work/ solutions] -->|bun run sync| docs[docs/solutions/*.mdx]
|
||||
work -->|close-solved.yml| PI[problem issues]
|
||||
PI -->|close-topics.yml| TI[topic issues]
|
||||
SRS[.github/srs/srs.json] -->|srs-scheduler| Today[📋 Today issue]
|
||||
Today -->|/done comments| Logger[srs-logger] --> SRS
|
||||
SRS -->|mirror, best-effort| Proj[GitHub Project fields]
|
||||
SRS -->|gate.yml| Gate[weekly gate issue + docs/gate-log.md]
|
||||
GH[GitHub issues = catalog] -->|nightly reconcile| D1[(D1 = SRS state)]
|
||||
D1 -->|8 AM ET cron| Mail[digest email + one-tap links]
|
||||
Mail -->|GET /log| D1
|
||||
PI -->|/done webhook| D1
|
||||
D1 -->|best-effort mirror| Proj[GitHub Project fields]
|
||||
D1 -->|Sat midnight ET| Review[Review — Week N issue]
|
||||
D1 --> Charts[SVG charts + /api/stats]
|
||||
```
|
||||
|
||||
- **Reconcile, don't react.** `close-solved.ts`, `close-topics.ts`, and the SRS scripts recompute desired state from scratch each run: re-runs are no-ops, backfills need no special casing, closing is one-directional (nothing ever reopens).
|
||||
- **`.github/srs/srs.json` is the single source of truth.** Project fields (`Target Date`, `SRS Stage`, `First Attempt`) are a best-effort mirror via `scripts/srs-project.ts`: every GraphQL failure becomes a warning, never an error — an srs.json update must never be lost to a mirror failure. Topic rows' `Target Date` is never written (problem rows only).
|
||||
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `srs.ts`, FNV-1a → mulberry32, seed = date / ISO week), so running a workflow twice picks identical problems.
|
||||
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead.
|
||||
- **Reconcile, don't react.** `close-solved.ts`, `close-topics.ts`, and the Worker's catalog sync recompute desired state from scratch each run: re-runs are no-ops, backfills need no special casing, closing is one-directional.
|
||||
- **D1 owns SRS state; GitHub issues own the catalog; `api/data/schedule.json` owns the calendar.** The catalog reconcile never invents rows and never overwrites SRS columns (`stage`, `next_review`). Project fields (`Target Date`, `SRS Stage`, `First Attempt`) are a best-effort mirror (`api/src/mirror.ts`): failures are warnings, never lost D1 writes; topic rows' `Target Date` is never written.
|
||||
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
|
||||
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `api/src/srs.test.ts`.
|
||||
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
|
||||
|
||||
## Key Directories
|
||||
|
||||
@@ -31,8 +35,8 @@ flowchart LR
|
||||
| `work/<Difficulty>/<Category>/<num>.<slug>.{js,py}` | Solutions, e.g. `work/Easy/Array/1.two-sum.py`. Machine-parsed header comment (title / `Difficulty:` / URL / `─` rule / statement) — preserve its exact shape |
|
||||
| `scripts/` | Flat Bun TS: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import) |
|
||||
| `docs/` | Blume site. `docs/solutions/(<category>)/<num>-<slug>.mdx` generated by sync; `(algorithms)/`, `(data-structures)/` hand-written explainers |
|
||||
| `.github/srs/` | Machine state: `srs.json` (bot-committed), `schedule.json` (day → topic issue, human-edited), `badge.json` (shields endpoint) |
|
||||
| `.github/workflows/` | deploy, close-solved, close-topics, srs-scheduler, srs-logger, gate, srs-setup |
|
||||
| `api/` | Cloudflare Worker (own Bun workspace): `src/` modules, `data/schedule.json` (day → topic issue, human-edited, bundled at deploy), `migrations/`, `scripts/import-srs.ts` |
|
||||
| `.github/workflows/` | deploy (docs), close-solved, close-topics — Worker deploys are manual (`bun run api:deploy`) |
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -43,38 +47,40 @@ bun run sync # work/ → docs/solutions/ pages
|
||||
bun run dev|build # Blume docs site
|
||||
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
|
||||
bun run close-topics -- --dry-run
|
||||
SRS_DRY=1 SRS_TODAY=2026-08-30 bun scripts/srs-scheduler.ts # preview a brief
|
||||
bun scripts/srs-logger.ts --issue N --author prdlk --body "/done 704 pass"
|
||||
bun scripts/srs-gate.ts create|close --issue N
|
||||
bun run api:dev # wrangler dev on :8787 (local D1); api:test = DST guard tests
|
||||
bun run api:deploy # deploy the Worker (manual by design)
|
||||
curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
||||
"localhost:8787/admin/digest?dry=1&date=2026-08-30" # preview a digest, send nothing
|
||||
```
|
||||
|
||||
## Code Conventions & Common Patterns
|
||||
|
||||
- **Scripts are either entries or libraries.** Entries (`close-*.ts`, `srs-scheduler|logger|gate|setup.ts`) use shebang + top-level `await` and run their pipeline linearly. Libraries (`github.ts`, `report.ts`, `srs.ts`, `srs-project.ts`) must stay side-effect-free on import — nothing touches network/credentials until a factory (`github()`, `projectMirror()`) is awaited.
|
||||
- **Scripts are either entries or libraries.** Entries (`scripts/close-*.ts`) use shebang + top-level `await`. Libraries (`scripts/github.ts`, `scripts/report.ts`, everything in `api/src/`) are side-effect-free on import — nothing touches network/credentials until a factory (`github()`, `projectMirror()`) is called.
|
||||
- **Shared plumbing:** `github()` gives `repo`, `api()` (throws `METHOD path -> status body`), `list()` (paginating async generator), `closeIssue()` (comment **first**, then close — a failed PATCH still leaves a trace). `report.ts` gives `printTable`/`markdownTable`/`writeStepSummary`.
|
||||
- **Narrow API payloads with guards**, not inline casts: `if (!("number" in value) || typeof value.number !== "number") return;` (see `readProblemIssue` in `close-solved.ts`). Named-const casts only with a reason.
|
||||
- **Dry-run everything:** reconcilers take `--dry-run`/`DRY_RUN=1`; SRS takes `SRS_DRY=1` plus overrides `SRS_TODAY`, `SRS_STATE`, `SRS_SCHEDULE`, `SRS_GATE_LOG`, `SRS_BADGE` — point `SRS_STATE` at a scratch copy to exercise logic safely.
|
||||
- **Dates:** always ET calendar strings (`YYYY-MM-DD`), arithmetic anchored at noon UTC (`atNoon` in `srs.ts`) to dodge DST. Never `new Date()` math directly.
|
||||
- **Dry-run everything:** reconcilers take `--dry-run`/`DRY_RUN=1`; Worker admin routes take `?dry=1&force=1&date=` (the `SRS_TODAY` equivalent) and `wrangler dev` runs against local D1 — exercise logic there before touching production state.
|
||||
- **Dates:** always ET calendar strings (`YYYY-MM-DD`), arithmetic anchored at noon UTC (`atNoon` in `api/src/srs.ts`) to dodge DST. Never `new Date()` math directly.
|
||||
- **Style:** section-divider comments (`// ── name ───`), file-top doc comments explaining the *why* and invariants, 2-space JSON with trailing newline, `Map` for dynamic keys / `Record` for static tables, no tiny one-expression wrapper functions.
|
||||
- **Known intentional duplication:** `close-solved.ts` re-implements header stripping instead of importing from `sync.ts` because `sync.ts` runs its pipeline on import. Don't "deduplicate" it.
|
||||
|
||||
## Important Files
|
||||
|
||||
- `scripts/srs.ts` — SRS domain: ladder (`new → +2 → +5 → +10 → retired`; fail resets to `+2`), `fetchCatalog()` (walks topic sub-issues, parses `LC <n> · <name> · <diff> · <set>` titles), campaign math (`CAMPAIGN_START`).
|
||||
- `api/src/srs.ts` — SRS domain: ladder (`new → +2 → +5 → +10 → retired`; fail resets to `+2`; first-ever log enters at `+2`), ET date math, seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here).
|
||||
- `api/src/index.ts` — router + cron dispatch; `api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
|
||||
- `scripts/sync.ts` — work→docs contract: only `## Solution` onward is script-owned on existing pages; human prose is never touched; never hand-write solution pages or edit inside `## Solution`.
|
||||
- `.github/srs/srs.json`, `docs/gate-log.md`, `.github/srs/badge.json` — **bot-committed** (`github-actions[bot]`, `[skip ci]`). Don't hand-edit; fix via scripts.
|
||||
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`islands/ProgressDashboard.tsx`).
|
||||
- `blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`).
|
||||
- Workflows share concurrency group `srs-state` (never race on srs.json); `PROJECT_PAT` secret is required for user-Project GraphQL (default `GITHUB_TOKEN` cannot), used by srs-scheduler/logger/setup only.
|
||||
- Old `.github/srs/` JSON state and the `srs-*` Actions are RETIRED — do not resurrect; `api/scripts/import-srs.ts` documents the migration.
|
||||
|
||||
## Runtime/Tooling Preferences
|
||||
|
||||
- **Bun only**: run scripts with `bun scripts/<name>.ts`, install with `bun install --frozen-lockfile`; `bun.lock` is the sole lockfile. Node 22 appears only in `deploy.yml` because Blume requires it.
|
||||
- **No tsconfig, no ESLint, no Prettier** — match surrounding style by hand. `@types/bun` is the only type dependency.
|
||||
- Automation workflows skip `bun install` (scripts use Bun builtins + `fetch` only) — keep new automation dependency-free.
|
||||
- **Bun only**: run scripts with `bun scripts/<name>.ts`, install with `bun install --frozen-lockfile`; `api/` is its own workspace with its own lockfile. Node 22 appears only in `deploy.yml` because Blume requires it.
|
||||
- **No root tsconfig, no ESLint, no Prettier** — match surrounding style by hand. `api/` has a strict `tsconfig.json`; `Env` types are generated (`bunx wrangler types`), never hand-written.
|
||||
- **Zero runtime npm dependencies in automation and the Worker** — SVG, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only.
|
||||
- Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions.
|
||||
|
||||
## Testing & QA
|
||||
|
||||
- **No test framework, no `*.test.*` files** — `bun run test` submits one solution to LeetCode's judge.
|
||||
- QA is dry-runs + scratch state: verify schedulers/reconcilers with `--dry-run`/`SRS_DRY=1` and a copied `SRS_STATE` before live runs; idempotency check = run twice, `cmp` output.
|
||||
- Repo-wide: no test framework — `bun run test` submits one solution to LeetCode's judge. Exception: `api/src/srs.test.ts` (`bun:test`) proves the DST cron guards with fixed dates; run via `bun run api:test`.
|
||||
- QA is dry-runs + local state: Worker logic against `wrangler dev` + local D1 with `?dry=1&date=`; reconcilers with `--dry-run`; idempotency check = run twice, `cmp` output.
|
||||
- Docs changes: `bunx blume build --isolated` must pass with no new warnings (then `rm -rf .blume-verify`); after sync, review only *created* pages (prose transforms are heuristic).
|
||||
|
||||
Reference in New Issue
Block a user