mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 07:26:27 +00:00
refactor(api): limit project mirror to Target Date only
This commit is contained in:
+7
-3
@@ -16,9 +16,13 @@ Live at `https://srs-api.prdlk.workers.dev`.
|
|||||||
never overwrites SRS-owned columns (`stage`, `next_review`).
|
never overwrites SRS-owned columns (`stage`, `next_review`).
|
||||||
- **The repo owns the schedule** — `data/schedule.json`, human-edited,
|
- **The repo owns the schedule** — `data/schedule.json`, human-edited,
|
||||||
bundled at deploy. Git history is its audit log.
|
bundled at deploy. Git history is its audit log.
|
||||||
- The GitHub Project mirror (`Target Date`, `SRS Stage`, `First Attempt`)
|
- The GitHub Project mirror writes `Target Date` and nothing else: it is
|
||||||
is best-effort: failures log warnings, never block a D1 write. Topic rows
|
best-effort, so failures log warnings and never block a D1 write, and topic
|
||||||
are never written.
|
rows are never written. SRS stage and first-attempt result stay in D1 only —
|
||||||
|
the charts, digest and `/api/stats` read them from there, so mirroring them
|
||||||
|
into Project single-selects bought only drift. Do not re-add them.
|
||||||
|
`Set`/`Difficulty` are a projection of the issue labels, reconciled by
|
||||||
|
`bun run sync-project-fields` at the repo root, not by this Worker.
|
||||||
|
|
||||||
## The ladder
|
## The ladder
|
||||||
|
|
||||||
|
|||||||
+4
-10
@@ -30,22 +30,16 @@ import { buildStats } from "./stats.ts";
|
|||||||
|
|
||||||
// ── shared side effects after a state write ──────────────────────
|
// ── shared side effects after a state write ──────────────────────
|
||||||
|
|
||||||
/** Project-field half of a mirror; shared by every write path. */
|
|
||||||
async function mirrorFields(mirror: Mirror, outcome: LogOutcome): Promise<void> {
|
|
||||||
await mirror.setStage(outcome.issue, outcome.stage);
|
|
||||||
await mirror.setTargetDate(outcome.issue, outcome.next_review);
|
|
||||||
if (outcome.first) await mirror.setFirstAttempt(outcome.issue, outcome.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirror one logged attempt into GitHub: Project fields always; on a
|
* Mirror one logged attempt into GitHub: the Project's Target Date always; on a
|
||||||
* first-ever pass also close the problem's sub-issue (comment first — repo
|
* first-ever pass also close the problem's sub-issue (comment first — repo
|
||||||
* convention). Runs inside ctx.waitUntil; failures are warnings.
|
* convention). Runs inside ctx.waitUntil; failures are warnings.
|
||||||
*/
|
*/
|
||||||
async function mirrorOutcome(env: Env, outcome: LogOutcome, source: string): Promise<void> {
|
async function mirrorOutcome(env: Env, outcome: LogOutcome, source: string): Promise<void> {
|
||||||
if (outcome.error || outcome.duplicate) return;
|
if (outcome.error || outcome.duplicate) return;
|
||||||
const gh = github(env.GH_PAT, env.REPO);
|
const gh = github(env.GH_PAT, env.REPO);
|
||||||
await mirrorFields(projectMirror(gh, env.REPO.split("/")[0]!), outcome);
|
const mirror = projectMirror(gh, env.REPO.split("/")[0]!);
|
||||||
|
await mirror.setTargetDate(outcome.issue, outcome.next_review);
|
||||||
if (outcome.first && outcome.result === "pass") {
|
if (outcome.first && outcome.result === "pass") {
|
||||||
try {
|
try {
|
||||||
await gh.closeIssue(
|
await gh.closeIssue(
|
||||||
@@ -248,7 +242,7 @@ async function handleSolved(request: Request, env: Env, date: string): Promise<R
|
|||||||
logged.push(outcomeLine(outcome));
|
logged.push(outcomeLine(outcome));
|
||||||
// One mirror for the batch: field IDs resolve once, not per problem.
|
// One mirror for the batch: field IDs resolve once, not per problem.
|
||||||
mirror ??= projectMirror(github(env.GH_PAT, env.REPO), env.REPO.split("/")[0]!);
|
mirror ??= projectMirror(github(env.GH_PAT, env.REPO), env.REPO.split("/")[0]!);
|
||||||
await mirrorFields(mirror, outcome);
|
await mirror.setTargetDate(outcome.issue, outcome.next_review);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Response.json({ date, logged, skipped, warnings: mirror?.warnings ?? [] });
|
return Response.json({ date, logged, skipped, warnings: mirror?.warnings ?? [] });
|
||||||
|
|||||||
+10
-39
@@ -4,30 +4,26 @@
|
|||||||
* console warning and never blocks a state write. Only problem issues are
|
* console warning and never blocks a state write. Only problem issues are
|
||||||
* ever passed in, so topic rows' Target Date is never written.
|
* ever passed in, so topic rows' Target Date is never written.
|
||||||
*
|
*
|
||||||
* Field/option IDs are resolved by NAME per invocation (a Worker isolate is
|
* Target Date is the ONLY mirrored column. SRS stage and first-attempt result
|
||||||
* short-lived; the two extra queries per mirror burst are irrelevant at this
|
* live exclusively in D1 — they are read from there (charts, digest, /api/stats)
|
||||||
* volume and survive field re-creation).
|
* and duplicating them into Project single-selects bought nothing but drift and
|
||||||
|
* two extra writes per log. Do not re-add them here.
|
||||||
|
*
|
||||||
|
* Field IDs are resolved by NAME per invocation (a Worker isolate is
|
||||||
|
* short-lived; the extra query per mirror burst is irrelevant at this
|
||||||
|
* volume and survives field re-creation).
|
||||||
*/
|
*/
|
||||||
import type { GitHub } from "./github.ts";
|
import type { GitHub } from "./github.ts";
|
||||||
|
|
||||||
const PROJECT_TITLE = "Interview Prep";
|
const PROJECT_TITLE = "Interview Prep";
|
||||||
|
|
||||||
interface SelectField {
|
|
||||||
id: string;
|
|
||||||
options: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectInfo {
|
interface ProjectInfo {
|
||||||
id: string;
|
id: string;
|
||||||
targetDate: string;
|
targetDate: string;
|
||||||
srsStage: SelectField;
|
|
||||||
firstAttempt: SelectField;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Mirror {
|
export interface Mirror {
|
||||||
setTargetDate(issue: number, date: string | null): Promise<void>;
|
setTargetDate(issue: number, date: string | null): Promise<void>;
|
||||||
setStage(issue: number, stage: string): Promise<void>;
|
|
||||||
setFirstAttempt(issue: number, result: string): Promise<void>;
|
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +43,6 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
fields(first: 30) {
|
fields(first: 30) {
|
||||||
nodes {
|
nodes {
|
||||||
... on ProjectV2FieldCommon { id name dataType }
|
... on ProjectV2FieldCommon { id name dataType }
|
||||||
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +57,7 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
fields: {
|
fields: {
|
||||||
nodes: { id: string; name: string; options?: { id: string; name: string }[] }[];
|
nodes: { id: string; name: string }[];
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
@@ -70,19 +65,9 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
};
|
};
|
||||||
const node = data.user.projectsV2.nodes.find((n) => n.title === PROJECT_TITLE);
|
const node = data.user.projectsV2.nodes.find((n) => n.title === PROJECT_TITLE);
|
||||||
if (!node) throw new Error(`project "${PROJECT_TITLE}" not found for @${owner}`);
|
if (!node) throw new Error(`project "${PROJECT_TITLE}" not found for @${owner}`);
|
||||||
const select = (name: string): SelectField => {
|
|
||||||
const f = node.fields.nodes.find((n) => n.name === name);
|
|
||||||
if (!f?.options) throw new Error(`single-select field "${name}" missing`);
|
|
||||||
return { id: f.id, options: Object.fromEntries(f.options.map((o) => [o.name, o.id])) };
|
|
||||||
};
|
|
||||||
const date = node.fields.nodes.find((n) => n.name === "Target Date");
|
const date = node.fields.nodes.find((n) => n.name === "Target Date");
|
||||||
if (!date) throw new Error(`date field "Target Date" missing`);
|
if (!date) throw new Error(`date field "Target Date" missing`);
|
||||||
info = {
|
info = { id: node.id, targetDate: date.id };
|
||||||
id: node.id,
|
|
||||||
targetDate: date.id,
|
|
||||||
srsStage: select("SRS Stage"),
|
|
||||||
firstAttempt: select("First Attempt"),
|
|
||||||
};
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,19 +139,5 @@ export function projectMirror(gh: GitHub, owner: string): Mirror {
|
|||||||
await setField(issue, project.targetDate, { date });
|
await setField(issue, project.targetDate, { date });
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
setStage: (issue, stage) =>
|
|
||||||
attempt(`SRS Stage #${issue}`, async () => {
|
|
||||||
const project = await resolve();
|
|
||||||
const option = project.srsStage.options[stage];
|
|
||||||
if (!option) throw new Error(`no option "${stage}"`);
|
|
||||||
await setField(issue, project.srsStage.id, { singleSelectOptionId: option });
|
|
||||||
}),
|
|
||||||
setFirstAttempt: (issue, result) =>
|
|
||||||
attempt(`First Attempt #${issue}`, async () => {
|
|
||||||
const project = await resolve();
|
|
||||||
const option = project.firstAttempt.options[result];
|
|
||||||
if (!option) throw new Error(`no option "${result}"`);
|
|
||||||
await setField(issue, project.firstAttempt.id, { singleSelectOptionId: option });
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user