Review 4 closes out today (Nov 2); Reviews 7-20 move to Mon/Tue/Thu/Fri from Fri Nov 6 to Tue Dec 1 (Thanksgiving unbooked), then two sweep days and a gate: [] Saturday that makes Dec 5 day 30. The digest counts the sprint (SPRINT_START, length derived from the plan's end) and counts down before it. REVIEW_CAP 3 -> 5 so the ~110-review backlog drains inside the sprint.
39 KiB
Repository Guidelines
Project Overview
LeetCode interview-prep campaign (2026-08-17 → Dec 5: eight weeks of topics, the Review-issue phase, a two-week break, then a 30-day interview sprint from Fri Nov 6 (SPRINT_START); the end is the last date in apps/api/data/schedule.json — CAMPAIGN_END/CAMPAIGN_DAYS/SPRINT_DAYS in apps/api/src/srs.ts are derived from it, never declared) 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 (Topic NN — Name) own ~161 problem sub-issues titled LC <num> · <Name>, with difficulty and set carried only by labels (set:core|optional|deferred, diff:*) — never by the title; 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[apps/docs/content/*.mdx]
work -->|close-solved.yml| PI[problem issues]
work -->|close-solved.yml POST /admin/solved| D1[(D1 = SRS state)]
PI -->|close-topics.yml| TI[topic + Review N issues]
PI -->|close-solved.yml release job| Rel[timever release per finished topic / Review N]
GH[GitHub issues = catalog] -->|nightly reconcile| D1
D1 -->|8 AM ET cron| Mail[digest email + one-tap links]
Mail -->|GET /log| D1
PI -->|/done webhook| D1
D1 -->|best-effort mirror: Target Date only| Proj[GitHub Project fields]
GH -->|close-solved.yml sync-project-fields| Proj
D1 -->|Sat midnight ET| Review[Review — Week N issue]
PI -->|"spaced-repetition.yml (6 AM ET cron, closed +3d/+7d)"| SR[Spaced Repetition — Month D issue]
D1 --> Charts[SVG charts + /api/stats]
Charts -->|bun run stats| TUI[terminal dashboard]
- 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.close-solvedposts its whole implemented set to/admin/solved, not just the issues it closed this run. - Solving has three doors, one write path. A digest one-tap, a
/donecomment, and a solution landing inwork/all end inlogAttempt(). Closing an issue is not one of them: the catalog reconcile never reads issue state, so awork/push that skipped the email would otherwise leavestage='new'— invisible to the charts, the digest's solved ticks, and the drill/gate pools. Hence/admin/solved, which takes{lc, bucket}pairs (source='commit'): the bucket names the rung the file settles —work/1settlesnew,work/3settles+3,work/7settles+7— so a pushed re-solve advances the ladder instead of vanishing, and re-posting the whole implemented set writes nothing because the rung named has already been left behind. That stage check is the entire idempotency mechanism; no index, no dedupe table, no state file.close-topicsneeds no such call — D1 stores no topic completion, only problem rows. - The rest day is structural, not cosmetic. Sunday is never booked: topics run Mon–Fri (
apps/api/data/schedule.json), the gate is Saturday, and the ladder's windows are 3 and 7 precisely because those return a solve to a working day. Every scheduled date — ladder review, levelled overflow, deferred-Hard release, review-issue window — comes out ofworkingDay()inapps/api/src/srs.ts, which slides the one exception (a Thursday solve's+3) forward to Monday. Forward, never back: a review may slip later than its interval, never shorten it. Never mint a scheduled date with bareaddDays(), and never "fix" a Sunday landing downstream — the digest's Sunday rest branch is a courtesy, not the mechanism.apps/api/src/srs.test.tssweeps the campaign calendar to keep this honest. - One schedule, one answer to "what is open today". D1's ladder IS the schedule;
dueToday()(POST /admin/due) is its read side andDUE_WHEREis the single SQL definition of "due", shared by the digest, the review issue and the docs queue chart.spaced-repetition.ymlrenders that answer into today'sSpaced Repetition — <Month D, YYYY>issue — grouped by rung,WINDOW_CAP(3) required per rung with the overflow listed as optional, keyed by the ET date in the title so a re-run rewrites one body. It schedules nothing, stores nothing, and writes nothing on Sunday. WithoutSRS_ADMIN_KEYit FAILS rather than inventing a second schedule from issue close dates — that reconstruction was the drift, and it is gone. NEVER add a JSON state file (.github/problems.jsonand friends) or a second "when is this due" rule: D1 answers it, with one writer (logAttempt) and git-free history inattempts. - The picker owns the local review loop; the buckets are its state.
work/holds three buckets (1= first solve,3/7= blind re-solves, named after the ladder rungs) andbun run pickhas three sections — Tab cycles them. Pools are reconciled from the filesystem plus onegit log(apps/cli/work.ts): a first solve dates from the ET date of the commit that added itswork/1file — the same push that closes the problem issue and starts the +3/+7 windows — and a window stops being owed the moment its bucket holds a file. Uncapped, unlike the review issue'sWINDOW_CAP: that issue is one day's assignment, the picker is everything still owed. An empty scaffold underwork/1owes nothing (it is unsolved), which is whyisImplemented()lives inapps/cli/source.tsand is shared withclose-solved.ts. leetcode-cli reads its output directory from~/.leetcode/workspaces/<active>/config.jsonand nowhere else — no flag, no env var — sopick.tsMERGESworkDirinto that file around the child (never replays saved bytes: the CLI owns the file too) and leaves it pointed atwork/1, then treats the scaffold's existence as the acceptance test, since leetcode-cli exits 0 even when an expired session made it write nothing. The picker itself writes no state: pushing the re-solve is what moves the ladder, viaclose-solved→/admin/solvedwith that file's bucket. - The README charts are live, never committed. They are Worker endpoints reading D1 per request (
Cache-Control: max-age=300, honored by GitHub Camo), so a solved push moves them within ~5 minutes with no commit and no workflow of their own. - D1 owns SRS state; GitHub issues own the catalog;
apps/api/data/schedule.jsonowns the calendar. The catalog reconcile never invents rows and never overwrites SRS columns (stage,next_review).Target Dateis the only Project field the Worker mirrors (apps/api/src/mirror.ts): failures are warnings, never lost D1 writes; topic rows'Target Dateis never written. SRS stage and first-attempt result live only in D1 — do not re-add them as Project fields. The Project'sSet/Difficultysingle-selects are a projection of the issue labels, reconciled bybun run sync-project-fields— a best-effort final step ofclose-solved.yml, never the Worker. - The plan names days; the samplers fill the gaps.
apps/api/data/schedule.jsonmaps a date to aScheduledDay(apps/api/src/srs.ts):topics(one or more topic issues — a doubled day lists two, and the digest renders each with its core list then the optional "if time" list),problems(a set day: fixed numbers, rendered like a topic),gate(Saturday's blind set, verbatim —createReviewIssuerefuses to create the issue if any number is not a catalog row, so gate problems must exist asset:optionalsub-issues of their topic first), orsweep(every problem whose last attempt failed, taps to re-log, no drills). Every number agateorproblemsentry names is inRESERVED, and the drill/gate samplers never draw it — that is what keeps a planned gate problem unseen until its Saturday. Dates absent from the plan still get the ladder's reviews and drills; a Saturday without agateentry falls back to sampling this week's optional pools. - Determinism = idempotency. Drill/gate sampling uses a seeded PRNG (
rng()inapps/api/src/srs.ts, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date inemail_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 inapps/api/src/srs.test.ts. - Chained workflows:
GITHUB_TOKEN-driven issue closes fire noissuesevents, soclose-topics.ymlchains offworkflow_runof Close Solved instead. The Worker's webhook uses its ownGH_PAT, so its events flow normally. - One "finished topic" rule, two consumers.
apps/cli/topics.ts(coreSet(): everyset:coresub-issue closed, zero core = never) is read byclose-topics.ts(closes the topic issue) andrelease-topics.ts(thereleasejob ofclose-solved.yml). The release reads the sub-issues, not the topic issue, so the push that finishes a topic releases it in the same run instead of waiting for Close Topics to chain after it. Version = timever (YYYYMMDD.HHMMSS.NNNNNNNNN, UTC) of the instant the last core problem closed; the tag is annotated and backdated to that instant, on the last first-parent main commit before it; the body is a changelog snapshot at that instant (a one-sentence summary first, then the problems closed on the finish day / earlier, with solution links at the tag — problems still open at the finish are left out). A release is found by its<!-- release-topics: #N -->body marker, never by tag name, so re-closing a core problem never mints a second one; an existing release keeps its tag and has its name (the topic title) and body rewritten whenever the render changes. The docs/changelogis ONE custom page,apps/docs/pages/changelog.astro(a NeonUI-style timeline: date left, a card per release right, no per-release subpages, so nothing of it enters the sidebar or prev/next), linked from the header by anavigation.tabsentry (beside/appendix's).apps/docs/lib/releases.tsfetches the releases at build (GITHUB_TOKENindeploy.yml; a failed fetch fails the build), dates each by its timever tag, notpublished_at(which the API cannot backdate), tags itTopicorReviewfrom the release name, and lists the rows of its### Closed …tables — so the tag format and that table shape are the page's contract withrender(). There is no changelog RSS feed. - Review issues re-parent problems; topics still own them. The transfer phase's 20
Review <N> — <Phase>issues (labelreview, milestoneReview — Transfer & Mocks; not the Worker'sReview — Week Ngate issues) hold open optional/deferred problems as sub-issues in solve order, moved there withreplace_parent.apps/api/src/reviews.tsis the ONE recovery of ownership —readReviews()(one GraphQL request per 50reviewissues, cheap enough for the Worker's cron subrequest budget) reads each moved problem's topic off its lastparent_issue_removedevent naming a topic,movedFromTopics()groups them — shared by the Worker's catalog reconcile (a moved problem's D1 row keeps syncing its labels andtopic_issue) andapps/cli/topics.ts(readProblems()returns a topic's sub-issues plus its moved problems, so the finished rule and existing release bodies do not change when a problem moves). A Review issue finishes bycompletion()intopics.ts— every sub-issue closed, any set — whichclose-topics.tscloses it on andrelease-topics.tsreleases it on, exactly like a topic: timever of the last close, same<!-- release-topics: #N -->marker (issue numbers never collide), body fields from the review's**Scheduled:**/**Phase:**/**Load:**lines. - Review days are plan days.
schedule.jsonbooks each Review issue on its own date — Mon/Tue/Thu/Fri only; Reviews 1–6 ran Oct 1–9, Review 4 closed out Nov 2 after the break, and Reviews 7–20 run through the sprint (Nov 6 → Dec 1, Thanksgiving left unbooked), followed by two sweep days and agate: []Saturday that marks the last day — asreview: { issue, title, problems }(solve order). The digest renders it as a linked "Today's review" section with the issue's day rules (REVIEW_RULESinemail.tsx, one sentence for HTML and text), its problems joinRESERVEDso drills and sampled gates never draw them early, andgate: []skips a Saturday's checkpoint (Oct 3, Dec 5). The issue's sub-issues stay the truth: the catalog reconcile compares every booked review's title and problem list with GitHub and returns any mismatch asreviewDrift(also logged), so a re-arranged Review issue shows up the next sync instead of silently disagreeing with the email. Move a review by editing itsschedule.jsondate AND the issue's**Scheduled:**line (the release body reads it).
Key Directories
| Path | Purpose |
|---|---|
work/<1|3|7>/<Difficulty>/<Category>/<num>.<slug>.{js,py} |
Solutions, e.g. work/1/Easy/Array/1.two-sum.py. The top directory is the spaced-repetition bucket: 1 = first solve, 3/7 = the blind re-solves of that problem 3 and 7 days later. Machine-parsed header comment (title / Difficulty: / URL / ─ rule / statement) — preserve its exact shape |
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/, pages/, components/ + components.ts, islands/, public/). content/(0N-<phase>)/<topic>/(problems)/<num>_<slug>.mdx plus one meta.ts per phase and per topic folder, all generated by sync (a phase's once, then hand-owned): the phase folder is a (group) (sidebar section, no route segment), the topic folder is the README campaign topic slug (tree-dfs/, heap-priority-queue/) and a real route segment, and (problems) is a second (group) inside it, so tree-dfs/(problems)/226_invert-binary-tree.mdx routes to /tree-dfs/invert-binary-tree (Blume sorts by the <num>_ prefix and strips it from the route). The meta.ts files carry the exact labels, phase/topic numbers and icons so the sidebar reads in curriculum order; every topic meta is display: "page", so a topic is one sidebar row whose panel holds its landing page and a "Problems" section. A problem page's badge row (between the description and the body) is rendered, not written: the MDX wrapper (components/ProblemBadges.astro, registered as mdx.wrapper in components.ts — Blume's page template renders the title and description itself with no slot before the body, so the wrapper, which MDX hands no route, reads it off Astro.url) reads the page's topics: (LeetCode's topic tags, from sync) and video: (NeetCode YouTube id, from bun run videos) frontmatter — both declared via frontmatter.extend in blume.config.ts, hand-rolled Standard Schemas, no zod — and links a tag the campaign teaches to its topic landing page (filled), any other tag to its card on the Appendix glossary /appendix#<slug> (outlined; LeetCode's problem list if the tag has no term, gray and iconless), and the video as a red Explanation badge. Every badge in the row is a pill. A tag badge carries the icon its target shows (a landing's: the topic meta.ts icon read off data.navigation; an Appendix term's: its icon in appendix.ts) and the tag's own colour from lib/tag-colors.ts — unique per tag site-wide, assigned greedily over every tag with a landing or a term so tags that share problem pages sit furthest apart in OKLab, in a lightness band that keeps ≥ 3:1 contrast in both themes (Blume's Badge takes one hex for both). The reverse direction is the PageFooter slot (components/BacklinksList.astro): an Obsidian-style <num> <name> list under every topic landing page of the problems whose badges link there, plus a topic's own folder members (so a topic no tag names, hybrid-structures, still lists its problems), in a native <details> open on load whose top-right chevron collapses it (no JS); problem pages are recognised by their <num>. <name> title and render no list. The sidebar is Blume's default nav tree (no Sidebar slot override). The custom pages /progress, /changelog and /appendix are header navigation.tabs (on mobile, Blume lists them at the top of the drawer); never navigation.featured, which Blume renders above the nav tree. Which tags the campaign teaches is ONE map, apps/docs/topic-landings.ts (tag slug → landing routes), and where a badge points is ONE function beside it, tagTarget() — read by both components and the Appendix page (so badges, backlinks and term cards cannot disagree) and the map by sync. Every other tag a problem carries is ONE hand-written table, apps/docs/appendix.ts (APPENDIX: slug → { name, icon, category, definition }), rendered by ONE custom page, pages/appendix.astro — a glossary with an A–Z letter index and a filter box (an <appendix-filter> custom element over static markup; no JS, the full list), one card per term (icon tinted the tag's colour, category, definition, LeetCode list link, and a <details> "Solved" list of the problem pages whose badge tagTarget() sends there, paged PAGE_SIZE (5) rows at a time by numbered buttons under it — a <solved-pages> custom element; without JS every row shows and the buttons stay hidden), each card id'd by slug and highlighted as :target. It is no content folder, so it enters neither the sidebar tree nor prev/next: its header tab is its link. Sync writes nothing for it — it warns about a carried tag with no term, a term whose tag a topic now teaches (delete it), and a term icon that is a TOPIC_ICONS value or another term's. The former per-term pages /appendix/<slug> are exact redirects to /appendix#<slug> in blume.config.ts, generated from APPENDIX's keys. A hand-written (0N-<phase>)/<topic>/index.mdx (hidden: true) is that topic's landing page: Blume ≥ 1.7.3 links the sidebar group row to it (sliding-window/index.mdx → /sliding-window) and the hidden flag drops the duplicate row. Two kinds of custom MDX component, both import-free on every page: islands/<Name>.tsx is hydrated React (auto-discovered; DependencySpine — the README topic map as data, zoom/pan, nodes link to whatever route Blume's /api/docs/navigation.json reports for the topic folder, inert until a topic has pages), components/<Name>.astro registered in components.ts is static markup plus a custom element for behaviour, the way Blume's own Frame is built (Carousel/CarouselImage: src takes a URL, /file.png, or the page-relative ../../public/file.png) |
apps/api/ |
Cloudflare Worker workspace: src/ modules, data/schedule.json (the plan: `date → { topics |
.github/workflows/ |
deploy (docs → Pages with GITHUB_TOKEN for the releases-backed changelog, Worker → Cloudflare with the OPENROUTER_API_KEY Actions secret put into it first, on every main push), close-solved (issues + /admin/solved + Project Set/Difficulty, needs SRS_ADMIN_KEY and PROJECT_PAT; then the release job: timever tag + release per finished topic, contents: write), close-topics, sync-d1 (issue edits → /admin/reconcile), spaced-repetition (daily 06:00 ET cron → today's review issue; GITHUB_TOKEN only, no secrets) |
.githooks/ |
Tracked git hooks; bun install points core.hooksPath here (root prepare script, no husky). pre-commit runs bun run sync and stages only what sync changed (pages, apps/docs/redirects.ts), so a solution, its page and its /<num> short link land in one commit; it refuses to run while work/1 has anything unstaged (sync reads the tree, so the page would not match the commit), and leaves pre-existing dirt under apps/docs/content (in-progress ## Approach prose) unstaged. git commit --no-verify for a deliberate partial commit |
Development Commands
All from the repo root (a Bun workspace over apps/*):
bun run pick # scaffold a solution via leetcode-cli (fuzzy picker; Tab cycles the
# sections: new problems → work/1, then the owed 3-day and 7-day
# re-solves → work/3, work/7. Each section hides what its own bucket
# already holds; index+descriptions cached in
# ~/.local/share/leetcode/data.db — see apps/cli/db.ts)
bun run stats # terminal dashboard over GET /api/stats — four panes (Overview,
# Ladder, Concepts, Activity); Tab/1-4 switches, ↑↓ scrolls,
# r refetches, q quits. Outside a TTY it prints every pane once.
# SRS_API points it at a local Worker like close-solved does.
bun run test # run ONE solution against LeetCode's judge (not a test suite;
# files git sees as modified/untracked first, so Enter runs the
# one in progress; then most recently tested; stamped on exit 0)
bun run submit # submit ONE solution to LeetCode (uncommitted first, then newest
# scaffold, nothing hidden; last_submitted stamped on exit 0).
# Both fold in the signed-in profile's submission history once
# a day (leetcode-cli's keychain login; GraphQL only — LeetCode's
# REST /api/* answers 403), so a lost db or fresh clone recovers
# both orderings
bun run sync # work/1 → apps/docs/content/ pages + apps/docs/redirects.ts (/<num> short
# links; re-solves are never published)
bun run videos # docs pages with no `video:` frontmatter → NeetCode video per page
# (channel search scraped, no API key; --write adds the
# `video:` key; "no NeetCode match" pages stay untouched)
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 release-topics -- --dry-run # prints every would-be topic release body
# close-solved also pushes the solved set to the Worker; point it at a local
# one and nothing production is touched:
SRS_API=http://localhost:8787 SRS_ADMIN_KEY=$LINK_KEY bun run close-solved -- --dry-run
# Project Set/Difficulty from the issue labels (needs a `project`-scoped token;
# runs as the last step of close-solved.yml in CI):
GH_TOKEN=$PROJECT_PAT bun run sync-project-fields -- --dry-run
# today's review issue from the +3d/+7d closes (--date backfills a missed day;
# a Sunday date writes nothing — the rest day has no assignment):
bun run spaced-repetition -- --dry-run --date=2026-08-31
bun run api:dev # wrangler dev on :8787 (local D1)
bun run api:test # DST guards + the rest-day calendar sweep
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 (
apps/cli/close-*.ts) use shebang + top-levelawait. Libraries (apps/cli/github.ts,apps/cli/report.ts, everything inapps/api/src/) are side-effect-free on import — nothing touches network/credentials until a factory (github(),projectMirror()) is called. - Shared plumbing:
github()givesrepo,api()(throwsMETHOD path -> status body),list()(paginating async generator),closeIssue()(comment first, then close — a failed PATCH still leaves a trace).report.tsgivesprintTable/markdownTable/writeStepSummary. - Narrow API payloads with guards, not inline casts:
if (!("number" in value) || typeof value.number !== "number") return;(seereadProblemIssueinclose-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=(theSRS_TODAYequivalent) andwrangler devruns against local D1 — exercise logic there before touching production state. - Dates: always ET calendar strings (
YYYY-MM-DD), arithmetic anchored at noon UTC (atNooninapps/api/src/srs.ts) to dodge DST. Nevernew Date()math directly. - Style: section-divider comments (
// ── name ───), file-top doc comments explaining the why and invariants, 2-space JSON with trailing newline,Mapfor dynamic keys /Recordfor static tables, no tiny one-expression wrapper functions. - Known intentional duplication:
sync.tskeeps its own header split instead of sharingapps/cli/source.ts, becausesync.tsruns its whole pipeline on import and so cannot be imported from. Don't "deduplicate" it.close-solved.tsandwork.tsdo share it — the stub test decides both "close the issue" and "owes a review", and the two must never disagree.
Important Files
apps/api/src/srs.ts— SRS domain: the ladder (new → +3 → +7 → retired; fail resets to+3; first-ever log enters at+3),WINDOWS/STAGES/TEMPERATURES/DIFFICULTIES/SETS(the one vocabulary shared by the charts, thework/<n>buckets, the review issues and the stats dashboard), ET date math,workingDay()(no scheduled date is ever a Sunday),temperatureOf()(days since a concept was last exercised →hot/fresh/fading/cold, bands derived fromWINDOWSso they cannot drift; read-side only, nothing is scheduled off it), seeded sampling,logAttempt()(the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at mostREVIEW_CAP(5 since the break, to drain its backlog within the sprint) reviews surface per day —levelReviews()(run bysendDigeston real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 5 per day, oldest first, instead of letting the due pile grow.apps/api/src/index.ts— router + cron dispatch;apps/api/wrangler.jsonc— bindings (DB,EMAIL), crons, vars; secretsGH_PAT/WEBHOOK_SECRET/LINK_KEYviawrangler secret put, andOPENROUTER_API_KEY, whose one copy is the repo's Actions secret (deploy.yml puts it before every deploy — rotate it there).apps/api/src/ask.ts—POST /ask, the docs assistant's backend. Blume's own assistant route is server-rendered and cannot ship in the static Pages build (BLUME_SERVER_FEATURE_REQUIRED), soblume.config.tssetsai.assistant.endpointto this route instead of aprovider. It speaks the Blume island's wire contract ({ messages, page: { path } }in, a plain-text stream out, a 503 "The assistant is not configured…" when the key is missing), validates like Blume's generated route (1–40 user/assistant messages, 24 000 chars, 64 KB — no caller system prompt), grounds on the publishedllms-full.txt(current page first, then keyword-ranked excerpts in Blume's 6 × 2 000 / 10 000-char budget, cached 5 min), and streams OpenRouter's chat completions over plainfetch— no AI SDK — for theASK_MODELvar.:freemodels share an upstream pool and often answer 429, which the island shows as its rate-limit notice.apps/api/src/digest.ts/apps/api/src/email.tsx— the daily digest, split data/presentation.digest.tsreads D1 into aDigestData;email.tsxowns every colour and every sentence, and renders both the HTML and (via its ownplainDigest, not React Email'splainTextmode, which flattens the tables) the text alternative. The retrieval rules hold by construction:DigestRowhas no title and no issue field, so a review or drill line cannot leak the topic or a solution link. Subject is(Day N/<SPRINT_DAYS>) LeetCode Daily Digestand the progress lineDay N of <SPRINT_DAYS> · Week k(Mon–Sun weeks, so the Friday start makes week 1 three days long) — 30 with the sprint running Nov 6 → Dec 5; before day 1 they count down ((Sprint in N days)). Booking a later day inschedule.jsonlengthens the sprint, and the whole-campaign heatmaps and/api/statsheat(still fromCAMPAIGN_START) with it.apps/api/src/png.ts— hand-rolled PNG encoder (RGB8, one IDAT, zlib viaCompressionStream("deflate"), CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so/chart/heatmap.pngrasters the heatmap for the digest while/chart/heatmap.svgkeeps serving the README byte-for-byte. Both come from oneheatmapCells()so the two pictures cannot drift.apps/cli/sync.ts— work→docs contract: on existing pages only the frontmattericon:and the## Solutionsection are regenerated, andtopics:(LeetCode'stopicTagsas{ name, slug }, fetched by GraphQL) andissue:(the URL of the problem'sproblem-labelled GitHub issue,LC <num> · …, listed viagithub()— rendered as the neutralIssue #Nbadge afterExplanation) are filled once — on create, or on any run that finds them missing — never refreshed (delete a key to refetch; offline or withoutghauth, the page is written without it and the next sync retries; a sync with nothing missing makes no request). Only a problem the README plan lists getsissue:— an unplanned warm-up has no catalog issue, so it is never looked up.companies:({ name, period, frequency }: every company that asked the problem in the last six months, at the most recent of theThirty Days/Three Months/Six Monthswindows it appears in and its frequency there, most recent first — the vocabulary is ONE module,apps/docs/companies.ts, shared by sync, the schema and both readers) is the exception that IS rewritten every run, since recency ages: it comes from the local DuckDB company-frequency database (~/.local/share/duckdb/data/leetcode-company-frequency.db, built by that project'smise run lc:build) via oneduckdb -readonly -jsonquery, and where the database or theduckdbCLI is missing (CI) the step is skipped and pages keep the key as it is.ProblemBadges.astrorenders it as a TypeTable-style table under the badge row (company pill · asked within · frequency bar), each pill linking tocompanyHref()— the company's Appendix card, filed A–Z among the glossary terms (categoryCompany, no definition, no hand-written table: derived from every page'scompanies:), whose Solved backlinks are the problems it asked, each with how recently.## Solutionis the latter bounded bysolutionEnd()at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside## Solution. Two classifications, two homes: the sidebar group is the README topic (readmeTopics(), first listing wins for a problem under two topics; a warm-up outside the plan groups under its category atorder: 0, ahead of topic 1) and thework/1/<Difficulty>/<Category>/category, which only places an unplanned problem — the page's topic badges are LeetCode's tags, not either classification. Topics nest under their README phase (## Phase N — Nameheadings, shortened viaPHASE_ALIASES):content/(0N-<phase>)/<topic>/, with every problem page in the topic's(problems)/group as<num>_<slug>.mdx(a page is found by its frontmatter title, wherever it sits, so a layout change is one sync ofrenames). The topicmeta.tsis script-owned and rewritten every run (label, curriculum order,TOPIC_ICONSicon,display: "page"); the phasemeta.tsis written once when missing (Phase N: Name,order: N) and hand-owned after — rename its title freely. Never rewriting it loses nothing: its order and name are also the folder name, so a README change to either lands in a new folder with a fresh meta. The phase folder is a Blume(group)— no route segment, so/tree-dfsis unchanged and a landing page there is one level deep, never at/— and the topic folder is the real segment; never put a topicindex.mdxdirectly inside a(group). A page isrenamed when its topic or phase changes; a folder every page has left is deleted (climbing through an emptied(problems)and topic into an emptied phase folder) unless it still holds a hand-writtenindex.mdx(sync never writes, moves, or deletes one — so page-relative asset paths in a moved landing page are yours to fix). Slugs come fromslugify(), which glues a leading number to the next token (1d-dynamic-programming) because Blume strips<digits>-prefixes from routes. The hand-owned parts have their own owners:## Approach(a Mermaid flowchart tracing the page's code, placed before## Solutionso a re-sync keeps it) is written per theproblem-approachskill (/problem-approachfills every page missing one); the frontmattervideo:(the NeetCode walkthrough, rendered as the redExplanationbadge) is added bybun run videos --write(apps/cli/videos.ts).apps/cli/work.ts— thework/layout: bucket paths,isSolution()/solvedInBucket()(the stub test, shared withclose-solved.ts),firstSolved()(onegit log, ET dates, keyed by LC number so past layout moves don't matter), andreviewQueues()(what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.apps/api/src/stats.ts/apps/cli/stats.ts/apps/cli/tui.ts/apps/cli/api.ts/apps/docs/lib/stats.ts— the read side.buildStats()is the ONE aggregation: phases, ladder, difficulty,attempting(open misses, viaMISS_WHEREinsrs.ts— the sweep day's definition), per-topic temperature, gates, the 14-day due queue,heat(every campaign day, zeros and future days included — the TUI windows its tail rather than asking a second question),recent(the ten problems most recently solved, dated by each one's first pass) andyear(the 365 days ending today — always the calendar year to date — each split by difficulty into attempts and first solves; the docs cards window and bucket it themselves). Two readers consume it, the docs/progressislands andbun run stats, so its shape is their contract: change the Worker and both readers together (additive fields a reader ignores are safe).tui.tsis hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped orNO_COLORdashboard still reads. The TUI recomputes nothing: the only local fact it shows is thework/solution count, which is inventory, not schedule.README.mdlive charts are Worker endpoints (/chart/*.svg,/badge/gate.svg, 5-min Camo cache). The docs/progresspage is a custom page,apps/docs/pages/progress.astro(no content folder, so outside the sidebar and prev/next; its header tab is its link), mounting five LeetCode-style React cards fromcomponents/progress/withclient:load— imported by the page, never used from MDX, so they are not inislands/—SolvedRing(difficulty gauge + "Attempting"),SubmissionHeatmapandSubmissionChartsharing one row (the heatmap: the last six months in month blocks, the README's colour ramp as theme.css--heat-0..4, its attempts / active days / max streak counted from the cells it draws; the chart: this calendar year as stacked Easy/Med./Hard bars per month or week, first solves or every attempt, via two shadcnToggleGroups),TopicBubbles(the campaign topics circle-packed byd3-hierarchy, filled by solved share),RecentSolves(the ten most recent, linked throughredirects.ts) — composed from shadcn/ui (components/ui/{card,chart,skeleton,toggle,toggle-group}.tsx, Recharts underchart, Radix under the toggles) on Blume's tokens:apps/docs/theme.cssmaps shadcn's names onto--blume-*(neveraccent, which is Blume's brand colour — vendored components that use it, liketoggle.tsx, are retokened tobackground/muted) and gives shadcn'sdata-slotparts their border colour. They sharelib/stats.ts(useStats()), so the page makes ONE/api/statsrequest however many cards hydrate. shadcn sources live outsideislands/(every.tsxthere becomes an island) and importlib/utilsrelatively;components.json+tsconfig.jsonexist for the shadcn CLI andtsc, so aftershadcn addrewrite@/…imports to relative ones (house style) — and checkpackage.json: the CLI has twice installed an unrelatedcnpackage forlib/utilsand skipped real deps (class-variance-authority). Blume turns every tsconfigpathstarget into a Vite alias AND a dev dep-scan glob (<target>/**/*.astro…), sopathsmaps only@/components,@/lib,@/hooks— each bare and/*, sincecomponents.jsonnames the bare forms — never@/*→./*: that glob reaches throughnode_modules/blume, where Vite's scanner reads a<script>inside aRootLayout.astrocomment as a tag andblume devskips dependency pre-bundling. Blume's.prose :where(a, p, ol)rules ignorenot-prose, so island markup overrides them with!utilities, as Blume's own components do.apps/docs/blume.config.ts— site base/leetcode; importsapps/docs/redirects.ts, sync-generated (/<num>→/<topic>/<slug>, one row per page, exact root-relative paths — Blume adds the base and, for the static Pages build, emits a meta-refresh page per rule plusblume-redirects.json);README.mdcampaign table doubles as topic-map input tosync.ts(readmeTopics(),TOPIC_ALIASES,PHASE_ALIASES).- Old
.github/srs/JSON state, itssrs-*Actions, and the one-shotimport-srs.tsimporter are RETIRED and deleted — do not resurrect; git history is the record.
Runtime/Tooling Preferences
- Bun only: run scripts with
bun apps/cli/<name>.ts(or the rootbun runaliases), install withbun install --frozen-lockfile. One workspace (apps/*), one rootbun.lock. Node 22 appears only indeploy.ymlbecause Blume requires it. - Near-zero runtime npm dependencies — SVG, PNG, terminal ANSI, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins +
fetchonly. Two exceptions, each confined to one surface. The digest email:apps/api/src/email.tsxrenders 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 nonodejs_compat(it resolves torenderToReadableStream), and requires"jsx": "react-jsx"inapps/api/tsconfig.json. And the docs/progressislands: shadcn/ui (recharts,radix-ui,class-variance-authority,clsx,tailwind-merge) plusd3-hierarchyfor the bubble packing, client-side inapps/docsonly. Do not extend either 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 testsubmits one solution to LeetCode's judge. Exception:apps/api/src/srs.test.ts(bun:test) proves the DST cron guards with fixed dates; run viabun 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,cmpoutput. - Docs changes:
bunx blume build --isolated(fromapps/docs/) must pass with no new warnings (thenrm -rf apps/docs/.blume-verify); after sync, review only created pages (prose transforms are heuristic).