Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/add-contest-table-provider/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ Add a new contest table provider for: $ARGUMENTS

> When in doubt at any step, use AskUserQuestion before proceeding.

0. **Seed check** — grep `prisma/tasks.ts` for the contest_id(s); report count + task_ids; if absent or incomplete, ask the user to add missing rows (reference: `https://kenkoooo.com/atcoder/resources/problems.json`)
0. **Seed check** — grep `prisma/tasks.ts` for the contest_id(s); report count + task_ids; if absent or incomplete, ask the user to paste the missing rows (source: `https://kenkoooo.com/atcoder/resources/problems.json`). Do not fetch it yourself: atcoder.jp is unreachable, and WebFetch truncates problems.json, so a miss there is not evidence of absence
1. **Gather requirements** — infer the implementation pattern; confirm per [instructions.md §Requirements](instructions.md)
2. **Implement** — follow [instructions.md](instructions.md) for the confirmed pattern across 5 layers (TDD)
5 changes: 3 additions & 2 deletions .agents/skills/add-contest-table-provider/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
- 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 by year)
- task_id format? Do not derive it from contest_id (`game-24` → `game_a`, but `fps-24` → `fps_24_a`)
- Nearest neighbor ContestType for insertion order in `contestTypePriorities`?
- New group or merge into existing? If new: group name / `buttonLabel` / `ariaLabel`?

Expand All @@ -32,7 +33,7 @@ Step 0 (seed check) is already done. Confirm the following before touching code:

> Skip when an existing ContestType already covers this contest family (e.g. `AOJ_JAG` for JAG Prelim).

- [ ] Add to `prisma/schema.prisma` ContestType enum → `pnpm exec prisma generate`
- [ ] Add to `prisma/schema.prisma` ContestType enum → `pnpm exec prisma migrate dev --name add_<type>_to_contest_type` (also regenerates the client)
- [ ] Add to `ContestType` in `src/lib/contests/types/contest.ts` (same position as schema)
- [ ] `pnpm check` — confirm error resolved

Expand All @@ -51,7 +52,7 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
- [ ] `CONTEST_TYPES_BY_ID` or `CLASSIFICATION_RULES` in `src/lib/contests/utils/classification.ts`
- [ ] `contestTypePriorities` in `src/lib/contests/utils/priority.ts`
- 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)
- Fix hardcoded priority-diff expected values in `src/test/lib/utils/task.test.ts` only when they involve a shifted entry (±1 each)
- [ ] `LABEL_GENERATORS` in `src/lib/contests/utils/labels/index.ts` (add label generator, create per-type file if complex)
- [ ] **GREEN**

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/agent-rules/prisma-db.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ paths:

1. Edit `prisma/schema.prisma`
2. Run `pnpm exec prisma migrate dev --name <snake_case_description>`
3. Production needs no manual step: CI (`.github/workflows/ci.yml`) runs `prisma migrate deploy` on push to `main`, before deploying. Do not add a manual deploy step to plans

## Naming

Expand All @@ -21,7 +22,6 @@ paths:
## Server-Only Code

- Import the DB client (`$lib/server/database`) only in `src/lib/server/**` and in server-only services under `src/lib/services/**` and `src/features/**/services/**`
- Route handlers must not import the DB client directly — call service methods instead
- Never import server-only code in client components

## Service Layer
Expand Down
5 changes: 2 additions & 3 deletions docs/guides/agent-rules/svelte-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Use `$props()`, `$state()`, `$derived()`, `$effect()` in all components:

- Components: `PascalCase.svelte`
- Stores: `snake_case.svelte.ts`; class-based with `$state()`, singleton export
- Values persisted to `localStorage` or URL params are a contract: rename display labels freely, but changing a stored key/value (e.g. preset key `dps`) silently drops users' saved state

## Props & Reactivity

Expand All @@ -37,7 +38,7 @@ Use `$props()`, `$state()`, `$derived()`, `$effect()` in all components:
## `{#each}` Patterns

- Always key: `(item.id)` or `(i)`
- **Key MUST be unique per iteration** — if domain allows duplicates, use composite key (e.g. `contest_id + '-' + task_id`)
- **Key MUST be unique per iteration** — if domain allows duplicates, use composite key. Trap: `task_id` alone repeats when a task appears in multiple contests; use `contest_id + '-' + task_id` (#3460, #3442)
- `(i)` is only safe when the list's order and length never change (e.g. a fixed split of a string into lines). For a mutable list — reorderable, insertable, or deletable — use a persistent unique ID or composite key; `(i)` there causes Svelte to reuse DOM/state across the wrong items after a mutation.
- Filter **before**, not inside with `{#if}`
- Use `{:else}` for empty lists
Expand All @@ -50,8 +51,6 @@ Use `$props()`, `$state()`, `$derived()`, `$effect()` in all components:
{/each}
```

**Common Trap:** `task_id` alone is NOT unique when same task appears in multiple contests. Use `contest_id + '-' + task_id` as composite key (see issue #3460 & PR #3442).

## Snippets vs Components

- **Snippet**: needs `$state` access, pure display, same-file; params require explicit type `{#snippet label(item: Item)}`
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/agent-rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ paths:
- Enum membership: `Object.hasOwn(Enum, value)`, not `in` (avoids prototype chain)
- `Promise<void>`: use `await fn()` to assert no throw, or `.resolves.toBeUndefined()`. **Never** bare `.resolves` (false-positive)
- **Stubs**: parameter types must match production signature — use domain types (`TaskGrade`), not `string`
- **Test data**: realistic values (real task IDs, grade names). Extract shared fixtures to file/describe scope; inline for single-use
- **Test data**: realistic values (real task IDs, grade names). Extract shared fixtures to file/describe scope; inline for single-use. When copying a neighbor's tests, grep each fixture/helper's usages first — do not replicate unreferenced ones
- **Registry exhaustiveness**: `Record<string, T>` and `new Map([...])` give no compile-time enum coverage — assert it at runtime, with a named, commented exception list so deliberate gaps stay documented: `expect(Object.values(Enum).filter((key) => !REGISTRY.has(key))).toEqual(KNOWN_GAPS)` (`Record` registries: swap `REGISTRY.has(key)` for the `Object.hasOwn` form above)
- **Test count as a proxy**: after a file split, compare counts (`--reporter=verbose`) — a drop means a describe block was lost in the move. A deliberate drop is valid only when mutation analysis shows detection is unchanged; record the evidence in the commit

Expand Down
3 changes: 3 additions & 0 deletions docs/guides/how-to-add-contest-table-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,15 @@ AOJ/JAG 固有の詳細(`AOJ_LABEL_OVERRIDES`、`titleStyle`、同一年2回
| TDPC | `'tdpc'` | 26問 | A~Z |
| NDPC | `'ndpc'` | 20問 | A~T |
| FPS_24 | `'fps-24'` | 24問 | A~X |
| GAME_24‡ | `'game-24'` | 24問 | A~X |
| ACL_PRACTICE | `'practice2'` | 12問 | A~L |
| ACL_BEGINNER / ACL_CONTEST1 | `'abl'`/`'acl1'` | 6問 | A~F |
| AWC0100 / AWC0150† | `'awc0100'`/`'awc0150'` | 15問 | A~O |

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

‡注: task_id は `game_a`〜`game_x` で、contest_id の数字を含まない(`fps-24` は `fps_24_a`)。

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

| コンテスト | フィルタ方式 | 備考 |
Expand Down
21 changes: 21 additions & 0 deletions e2e/problems_contest_table.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from '@playwright/test';

const XDPC_GROUP_ARIA_LABEL = 'xDPC and x 24 contests';
const XDPC_GROUP_TABLE_TITLES = [
'Educational DP Contest / DP まとめコンテスト',
'Typical DP Contest',
'Next DP Contest',
'FPS 24 題',
'組合せゲーム 24 題',
];

test.describe('xDPC group in contest table', () => {
test('shows Game 24 table right after FPS 24 in the xDPC group', async ({ page }) => {
await page.goto('/problems');

await page.getByRole('button', { name: XDPC_GROUP_ARIA_LABEL }).click();

// Headings render even when the seed has no tasks for a provider, so this stays seed-independent.
await expect(page.getByRole('heading', { level: 2 })).toHaveText(XDPC_GROUP_TABLE_TITLES);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ContestType" ADD VALUE 'GAME_24';
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ enum ContestType {
AWC // AtCoder Weekday Contest
UNIVERSITY // University Programming Contests in AtCoder (e.g., UTPC)
FPS_24 // 24 Problems on Formal Power Series
GAME_24 // 24 Problems on Combinatorial Games
ATCODER_MAIN_OFFICIAL_ONSITE // AtCoder World Tour Finals (official onsite finals)
OTHERS // AtCoder (その他)
AOJ_COURSES // AIZU ONLINE JUDGE Courses
Expand Down
168 changes: 168 additions & 0 deletions prisma/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8809,6 +8809,174 @@ export const tasks = [
name: 'Bacteria Growth Experiment',
title: 'A. Bacteria Growth Experiment',
},
{
id: 'game_x',
contest_id: 'game-24',
problem_index: 'X',
name: 'XOR Bag',
title: 'X. XOR Bag',
},
{
id: 'game_w',
contest_id: 'game-24',
problem_index: 'W',
name: 'Parity Game',
title: 'W. Parity Game',
},
{
id: 'game_v',
contest_id: 'game-24',
problem_index: 'V',
name: 'Delete Game',
title: 'V. Delete Game',
},
{
id: 'game_u',
contest_id: 'game-24',
problem_index: 'U',
name: 'Rectangle Flip Game',
title: 'U. Rectangle Flip Game',
},
{
id: 'game_t',
contest_id: 'game-24',
problem_index: 'T',
name: 'Consecutive Move Game',
title: 'T. Consecutive Move Game',
},
{
id: 'game_s',
contest_id: 'game-24',
problem_index: 'S',
name: 'Odd and Even',
title: 'S. Odd and Even',
},
{
id: 'game_r',
contest_id: 'game-24',
problem_index: 'R',
name: 'Delete and Divide',
title: 'R. Delete and Divide',
},
{
id: 'game_q',
contest_id: 'game-24',
problem_index: 'Q',
name: 'Moving Pieces on Graph',
title: 'Q. Moving Pieces on Graph',
},
{
id: 'game_p',
contest_id: 'game-24',
problem_index: 'P',
name: 'Game on Tree',
title: 'P. Game on Tree',
},
{
id: 'game_o',
contest_id: 'game-24',
problem_index: 'O',
name: 'Moving Pieces on Grid',
title: 'O. Moving Pieces on Grid',
},
{
id: 'game_n',
contest_id: 'game-24',
problem_index: 'N',
name: 'One and All',
title: 'N. One and All',
},
{
id: 'game_m',
contest_id: 'game-24',
problem_index: 'M',
name: 'LRUD Game',
title: 'M. LRUD Game',
},
{
id: 'game_l',
contest_id: 'game-24',
problem_index: 'L',
name: 'Unfair Nim',
title: 'L. Unfair Nim',
},
{
id: 'game_k',
contest_id: 'game-24',
problem_index: 'K',
name: 'Domino in Tetromino 2',
title: 'K. Domino in Tetromino 2',
},
{
id: 'game_j',
contest_id: 'game-24',
problem_index: 'J',
name: 'Restricted Nim 2',
title: 'J. Restricted Nim 2',
},
{
id: 'game_i',
contest_id: 'game-24',
problem_index: 'I',
name: 'Necklace 2',
title: 'I. Necklace 2',
},
{
id: 'game_h',
contest_id: 'game-24',
problem_index: 'H',
name: 'Lexicographic Order Game',
title: 'H. Lexicographic Order Game',
},
{
id: 'game_g',
contest_id: 'game-24',
problem_index: 'G',
name: 'Round Nim',
title: 'G. Round Nim',
},
{
id: 'game_f',
contest_id: 'game-24',
problem_index: 'F',
name: 'Necklace 1',
title: 'F. Necklace 1',
},
{
id: 'game_e',
contest_id: 'game-24',
problem_index: 'E',
name: 'Restricted Nim 1',
title: 'E. Restricted Nim 1',
},
{
id: 'game_d',
contest_id: 'game-24',
problem_index: 'D',
name: 'Parallel Nim',
title: 'D. Parallel Nim',
},
{
id: 'game_c',
contest_id: 'game-24',
problem_index: 'C',
name: 'Domino in Tetromino 1',
title: 'C. Domino in Tetromino 1',
},
{
id: 'game_b',
contest_id: 'game-24',
problem_index: 'B',
name: 'Chocolate',
title: 'B. Chocolate',
},
{
id: 'game_a',
contest_id: 'game-24',
problem_index: 'A',
name: 'Nim',
title: 'A. Nim',
},
{
id: 'fps_24_x',
contest_id: 'fps-24',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,22 @@ describe('prepareContestProviderPresets', () => {
}
});

test('dps registers EDPC, TDPC, NDPC, and FPS 24 providers', () => {
test('dps registers EDPC, TDPC, NDPC, FPS 24, and Game 24 providers', () => {
const group = presets.dps();

expect(group.getGroupName()).toBe('xDPC・FPS 24');
expect(group.getGroupName()).toBe('xDPC・x 24 題');
expect(group.getMetadata()).toEqual({
buttonLabel: 'xDPC・FPS 24',
ariaLabel: 'xDPC and FPS 24 contests',
buttonLabel: 'xDPC・x 24 題',
ariaLabel: 'xDPC and x 24 contests',
});
expect(group.getSize()).toBe(4);
expect(group.getSize()).toBe(5);

const contestTypes = [
ContestType.EDPC,
ContestType.TDPC,
ContestType.NDPC,
ContestType.FPS_24,
ContestType.GAME_24,
];

for (const contestType of contestTypes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { MathAndAlgorithmProvider } from './math_and_algorithm_provider';
import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers';
import { FPS24Provider } from './fps24_provider';
import { Game24Provider } from './game24_provider';
import { ACLPracticeProvider, ACLBeginnerProvider, ACLProvider } from './acl_providers';
import {
JOIFirstQualRoundProvider,
Expand Down Expand Up @@ -239,17 +240,18 @@ export const prepareContestProviderPresets = () => {
}).addProvider(new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM)),

/**
* DP group (EDPC, TDPC, NDPC, and FPS 24)
* DP group (EDPC, TDPC, NDPC, FPS 24, and Game 24)
*/
dps: () =>
new ContestTableProviderGroup(`xDPC・FPS 24`, {
buttonLabel: 'xDPC・FPS 24',
ariaLabel: 'xDPC and FPS 24 contests',
new ContestTableProviderGroup(`xDPC・x 24 題`, {
buttonLabel: 'xDPC・x 24 題',
ariaLabel: 'xDPC and x 24 contests',
}).addProviders(
new EDPCProvider(ContestType.EDPC),
new TDPCProvider(ContestType.TDPC),
new NDPCProvider(ContestType.NDPC),
new FPS24Provider(ContestType.FPS_24),
new Game24Provider(ContestType.GAME_24),
),

/**
Expand Down
Loading
Loading