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
19 changes: 19 additions & 0 deletions .claude/rules/svelte-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ Import from `flowbite-svelte`. Use Tailwind v4 `dark:` prefix. When copying butt
- Move business logic to `_utils/` or `utils/` with tests
- Keep components thin: one responsibility

## `{@const}` Placement Restriction

`{@const}` is only valid as the immediate child of a control-flow block (`{#if}`, `{#each}`, `{:else}`, etc.) — placing it at the template top level is a compile error.

When you need a derived value in both a `{#if}` condition and its body, declare it with `$derived` in `<script>` instead:

```typescript
// ✅ in <script>
let groupMetadata = $derived(store?.getMetadata());
```

```svelte
{#if groupMetadata?.mainTitle}
<Heading>{groupMetadata.mainTitle}</Heading>
{/if}
```

This also avoids calling the getter twice per render.

## Reactive Data Pitfalls

- `let` captures initial value only; use `$derived` for derived/prop data
Expand Down
19 changes: 19 additions & 0 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@ afterEach(() => {
});
```

### Mutable Module-Level Exports

When a module exports a mutable `const` object (e.g. an override map), mutate it directly in `beforeEach`/`afterEach` to test override paths — no `vi.mock` needed:

```typescript
import { buildFn, OVERRIDE_MAP } from './module';

beforeEach(() => {
OVERRIDE_MAP['testKey'] = { '100': 'A', '102': 'C' };
});
afterEach(() => {
delete OVERRIDE_MAP['testKey'];
});

test('uses override map when entry exists', () => {
expect(buildFn('testKey', ['100', '102']).get('100')).toBe('A');
});
```

### Test Stubs

Parameter types **must match** production signature — use domain types (`TaskGrade`), not `string`. Mismatch compiles silently but breaks type safety.
Expand Down
23 changes: 22 additions & 1 deletion .claude/skills/add-contest-table-provider/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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)
- Nearest neighbor ContestType for insertion order in `contestTypePriorities`?
- New group or merge into existing? If new: group name / `buttonLabel` / `ariaLabel`?

Expand All @@ -23,6 +24,15 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
- Shared problems with another contest (e.g. ARC–ABC overlap)? Which contest_ids appear in both?
- Round label format (e.g. `ABC 042`)?

**Pattern 4 additional:**

- 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 title transform needed (e.g. prepend letter)? → override `generateTable` for the transform AND override `getHeaderIdsForTask` using the same key derivation; mismatched keys between the two methods cause missing cells.
- 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?
Expand Down Expand Up @@ -87,6 +97,16 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
- [ ] Implement Provider using `parseContestRound()` range check
- [ ] `pnpm test:unit <providers.test.ts>` — **expect GREEN**

### Pattern 4: N-instances via constructor parameter

- [ ] 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 `generateTable` is overridden to key the table by `task_table_index` directly: also override `getHeaderIdsForTask` using the same field and sort order
- [ ] If display title needs transformation (e.g. prepend "A. "): do it inside `generateTable`; DB data must remain unchanged
- [ ] 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
- [ ] `pnpm test:unit <providers.test.ts>` — **expect GREEN**

### Pattern 3: composite

- [ ] Confirm whether `prisma/contest_task_pairs.ts` needs new entries before writing tests
Expand All @@ -101,14 +121,15 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
## Layer 5 — Group registration (TDD)

- [ ] Update `contest_table_provider_groups.test.ts`:
- New group name string, `buttonLabel`, `ariaLabel`
- New group name string, `buttonLabel`, `ariaLabel` (add `mainTitle` if used)
- `getSize()` incremented to reflect the new provider count
- Add `getProvider(ContestType.XXX)` assertion
- 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 `mainTitle: 'XXX'` if the group needs a single h2 heading rendered above all providers (opt-in; omit when not needed)
- Add `new XXXProvider(ContestType.XXX)` to `addProviders()`
- [ ] `pnpm test:unit src/features/tasks/utils/contest-table/` — **expect GREEN**

Expand Down
61 changes: 61 additions & 0 deletions docs/guides/how-to-add-contest-table-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- **パターン1(範囲フィルタ型)**: ABC 001~041、ARC 058~103 など → 数値範囲でフィルタ
- **パターン2(単一ソース型)**: EDPC、TDPC、ACL_PRACTICE など → 単一 contest_id のみ
- **パターン3(複合ソース型)**: ABS、ABC-Like など → 複数 contest_id を統一表示
- **パターン4(コンストラクタパラメータ型)**: ICPC 国内予選など → 1クラスをコンストラクタ引数(年度等)で N 回インスタンス化
- 対応セクション: [実装パターン](#実装パターン)

- [ ] **JOI の contest_id サフィックス変更の確認**
Expand Down Expand Up @@ -192,6 +193,66 @@ protected setFilterCondition(): (taskResult: TaskResult) => boolean {

---

### パターン4: コンストラクタパラメータ型(ICPC 国内予選)

**特徴**:

- 年度などのパラメータを受け取る1つのクラスを N 回インスタンス化
- `super(contestType, String(year))` でセクションを年度文字列にし、プロバイダキーを `AOJ_ICPC::2025` のように一意化
- 年度範囲定数(`OLDEST_YEAR` / `LATEST_YEAR`)をモジュールトップで `export` し、tests でも参照できるようにする
- グループ登録時は最新年から古い年へ降順ループし、テーブルを新しい順に並べる

**実装例**:

```typescript
export const ICPC_PRELIM_OLDEST_YEAR = 1998;
export const ICPC_PRELIM_LATEST_YEAR = 2025;

export class AojIcpcPrelimProvider extends ContestTableProviderBase {
private readonly year: number;
private readonly contestId: string;

constructor(contestType: ContestType, year: number) {
super(contestType, String(year)); // provider key: AOJ_ICPC::2025
this.year = year;
this.contestId = `ICPCPrelim${year}`;
}
// ...
}

// グループ登録(最新年が上に来るよう降順)
for (let year = ICPC_PRELIM_LATEST_YEAR; year >= ICPC_PRELIM_OLDEST_YEAR; year--) {
group.addProvider(new AojIcpcPrelimProvider(ContestType.AOJ_ICPC, year));
}
```

**注意**:

- `generateTable` をオーバーライドして `task_table_index` をキーにする場合、`getHeaderIdsForTask` も**必ず同じフィールド・同じソート順**でオーバーライドすること。ベースクラスの `getHeaderIdsForTask` は `getTaskTableHeaderName()` 経由でキーを導出するため、`generateTable` と一致しないとセルが表示されない。
- `task_table_index` が数値文字列(例: `'1664'`)の場合、辞書順ではなく数値昇順ソートが必要: `Number(a) - Number(b)`
- アルゴリズムが成立しない例外年度には上書き Map を用意する:

```typescript
// contest_id -> (task_table_index -> letter). Used only for years with judge gaps.
export const ICPC_PRELIM_LABEL_OVERRIDES: Record<string, Record<string, string>> = {};
```

上書き Map のテストは `beforeEach`/`afterEach` でエントリを直接追加・削除する(`vi.mock` 不要):

```typescript
beforeEach(() => {
ICPC_PRELIM_LABEL_OVERRIDES['ICPCPrelimTest'] = { '1150': 'A', '1152': 'C' };
});
afterEach(() => {
delete ICPC_PRELIM_LABEL_OVERRIDES['ICPCPrelimTest'];
});
```

- グループ全体に一度だけ大見出し(h2)を表示したい場合は、グループ登録時に `mainTitle: 'XXX'` を追加する。省略すると描画されない。個々の provider の `title` が冗長になるなら年や回だけに絞っても良い(ICPC 国内予選は敢えて重複させた)。
- provider 見出しのフォント・太字・余白をデフォルトから変えたい場合は `getMetadata()` で `titleStyle` を返す。`ContestTableTitleStyle`(`headingTag` / `fontSize` / `fontWeight` / `bottomGap`)のうち必要なフィールドだけ指定すればよい。

---

### パターン3: 複合ソース型(ABS / TESSOKU_BOOK / MATH_AND_ALGORITHM)

**特徴**:
Expand Down
Loading
Loading