Skip to content
2 changes: 1 addition & 1 deletion .claude/skills/add-contest-table-provider/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: add-contest-table-provider
description: Add a new ContestType and ContestTableProvider across 5 layers using TDD. Covers all 3 patterns. Asks targeted questions to gather pattern-specific requirements before touching code.
description: Add a new ContestType and ContestTableProvider across 5 layers using TDD. Covers all 4 patterns. Asks targeted questions to gather pattern-specific requirements before touching code.
argument-hint: '<ContestType> <contest_id>'
---

Expand Down
132 changes: 39 additions & 93 deletions .claude/skills/add-contest-table-provider/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,138 +14,84 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
- Pattern 1: numeric range filter (e.g. ABC 001–041)
- Pattern 2: single fixed contest_id (e.g. NDPC, TDPC, FPS_24)
- Pattern 3: multiple contest_ids unified in one table (e.g. ABS, ABC-Like)
- Pattern 4: one class instantiated N times via constructor parameter (e.g. ICPC Prelim by year)
- Pattern 4: one class instantiated N times via constructor parameter (e.g. ICPC by year)
- Nearest neighbor ContestType for insertion order in `contestTypePriorities`?
- New group or merge into existing? If new: group name / `buttonLabel` / `ariaLabel`?

**Pattern 1 additional:**
**Pattern 1:** For AWC, use config-driven `AWCRangeProvider` (skip Layer 4; instantiate in Layer 5 only. See `awc_provider.ts`). Otherwise confirm range, shared problems, and round label format.

- Numeric range: start and end (open-ended if no upper bound)?
- Splitting an existing "Onwards" provider? Rename to `{Start}To{End}Provider`; the new special-edition provider uses a fixed section string (e.g. `super(contestType, '0100')`) to coexist under the same ContestType.
- Shared problems with another contest (e.g. ARC–ABC overlap)? Which contest_ids appear in both?
- Round label format (e.g. `ABC 042`)?
**Pattern 2:** For AWC special rounds, use config-driven `AWCSpecialContestProvider` (same as above).

**Pattern 4 additional:**
**Pattern 3:** Show the contest_id list from `prisma/tasks.ts`. Confirm `contest_task_pairs.ts` update needed, index format (numeric/alphabetic), section splits.

- Constructor parameter name and type (e.g. `year: number`)?
- Year/ID range: oldest and latest? Export both as named constants so tests can reference them.
- Iteration order: latest-first so newest table renders on top.
- `task_table_index` values numeric strings? → override `getHeaderIdsForTask`; sort with `Number(a) - Number(b)`.
- Display-only positional label needed (e.g. prepend "A. ")? → override `getTaskLabels` to return `{ [contestId]: { index: letter } }`; **never mutate title inside `generateTable`** (transformed objects written back via optimistic update cause prefix accumulation on the next `$derived` re-run).
- Known edge cases where the default algorithm breaks? → add a `Record<string, Record<string, string>>` module-level override map keyed by contest_id; exercise the override path in tests by mutating the export in `beforeEach` and cleaning up in `afterEach`.

**Pattern 3 additional:**

- Show the full contest_id list found in `prisma/tasks.ts` — any missing or to exclude?
- Does `prisma/contest_task_pairs.ts` need updating (shared task_ids across contests)?
- task_table_index format: numeric (`001–`) or alphabetic (`A–`)?
- Section splits needed? If yes: split key and section names?
**Pattern 4:** Confirm parameter name/type, range (oldest/latest), whether index is numeric string, display label transformation needed, known edge cases.

---

## Layer 1 — Prisma schema

> **Skip Layers 1–2** when an existing ContestType already covers this contest's family — e.g.
> `AOJ_JAG` spans JAG Prelim / Regional / Camp, so adding Prelim-only reuses it with no schema /
> priority / type change. Only add a new ContestType when no existing one fits; then Layer 3 is
> also just a classify-regex tweak, not a new branch.
## Layers 1–2 — Add ContestType

- [ ] Add `XXX // Full Contest Name` to `prisma/schema.prisma` ContestType enum (after nearest neighbor)
- [ ] `pnpm exec prisma generate` — non-interactive env; `migrate dev` requires interactive shell
- [ ] `pnpm check` — expect a type error in `src/lib/types/contest.ts` (confirms client regenerated)
> Skip when an existing ContestType already covers this contest family (e.g. `AOJ_JAG` for JAG Prelim).

## Layer 2 — TypeScript ContestType constant

- [ ] Add `XXX: 'XXX', // Full Contest Name` to `ContestType` in `src/lib/types/contest.ts` (same position as schema)
- [ ] `pnpm check` — error should be gone
- [ ] Add to `prisma/schema.prisma` ContestType enum → `pnpm exec prisma generate`
- [ ] Add to `ContestType` in `src/lib/types/contest.ts` (same position as schema)
- [ ] `pnpm check` — confirm error resolved

## Layer 3 — Contest utilities (TDD)

### Write tests first
### Tests first

- [ ] Add export to `src/test/lib/utils/test_cases/contest_type.ts` (after nearest neighbor)
- [ ] Add export to `src/test/lib/utils/test_cases/contest_name_labels.ts` (after nearest neighbor)
- [ ] Add three `describe('when contest_id is xxx')` blocks to `src/test/lib/utils/contest.test.ts`:
- under `classify contest`
- under `get contest priority`
- under `get contest name label`
- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **expect RED**
- [ ] Add exports to `src/test/lib/utils/test_cases/contest_type.ts` and `contest_name_labels.ts`
- [ ] Add 3 `describe` blocks to `src/test/lib/utils/contest.test.ts`: classify / priority / name label
- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **RED**

### Implement

- [ ] Add `classifyContest` branch after nearest neighbor's branch in `src/lib/utils/contest.ts`
- [ ] Insert `[ContestType.XXX, N]` into `contestTypePriorities` after nearest neighbor
- All entries after the insertion point shift by +1
- **Update the JSDoc numeric ranges** — do NOT rename or split the existing four categories
(Educational / Contests for genius / Special contests / External platforms)
- **Search `src/test/lib/utils/task.test.ts` for hardcoded priority-diff expected values**
and decrement by 1 for every ContestType that shifted
- [ ] Add `getContestNameLabel` branch after nearest neighbor's branch
- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **expect GREEN**
- [ ] Add branches to `classifyContest` / `contestTypePriorities` / `getContestNameLabel`
- After priority insertion, all later entries shift +1 → **update JSDoc numeric ranges** (4 category names are immutable)
- Fix hardcoded priority-diff expected values in `src/test/lib/utils/task.test.ts` (-1 per shifted entry)
- [ ] **GREEN**

---

## Layer 4 — Provider class (TDD)

### Pattern 2: single source

- [ ] Add entry to `describe.each` array in `dp_providers.test.ts` (or the appropriate `*_providers.test.ts`)
- [ ] Add import of new Provider class
- [ ] `pnpm test:unit <providers.test.ts>` — **expect RED**
- [ ] Implement Provider class in the appropriate `*_providers.ts` after nearest neighbor
- [ ] `pnpm test:unit <providers.test.ts>` — **expect GREEN**

### Pattern 1: range filter

- [ ] Add test cases covering range boundaries and at least one mid-range value
- [ ] If shared problems exist: add a test case with mixed contest_ids to confirm exclusion
- [ ] If splitting an existing range: add a combined-fixture test confirming the upper bound excludes the adjacent range's contest_id: `[...fixtureA, ...fixtureB]` → `filter` → assert `some(task => task.contest_id === 'out-of-range') === false`
- [ ] `pnpm test:unit <providers.test.ts>` — **expect RED**
- [ ] Implement Provider using `parseContestRound()` range check
- [ ] `pnpm test:unit <providers.test.ts>` — **expect GREEN**
- [ ] Tests: boundary values + mid-range. If shared problems exist, verify exclusion with mixed contest_ids. If splitting a range, verify adjacent range exclusion with `[...fixtureA, ...fixtureB]`
- [ ] **RED** → implement with `parseContestRound()` range check → **GREEN**

### Pattern 4: N-instances via constructor parameter
### Pattern 2: single source

- [ ] Export `OLDEST_YEAR` / `LATEST_YEAR` constants (module-level, before `prepareContestProviderPresets`) so tests can assert `getSize() === LATEST - OLDEST + 1`
- [ ] Pass the parameter as `section` in `super(contestType, String(param))` → provider key becomes `TYPE::value` (unique per instance)
- [ ] If `task_table_index` is a numeric string key: override `getHeaderIdsForTask` with numeric sort (`Number(a) - Number(b)`)
- [ ] If display title needs transformation (e.g. prepend "A. "): override `getTaskLabels` to return `{ [contestId]: { index: letter } }`; do NOT mutate title in `generateTable`
- [ ] Write override map (`Record<string, Record<string, value>>`) for known edge cases; test the override path by mutating the export in `beforeEach` and cleaning up in `afterEach`
- [ ] If provider headings need non-default font/weight/gap: return `titleStyle` (`headingTag` / `fontSize` / `fontWeight` / `bottomGap`) from `getMetadata()`; include all set fields in the `titleStyle` assertion
- [ ] If column wrap threshold differs from default (8): return `columnWrapThreshold: N` from `getDisplayConfig()`; include it in the `getDisplayConfig` assertion
- [ ] `pnpm test:unit <providers.test.ts>` — **expect GREEN**
- [ ] Tests: add entry to `describe.each`
- [ ] **RED** → implement Provider class → **GREEN**

### Pattern 3: composite

- [ ] Confirm whether `prisma/contest_task_pairs.ts` needs new entries before writing tests
- [ ] Add test cases for each constituent contest_id, plus a mixed-source test
- [ ] If section splits: add one test per section
- [ ] `pnpm test:unit <providers.test.ts>` — **expect RED**
- [ ] Implement Provider (filter by `classifyContest` equality; add section subclasses if needed)
- [ ] `pnpm test:unit <providers.test.ts>` — **expect GREEN**
- [ ] Check whether `prisma/contest_task_pairs.ts` needs updates first
- [ ] Tests: each contest_id + mixed-source. If section splits, one test per section
- [ ] **RED** → implement with `classifyContest` equality filter → **GREEN**

### Pattern 4: constructor parameter

- [ ] Export `OLDEST_YEAR` / `LATEST_YEAR` constants. Use `super(contestType, String(param))` for unique provider key
- [ ] Tests: `getSize() === LATEST - OLDEST + 1`, etc.
- [ ] **RED** → implement → **GREEN**
- [ ] If display label needed: use `getTaskLabels` — do NOT mutate title inside `generateTable` (optimistic update + `$derived` re-run causes prefix accumulation)
- [ ] For AOJ-specific options (`getTaskLabels` / override map / `titleStyle` / `columnWrapThreshold`), see `aoj_icpc_providers.ts` / `aoj_jag_providers.ts`

---

## Layer 5 — Group registration (TDD)

- [ ] Update `contest_table_provider_groups.test.ts`:
- New group name string, `buttonLabel`, `ariaLabel`
- `getSize()` incremented to reflect the new provider count
- Add `getProvider(ContestType.XXX)` assertion; for section-based providers use `getProvider(ContestType.XXX, 'section')`
- Add import of new Provider class
- [ ] `pnpm test:unit contest_table_provider_groups.test.ts` — **expect RED**
- [ ] Update `contest_table_provider_groups.ts`:
- Add import of new Provider class
- Update group name string, `buttonLabel`, `ariaLabel`
- Add `new XXXProvider(ContestType.XXX)` to `addProvider()` chain — **`addProvider` call order = display order (first = top)**
- [ ] `pnpm test:unit src/features/tasks/utils/contest-table/` — **expect GREEN**
- [ ] `contest_table_provider_groups.test.ts`: add group name, `buttonLabel`, `ariaLabel`, `getSize()`, `getProvider()` assertions; for section-based providers use `getProvider(ContestType.XXX, 'section')`
- [ ] **RED**
- [ ] `contest_table_provider_groups.ts`: add import + `addProvider()` (**call order = display order; first = top**)
- [ ] `pnpm test:unit src/features/tasks/utils/contest-table/` — **GREEN**

---

## Final verification

- [ ] `pnpm test:unit`
- [ ] `pnpm check`
- [ ] `pnpm lint`
- [ ] `pnpm test:unit` / `pnpm check` / `pnpm lint`

Commit Layer 1–3 and Layer 4–5 as separate commits.
30 changes: 27 additions & 3 deletions docs/guides/how-to-add-contest-table-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@

`contest_id` の数値部分で範囲フィルタ。ラウンド名・ヘッダー表示、問題 id 非表示。

**AWC** は config-driven な `AWCRangeProvider` を使い、`minRound`/`maxRound` 等を config で渡してインスタンス化する(具象クラスの追加不要)。

```typescript
// AWC: config-driven(新規追加時はインスタンス化のみ)
new AWCRangeProvider(ContestType.AWC, {
section: '0001To0099',
minRound: 1,
maxRound: 99,
title: 'AtCoder Weekday Contest 0001 〜 0099',
abbreviationName: 'awc0001To0099',
});

// ABC 等: 具象クラスで実装(従来パターン)
protected setFilterCondition(): (taskResult: TaskResult) => boolean {
return (taskResult: TaskResult) => {
if (classifyContest(taskResult.contest_id) !== this.contestType) return false;
Expand All @@ -34,7 +46,18 @@ protected setFilterCondition(): (taskResult: TaskResult) => boolean {

単一 `contest_id` のみフィルタ。問題 id 表示、ラウンド名・ヘッダー非表示。

**AWC 特殊回** は config-driven な `AWCSpecialContestProvider` を使い、`contestId` 等を config で渡してインスタンス化する(具象クラスの追加不要)。

```typescript
// AWC 特殊回: config-driven(新規追加時はインスタンス化のみ)
new AWCSpecialContestProvider(ContestType.AWC, {
section: '0100',
contestId: 'awc0100',
title: 'AtCoder Weekday Contest 0100',
abbreviationName: 'awc0100',
});

// EDPC 等: 具象クラスで実装(従来パターン)
protected setFilterCondition(): (taskResult: TaskResult) => boolean {
return (taskResult: TaskResult) => {
if (classifyContest(taskResult.contest_id) !== this.contestType) return false;
Expand All @@ -61,7 +84,7 @@ AOJ/JAG 固有の詳細(`AOJ_LABEL_OVERRIDES`、`titleStyle`、同一年2回

### 補足: 固定セクションによる同一 ContestType 内の共存

同一 `ContestType` で異なるクラスを共存させる場合、`super(contestType, '0100')` のように固定文字列で provider key を一意化する(例: `AWC::0100`)。AWC0100 / AWC0150 はパターン2だが、範囲フィルタ型の AWC0001-0099 等と同じ `ContestType.AWC` グループに登録されている。
同一 `ContestType` で異なるプロバイダを共存させる場合、config の `section` フィールドで provider key を一意化する(例: `AWC::0100`)。AWC は `AWCRangeProvider`(通常回)と `AWCSpecialContestProvider`(特殊回)の2クラスを config で使い分けており、すべて同じ `ContestType.AWC` グループに登録される。新しい AWC 回の追加は `contest_table_provider_groups.ts` で `new AWCRangeProvider(...)` または `new AWCSpecialContestProvider(...)` をインスタンス化するだけでよい。

---

Expand All @@ -82,6 +105,7 @@ AOJ/JAG 固有の詳細(`AOJ_LABEL_OVERRIDES`、`titleStyle`、同一年2回
| AGC 001- | 001~ | 4~7問 | - |
| AWC 0001-0099 | 0001~0099 | A~E | - |
| AWC 0101-0149 | 0101~0149 | A~E | - |
| AWC 0151- | 0151~ | A~E | - |

### 単一ソース型

Expand All @@ -95,7 +119,7 @@ AOJ/JAG 固有の詳細(`AOJ_LABEL_OVERRIDES`、`titleStyle`、同一年2回
| ACL_BEGINNER / ACL_CONTEST1 | `'abl'`/`'acl1'` | 6問 | A~F |
| AWC0100 / AWC0150† | `'awc0100'`/`'awc0150'` | 15問 | A~O |

†注: ContestType.AWC を再利用し、section(`'0100'`, `'0150'`)で provider key を一意化。ACL 系は `Acl` グループ、DP 系は `dps` グループで統一管理。
†注: ContestType.AWC を再利用し、section(`'0100'`, `'0150'`, `'0151Onwards'`)で provider key を一意化。ACL 系は `Acl` グループ、DP 系は `dps` グループで統一管理。

### コンストラクタパラメータ型 / regex フィルタ型

Expand Down Expand Up @@ -142,4 +166,4 @@ AOJ/JAG 固有の詳細(`AOJ_LABEL_OVERRIDES`、`titleStyle`、同一年2回

---

**最終更新**: 2026-09-03
**最終更新**: 2026-09-05
Loading
Loading