From 099123539e416743090c01d159afb0cda80cd850 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Mon, 5 Oct 2026 12:34:17 -0400 Subject: [PATCH] refactor(cli): drop guide page creation and streamline appendix term handling in sync script --- apps/cli/sync.ts | 141 ++++++++++------------------------------------- 1 file changed, 29 insertions(+), 112 deletions(-) diff --git a/apps/cli/sync.ts b/apps/cli/sync.ts index 610a4f6..c617604 100755 --- a/apps/cli/sync.ts +++ b/apps/cli/sync.ts @@ -28,17 +28,12 @@ * page is written without it and the next sync retries. Delete the key to * refetch it. * - Every tag a page carries that no campaign topic teaches (not a key of - * `apps/docs/topic-landings.ts`) gets a placeholder guide page in the - * Appendix, `appendix/.mdx`, with `list:` pointing at LeetCode's - * problem list for the tag. Created once (a missing or empty file), then - * hand-owned except for `sidebar.badge`, which sync keeps at the guide's - * backlink count (the problem pages carrying the tag; dropped at zero); the - * tag's badge on a problem page links to it. A guide may be filed into a - * category `(group)` folder (e.g. `appendix/(2-algorithms)/`) — no route - * segment, so `/appendix/` holds — and is found there by slug; a new - * placeholder lands at the `appendix/` root for a human to file (and give - * an `icon:`, which every badge linking to the guide shows; sync warns - * while one is missing or shared). + * `apps/docs/topic-landings.ts`) belongs in the Appendix glossary, + * `apps/docs/appendix.ts`, which is hand-written: sync only checks it, + * warning about a carried tag with no term (its badge falls back to + * LeetCode's list), a term whose tag a topic now teaches, and a term icon + * that is missing, a topic's (`TOPIC_ICONS`), or another term's — every + * badge linking to the term shows it. * * The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`, * ordered by topic number via a script-owned `meta.ts`). A problem outside the @@ -68,6 +63,7 @@ import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises"; import { basename, dirname, join } from "node:path"; +import { APPENDIX } from "../docs/appendix.ts"; import { TOPIC_LANDINGS } from "../docs/topic-landings.ts"; import { github } from "./github.ts"; @@ -452,56 +448,6 @@ async function issueUrl(sol: Solution): Promise { return url; } -// ── guides ─────────────────────────────────────────────────────── - -/** - * Placeholder for a tag no campaign topic teaches: the `/appendix/` page a - * problem's topic badge links to. `list:` is the tag's LeetCode problem list, - * rendered as a badge by the docs MDX `wrapper`. Written once — the page - * is hand-owned from then on. - */ -function guidePage(tag: TopicTag): string { - return [ - "---", - `title: ${JSON.stringify(tag.name)}`, - `list: ${JSON.stringify(`https://leetcode.com/problem-list/${tag.slug}/`)}`, - "---", - "", - ":::note", - `This guide has not been written yet. The LeetCode list above has every problem tagged ${tag.name}.`, - ":::", - "", - ].join("\n"); -} - -/** - * Folder meta for `appendix/`: a drill-in (`display: "page"`) sidebar row - * sorted ahead of the phase groups, so it sits right under the root pages - * (Introduction, Keywords, Progress). - */ -const APPENDIX_META = `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: "Appendix", icon: "library", order: 0, display: "page" });\n`; - -/** - * A guide's frontmatter with `sidebar.badge` set to its backlink count — the - * one key sync owns on a hand-owned guide — or dropped (with a `sidebar:` left - * empty) at zero, so the sidebar shows a count only where there is something - * to count. - */ -function withBacklinkBadge(page: string, count: number): string { - const fm = page.match(/^---\n[\s\S]*?\n---/); - if (!fm) return page; - let head = fm[0].replace(/^ {2}badge: .*\n/m, ""); - if (count > 0) { - const line = ` badge: '${count}'`; - head = /^sidebar:$/m.test(head) - ? head.replace(/^sidebar:$/m, `sidebar:\n${line}`) - : head.replace(/\n---$/, `\nsidebar:\n${line}\n---`); - } else { - head = head.replace(/^sidebar:\n(?! {2})/m, ""); - } - return head + page.slice(fm[0].length); -} - // ── inline-code formatting heuristics ──────────────────────────── /** Identifiers worth backticking in prose, harvested from the code. */ @@ -756,7 +702,6 @@ const phases = new Map(); // phase folder -> phase, for meta.ts const vacated = new Set(); // folders a page moved out of; removed below if empty const redirects: string[] = []; // `{ from: "/", to: "//" }` rows, numeric order const tags = new Map(); // every tag slug any problem page carries -> tag -const tagged = new Map(); // tag slug -> how many problem pages carry it for (const sol of ordered) { const langs = Object.keys(sol.code).sort().join("+"); @@ -801,10 +746,7 @@ for (const sol of ordered) { report.push([workRef, name, "created"]); written = page; } - for (const tag of frontmatterTopics(written)) { - tags.set(tag.slug, tag); - tagged.set(tag.slug, (tagged.get(tag.slug) ?? 0) + 1); - } + for (const tag of frontmatterTopics(written)) tags.set(tag.slug, tag); // The route drops the phase `(group)` and the page's number prefix. redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`); } @@ -825,48 +767,22 @@ for (const sol of ordered) { } } -// One guide page per tag that no campaign topic teaches, created when missing -// (an empty file counts as missing) and never rewritten. A guide is found by -// slug anywhere under appendix/ — a human files it into a category `(group)`, -// which leaves its route alone — and a new one lands at the appendix/ root. A -// guide whose tag no page carries any more is left alone: it is hand-owned. -const guideTags = [...tags.values()] - .filter((tag) => !TOPIC_LANDINGS[tag.slug]) - .sort((a, b) => a.slug.localeCompare(b.slug)); -const guideFiles = new Map(); // slug -> path relative to DOCS -for await (const rel of new Bun.Glob("appendix/**/*.mdx").scan(DOCS)) guideFiles.set(basename(rel, ".mdx"), rel); -for (const tag of guideTags) { - const rel = guideFiles.get(tag.slug) ?? `appendix/${tag.slug}.mdx`; - const file = Bun.file(join(DOCS, rel)); - if ((await file.exists()) && file.size > 0) continue; - await Bun.write(file, guidePage(tag)); - report.push([`tag ${tag.slug}`, rel, "created"]); +// Every carried tag no campaign topic teaches is an Appendix term, filed by +// hand in apps/docs/appendix.ts: warn about the gaps and the dead weight. A +// term's icon is also the icon on every problem-page badge linking to its +// card, so it must not be a topic group's (`TOPIC_ICONS`), so a term badge +// never reads as a topic's, nor another term's. +for (const tag of [...tags.values()].sort((a, b) => a.slug.localeCompare(b.slug))) { + if (!TOPIC_LANDINGS[tag.slug] && !APPENDIX[tag.slug]) + console.warn(`tag ${tag.slug}: no Appendix term; add ${JSON.stringify(tag.name)} to apps/docs/appendix.ts`); } - -// Every guide's sidebar badge is its backlink count: the problem pages whose -// tag badge links to it, which is every page carrying the tag, since a guide's -// tag has no landing (`tagTarget()`). A guide whose tag a landing has since -// claimed has no backlinks left. Counts move with every solved problem, so the -// badge is rewritten whenever it differs; the rest of the guide is untouched. -// The same pass checks each guide's hand-picked `icon:`, which is also the icon -// on every problem-page badge linking to the guide: it must exist, must not be -// a topic group's (`TOPIC_ICONS`), so a guide badge never reads as a topic's, -// and must not be another guide's. const topicIcons = new Set(Object.values(TOPIC_ICONS)); -const guideIcons = new Map(); // icon -> the guide that took it first -for await (const rel of new Bun.Glob("appendix/**/*.mdx").scan(DOCS)) { - const slug = basename(rel, ".mdx"); - const count = TOPIC_LANDINGS[slug] ? 0 : (tagged.get(slug) ?? 0); - const page = await Bun.file(join(DOCS, rel)).text(); - const icon = page.match(/^---\n[\s\S]*?\n---/)?.[0].match(/^icon: (\S+)$/m)?.[1]; - if (!icon) console.warn(`${rel}: no icon; its badges render without one — file it and add \`icon:\``); - else if (topicIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is a topic's (TOPIC_ICONS); pick another`); - else if (guideIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is ${guideIcons.get(icon)}'s too; pick another`); - else guideIcons.set(icon, rel); - const next = withBacklinkBadge(page, count); - if (next === page) continue; - await Bun.write(join(DOCS, rel), next); - report.push([`${count} backlinks`, rel, "badged"]); +const termIcons = new Map(); // icon -> the term that took it first +for (const [slug, term] of Object.entries(APPENDIX)) { + if (TOPIC_LANDINGS[slug]) console.warn(`appendix ${slug}: a campaign topic teaches it now; delete its term`); + if (topicIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is a topic's (TOPIC_ICONS); pick another`); + else if (termIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is ${termIcons.get(term.icon)}'s too; pick another`); + else termIcons.set(term.icon, slug); } // Phase metas: created when missing (an empty file counts as missing), never @@ -878,12 +794,13 @@ for (const [dir, phase] of phases) { report.push([`phase ${phase.order}`, `${dir}/meta.ts`, "created"]); } -// Script-owned folder metas — one per live topic folder, plus appendix/ — -// rewritten only when they differ. -const metas: [label: string, dir: string, meta: string][] = [ - ...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]), - ...(guideTags.length ? [["appendix", "appendix", APPENDIX_META] as [string, string, string]] : []), -]; +// Script-owned folder metas — one per live topic folder — rewritten only when +// they differ. +const metas: [label: string, dir: string, meta: string][] = [...groups].map(([dir, topic]) => [ + `topic ${topic.order ?? "-"}`, + dir, + groupMeta(topic), +]); for (const [label, dir, meta] of metas) { const path = join(DOCS, dir, "meta.ts"); if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue;