mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
feat: migrate docs
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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/`, 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.
|
||||
LeetCode interview-prep campaign (8 weeks, 2026-08-17 → Oct 11) run as a repo: solution files under `work/`, a Blume docs site under `apps/docs/`, GitHub Actions that reconcile issues and publish docs, and a Cloudflare Worker (`apps/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
|
||||
|
||||
@@ -10,7 +10,7 @@ Three owners, one direction of truth:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
work[work/ solutions] -->|bun run sync| docs[docs/solutions/*.mdx]
|
||||
work[work/ solutions] -->|bun run sync| docs[apps/docs/content/*.mdx]
|
||||
work -->|close-solved.yml| PI[problem issues]
|
||||
PI -->|close-topics.yml| TI[topic issues]
|
||||
GH[GitHub issues = catalog] -->|nightly reconcile| D1[(D1 = SRS state)]
|
||||
@@ -23,9 +23,9 @@ flowchart LR
|
||||
```
|
||||
|
||||
- **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`.
|
||||
- **D1 owns SRS state; GitHub issues own the catalog; `apps/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 (`apps/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 `apps/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 `apps/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
|
||||
@@ -33,56 +33,58 @@ flowchart LR
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `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, sync-d1 (issue edits → `/admin/reconcile`) — Worker deploys are manual (`bun run api:deploy`) |
|
||||
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
|
||||
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `islands/`, `public/`). `content/(<category>)/<num>-<slug>.mdx` generated by sync |
|
||||
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (day → topic issue, human-edited, bundled at deploy), `migrations/`, `scripts/import-srs.ts` |
|
||||
| `.github/workflows/` | deploy (docs → Pages, Worker → Cloudflare, on every main push), close-solved, close-topics, sync-d1 (issue edits → `/admin/reconcile`) |
|
||||
|
||||
## Development Commands
|
||||
|
||||
All from the repo root (a Bun workspace over `apps/*`):
|
||||
|
||||
```sh
|
||||
bun run pick # scaffold a solution via leetcode-cli (fuzzy picker)
|
||||
bun run test # run ONE solution against LeetCode's judge (not a test suite)
|
||||
bun run sync # work/ → docs/solutions/ pages
|
||||
bun run dev|build # Blume docs site
|
||||
bun run sync # work/ → apps/docs/content/ pages
|
||||
bun run dev|build # Blume docs site (runs in apps/docs/)
|
||||
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
|
||||
bun run close-topics -- --dry-run
|
||||
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)
|
||||
bun run api:deploy # deploy the Worker by hand (CI also deploys on main pushes)
|
||||
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 (`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.
|
||||
- **Scripts are either entries or libraries.** Entries (`apps/cli/close-*.ts`) use shebang + top-level `await`. Libraries (`apps/cli/github.ts`, `apps/cli/report.ts`, everything in `apps/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`; 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.
|
||||
- **Dates:** always ET calendar strings (`YYYY-MM-DD`), arithmetic anchored at noon UTC (`atNoon` in `apps/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
|
||||
|
||||
- `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`.
|
||||
- `api/src/digest.ts` / `api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
|
||||
- `api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
|
||||
- `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`.
|
||||
- `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`).
|
||||
- Old `.github/srs/` JSON state and the `srs-*` Actions are RETIRED — do not resurrect; `api/scripts/import-srs.ts` documents the migration.
|
||||
- `apps/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).
|
||||
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
|
||||
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
|
||||
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
|
||||
- `apps/cli/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`.
|
||||
- `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 (`apps/docs/islands/ProgressDashboard.tsx`).
|
||||
- `apps/docs/blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`).
|
||||
- Old `.github/srs/` JSON state and the `srs-*` Actions are RETIRED — do not resurrect; `apps/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`; `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.
|
||||
- **Near-zero runtime npm dependencies** — SVG, PNG, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. The **one** exception is the digest email: `api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `api/tsconfig.json`. Do not extend this exception to any other module.
|
||||
- **Bun only**: run scripts with `bun apps/cli/<name>.ts` (or the root `bun run` aliases), install with `bun install --frozen-lockfile`. One workspace (`apps/*`), one root `bun.lock`. Node 22 appears only in `deploy.yml` because Blume requires it.
|
||||
- **No root tsconfig, no ESLint, no Prettier** — match surrounding style by hand. `apps/api/` has a strict `tsconfig.json`; `Env` types are generated (`bunx wrangler types`), never hand-written.
|
||||
- **Near-zero runtime npm dependencies** — SVG, PNG, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. The **one** exception is the digest email: `apps/api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `apps/api/tsconfig.json`. Do not extend this exception to any other module.
|
||||
- Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions.
|
||||
|
||||
## Testing & QA
|
||||
|
||||
- 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`.
|
||||
- Repo-wide: no test framework — `bun run test` submits one solution to LeetCode's judge. Exception: `apps/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).
|
||||
- Docs changes: `bunx blume build --isolated` (from `apps/docs/`) must pass with no new warnings (then `rm -rf apps/docs/.blume-verify`); after sync, review only *created* pages (prose transforms are heuristic).
|
||||
|
||||
Reference in New Issue
Block a user