feat(cli): add company frequency frontmatter to docs sync

This commit is contained in:
Prad Nukala committed 2026-10-14 15:51:56 -04:00
1 parent 8e07899157
commit c753937852
1 file changed
+94 -2
+94 -2
View File
@@ -27,6 +27,15 @@
* (never on a sync with nothing new); offline, or with no `gh` auth, the
* page is written without it and the next sync retries. Delete the key to
* refetch it.
* - Every page carries the companies that asked it in the last six months as
* frontmatter `companies:` (`{ name, period, frequency }`, the most recent
* window each company falls in and its frequency there, most recent first;
* `apps/docs/companies.ts`), read from the local DuckDB
* company-frequency database (`COMPANY_DB`, built by its own `mise run
* lc:build`) with the `duckdb` CLI. Unlike the keys above it is rewritten
* every run, since recency ages and a rebuilt database should show; a
* problem no company asked lately carries no key. Where the database or the
* CLI is missing (CI), the step is skipped and pages keep the key as is.
* - Every tag a page carries that no campaign topic teaches (not a key of
* `apps/docs/topic-landings.ts`) belongs in the Appendix glossary,
* `apps/docs/appendix.ts`, which is hand-written: sync only checks it,
@@ -63,10 +72,13 @@
* `apps/docs/redirects.ts`, which `blume.config.ts` imports — Blume takes exact
* root-relative paths and prepends the deployment base itself.
*/
import { existsSync } from "node:fs";
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { APPENDIX } from "../docs/appendix.ts";
import { RECENCY, type Company } from "../docs/companies.ts";
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
import { github } from "./github.ts";
@@ -506,6 +518,62 @@ async function issueUrl(sol: Solution): Promise<string | undefined> {
return url;
}
// ── company frequency ────────────────────────────────────────────
// The windows and the key's shape are `apps/docs/companies.ts`, shared with
// the schema and the readers; `More Than Six Months` and `All` are not recent.
const COMPANY_DB = join(
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
"duckdb",
"data",
"leetcode-company-frequency.db",
);
/**
* LC number → the companies that asked it in the last six months, each at the
* most recent window it appears in (the windows nest), ordered most recent
* first, then by that window's frequency. Undefined when this machine has no
* database or no `duckdb` CLI — the step is skipped — or the query fails
* (warned). One query for every solved problem.
*/
function recentCompanies(nums: number[]): Map<number, Company[]> | undefined {
if (!existsSync(COMPANY_DB) || !Bun.which("duckdb")) return undefined;
const order = `list_position([${RECENCY.map((p) => `'${p}'`).join(", ")}], period)`;
const sql = `SELECT question_id, company, period, frequency FROM company_frequency
WHERE question_id IN (${nums.join(", ")}) AND ${order} IS NOT NULL
QUALIFY row_number() OVER (PARTITION BY question_id, company ORDER BY ${order}) = 1
ORDER BY question_id, ${order}, frequency DESC, company`;
const proc = Bun.spawnSync(["duckdb", "-readonly", "-json", COMPANY_DB, "-c", sql]);
if (!proc.success) {
console.warn(`no companies: duckdb -> ${proc.stderr.toString().trim()}`);
return undefined;
}
const out = proc.stdout.toString().trim();
const rows: unknown = out ? JSON.parse(out) : [];
const companies = new Map<number, Company[]>(nums.map((n) => [n, []]));
for (const row of Array.isArray(rows) ? rows : []) {
if (!row || typeof row !== "object") continue;
if (!("question_id" in row) || typeof row.question_id !== "number") continue;
if (!("company" in row) || typeof row.company !== "string") continue;
if (!("period" in row) || !("frequency" in row) || typeof row.frequency !== "number") continue;
const period = RECENCY.find((p) => p === row.period);
if (period)
companies.get(row.question_id)?.push({ name: row.company, period, frequency: row.frequency });
}
return companies;
}
/** The frontmatter `companies:` block, one flow mapping per company (JSON strings are valid YAML). */
function companiesYaml(list: Company[]): string {
return [
"companies:",
...list.map(
(c) =>
` - { name: ${JSON.stringify(c.name)}, period: ${JSON.stringify(c.period)}, frequency: ${c.frequency} }`,
),
].join("\n");
}
// ── inline-code formatting heuristics ────────────────────────────
/** Identifiers worth backticking in prose, harvested from the code. */
@@ -624,6 +692,7 @@ function renderPage(
sol: Solution,
tags: TopicTag[] | undefined,
issue: string | undefined,
companies: Company[] | undefined,
): string {
const h = sol.header;
const ids = identifiers(sol);
@@ -639,6 +708,7 @@ function renderPage(
`icon: ${PAGE_ICON}`,
...(tags ? [topicsYaml(tags)] : []),
...(issue ? [`issue: ${JSON.stringify(issue)}`] : []),
...(companies?.length ? [companiesYaml(companies)] : []),
"sidebar:",
` label: '${h.title.replaceAll("'", "''")}'`,
` badge: '${h.difficulty}'`,
@@ -704,6 +774,7 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
}
const topics = await readmeTopics();
const companies = recentCompanies([...solutions.keys()]);
// An unplanned problem whose work/ category is also a README topic name (a
// `Hash Table` warm-up) files under that topic, in its phase, not a second
// root-level `hash-table/`.
@@ -753,6 +824,23 @@ async function ensureIssue(page: string, sol: Solution): Promise<string> {
);
}
/**
* Replace the frontmatter's `companies:` with this run's list, inserted before
* `sidebar:` (dropped when no company asked lately). Without the database the
* page is returned untouched, so an environment lacking it never strips the key.
*/
function ensureCompanies(page: string, sol: Solution): string {
if (!companies) return page;
const fm = page.match(/^---\n[\s\S]*?\n---/);
if (!fm) return page;
const list = companies.get(sol.num) ?? [];
const without = fm[0].replace(/^companies:.*\n(?: {2}- .*\n)*/m, "");
const next = list.length
? without.replace(/^sidebar:$/m, `${companiesYaml(list)}\nsidebar:`)
: without;
return next + page.slice(fm[0].length);
}
/**
* Offset just past the script-owned `## Solution` section: the next top-level
* heading that is not inside a code fence, or end of file. Sections a human
@@ -815,8 +903,11 @@ for (const sol of ordered) {
const tail = page.slice(solutionEnd(page, at));
const body =
page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
const next = await ensureIssue(
await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol),
const next = ensureCompanies(
await ensureIssue(
await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol),
sol,
),
sol,
);
const actions: string[] = [];
@@ -836,6 +927,7 @@ for (const sol of ordered) {
sol,
await fetchTopicTags(sol),
await issueUrl(sol),
companies?.get(sol.num),
);
await Bun.write(join(DOCS, name), page);
report.push([workRef, name, "created"]);