Skip to content

Commit ba42850

Browse files
authored
Merge pull request #3252 from AtCoder-NoviSteps/#943
feat: Enable sorting of workbook order in Kanban board (#943)
2 parents 0df3181 + 5241da6 commit ba42850

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3899
-139
lines changed

.claude/rules/auth.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
---
22
description: Authentication rules
3-
globs:
3+
paths:
44
- 'src/lib/server/auth.ts'
55
- 'src/routes/(auth)/**'
6+
- 'src/routes/(admin)/_utils/auth.ts'
7+
- 'src/routes/(admin)/**'
68
- 'src/hooks.server.ts'
79
---
810

@@ -30,6 +32,9 @@ globs:
3032

3133
- `src/lib/server/auth.ts`: Lucia configuration
3234
- `src/hooks.server.ts`: Global request handler
35+
- `src/routes/(admin)/_utils/auth.ts`:
36+
- `validateAdminAccess(locals)` — for page routes; redirects to `/login` for both unauthenticated and non-admin users (do not use in `+server.ts`)
37+
- `validateAdminAccessForApi(locals)` — for API routes (`+server.ts`); throws `error(401)` if unauthenticated, `error(403)` if not admin
3338
- `prisma/schema.prisma`: User, Session, Key models
3439

3540
## Security

.claude/rules/coding-style.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Coding Style
2+
3+
## Naming
4+
5+
- **Abbreviations**: avoid non-standard abbreviations (`res``response`, `btn``button`). When in doubt, spell it out.
6+
- **Lambda parameters**: no single-character names (e.g., use `placement`, `workbook`). Iterator index `i` is the only exception.
7+
- **`upsert`**: only use when the implementation performs both insert and update. For insert-only, use `initialize`, `seed`, or another accurate verb.
8+
- **`any`**: before using `any`, check the value's origin — adding a missing `@types/*` or `devDependency` often provides the correct type.
9+
- **UI labels**: if a label does not match actual behavior, update it or add an inline comment explaining the intentional mismatch.
10+
11+
## Syntax
12+
13+
- **Braces**: always use braces for single-statement `if` blocks. Never `if () return;` — write `if () { return; }`.
14+
- **Plural type aliases**: define `type Placements = Placement[]` instead of using `Placement[]` directly in signatures and variables.
15+
16+
## Markdown Code Blocks
17+
18+
Always specify a language identifier on every fenced code block. Never write bare ` ``` `.
19+
20+
Common identifiers: `typescript`, `svelte`, `sql`, `bash`, `mermaid`, `json`, `prisma`, `html`, `css`.
21+
22+
## SvelteKit: Routes vs API Endpoints
23+
24+
- Page routes (`+page.server.ts`): use `redirect()` to navigate
25+
- API routes (`+server.ts`): use `error()` — throwing `redirect()` returns a 3xx response; `fetch` follows it by default and receives the HTML page at the redirect target instead of a JSON error
26+
27+
## Dual-Enforcement Constraints
28+
29+
When the same constraint is enforced in two layers (e.g. Zod validation + SQL `CHECK`), add an inline comment stating each layer's role and the obligation to keep them in sync:
30+
31+
```typescript
32+
// XOR constraint: dual enforcement via Zod (early validation) and a CHECK in migration.sql (last line of defence).
33+
// Prisma lacks @@check, so the SQL constraint is maintained manually. Keep both in sync.
34+
.refine(...)
35+
```
36+
37+
## Async Rollback: Capture State Before `await`
38+
39+
Capture `$state` values before the first `await` for safe rollback. A concurrent update can overwrite the variable while awaiting:
40+
41+
```typescript
42+
const previous = items; // capture before await
43+
try {
44+
await saveToServer(items);
45+
} catch {
46+
items = previous;
47+
}
48+
```

.claude/rules/prisma-db.md

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,69 @@
11
---
22
description: Prisma and database rules
3-
globs:
3+
paths:
44
- 'prisma/**'
55
- 'src/lib/server/**'
66
- 'src/lib/services/**'
7+
- 'src/features/**/services/**'
78
---
89

910
# Prisma & Database
1011

1112
## Schema Changes
1213

1314
1. Edit `prisma/schema.prisma`
14-
2. Run `pnpm exec prisma migrate dev --name <description>` to create migration
15-
3. Run `pnpm exec prisma generate` to update client (auto-runs after migrate)
15+
2. Run `pnpm exec prisma migrate dev --name <snake_case_description>`
1616

1717
## Naming
1818

19-
- Model names: `PascalCase` (e.g., `User`, `TaskAnswer`)
20-
- Field names: `camelCase` (preferred) or `snake_case` (legacy)
21-
- Relation fields: Descriptive names matching the relation
22-
23-
## Key Models
24-
25-
- `User`: User accounts with AtCoder validation status
26-
- `Task`: Tasks with difficulty grades (Q11-D6)
27-
- `TaskAnswer`: User submission status per task
28-
- `WorkBook`: task collections
29-
- `Tag` / `TaskTag`: task categorization
19+
- Models: `PascalCase` | Fields: `camelCase` (preferred) or `snake_case` (legacy)
3020

3121
## Server-Only Code
3222

33-
- Import database client only in `src/lib/server/`
34-
- Use `$lib/server/database` for Prisma client access
23+
- Import DB client only in `src/lib/server/` via `$lib/server/database`
3524
- Never import server code in client components
3625

26+
## Service Layer
27+
28+
- All CRUD through the service layer (`src/lib/services/` or `src/features/**/services/`)
29+
- Route handlers call service methods — no direct Prisma in `+server.ts` / `+page.server.ts`
30+
- Service functions return pure values (`{ error: string } | null`), never `Response` / `json()`
31+
3732
## Transactions
3833

39-
- Use `prisma.$transaction()` for multi-step operations
40-
- Handle errors with try-catch and proper rollback
34+
Use `prisma.$transaction()` for multi-step operations.
35+
36+
## N+1 Queries
37+
38+
Replace per-item DB calls in loops with a bulk fetch + `Map`:
39+
40+
```typescript
41+
const records = await prisma.foo.findMany({ where: { id: { in: ids } } });
42+
const map = new Map(records.map((r) => [r.id, r]));
43+
```
44+
45+
## Enum Types
46+
47+
Prisma-generated enums and app-defined enums are distinct TypeScript types even with identical members. Keep explicit casts at the boundary — do not remove them as "redundant".
48+
49+
## Idempotent Writes
50+
51+
Prefer `createMany({ skipDuplicates: true })` over catching P2002 for expected unique violations (e.g., double-submit). Maps to `INSERT ... ON CONFLICT DO NOTHING`. Top-level only (not nested); PostgreSQL/CockroachDB/SQLite only.
52+
53+
## Zod Schema for Int Fields
54+
55+
`z.number().positive()` passes decimals. For Prisma `Int` fields use `z.number().int().positive()`.
56+
57+
## Validate Constraints
58+
59+
Prisma does not support `@@check`. To add one:
60+
61+
1. `pnpm exec prisma migrate dev --create-only --name <description>` — generate migration without applying
62+
2. Edit the generated `migration.sql` to add the CHECK constraint manually
63+
3. `pnpm exec prisma migrate dev` — apply
64+
65+
Document the constraint in `prisma/ERD.md` (the only place it's visible):
66+
67+
```mermaid
68+
%% XOR constraint: workbookplacement_xor_grade_category — exactly one of taskGrade or solutionCategory must be non-null
69+
```

.claude/rules/svelte-components.md

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Svelte component development rules
3-
globs:
3+
paths:
44
- 'src/**/*.svelte'
55
- 'src/lib/components/**'
66
- 'src/lib/stores/**/*.svelte.ts'
@@ -10,37 +10,96 @@ globs:
1010

1111
## Runes Mode (Required)
1212

13-
- Use `$props()` for component props
14-
- Use `$state()` for reactive state
15-
- Use `$derived()` for computed values
16-
- Use `$effect()` for side effects
17-
18-
## Props Pattern
13+
Use `$props()`, `$state()`, `$derived()`, `$effect()` in all components. Props pattern:
1914

2015
```svelte
2116
<script lang="ts">
2217
interface Props {
2318
title: string;
2419
count?: number;
2520
}
26-
2721
let { title, count = 0 }: Props = $props();
2822
</script>
2923
```
3024

31-
## Stores
25+
## File Naming
3226

33-
- Place store files in `src/lib/stores/` with `.svelte.ts` extension
34-
- Use class-based stores with `$state()` for internal state
35-
- Export singleton instances
27+
- Components: `PascalCase.svelte`
28+
- Stores: `snake_case.svelte.ts` in `src/lib/stores/`, class-based with `$state()`, export singleton. Pre-Runes stores (using `writable()`, `.ts` extension) must be migrated to this pattern before adding features or extending them.
3629

3730
## Flowbite Svelte
3831

39-
- Import components from `flowbite-svelte`
40-
- Use Tailwind CSS v4 utility classes
41-
- Dark mode: Use `dark:` prefix for dark mode variants
32+
Import from `flowbite-svelte`. Use Tailwind CSS v4 utility classes. Dark mode: `dark:` prefix.
4233

43-
## File Naming
34+
## `$state()` Initialization with `$props()`
4435

45-
- Components: `PascalCase.svelte`
46-
- Stores: `snake_case.svelte.ts`
36+
Referencing `$props()` inside `$state()` initializer triggers "This reference only captures the initial value". Wrap with `untrack` if intentional:
37+
38+
```svelte
39+
let count = $state(untrack(() => initialCount)); // intentional: prop is initial seed only
40+
```
41+
42+
## `{#snippet}` Placement
43+
44+
Define snippets at the **top level**, outside component tags. Inside a tag = named slot = type error:
45+
46+
```svelte
47+
<!-- Good -->
48+
{#snippet footer()}...{/snippet}
49+
<Dialog {footer} />
50+
51+
<!-- Bad: named slot, not a snippet prop -->
52+
<Dialog>{#snippet footer()}...{/snippet}</Dialog>
53+
```
54+
55+
## Snippet vs Component
56+
57+
Prefer `{#snippet}` when: (1) needs direct `$state` access, (2) pure display only, (3) same-file DRY.
58+
Promote to component when: independent state/lifecycle needed, exceeds ~30 lines, or reused across files.
59+
60+
## Component Boundaries
61+
62+
- One component, one responsibility: don't mix display, state management, and data fetching
63+
- Extract `$derived`/`$effect` logic exceeding ~5 lines to a custom store
64+
- Extract repeated UI patterns (2+ uses) to a snippet or component (see Snippet vs Component)
65+
66+
## Keep Components Thin
67+
68+
Business logic and pure utilities belong outside `<script>` blocks — in the nearest `utils/` (or `_utils/` for routes), with adjacent unit tests. See `docs/guides/architecture.md` for layer-specific placement.
69+
70+
## Pure Functions and Side Effect Separation
71+
72+
Extract business logic as pure functions to `utils/` (or `_utils/` for routes); keep side effects in the caller:
73+
74+
```typescript
75+
// Pure function in _utils/ — testable, no side effects
76+
export function buildUpdatedUrl(url: URL, activeTab: ActiveTab): URL { ... }
77+
78+
// Side effect stays in the caller
79+
replaceState(buildUpdatedUrl($page.url, activeTab), {});
80+
```
81+
82+
## Empty-list Fallback in `{#each}`
83+
84+
Use `{:else}` to render a placeholder when the list is empty — no wrapper conditional needed:
85+
86+
```svelte
87+
{#each items as item (item.id)}
88+
<Card {item} />
89+
{:else}
90+
<p>No items yet.</p>
91+
{/each}
92+
```
93+
94+
## Eliminate Branching with Records
95+
96+
Replace `if`/ternary chains with `Record<EnumType, T>`:
97+
98+
```typescript
99+
const TAB_CONFIGS: Record<ActiveTab, TabConfig> = {
100+
curriculum: { label: 'Curriculum', ... },
101+
solution: { label: 'Solution', ... },
102+
};
103+
```
104+
105+
Use the enum type as the key type, not `string`.

.claude/rules/testing.md

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Testing rules and patterns
3-
globs:
3+
paths:
44
- '**/*.test.ts'
55
- '**/*.spec.ts'
66
- 'tests/**'
@@ -9,46 +9,75 @@ globs:
99

1010
# Testing
1111

12+
## Test Integrity
13+
14+
- Never delete, comment out, or weaken assertions (e.g. `toEqual``toBeDefined`) to make tests pass
15+
- Fix the implementation, not the test; if the test itself is wrong, explain why in a comment or commit message
16+
1217
## Test Types
1318

14-
| Type | Tool | Location | Run Command |
15-
| ----------- | ---------- | ----------------------- | ----------------------- |
16-
| Unit | Vitest | `src/test/**/*.test.ts` | `pnpm test:unit` |
17-
| Integration | Vitest | `src/test/` | `pnpm test:unit` |
18-
| E2E | Playwright | `tests/*.test.ts` | `pnpm test:integration` |
19+
| Type | Tool | Location | Run Command |
20+
| ---- | ---------- | ----------------------------------------------------------------- | ----------------------- |
21+
| Unit | Vitest | `src/test/` (mirrors `src/lib/`) or co-located in `src/features/` | `pnpm test:unit` |
22+
| E2E | Playwright | `tests/` | `pnpm test:integration` |
23+
24+
## Assertions
25+
26+
- Use `toBe(true)` / `toBe(false)` over `toBeTruthy()` / `toBeFalsy()`
27+
- For DB query tests, assert `orderBy`, `include`, and other significant parameters with `expect.objectContaining` — not just `where`
28+
- Enum membership: `in` traverses the prototype chain; use `Object.hasOwn(Enum, value)` instead
1929

20-
## Unit Tests
30+
## Cleanup in Tests
31+
32+
Wrap DB-mutating cleanup in `try/finally` — a failing assertion skips cleanup and contaminates later tests:
33+
34+
```typescript
35+
try {
36+
await doSomething();
37+
expect(result).toBe(expected);
38+
} finally {
39+
await restoreState();
40+
}
41+
```
2142

22-
- Place tests in `src/test/` mirroring `src/lib/` structure
23-
- Use `@quramy/prisma-fabbrica` for test data factories
24-
- Mock external APIs with Nock
43+
## Test Data
2544

26-
## E2E Tests
45+
- Use realistic fixture values (real task IDs, grade names) instead of placeholders like `'t1'`
46+
- Extract shared data into fixture files; inline is fine for single-use cases
47+
- After `.filter()` on fixtures, verify actual contents — same ID may refer to a different entity after fixture updates
2748

28-
- Place in `tests/` directory
29-
- Use Playwright test utilities
30-
- Test user flows, not implementation details
49+
## Mock Helpers
3150

32-
## Patterns
51+
Extract repeated mock patterns into a helper in the test file:
3352

3453
```typescript
35-
import { describe, test, expect, vi } from 'vitest';
36-
37-
describe('functionName', () => {
38-
test('expects to do something', () => {
39-
// Arrange
40-
// Act
41-
// Assert
42-
});
43-
});
54+
function mockFindMany(value: WorkBookPlacements) {
55+
vi.mocked(prisma.workBookPlacement.findMany).mockResolvedValue(
56+
value as unknown as Awaited<ReturnType<typeof prisma.workBookPlacement.findMany>>,
57+
);
58+
}
4459
```
4560

61+
## Testing Extracted Utilities
62+
63+
- Add tests at extraction time, not later
64+
- For URL manipulation: assert the original URL is not mutated
65+
- For multi-column operations (e.g., DnD): assert both source and destination columns
66+
4667
## Coverage
4768

4869
- Run `pnpm coverage` for coverage report
4970
- Target: 80% lines, 70% branches
5071

72+
## Service Layer Split for Testability
73+
74+
When a service file mixes DB operations and pure functions, split it into two files:
75+
76+
- `crud.ts` — DB operations (`getXxx`, `updateXxx`, `createXxx`); tests need Prisma mocks
77+
- `initializers.ts` — pure computation (grade grouping, priority assignment); tests need no mocks
78+
79+
Stop the split if internal helpers (e.g. `fetchUnplacedWorkbooks`) would be fragmented across files — cohesion matters more than the split itself.
80+
5181
## HTTP Mocking
5282

53-
- Use Nock for mocking external HTTP calls
54-
- See `src/test/lib/clients/` for examples
83+
Use Nock for external HTTP calls. See `src/test/lib/clients/` for examples.

0 commit comments

Comments
 (0)