From f34a9bb56d774c7ad0e18489ed22b4bcb27f5d4f Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 24 Sep 2026 16:15:59 -0400 Subject: [PATCH] ci(git): sync redirects in pre-commit hook --- .githooks/pre-commit | 13 +++++++------ AGENTS.md | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 568e464..95c1620 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,8 +1,9 @@ #!/bin/sh -# Pre-commit: the docs pages under apps/docs/content are generated from work/1 -# by `bun run sync`, and deploy.yml publishes whatever main holds. Regenerate -# them now and stage what sync changed, so a solution and its page land in the -# same commit and a stale `## Solution` never reaches main. +# Pre-commit: the docs pages under apps/docs/content (and the `/` short +# links in apps/docs/redirects.ts) are generated from work/1 by `bun run sync`, +# and deploy.yml publishes whatever main holds. Regenerate them now and stage +# what sync changed, so a solution, its page and its redirect land in the same +# commit and a stale `## Solution` never reaches main. # # Sync reads the working tree, not the index, so the pages it writes only # match this commit when work/1 has nothing unstaged: an untracked or @@ -26,9 +27,9 @@ fi before=$(mktemp) trap 'rm -f "$before"' EXIT -git status --porcelain -- apps/docs/content | sort > "$before" +git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort > "$before" report=$(bun run --silent sync) -generated=$(git status --porcelain -- apps/docs/content | sort | comm -13 "$before" - | cut -c4-) +generated=$(git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort | comm -13 "$before" - | cut -c4-) if [ -n "$generated" ]; then printf '%s\n' "$report" | grep -v 'skipped (in sync)' >&2 printf '%s\n' "$generated" | xargs git add -- diff --git a/AGENTS.md b/AGENTS.md index 70a687e..6246da6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,7 +47,7 @@ flowchart LR | `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `components/` + `components.ts`, `islands/`, `public/`). `content/(0N-)//-.mdx` plus one `meta.ts` per phase and per topic folder, all generated by sync: 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 (`/tree-dfs/invert-binary-tree`), the `meta.ts` files carry the exact labels, phase/topic numbers and icons so the sidebar reads in curriculum order with each topic a collapsible group, and the page badge is the `work/` data-structure category. A hand-written `(0N-)//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/.tsx` is hydrated React (auto-discovered; `ProgressDashboard`, `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/.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 | problems | gate | sweep }`, human-edited, bundled at deploy), `migrations/` (append-only; `0002` is the +3/+7 ladder rebuild) | | `.github/workflows/` | deploy (docs → Pages, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`), 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 the pages sync changed, so a solution and its page 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 | +| `.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 `/` 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 @@ -67,7 +67,8 @@ bun run test # run ONE solution against LeetCode's judge (not a test # most recently tested first, stamped in the db on exit 0) bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing # hidden; last_submitted stamped in the db on exit 0) -bun run sync # work/1 → apps/docs/content/ pages (re-solves are never published) +bun run sync # work/1 → apps/docs/content/ pages + apps/docs/redirects.ts (/ short + # links; re-solves are never published) bun run videos # docs pages with no embed → NeetCode video per page # (channel search scraped, no API key; --write appends the # `## Explanation` embed; "no NeetCode match" pages stay untouched) @@ -110,7 +111,7 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \ - `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (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` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, per-topic temperature, gates, the 14-day due queue, and `heat` (every campaign day, zeros and future days included — the docs page windows the tail off it rather than asking a second question). Two readers consume it, the docs island and `bun run stats`, so its shape is both their contract: change all three together. `tui.ts` is 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 or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule. - `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`, `PHASE_ALIASES`). +- `apps/docs/blume.config.ts` — site base `/leetcode`; imports `apps/docs/redirects.ts`, sync-generated (`/` → `//`, 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 plus `blume-redirects.json`); `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`, `PHASE_ALIASES`). - Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record. ## Runtime/Tooling Preferences