mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 15:36:26 +00:00
feat(api): add dueToday endpoint, shared DUE_WHERE constant, and commit rung handling
This commit is contained in:
+87
-14
@@ -203,20 +203,85 @@ export async function getProblem(db: D1Database, lc: number): Promise<ProblemRow
|
||||
return db.prepare("SELECT * FROM problems WHERE lc_number = ?").bind(lc).first<ProblemRow>();
|
||||
}
|
||||
|
||||
/**
|
||||
* What "due" means, in one place: not retired, scheduled on/before ?1, and
|
||||
* past its deferral if it has one. Every caller that asks "what is open on
|
||||
* this day" — the digest, the review issue, the docs queue chart — shares
|
||||
* this clause so they cannot answer differently.
|
||||
*/
|
||||
export const DUE_WHERE = `stage != 'retired' AND next_review IS NOT NULL AND next_review <= ?1
|
||||
AND (defer_until IS NULL OR defer_until <= ?1)`;
|
||||
|
||||
/** Reviews due on/before `date`, oldest first — the overflow carry order. */
|
||||
export async function dueReviews(db: D1Database, date: string): Promise<ProblemRow[]> {
|
||||
const { results } = await db
|
||||
.prepare(
|
||||
`SELECT * FROM problems
|
||||
WHERE stage != 'retired' AND next_review IS NOT NULL AND next_review <= ?1
|
||||
AND (defer_until IS NULL OR defer_until <= ?1)
|
||||
ORDER BY next_review, lc_number`,
|
||||
)
|
||||
.prepare(`SELECT * FROM problems WHERE ${DUE_WHERE} ORDER BY next_review, lc_number`)
|
||||
.bind(date)
|
||||
.all<ProblemRow>();
|
||||
return results;
|
||||
}
|
||||
|
||||
/** One problem the ladder wants re-solved today. */
|
||||
export interface DueRow {
|
||||
lc: number;
|
||||
issue: number;
|
||||
title: string;
|
||||
difficulty: string;
|
||||
/** The rung being settled: also the `work/<n>` bucket the re-solve goes in. */
|
||||
stage: Stage;
|
||||
bucket: number;
|
||||
/** Scheduled day; earlier than `date` when a day was missed. */
|
||||
due: string;
|
||||
/** Days late (0 = due today), so a renderer can say so. */
|
||||
late: number;
|
||||
/** Last logged attempt, and how long ago — the "last seen" line. */
|
||||
last_seen: string | null;
|
||||
days_since: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything the ladder wants re-solved on `date`, oldest first.
|
||||
*
|
||||
* The read side of "which problems are open today": the same rows the digest
|
||||
* mails, shaped for whoever renders them (the daily Spaced Repetition issue).
|
||||
* Nothing here writes, so it is safe to call repeatedly, and there is no
|
||||
* second schedule to drift — D1's ladder is the schedule.
|
||||
*/
|
||||
export async function dueToday(db: D1Database, date: string): Promise<{ date: string; due: DueRow[] }> {
|
||||
const { results } = await db
|
||||
.prepare(
|
||||
`SELECT lc_number, issue, title, difficulty, stage, next_review,
|
||||
(SELECT MAX(a.date) FROM attempts a WHERE a.lc_number = problems.lc_number) AS last_seen
|
||||
FROM problems WHERE ${DUE_WHERE} ORDER BY next_review, lc_number`,
|
||||
)
|
||||
.bind(date)
|
||||
.all<{
|
||||
lc_number: number;
|
||||
issue: number;
|
||||
title: string;
|
||||
difficulty: string;
|
||||
stage: Stage;
|
||||
next_review: string;
|
||||
last_seen: string | null;
|
||||
}>();
|
||||
return {
|
||||
date,
|
||||
due: results.map((r) => ({
|
||||
lc: r.lc_number,
|
||||
issue: r.issue,
|
||||
title: r.title,
|
||||
difficulty: r.difficulty,
|
||||
stage: r.stage,
|
||||
// "+3" -> 3: the rung's number IS its work/ bucket.
|
||||
bucket: Number(r.stage.slice(1)),
|
||||
due: r.next_review,
|
||||
late: daysBetween(r.next_review, date),
|
||||
last_seen: r.last_seen,
|
||||
days_since: r.last_seen === null ? null : daysBetween(r.last_seen, date),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** Reviews surfaced per day; the digest levels everything past this forward. */
|
||||
export const REVIEW_CAP = 3;
|
||||
|
||||
@@ -326,9 +391,14 @@ export interface LogOutcome {
|
||||
* Email idempotency comes from the partial unique index on
|
||||
* (lc_number, date, kind) WHERE source='email': a replayed link inserts
|
||||
* nothing and must not touch the ladder. Webhook corrections (pass then fail
|
||||
* on the same day) remain legal — every webhook attempt appends. Commit
|
||||
* idempotency needs no index: a commit records a FIRST solve only, so the
|
||||
* stage check below turns every re-push into a no-op.
|
||||
* on the same day) remain legal — every webhook attempt appends.
|
||||
*
|
||||
* Commit idempotency needs no index either. A commit names the rung the file
|
||||
* it landed in settles — `work/1` settles `new`, `work/3` settles `+3`,
|
||||
* `work/7` settles `+7` — and the stage check below writes only when the
|
||||
* ladder is actually standing on that rung. So re-pushing a solution, or the
|
||||
* reconciler re-sending its whole implemented set on every push, moves
|
||||
* nothing: the rung it names has already been left behind.
|
||||
*/
|
||||
export async function logAttempt(
|
||||
db: D1Database,
|
||||
@@ -337,6 +407,8 @@ export async function logAttempt(
|
||||
date: string;
|
||||
result: Result;
|
||||
source: "email" | "webhook" | "commit";
|
||||
/** Commit only: the stage this file settles. Default `new` = first solve. */
|
||||
rung?: Stage;
|
||||
gate?: boolean;
|
||||
},
|
||||
): Promise<LogOutcome> {
|
||||
@@ -353,11 +425,12 @@ export async function logAttempt(
|
||||
duplicate: false,
|
||||
};
|
||||
if (!p) return { ...nothing, error: `LC ${opts.lc} is not in the curriculum` };
|
||||
// A commit only ever reports a first solve: pushing the file again — or the
|
||||
// reconciler re-sending the whole solved set — must never move the ladder,
|
||||
// and must never wake a retired problem. Reviews come from the digest tap,
|
||||
// a /done comment, or a gate.
|
||||
if (opts.source === "commit" && p.stage !== "new") {
|
||||
// A commit reports a file, not an event: it may only settle the rung that
|
||||
// file's bucket IS. Any other stage means the push is old news (the ladder
|
||||
// already moved past it) or premature — no write either way, and a retired
|
||||
// problem can never be woken. Fresh reviews otherwise come from the digest
|
||||
// tap, a /done comment, or a gate.
|
||||
if (opts.source === "commit" && p.stage !== (opts.rung ?? "new")) {
|
||||
return {
|
||||
...nothing,
|
||||
title: p.title,
|
||||
|
||||
Reference in New Issue
Block a user