Files
leetcode/AGENTS.md
T

8.0 KiB

Repository Guidelines

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.

Architecture & Data Flow

Three owners, one direction of truth:

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]
  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 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

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)

Development Commands

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 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)
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.
  • 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.
  • 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.
  • 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.

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.
  • 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

  • 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).