mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 15:36:26 +00:00
109 lines
5.6 KiB
Markdown
109 lines
5.6 KiB
Markdown
---
|
|||
|
|
name: project-fields
|
||
|
|
description: Reconcile the "Interview Prep" GitHub Project (user project #2) fields — Set and Difficulty projected from issue labels — and understand which fields the Worker mirrors vs which live only in D1. Use when asked to sync/fix/backfill Project fields, when Project columns look empty or stale, or when adding a field to the project.
|
||
|
|
---
|
||
|
|
|
||
|
|
# Interview Prep Project fields
|
||
|
|
|
||
|
|
The user-level Project **"Interview Prep"** (`prdlk` project #2, id `PVT_kwHOADoNjs4BhV1a`)
|
||
|
|
is a *board over* the issue catalog, never a source of truth. Every field is a projection
|
||
|
|
of something owned elsewhere.
|
||
|
|
|
||
|
|
## Field ownership — the rule that matters
|
||
|
|
|
||
|
|
| Field | Owner | Written by |
|
||
|
|
|---|---|---|
|
||
|
|
| `Set` (Core/Optional/Deferred) | issue label `set:*` | `sync-project-fields`, last step of `close-solved.yml` |
|
||
|
|
| `Difficulty` (Easy/Medium/Hard) | issue label `diff:*` | `sync-project-fields`, last step of `close-solved.yml` |
|
||
|
|
| `Target Date` | D1 `next_review` | Worker mirror, per log (`apps/api/src/mirror.ts`) |
|
||
|
|
| `Status`, `Labels`, `Milestone`, `Parent issue`, … | GitHub built-ins | GitHub |
|
||
|
|
|
||
|
|
**SRS stage and first-attempt result are deliberately NOT Project fields.** They live only
|
||
|
|
in D1; the charts, digest and `/api/stats` read them from there. Duplicating them into
|
||
|
|
Project single-selects bought nothing but drift and two extra writes per log. Do not re-add
|
||
|
|
them to the project, and do not re-add `setStage`/`setFirstAttempt` to `mirror.ts`.
|
||
|
|
|
||
|
|
## Reconciling Set / Difficulty
|
||
|
|
|
||
|
|
`apps/cli/sync-project-fields.ts` recomputes every desired value from the issue labels and
|
||
|
|
writes only what differs — same reconcile-don't-react shape as `close-solved`.
|
||
|
|
|
||
|
|
**In CI it runs itself.** It is the final step of `.github/workflows/close-solved.yml`, so
|
||
|
|
any push under `work/` reconciles the board along with the issues and D1. The step is
|
||
|
|
`continue-on-error: true` and skips cleanly when `PROJECT_PAT` is unset — D1 and the issues
|
||
|
|
are the truth, so a Project failure must never fail a run that already closed issues and
|
||
|
|
logged the solve. It honours the workflow's `dry_run` input via `DRY_RUN=true`.
|
||
|
|
|
||
|
|
Run it by hand when issues changed without a `work/` push (e.g. you just filed a problem
|
||
|
|
issue, or relabelled one):
|
||
|
|
|
||
|
|
```sh
|
||
|
|
# needs a token with the `project` scope — neither `gh auth token` nor GITHUB_TOKEN has it.
|
||
|
|
export GH_TOKEN=$(grep '^GH_PAT=' apps/api/.dev.vars | cut -d= -f2-)
|
||
|
|
|
||
|
|
bun run sync-project-fields -- --dry-run # or DRY_RUN=1|true; prints the table, writes nothing
|
||
|
|
bun run sync-project-fields # apply
|
||
|
|
bun run sync-project-fields # re-run: must report 0 writes
|
||
|
|
```
|
||
|
|
|
||
|
|
Invariants the script holds — preserve them in any edit:
|
||
|
|
|
||
|
|
- **Only `problem`-labelled items are touched.** Topic, cadence and review issues have no
|
||
|
|
set/difficulty; they are skipped, never written and never cleared.
|
||
|
|
- **Idempotent.** A second run reports `0 field value(s)`. That re-run *is* the verification.
|
||
|
|
- **Labels are truth.** Never "fix" a field by hand in the UI — fix the label, then reconcile.
|
||
|
|
A hand-edited field is silently reverted on the next run.
|
||
|
|
- **Fields and options resolve by NAME**, so re-creating a field in the UI doesn't strand a
|
||
|
|
dead id. A missing field or option is a hard error, not a silent skip.
|
||
|
|
- **A problem missing a `set:`/`diff:` label** warns and is left untouched (it means the
|
||
|
|
issue is mislabelled — fix it with the `campaign-issues` skill).
|
||
|
|
|
||
|
|
## Auth gotcha
|
||
|
|
|
||
|
|
Project v2 needs the `project` scope. The local `gh` login has only
|
||
|
|
`gist, read:org, repo, workflow`, so **any** `gh project`/GraphQL project call fails with
|
||
|
|
the default token. Use the Worker's `GH_PAT` from `apps/api/.dev.vars` via `GH_TOKEN`
|
||
|
|
(never echo it). Symptom of the wrong token: `Resource not accessible by personal access
|
||
|
|
token` or an empty `projectsV2.nodes`.
|
||
|
|
|
||
|
|
## Inspecting the project
|
||
|
|
|
||
|
|
```sh
|
||
|
|
export GH_TOKEN=$(grep '^GH_PAT=' apps/api/.dev.vars | cut -d= -f2-)
|
||
|
|
|
||
|
|
# fields + single-select options
|
||
|
|
gh api graphql -f owner=prdlk -f title="Interview Prep" -f query='
|
||
|
|
query($owner: String!, $title: String!) {
|
||
|
|
user(login: $owner) { projectsV2(first: 10, query: $title) { nodes { id title number
|
||
|
|
items(first: 1) { totalCount }
|
||
|
|
fields(first: 30) { nodes {
|
||
|
|
... on ProjectV2FieldCommon { id name dataType }
|
||
|
|
... on ProjectV2SingleSelectField { options { id name } } } } } } }
|
||
|
|
}' --jq '.data.user.projectsV2.nodes[] | select(.title=="Interview Prep")
|
||
|
|
| .fields.nodes[] | "\(.name)\t\(.dataType)\t\((.options // []) | map(.name) | join(","))"'
|
||
|
|
```
|
||
|
|
|
||
|
|
Writes use `updateProjectV2ItemFieldValue` with
|
||
|
|
`value: { singleSelectOptionId: … }` (dates use `{ date: "YYYY-MM-DD" }`); clearing uses
|
||
|
|
`clearProjectV2ItemFieldValue`. An item id is *not* an issue number — resolve it from the
|
||
|
|
project's `items` connection (or the issue's `projectItems`), as `mirror.ts` does.
|
||
|
|
|
||
|
|
## Adding a new projected field
|
||
|
|
|
||
|
|
1. Create the field + options in the Project UI.
|
||
|
|
2. Add it to the `FIELDS` table in `apps/cli/sync-project-fields.ts` — one entry mapping
|
||
|
|
label → option name. Nothing else needs to change; resolution, diffing, dry-run and
|
||
|
|
reporting are generic over that table.
|
||
|
|
3. Dry-run, apply, re-run to prove idempotency.
|
||
|
|
|
||
|
|
If the new field's truth lives in **D1** rather than a label, it belongs in the Worker
|
||
|
|
mirror instead (`apps/api/src/mirror.ts`) — but read the ownership rule above first and
|
||
|
|
default to *not* mirroring.
|
||
|
|
|
||
|
|
## Known trap
|
||
|
|
|
||
|
|
`resolve()` in `mirror.ts` throws if a field it asks for is absent, *before* caching
|
||
|
|
`info` — so one missing field breaks **every** mirrored write, including `Target Date`,
|
||
|
|
and only as a `console.warn`. If Target Date silently stops updating, check that every
|
||
|
|
field `resolve()` looks up still exists in the project.
|