mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
90 lines
4.2 KiB
TypeScript
90 lines
4.2 KiB
TypeScript
/**
|
|
* Topic issues and their problem sub-issues: the ONE reading of "is this topic
|
|
* finished", shared by close-topics.ts (closes the topic issue) and
|
|
* release-topics.ts (publishes the topic's timever release) so the two can
|
|
* never disagree about when a topic was done. Also the one reading of "is this
|
|
* Review issue finished", which close-topics.ts closes on.
|
|
*
|
|
* Rule: a topic is finished when it has a core set (`set:core` sub-issues) and
|
|
* every one of them is closed. Optional and deferred problems are extra credit
|
|
* and never hold a topic open. A topic with no core sub-issues is never
|
|
* finished — that would be indistinguishable from a mis-labelled problem set.
|
|
*
|
|
* Review issues (`Review <N> — <Phase>`, label `review`) are day blocks for
|
|
* the transfer phase: problems were re-parented into them from their topics
|
|
* (a GitHub issue has one parent). A topic still OWNS such a problem —
|
|
* readProblems() returns it alongside the topic's direct sub-issues, with the
|
|
* ownership recovered by apps/api/src/reviews.ts (shared with the Worker's
|
|
* catalog reconcile) — so the finished rule and the release snapshots do not
|
|
* change when a problem moves. A Review issue is finished when every one of
|
|
* its sub-issues is closed: it is a fixed assignment, so the set labels do
|
|
* not matter there.
|
|
*
|
|
* Library: side-effect-free on import; every call takes the caller's github().
|
|
*/
|
|
import type { Problem } from "../api/src/reviews.ts";
|
|
import type { GitHub } from "./github.ts";
|
|
|
|
/** Completion of a required set: a topic's core problems, or every problem of a review. */
|
|
export interface Core {
|
|
/** The required problems, closed and open alike, in the order given. */
|
|
all: number[];
|
|
/** The required problems still open — non-empty means the issue stays open. */
|
|
pending: number[];
|
|
/** When the last required problem closed; null until the set is finished (never, if empty). */
|
|
finishedAt: string | null;
|
|
}
|
|
|
|
/**
|
|
* Every problem a topic owns: its sub-issues plus the ones moved from it into
|
|
* a Review issue (`moved`, from movedFromTopics()). The sub_issues payload
|
|
* carries the full issue objects (state + labels), so no per-problem
|
|
* follow-up request is needed.
|
|
*/
|
|
export async function readProblems(
|
|
gh: GitHub,
|
|
topic: number,
|
|
moved: Map<number, Problem[]>,
|
|
): Promise<Problem[]> {
|
|
const problems = [...(moved.get(topic) ?? [])];
|
|
for await (const raw of gh.list(`/repos/${gh.repo}/issues/${topic}/sub_issues`)) {
|
|
if (!raw || typeof raw !== "object") continue;
|
|
if (!("number" in raw) || typeof raw.number !== "number") continue;
|
|
if (!("title" in raw) || typeof raw.title !== "string") continue;
|
|
if (!("state" in raw) || typeof raw.state !== "string") continue;
|
|
if (!("labels" in raw) || !Array.isArray(raw.labels)) continue;
|
|
const closedAt =
|
|
raw.state === "closed" && "closed_at" in raw && typeof raw.closed_at === "string"
|
|
? raw.closed_at
|
|
: null;
|
|
const labels = raw.labels.flatMap((label: unknown) =>
|
|
label && typeof label === "object" && "name" in label && typeof label.name === "string"
|
|
? [label.name]
|
|
: [],
|
|
);
|
|
problems.push({ number: raw.number, title: raw.title, closedAt, labels });
|
|
}
|
|
return problems.sort((a, b) => a.number - b.number);
|
|
}
|
|
|
|
/** The value of a `prefix:value` label (`set:` → `core`), if the problem has one. */
|
|
export function labelValue(problem: Problem, prefix: string): string | undefined {
|
|
return problem.labels.find((l) => l.startsWith(prefix))?.slice(prefix.length);
|
|
}
|
|
|
|
/** A topic's rule: its `set:core` problems are the required set. */
|
|
export function coreSet(problems: Problem[]): Core {
|
|
return completion(problems.filter((p) => labelValue(p, "set:") === "core"));
|
|
}
|
|
|
|
/** The shared rule: finished when non-empty and every required problem is closed. */
|
|
export function completion(required: Problem[]): Core {
|
|
const pending = required.filter((p) => !p.closedAt).map((p) => p.number);
|
|
// ISO instants from the API share one format, so string order is time order.
|
|
const finishedAt =
|
|
required.length && !pending.length
|
|
? required.map((p) => p.closedAt!).reduce((a, b) => (a > b ? a : b))
|
|
: null;
|
|
return { all: required.map((p) => p.number), pending, finishedAt };
|
|
}
|