mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
feat(cli): add company frequency frontmatter to docs sync
This commit is contained in:
1 parent
8e07899157
commit
c753937852
1 file changed
+94
-2
+94
-2
@@ -27,6 +27,15 @@
|
|||||||
* (never on a sync with nothing new); offline, or with no `gh` auth, the
|
* (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
|
* page is written without it and the next sync retries. Delete the key to
|
||||||
* refetch it.
|
* 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
|
* - 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/topic-landings.ts`) belongs in the Appendix glossary,
|
||||||
* `apps/docs/appendix.ts`, which is hand-written: sync only checks it,
|
* `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
|
* `apps/docs/redirects.ts`, which `blume.config.ts` imports — Blume takes exact
|
||||||
* root-relative paths and prepends the deployment base itself.
|
* 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 { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
|
||||||
|
import { homedir } from "node:os";
|
||||||
import { basename, dirname, join } from "node:path";
|
import { basename, dirname, join } from "node:path";
|
||||||
|
|
||||||
import { APPENDIX } from "../docs/appendix.ts";
|
import { APPENDIX } from "../docs/appendix.ts";
|
||||||
|
import { RECENCY, type Company } from "../docs/companies.ts";
|
||||||
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
|
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
|
||||||
import { github } from "./github.ts";
|
import { github } from "./github.ts";
|
||||||
|
|
||||||
@@ -506,6 +518,62 @@ async function issueUrl(sol: Solution): Promise<string | undefined> {
|
|||||||
return url;
|
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 ────────────────────────────
|
// ── inline-code formatting heuristics ────────────────────────────
|
||||||
|
|
||||||
/** Identifiers worth backticking in prose, harvested from the code. */
|
/** Identifiers worth backticking in prose, harvested from the code. */
|
||||||
@@ -624,6 +692,7 @@ function renderPage(
|
|||||||
sol: Solution,
|
sol: Solution,
|
||||||
tags: TopicTag[] | undefined,
|
tags: TopicTag[] | undefined,
|
||||||
issue: string | undefined,
|
issue: string | undefined,
|
||||||
|
companies: Company[] | undefined,
|
||||||
): string {
|
): string {
|
||||||
const h = sol.header;
|
const h = sol.header;
|
||||||
const ids = identifiers(sol);
|
const ids = identifiers(sol);
|
||||||
@@ -639,6 +708,7 @@ function renderPage(
|
|||||||
`icon: ${PAGE_ICON}`,
|
`icon: ${PAGE_ICON}`,
|
||||||
...(tags ? [topicsYaml(tags)] : []),
|
...(tags ? [topicsYaml(tags)] : []),
|
||||||
...(issue ? [`issue: ${JSON.stringify(issue)}`] : []),
|
...(issue ? [`issue: ${JSON.stringify(issue)}`] : []),
|
||||||
|
...(companies?.length ? [companiesYaml(companies)] : []),
|
||||||
"sidebar:",
|
"sidebar:",
|
||||||
` label: '${h.title.replaceAll("'", "''")}'`,
|
` label: '${h.title.replaceAll("'", "''")}'`,
|
||||||
` badge: '${h.difficulty}'`,
|
` badge: '${h.difficulty}'`,
|
||||||
@@ -704,6 +774,7 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const topics = await readmeTopics();
|
const topics = await readmeTopics();
|
||||||
|
const companies = recentCompanies([...solutions.keys()]);
|
||||||
// An unplanned problem whose work/ category is also a README topic name (a
|
// 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
|
// `Hash Table` warm-up) files under that topic, in its phase, not a second
|
||||||
// root-level `hash-table/`.
|
// 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
|
* 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
|
* 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 tail = page.slice(solutionEnd(page, at));
|
||||||
const body =
|
const body =
|
||||||
page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
||||||
const next = await ensureIssue(
|
const next = ensureCompanies(
|
||||||
await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol),
|
await ensureIssue(
|
||||||
|
await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol),
|
||||||
|
sol,
|
||||||
|
),
|
||||||
sol,
|
sol,
|
||||||
);
|
);
|
||||||
const actions: string[] = [];
|
const actions: string[] = [];
|
||||||
@@ -836,6 +927,7 @@ for (const sol of ordered) {
|
|||||||
sol,
|
sol,
|
||||||
await fetchTopicTags(sol),
|
await fetchTopicTags(sol),
|
||||||
await issueUrl(sol),
|
await issueUrl(sol),
|
||||||
|
companies?.get(sol.num),
|
||||||
);
|
);
|
||||||
await Bun.write(join(DOCS, name), page);
|
await Bun.write(join(DOCS, name), page);
|
||||||
report.push([workRef, name, "created"]);
|
report.push([workRef, name, "created"]);
|
||||||
|
|||||||
Reference in new issue
Block a user