mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
feat(cli): send bucket info to SRS API and render due list from worker
This commit is contained in:
+26
-25
@@ -19,14 +19,12 @@
|
||||
* 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 { join } from "node:path";
|
||||
|
||||
import { github } from "./github.ts";
|
||||
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
||||
import { isImplemented } from "./source.ts";
|
||||
|
||||
const ROOT = join(import.meta.dir, "..", "..");
|
||||
const WORK = join(ROOT, "work");
|
||||
import { WORK, parseWorkPath, type Bucket } from "./work.ts";
|
||||
|
||||
const DRY =
|
||||
process.argv.includes("--dry-run") ||
|
||||
@@ -41,27 +39,27 @@ const gh = await github();
|
||||
|
||||
interface WorkEntry {
|
||||
files: string[];
|
||||
/** Any bucket implemented — this is what closes the problem issue. */
|
||||
implemented: boolean;
|
||||
/** Buckets holding a real solution: 1 = first solve, 3/7 = the re-solves. */
|
||||
solved: Set<Bucket>;
|
||||
}
|
||||
|
||||
/** LC number -> solution files (a problem may have both js and py). */
|
||||
/** LC number -> its solution files across every bucket (js and py both count). */
|
||||
const work = new Map<number, WorkEntry>();
|
||||
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}`);
|
||||
const file = parseWorkPath(rel);
|
||||
if (!file) {
|
||||
console.warn(`skip (off-layout): 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 });
|
||||
}
|
||||
const entry = work.get(file.lc) ?? { files: [], implemented: false, solved: new Set<Bucket>() };
|
||||
entry.files.push(`work/${rel}`);
|
||||
entry.implemented ||= implemented;
|
||||
if (implemented) entry.solved.add(file.bucket);
|
||||
work.set(file.lc, entry);
|
||||
}
|
||||
|
||||
// ── open problem issues ──────────────────────────────────────────
|
||||
@@ -157,14 +155,17 @@ for (const num of [...work.keys()].sort((a, b) => a - b)) {
|
||||
// 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.
|
||||
// Each entry carries the BUCKET the file sits in, which is the rung it
|
||||
// settles: work/1 is the first solve, work/3 the 3-day review, work/7 the
|
||||
// 7-day one. That is what makes a pushed re-solve advance the ladder instead
|
||||
// of vanishing — and the Worker writes only when a problem is standing on the
|
||||
// rung named, so the whole implemented set can go over on every push (a total
|
||||
// recompute that backfills whatever earlier runs missed) and re-runs write
|
||||
// nothing.
|
||||
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);
|
||||
.flatMap(([lc, entry]) => [...entry.solved].sort().map((bucket) => ({ lc, bucket })))
|
||||
.sort((a, b) => a.lc - b.lc || a.bucket - b.bucket);
|
||||
|
||||
let syncNote: string;
|
||||
let syncFailed = false;
|
||||
@@ -180,7 +181,7 @@ if (!process.env.SRS_ADMIN_KEY) {
|
||||
authorization: `Bearer ${process.env.SRS_ADMIN_KEY}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ lc: solved }),
|
||||
body: JSON.stringify({ solved }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
||||
// Narrow the report payload instead of casting it; a shape change should
|
||||
@@ -209,7 +210,7 @@ printTable(rows);
|
||||
|
||||
const actionable = report.filter((r) => r[3] !== "no open issue");
|
||||
console.log(
|
||||
`\n${work.size} in work/ · ${solved.length} implemented · ` +
|
||||
`\n${work.size} problems in work/ · ${solved.length} solved files across buckets · ` +
|
||||
`${actionable.length} matched an open issue · ` +
|
||||
`${DRY ? `would close ${actionable.filter((r) => r[3] === "would close").length}` : `closed ${closed}`}` +
|
||||
`\nd1: ${syncNote}`,
|
||||
@@ -217,7 +218,7 @@ console.log(
|
||||
|
||||
await writeStepSummary(
|
||||
`### close-solved${DRY ? " (dry run)" : ""}\n\n` +
|
||||
`${work.size} files in \`work/\`, ${solved.length} implemented, ` +
|
||||
`${work.size} problems in \`work/\`, ${solved.length} solved files across buckets, ` +
|
||||
`${actionable.length} matched an open issue.\n\n` +
|
||||
(actionable.length
|
||||
? `${markdownTable([rows[0]!, ...actionable])}\n`
|
||||
|
||||
+156
-119
@@ -1,39 +1,42 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Open today's spaced-repetition issue: everything whose problem issue closed
|
||||
* 3 and 7 days ago, linked straight to LeetCode.
|
||||
* Open today's spaced-repetition issue: whatever the SRS ladder has due,
|
||||
* linked straight to LeetCode.
|
||||
*
|
||||
* The issue itself holds no state and there is no scheduler here: "what do I
|
||||
* re-solve today" is a pure function of the tracker's close dates and today's
|
||||
* ET date, recomputed from scratch on every run. A day's issue is keyed by its
|
||||
* title (`Spaced Repetition — <Month D, YYYY>`), so re-runs rewrite that one
|
||||
* body instead of stacking duplicates, and a backfilled close shows up in the
|
||||
* next run's windows for free.
|
||||
* D1 is the schedule and this script is a renderer. `POST /admin/due` returns
|
||||
* exactly the rows the 8 AM digest mails — reviews standing at the `+3` and
|
||||
* `+7` rungs, oldest first — so the issue and the email can never disagree
|
||||
* about a day, and there is no second scheduler to drift. The rung is also
|
||||
* the `work/<n>` bucket the blind re-solve belongs in, which is the section
|
||||
* `bun run pick` opens on Tab.
|
||||
*
|
||||
* Windows are +3 and +7 because that is the campaign's ladder: WINDOWS lives
|
||||
* in apps/api/src/srs.ts, names the `work/3` and `work/7` buckets the picker
|
||||
* scaffolds into, and is chosen so a solve never comes back on the Sunday rest
|
||||
* day — the +3 pass catches a problem while the solution is half remembered,
|
||||
* the +7 pass after a week of interference. The one exception, a Thursday
|
||||
* solve's +3, is slid to Monday by the same workingDay() the Worker schedules
|
||||
* with, which is why a window can have two source days. Each window asks for
|
||||
* at most WINDOW_CAP problems, so a normal day is 3 new + up to 6 re-solves
|
||||
* with no levelling logic needed to bound the load. On Sunday this script
|
||||
* writes nothing at all.
|
||||
* How a problem gets here: closing its issue (a push, a digest tap, a `/done`
|
||||
* comment) enters it on the ladder at `+3` with a concrete next_review, and
|
||||
* passing that review moves it to `+7`. Nothing is "activated" by this script
|
||||
* — it asks what is standing today and writes it down. A day's issue is keyed
|
||||
* by its title (`Spaced Repetition — <Month D, YYYY>`), so re-runs rewrite
|
||||
* that one body instead of stacking duplicates.
|
||||
*
|
||||
* Links come from the problem issue body's first line (the canonical LeetCode
|
||||
* URL written by the campaign-issues convention). A body with no URL falls back
|
||||
* to a slug guessed from the title and is flagged `inferred` in the report,
|
||||
* because repo titles are sometimes abbreviated ("LC 167 · Two Sum II") and a
|
||||
* guessed slug can 404.
|
||||
* Sunday writes nothing: the ladder never schedules the rest day (WINDOWS of
|
||||
* 3 and 7 exist for exactly that reason, see apps/api/src/srs.ts), so there is
|
||||
* nothing to render. Each rung shows at most WINDOW_CAP problems, with the
|
||||
* overflow listed as optional — the digest's leveller moves that overflow to
|
||||
* later working days the next morning.
|
||||
*
|
||||
* Links: D1 stores titles, not URLs, so each due problem's issue body is read
|
||||
* for the canonical LeetCode link. A body with no URL falls back to a slug
|
||||
* guessed from the title and is flagged in the report, because repo titles are
|
||||
* sometimes abbreviated ("LC 167 · Two Sum II") and a guess can 404.
|
||||
*
|
||||
* bun apps/cli/spaced-repetition.ts # create/update today's issue
|
||||
* bun apps/cli/spaced-repetition.ts --dry-run # print the body, touch nothing
|
||||
* bun apps/cli/spaced-repetition.ts --date=2026-09-02 # pretend it is that ET day
|
||||
*
|
||||
* Auth: GH_TOKEN / GITHUB_TOKEN, else falls back to `gh auth token`.
|
||||
* SRS_ADMIN_KEY reads the due list; SRS_API overrides the Worker URL for
|
||||
* local `wrangler dev` runs.
|
||||
*/
|
||||
import { WINDOWS, addDays, etDate, isRestDay, workingDay } from "../api/src/srs.ts";
|
||||
import { WINDOWS, etDate, isRestDay } from "../api/src/srs.ts";
|
||||
import { github } from "./github.ts";
|
||||
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
||||
|
||||
@@ -93,42 +96,87 @@ if (isRestDay(today)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The close dates whose `days`-day review comes due today, in close order.
|
||||
*
|
||||
* Normally one day, `today - days`. Two when the plain interval would have
|
||||
* landed on the rest day: workingDay() slides that review to Monday, so a
|
||||
* Monday run also owes Thursday's +3 (Thu + 3 = Sunday). Nothing is dropped
|
||||
* and nothing is counted twice — a close date qualifies only when its shifted
|
||||
* review date IS today, which is exactly the rule srs.ts schedules by.
|
||||
*/
|
||||
function windowSources(days: number): string[] {
|
||||
return [addDays(today, -days - 1), addDays(today, -days)].filter(
|
||||
(closed) => workingDay(addDays(closed, days)) === today,
|
||||
);
|
||||
// ── what is due today (D1, via the Worker) ───────────────────────
|
||||
|
||||
// The ladder is the schedule: `POST /admin/due` returns exactly the rows the
|
||||
// morning digest mails, so the issue and the email can never disagree about a
|
||||
// day. This script adds no scheduling of its own — it renders, caps, and
|
||||
// links.
|
||||
const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev";
|
||||
|
||||
/** One problem the ladder wants re-solved today, as the Worker reports it. */
|
||||
interface Due {
|
||||
lc: number;
|
||||
issue: number;
|
||||
title: string;
|
||||
difficulty: string;
|
||||
/** The rung being settled: "+3" or "+7". */
|
||||
stage: string;
|
||||
/** The rung's number — also the `work/<n>` bucket the re-solve belongs in. */
|
||||
bucket: number;
|
||||
due: string;
|
||||
late: number;
|
||||
last_seen: string | null;
|
||||
days_since: number | null;
|
||||
}
|
||||
|
||||
const targets = new Set(WINDOWS.flatMap((days) => windowSources(days)));
|
||||
function readDue(value: unknown): Due | undefined {
|
||||
if (!value || typeof value !== "object") return;
|
||||
const num = (key: string): number | undefined =>
|
||||
key in value && typeof (value as Record<string, unknown>)[key] === "number"
|
||||
? ((value as Record<string, unknown>)[key] as number)
|
||||
: undefined;
|
||||
const str = (key: string): string | undefined =>
|
||||
key in value && typeof (value as Record<string, unknown>)[key] === "string"
|
||||
? ((value as Record<string, unknown>)[key] as string)
|
||||
: undefined;
|
||||
const lc = num("lc");
|
||||
const issue = num("issue");
|
||||
const bucket = num("bucket");
|
||||
const stage = str("stage");
|
||||
const title = str("title");
|
||||
const dueOn = str("due");
|
||||
if (lc === undefined || issue === undefined || bucket === undefined) return;
|
||||
if (stage === undefined || title === undefined || dueOn === undefined) return;
|
||||
const difficulty = str("difficulty") ?? "unrated";
|
||||
return {
|
||||
lc,
|
||||
issue,
|
||||
title,
|
||||
difficulty: difficulty.charAt(0).toUpperCase() + difficulty.slice(1),
|
||||
stage,
|
||||
bucket,
|
||||
due: dueOn,
|
||||
late: num("late") ?? 0,
|
||||
last_seen: str("last_seen") ?? null,
|
||||
days_since: num("days_since") ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!process.env.SRS_ADMIN_KEY) {
|
||||
// Failing loudly beats writing an issue from a second, guessed schedule:
|
||||
// there is only one schedule now, and it lives in D1.
|
||||
console.error("SRS_ADMIN_KEY unset — cannot read today's due list from the Worker.");
|
||||
process.exit(1);
|
||||
}
|
||||
const res = await fetch(`${SRS_API}/admin/due?date=${today}`, {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${process.env.SRS_ADMIN_KEY}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`GET due list -> ${res.status} ${await res.text()}`);
|
||||
const payload: unknown = await res.json();
|
||||
const dueRaw: unknown = payload && typeof payload === "object" && "due" in payload ? payload.due : null;
|
||||
const due: Due[] = (Array.isArray(dueRaw) ? dueRaw : []).flatMap((r) => {
|
||||
const row = readDue(r);
|
||||
return row ? [row] : [];
|
||||
});
|
||||
|
||||
// ── repo + auth ──────────────────────────────────────────────────
|
||||
|
||||
const gh = await github();
|
||||
|
||||
// ── closed problem issues ────────────────────────────────────────
|
||||
// ── canonical LeetCode links ─────────────────────────────────────
|
||||
|
||||
interface Solved {
|
||||
lc: number;
|
||||
name: string;
|
||||
issue: number;
|
||||
url: string;
|
||||
/** false when the URL was guessed from the title instead of read from the body. */
|
||||
linked: boolean;
|
||||
difficulty: string;
|
||||
/** ET date the issue closed. */
|
||||
closed: string;
|
||||
}
|
||||
|
||||
const TITLE = /^LC\s+(\d+)\s+·\s+(.+?)\s*$/;
|
||||
const LC_URL = /https:\/\/leetcode\.com\/problems\/[a-z0-9-]+/;
|
||||
|
||||
/** LeetCode's own slug shape, for bodies that lost their URL line. */
|
||||
@@ -140,92 +188,79 @@ function slugify(name: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow one element of the /issues payload to the fields this script needs.
|
||||
* Drops pull requests (the `problem` label filter cannot), anything whose title
|
||||
* is not a `LC <num> · <Name>` problem, and anything closed outside the two
|
||||
* review windows.
|
||||
* D1 stores titles, not URLs, so each due problem's issue body is read for the
|
||||
* canonical LeetCode link written by the campaign-issues convention. That is
|
||||
* one request per due problem — at most REVIEW_CAP per rung — instead of the
|
||||
* old walk over every closed issue in the repo. A body with no URL falls back
|
||||
* to a slug guessed from the title and is flagged in the report, because repo
|
||||
* titles are sometimes abbreviated ("LC 167 · Two Sum II") and a guess can 404.
|
||||
*/
|
||||
function readSolved(value: unknown): Solved | 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;
|
||||
if (!("closed_at" in value) || typeof value.closed_at !== "string") return;
|
||||
// Closed "not planned" means dropped from the curriculum, not solved — the
|
||||
// reconcilers only ever close as completed, so this is a human's decision.
|
||||
if ("state_reason" in value && value.state_reason === "not_planned") return;
|
||||
|
||||
const closed = etDate(new Date(value.closed_at));
|
||||
if (!targets.has(closed)) return;
|
||||
|
||||
const title = TITLE.exec(value.title);
|
||||
if (!title) return;
|
||||
|
||||
const body = "body" in value && typeof value.body === "string" ? value.body : "";
|
||||
const url = LC_URL.exec(body)?.[0];
|
||||
const labels =
|
||||
"labels" in value && Array.isArray(value.labels)
|
||||
? value.labels.flatMap((l) =>
|
||||
l && typeof l === "object" && "name" in l && typeof l.name === "string" ? [l.name] : [],
|
||||
)
|
||||
: [];
|
||||
const diff = labels.find((n) => n.startsWith("diff:"))?.slice(5) ?? "unrated";
|
||||
interface Linked extends Due {
|
||||
url: string;
|
||||
linked: boolean;
|
||||
}
|
||||
|
||||
async function link(row: Due): Promise<Linked> {
|
||||
let url: string | undefined;
|
||||
try {
|
||||
const issue: unknown = await gh.api(`/repos/${gh.repo}/issues/${row.issue}`);
|
||||
const body =
|
||||
issue && typeof issue === "object" && "body" in issue && typeof issue.body === "string"
|
||||
? issue.body
|
||||
: "";
|
||||
url = LC_URL.exec(body)?.[0];
|
||||
} catch (err) {
|
||||
console.warn(`#${row.issue}: could not read the issue body (${err})`);
|
||||
}
|
||||
return {
|
||||
lc: Number(title[1]),
|
||||
name: title[2]!,
|
||||
issue: value.number,
|
||||
url: `${url ?? `https://leetcode.com/problems/${slugify(title[2]!)}`}/`,
|
||||
...row,
|
||||
url: `${url ?? `https://leetcode.com/problems/${slugify(row.title)}`}/`,
|
||||
linked: url !== undefined,
|
||||
difficulty: diff.charAt(0).toUpperCase() + diff.slice(1),
|
||||
closed,
|
||||
};
|
||||
}
|
||||
|
||||
// The whole closed set is walked rather than filtered with `since`: it is two
|
||||
// pages for the entire 161-problem curriculum, and `since` filters on
|
||||
// updated_at, which a stray comment moves off the close date.
|
||||
const solved: Solved[] = [];
|
||||
for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=problem&state=closed`)) {
|
||||
const row = readSolved(raw);
|
||||
if (row) solved.push(row);
|
||||
}
|
||||
solved.sort((a, b) => a.issue - b.issue);
|
||||
|
||||
// ── issue body ───────────────────────────────────────────────────
|
||||
|
||||
const title = `Spaced Repetition — ${longDate(today)}`;
|
||||
|
||||
/** Every problem whose issue closed on one of a window's source days. */
|
||||
function bucket(days: number): Solved[] {
|
||||
const dates = new Set(windowSources(days));
|
||||
return solved.filter((s) => dates.has(s.closed));
|
||||
/** The window's due problems, capped, oldest scheduled first. */
|
||||
const byWindow = new Map<number, Linked[]>();
|
||||
for (const days of WINDOWS) {
|
||||
const rows = due.filter((d) => d.bucket === days);
|
||||
byWindow.set(days, await Promise.all(rows.map(link)));
|
||||
}
|
||||
|
||||
function bucket(days: number): Linked[] {
|
||||
return byWindow.get(days) ?? [];
|
||||
}
|
||||
|
||||
/** One checkbox line, straight to LeetCode. */
|
||||
function line(s: Solved, box: boolean): string {
|
||||
function line(s: Linked, box: boolean): string {
|
||||
const seen = s.days_since === null ? "unlogged" : `last seen ${s.days_since}d ago`;
|
||||
return (
|
||||
`${box ? "- [ ] " : "- "}[LC ${s.lc} · ${s.name}](${s.url}) · ${s.difficulty} · #${s.issue}` +
|
||||
`${box ? "- [ ] " : "- "}[LC ${s.lc} · ${s.title}](${s.url}) · ${s.difficulty} · #${s.issue}` +
|
||||
` · ${seen}${s.late > 0 ? ` · ⏰ ${s.late}d late` : ""}` +
|
||||
(s.linked ? "" : " · ⚠️ link guessed from title")
|
||||
);
|
||||
}
|
||||
|
||||
const sections = WINDOWS.map((days) => {
|
||||
const from = windowSources(days).map((d) => longDate(d)).join(" and ");
|
||||
const rows = bucket(days);
|
||||
const due = rows.slice(0, WINDOW_CAP);
|
||||
const required = rows.slice(0, WINDOW_CAP);
|
||||
const spill = rows.slice(WINDOW_CAP);
|
||||
return [
|
||||
`### +${days} days — solved ${from}`,
|
||||
`### +${days} days — re-solve into \`work/${days}/\``,
|
||||
"",
|
||||
...(due.length ? due.map((s) => line(s, true)) : ["_Nothing closed then._"]),
|
||||
// Overflow is shown, not dropped: a day that closed more than WINDOW_CAP
|
||||
// problems (a backfill, or a catch-up weekend) would otherwise silently
|
||||
// lose reviews, and this issue is the only record of what was due.
|
||||
...(required.length
|
||||
? required.map((s) => line(s, true))
|
||||
: [`_Nothing at the +${days} rung today._`]),
|
||||
// Overflow is shown, not dropped: the ladder levels everything past the
|
||||
// cap onto later working days, but until that morning's digest runs this
|
||||
// issue is the only record of what was standing.
|
||||
...(spill.length
|
||||
? [
|
||||
"",
|
||||
`<details><summary>${spill.length} more solved then — optional</summary>`,
|
||||
`<details><summary>${spill.length} more at this rung — optional</summary>`,
|
||||
"",
|
||||
...spill.map((s) => line(s, false)),
|
||||
"",
|
||||
@@ -243,12 +278,13 @@ const body = [
|
||||
"",
|
||||
"- 15 minutes per problem, then stop and read your own solution.",
|
||||
"- Say the invariant and the complexity out loud before typing.",
|
||||
"- Failed one? Tick it anyway and expect it back in the next window.",
|
||||
"- Failed one? Tick it anyway and expect it back at +3.",
|
||||
"- `bun run pick` → Tab to this rung's section scaffolds the blind re-solve.",
|
||||
"",
|
||||
...sections,
|
||||
`<sub>Recomputed from the problem issues closed ${WINDOWS.join(" and ")} days ago ` +
|
||||
"by `.github/workflows/spaced-repetition.yml` — re-runs on the same day rewrite this body. " +
|
||||
"A window that would fall on Sunday is served on the Monday instead.</sub>",
|
||||
"<sub>Today's due list straight from the SRS ladder in D1 (`POST /admin/due`), rendered by " +
|
||||
"`.github/workflows/spaced-repetition.yml` — re-runs on the same day rewrite this body. " +
|
||||
"The ladder never schedules a Sunday, so there is no issue on the rest day.</sub>",
|
||||
].join("\n");
|
||||
|
||||
// ── create or refresh today's issue ──────────────────────────────
|
||||
@@ -301,7 +337,7 @@ if (total === 0) {
|
||||
body: JSON.stringify({
|
||||
name: LABEL,
|
||||
color: "0E8A16",
|
||||
description: "Daily blind re-solve of problems closed 3 and 7 days ago",
|
||||
description: "Daily blind re-solve of whatever the SRS ladder has due",
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -323,18 +359,19 @@ const report = WINDOWS.flatMap((days) =>
|
||||
String(s.lc),
|
||||
`#${s.issue}`,
|
||||
s.difficulty,
|
||||
s.due + (s.late > 0 ? ` (${s.late}d late)` : ""),
|
||||
i < WINDOW_CAP ? "due" : "optional",
|
||||
s.linked ? s.url : `${s.url} (inferred)`,
|
||||
]),
|
||||
);
|
||||
const rows = [["window", "lc", "issue", "diff", "state", "leetcode"], ...report];
|
||||
const rows = [["rung", "lc", "issue", "diff", "scheduled", "state", "leetcode"], ...report];
|
||||
if (report.length) printTable(rows);
|
||||
|
||||
console.log(
|
||||
`\n${title}${DRY ? " (dry run)" : ""}\n` +
|
||||
WINDOWS.map((days) => {
|
||||
const n = bucket(days).length;
|
||||
return `+${days}d ${windowSources(days).join("+")}: ${Math.min(n, WINDOW_CAP)} due${n > WINDOW_CAP ? ` (+${n - WINDOW_CAP} optional)` : ""}`;
|
||||
return `+${days}d: ${Math.min(n, WINDOW_CAP)} due${n > WINDOW_CAP ? ` (+${n - WINDOW_CAP} optional)` : ""}`;
|
||||
}).join(" · ") +
|
||||
`\n${status}`,
|
||||
);
|
||||
@@ -343,5 +380,5 @@ if (DRY) console.log(`\n${body}`);
|
||||
await writeStepSummary(
|
||||
`### ${title}${DRY ? " (dry run)" : ""}\n\n` +
|
||||
`${total} problem${total === 1 ? "" : "s"} due · ${status}\n\n` +
|
||||
(report.length ? `${markdownTable(rows)}\n` : "Nothing closed in either window.\n"),
|
||||
(report.length ? `${markdownTable(rows)}\n` : "The ladder has nothing due today.\n"),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user