mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
feat(cli): add company frequency frontmatter to docs sync
This commit is contained in:
1 parent
8e07899157
commit
c753937852
1 file changed
+93
-1
+93
-1
@@ -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,9 +903,12 @@ 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(
|
||||
const next = ensureCompanies(
|
||||
await ensureIssue(
|
||||
await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol),
|
||||
sol,
|
||||
),
|
||||
sol,
|
||||
);
|
||||
const actions: string[] = [];
|
||||
if (existing !== name) {
|
||||
@@ -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"]);
|
||||
|
||||
Reference in new issue
Block a user