From cac2f744e4a7985553e99947491e6d7e08a39d91 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 27 Aug 2026 16:04:14 -0400 Subject: [PATCH] feat(api): extract difficulty and set from issue labels in catalog reconciliation --- apps/api/src/catalog.ts | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/apps/api/src/catalog.ts b/apps/api/src/catalog.ts index 0f1afc4..b785d8c 100644 --- a/apps/api/src/catalog.ts +++ b/apps/api/src/catalog.ts @@ -4,11 +4,19 @@ * rows from scratch on every run — re-runs are no-ops. SRS-owned columns * (stage, next_review) are NEVER overwritten; defer_until is set once, on * first sight of a deferred problem. The Worker never invents catalog rows. + * + * Difficulty and set come from the `diff:*` / `set:*` LABELS, never from the + * issue title — the title carries only the LC number and the problem's name, + * so relabelling a problem is enough to move it and titles stay readable. + * The `problem` label is what marks a sub-issue as curriculum. */ import { addDays } from "./srs.ts"; import type { GitHub } from "./github.ts"; -const TITLE_RE = /^LC (\d+) · (.+) · (Easy|Medium|Hard) · (core|optional|deferred)$/; +const TITLE_RE = /^LC (\d+) · (.+)$/; + +const DIFFICULTIES = ["easy", "medium", "hard"] as const; +const SETS = ["core", "optional", "deferred"] as const; /** Deferred Hards enter the queue from this date, two per day. */ const DEFER_FROM = "2026-09-28"; @@ -18,6 +26,18 @@ export interface ReconcileReport { problems: number; } +/** Label names on one issue payload, narrowed with guards rather than cast. */ +function labelNames(value: object): string[] { + if (!("labels" in value) || !Array.isArray(value.labels)) return []; + const names: string[] = []; + for (const label of value.labels) { + if (label && typeof label === "object" && "name" in label && typeof label.name === "string") { + names.push(label.name); + } + } + return names; +} + export async function reconcileCatalog(db: D1Database, gh: GitHub): Promise { interface TopicIssue { number: number; @@ -62,10 +82,20 @@ export async function reconcileCatalog(db: D1Database, gh: GitHub): Promise · "`); + continue; + } + const difficulty = DIFFICULTIES.find((d) => labels.includes(`diff:${d}`)); + const set = SETS.find((s) => labels.includes(`set:${s}`)); + if (!difficulty || !set) { + console.warn(`catalog: #${raw.number} is missing a diff:/set: label — skipped`); + continue; + } const lc = Number(m[1]); - const set = m[4]!; // Two deferred Hards per day from DEFER_FROM, in catalog walk order — // applied only when the row is first created (SRS owns it afterwards). const defer = set === "deferred" ? addDays(DEFER_FROM, Math.floor(deferredSeen / 2)) : null; @@ -78,7 +108,7 @@ export async function reconcileCatalog(db: D1Database, gh: GitHub): Promise