#!/usr/bin/env bun /** * Close the GitHub issue for every LeetCode problem actually solved under work/, * then report the solved set to the SRS Worker so D1 agrees. * * Reconciles state instead of reacting to a push diff: any `problem`-labelled * issue whose LC number has an *implemented* solution file in work/ gets * closed. Only open issues are touched, so re-runs are no-ops and backfilling * needs no special casing. Removing a solution never reopens an issue. * * `bun run pick` scaffolds a statement header plus an empty function body, so * file existence alone means nothing — a stub must not close its issue. See * isImplemented(). * * bun apps/cli/close-solved.ts # close matches, log them in D1 * bun apps/cli/close-solved.ts --dry-run # report only, touch nothing * * Auth: GH_TOKEN / GITHUB_TOKEN, else falls back to `gh auth token`. * SRS_ADMIN_KEY for the D1 push (unset = skip it); SRS_API overrides * the Worker URL for local `wrangler dev` runs. */ import { basename, join } from "node:path"; import { github } from "./github.ts"; import { markdownTable, printTable, writeStepSummary } from "./report.ts"; const ROOT = join(import.meta.dir, "..", ".."); const WORK = join(ROOT, "work"); const DRY = process.argv.includes("--dry-run") || process.env.DRY_RUN === "1" || process.env.DRY_RUN === "true"; // ── is the file a real solution or just a scaffolded stub? ──────── /** Placeholder bodies that leetcode-cli / a human leaves behind. */ const PLACEHOLDERS: Record = { pass: true, "...": true, TODO: true }; /** * Decide whether a work/ file contains an implementation. * * The header comment is dropped the same way sync.ts splitSource() does it * (duplicated rather than imported, because sync.ts runs its whole pipeline on * import). Comments must go before any brace analysis: the scaffold's JSDoc * carries `@param {number[]}`, whose braces would otherwise read as a body. * * A brace-language file counts as implemented when at least one *innermost* * brace pair holds real content. That distinguishes a bare stub * (`function(nums) {}`) and a class-shaped design stub (every method body * empty) from any genuine solution, whose innermost block always has code. */ function isImplemented(src: string, lang: "js" | "py"): boolean { if (lang === "py") { const open = src.indexOf('"""'); const close = src.indexOf('"""', open + 3); const code = open === -1 || close === -1 ? src : src.slice(close + 3); for (const raw of code.split("\n")) { const line = raw.replace(/#.*$/, "").trim(); if (!line || PLACEHOLDERS[line]) continue; if (/^(?:@|def\s|class\s)/.test(line)) continue; return true; // a statement inside some def body } return false; } const headerEnd = src.indexOf("*/"); const body = headerEnd === -1 ? src : src.slice(headerEnd + 2); const code = body.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/.*$/gm, " "); let innermost = -1; for (let i = 0; i < code.length; i++) { if (code[i] === "{") { innermost = i; } else if (code[i] === "}" && innermost !== -1) { const inner = code.slice(innermost + 1, i).replace(/[\s;]/g, ""); if (inner && !PLACEHOLDERS[inner]) return true; innermost = -1; // measured; the enclosing pair is not innermost } } return false; } // ── repo + auth ────────────────────────────────────────────────── const gh = await github(); // ── work/ inventory ────────────────────────────────────────────── interface WorkEntry { files: string[]; implemented: boolean; } /** LC number -> solution files (a problem may have both js and py). */ const work = new Map(); for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) { const m = basename(rel).match(/^(\d+)\.(.+)\.(?:js|py)$/); if (!m) { console.warn(`skip (unrecognized name): work/${rel}`); continue; } const num = Number(m[1]); const src = await Bun.file(join(WORK, rel)).text(); const implemented = isImplemented(src, rel.endsWith(".py") ? "py" : "js"); const entry = work.get(num); if (entry) { entry.files.push(`work/${rel}`); entry.implemented ||= implemented; } else { work.set(num, { files: [`work/${rel}`], implemented }); } } // ── open problem issues ────────────────────────────────────────── interface ProblemIssue { number: number; set: string; } /** * Narrow one element of the /issues payload to the fields this script needs. * Returns undefined for pull requests and for anything whose title is not a * `LC ...` problem, which is how non-curriculum rows get skipped. */ function readProblemIssue( value: unknown, ): { lc: number; issue: ProblemIssue } | undefined { if (!value || typeof value !== "object") return; if ("pull_request" in value) return; // the /issues route also lists PRs if (!("number" in value) || typeof value.number !== "number") return; if (!("title" in value) || typeof value.title !== "string") return; const lc = value.title.match(/^LC (\d+) /); if (!lc) return; let set = "—"; if ("labels" in value && Array.isArray(value.labels)) { for (const label of value.labels) { if ( label && typeof label === "object" && "name" in label && typeof label.name === "string" && label.name.startsWith("set:") ) { set = label.name; } } } return { lc: Number(lc[1]), issue: { number: value.number, set } }; } /** LC number -> open issue carrying the `problem` label. */ const open = new Map(); for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=problem&state=open`)) { const parsed = readProblemIssue(raw); if (parsed) open.set(parsed.lc, parsed.issue); } // ── reconcile ──────────────────────────────────────────────────── const sha = process.env.GITHUB_SHA; const report: string[][] = []; let closed = 0; for (const num of [...work.keys()].sort((a, b) => a - b)) { const entry = work.get(num)!; const issue = open.get(num); // Warm-ups and out-of-curriculum practice have no issue. Not an error. if (!issue) { report.push([`${num}`, "—", "—", "no open issue"]); continue; } if (!entry.implemented) { report.push([`${num}`, `#${issue.number}`, issue.set, "stub — skipped"]); continue; } if (DRY) { report.push([`${num}`, `#${issue.number}`, issue.set, "would close"]); continue; } const links = entry.files .map((f) => sha ? `[\`${f}\`](https://github.com/${gh.repo}/blob/${sha}/${encodeURI(f)})` : `\`${f}\``, ) .join(", "); await gh.closeIssue( issue.number, `Solved — solution committed at ${links}.\n\n` + "Closed automatically by `close-solved`. Fill in the close-out block above if you have not already.", ); report.push([`${num}`, `#${issue.number}`, issue.set, "closed"]); closed++; } // ── push the solved set into D1 ─────────────────────────────────── // Closing an issue is invisible to the Worker: its catalog reconcile reads // GitHub for titles, labels and milestones and never looks at issue state, // and D1's SRS columns only ever move through logAttempt(). So a solution // pushed here — rather than tapped in the digest email — has to be reported, // or the charts, the digest's "already solved" ticks, and the drill/gate // pools all keep treating it as untouched. // // The whole implemented set goes over, not just this run's closes: the // Worker drops anything already on the ladder, so the call is a total // recompute and backfills whatever earlier runs missed. const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev"; const solved = [...work.entries()] .filter(([, entry]) => entry.implemented) .map(([lc]) => lc) .sort((a, b) => a - b); let syncNote: string; let syncFailed = false; if (!process.env.SRS_ADMIN_KEY) { syncNote = "SRS_ADMIN_KEY unset — D1 not touched"; } else if (solved.length === 0) { syncNote = "nothing implemented — D1 not touched"; } else { try { const res = await fetch(`${SRS_API}/admin/solved${DRY ? "?dry=1" : ""}`, { method: "POST", headers: { authorization: `Bearer ${process.env.SRS_ADMIN_KEY}`, "content-type": "application/json", }, body: JSON.stringify({ lc: solved }), }); if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // Narrow the report payload instead of casting it; a shape change should // read as "logged nothing", not throw inside the reconciler. const payload: unknown = await res.json(); const fields = payload && typeof payload === "object" ? payload : {}; const strings = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : []); const logged = strings("logged" in fields ? fields.logged : null); const skipped = strings("skipped" in fields ? fields.skipped : null); syncNote = `${DRY ? "would log" : "logged"} ${logged.length} of ${solved.length} ` + `(${skipped.length} already on the ladder or off-curriculum)`; for (const line of logged) console.log(` d1: ${line}`); } catch (err) { // The closes above already landed; surface the failure instead of // letting D1 drift silently until the next push. syncFailed = true; syncNote = `D1 push FAILED: ${err instanceof Error ? err.message : String(err)}`; } } // ── report ─────────────────────────────────────────────────────── const rows = [["lc", "issue", "set", "status"], ...report]; printTable(rows); const actionable = report.filter((r) => r[3] !== "no open issue"); console.log( `\n${work.size} in work/ · ${solved.length} implemented · ` + `${actionable.length} matched an open issue · ` + `${DRY ? `would close ${actionable.filter((r) => r[3] === "would close").length}` : `closed ${closed}`}` + `\nd1: ${syncNote}`, ); await writeStepSummary( `### close-solved${DRY ? " (dry run)" : ""}\n\n` + `${work.size} files in \`work/\`, ${solved.length} implemented, ` + `${actionable.length} matched an open issue.\n\n` + (actionable.length ? `${markdownTable([rows[0]!, ...actionable])}\n` : "Nothing to close.\n") + `\nD1: ${syncNote}\n`, ); // Non-zero only for the D1 push: the closes are already reported above. if (syncFailed) process.exitCode = 1;