From 259531d65d55e5c4e4a71f6d4d616a13ff45592f Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 00:15:23 +0900 Subject: [PATCH 1/9] =?UTF-8?q?docs:=20add=20=C2=B10=20relative=20evaluati?= =?UTF-8?q?on=20badge=20design=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-05-20-zero-diff-badge-design.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md diff --git a/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md b/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md new file mode 100644 index 000000000..68293ce3f --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md @@ -0,0 +1,53 @@ +# ±0 相対評価バッジ表示機能 — 設計仕様 + +## 概要 + +ユーザ投票の中央値グレードと運営グレードの差(diff)が 0 のとき、グレードアイコン右上に `±0` バッジを表示する。現在は diff=0 のときバッジが非表示だが、「一致している」という情報を明示的に伝えるよう変更する。 + +## 設計根拠 + +- diff=0 は「ユーザの評価が運営グレードと一致」という有意な情報であり、表示しないと他の diff 値と非対称になる +- 既存の badge ロジック(`getRelativeEvaluationLabel` → `RelativeEvaluationBadge`)をそのまま流用できるため、変更箇所が最小限 + +## 却下した代替案 + +- **Option B(コンポーネント内で diff=0 を特別処理)**: `label` と `diff` の責務が混在し、変更箇所が増えるため却下 +- **Option C(`showZeroBadge` prop 追加)**: 全呼び出し箇所で同じ挙動をするため YAGNI。不要な複雑性を避けて却下 + +## 変更対象 + +### `src/features/votes/utils/relative_evaluation.ts` + +| 関数 | diff=0 の現在値 | 変更後 | +|---|---|---| +| `getRelativeEvaluationLabel` | `''` | `'±0'` | +| `getRelativeEvaluationBadgeColorClass` | `''` | `'bg-green-500 text-white dark:bg-green-600 dark:text-white'` | +| `getRelativeEvaluationTooltipText` | `''`(default) | `'ユーザは「ふつう」と評価'`(`'±0'` case 追加) | + +### `src/features/votes/components/RelativeEvaluationBadge.svelte` + +- `{#if label}` ブロックは変更不要(`'±0'` は truthy) +- `--`/`++` のスペーシング処理(`- -`)は `±0` に不要なため追加不要 +- `aria-label` / `tooltipText` は既存ロジック経由で自動的に機能する + +### `src/features/votes/utils/relative_evaluation.test.ts` + +- `getRelativeEvaluationLabel(0)` のアサーションを `'±0'` に更新 +- `getRelativeEvaluationBadgeColorClass(0)` のアサーションをグリーンクラスに更新 +- `getRelativeEvaluationTooltipText('±0')` → `'ユーザは「ふつう」と評価'` のテストを追加 + +## 影響範囲 + +`RelativeEvaluationBadge` を使う 3 箇所すべてで ±0 バッジが表示される: + +- `src/features/votes/components/VotableGrade.svelte`(グレードアイコンのドロップダウントリガー) +- `src/routes/votes/+page.svelte`(投票一覧) +- `src/routes/(admin)/vote_management/+page.svelte`(管理画面) + +`VotableGrade.svelte` の `relativeEvaluationLabel`(sr-only テキスト)も `'±0'` を返すようになり、アクセシビリティも維持される。 + +`getRelativeEvaluationJapaneseLabel(0)` は既に `'ふつう'` を返しており変更不要。 + +## テスト方針 + +TDD: テストを先に修正・追加してから実装コードを変更する。`pnpm test:unit` で全テスト通過を確認。 From 837b39cf34c57cff38f10efc4c5b99799fcdeb8b Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 00:19:07 +0900 Subject: [PATCH 2/9] =?UTF-8?q?docs:=20update=20=C2=B10=20badge=20spec=20w?= =?UTF-8?q?ith=204th=20callsite=20and=20clarifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specs/2026-05-20-zero-diff-badge-design.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md b/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md index 68293ce3f..e92168fcb 100644 --- a/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md +++ b/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md @@ -29,22 +29,26 @@ - `{#if label}` ブロックは変更不要(`'±0'` は truthy) - `--`/`++` のスペーシング処理(`- -`)は `±0` に不要なため追加不要 - `aria-label` / `tooltipText` は既存ロジック経由で自動的に機能する +- **ツールチップの新規表示**: 現在は diff=0 のとき `tooltipText === ''` のため `{#if showTooltip && tooltipText}` が false でツールチップ非表示だった。変更後は `tooltipText = 'ユーザは「ふつう」と評価'`(truthy)になるため、`showTooltip=true` の呼び出し箇所(`votes/+page.svelte`、`votes/[slug]/+page.svelte`、`vote_management/+page.svelte`)でツールチップが初めて表示される。これは意図した挙動変更である。 ### `src/features/votes/utils/relative_evaluation.test.ts` -- `getRelativeEvaluationLabel(0)` のアサーションを `'±0'` に更新 -- `getRelativeEvaluationBadgeColorClass(0)` のアサーションをグリーンクラスに更新 -- `getRelativeEvaluationTooltipText('±0')` → `'ユーザは「ふつう」と評価'` のテストを追加 +- `getRelativeEvaluationLabel(0)` の既存アサーション(`''`)を `'±0'` に更新 +- `getRelativeEvaluationBadgeColorClass(0)` の既存アサーション(`''`)をグリーンクラスに更新(既存テスト `'returns empty string for diff === 0 (badge not shown)'` を修正) +- `getRelativeEvaluationTooltipText('±0')` → `'ユーザは「ふつう」と評価'` のテストケースを追加。`default: return ''` ブランチは変更なし(無効な label 値に対して `''` を返す挙動は維持) ## 影響範囲 -`RelativeEvaluationBadge` を使う 3 箇所すべてで ±0 バッジが表示される: +`RelativeEvaluationBadge` を使う 4 箇所すべてで ±0 バッジが表示される: - `src/features/votes/components/VotableGrade.svelte`(グレードアイコンのドロップダウントリガー) - `src/routes/votes/+page.svelte`(投票一覧) +- `src/routes/votes/[slug]/+page.svelte`(投票詳細) - `src/routes/(admin)/vote_management/+page.svelte`(管理画面) -`VotableGrade.svelte` の `relativeEvaluationLabel`(sr-only テキスト)も `'±0'` を返すようになり、アクセシビリティも維持される。 +**スクリーンリーダーの読み上げ**: `VotableGrade.svelte` の sr-only テキストが `, relative evaluation: ±0` を含むようになる。`±0` はほとんどのスクリーンリーダーで "plus minus zero" または "plus-minus zero" として読み上げられる。これは意図した挙動変更である。 + +**`calcGradeDiff` の戻り値型**: `getGradeOrder` はグレード順序を表す整数を返すため、`calcGradeDiff` は常に整数を返す。`diff === 0` の厳密等価比較は安全である。 `getRelativeEvaluationJapaneseLabel(0)` は既に `'ふつう'` を返しており変更不要。 From 8a76ddb187fa5cc5b0effe3f7db0917dc6e9a736 Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 00:23:05 +0900 Subject: [PATCH 3/9] =?UTF-8?q?docs:=20add=20=C2=B10=20badge=20implementat?= =?UTF-8?q?ion=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-05-20-zero-diff-badge.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-20-zero-diff-badge.md diff --git a/docs/superpowers/plans/2026-05-20-zero-diff-badge.md b/docs/superpowers/plans/2026-05-20-zero-diff-badge.md new file mode 100644 index 000000000..14e68b0d1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-20-zero-diff-badge.md @@ -0,0 +1,227 @@ +# ±0 相対評価バッジ表示 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** diff=0(ユーザ投票中央値と運営グレードが一致)のとき、グレードアイコン右上に緑色の `±0` バッジを表示する。 + +**Architecture:** `getRelativeEvaluationLabel` が diff=0 で `'±0'` を返すよう変更し、対応するバッジ色・ツールチップ文言をユーティリティ関数に追加する。`RelativeEvaluationBadge.svelte` は `{#if label}` で表示制御しており `'±0'` は truthy なため、コンポーネント本体の変更は不要。 + +**Tech Stack:** TypeScript, Svelte 5 (Runes), Vitest, Tailwind CSS v4 + +--- + +## File Map + +| Action | File | 変更内容 | +|--------|------|----------| +| Modify | `src/features/votes/utils/relative_evaluation.ts` | `getRelativeEvaluationLabel(0)` → `'±0'`、`getRelativeEvaluationBadgeColorClass(0)` → グリーン、`getRelativeEvaluationTooltipText('±0')` → 日本語文言追加 | +| Modify | `src/features/votes/utils/relative_evaluation.test.ts` | diff=0 のアサーション更新・追加 | + +--- + +### Task 1: テストを更新して失敗させる + +**Files:** +- Modify: `src/features/votes/utils/relative_evaluation.test.ts` + +- [ ] **Step 1: `getRelativeEvaluationLabel(0)` のアサーションを `'±0'` に変更する** + +`src/features/votes/utils/relative_evaluation.test.ts` の `describe('getRelativeEvaluationLabel')` ブロック内にある diff=0 のテストを探す: + +```typescript +test('returns "" for diff === 0', () => { + expect(getRelativeEvaluationLabel(0)).toBe(''); +}); +``` + +これを以下に置き換える: + +```typescript +test('returns "±0" for diff === 0', () => { + expect(getRelativeEvaluationLabel(0)).toBe('±0'); +}); +``` + +- [ ] **Step 2: `getRelativeEvaluationBadgeColorClass(0)` のアサーションをグリーンクラスに変更する** + +同ファイルの `describe('getRelativeEvaluationBadgeColorClass')` ブロック内にある diff=0 のテストを探す: + +```typescript +test('returns empty string for diff === 0 (badge not shown)', () => { + expect(getRelativeEvaluationBadgeColorClass(0)).toBe(''); +}); +``` + +これを以下に置き換える: + +```typescript +test('returns green bg classes for diff === 0 (neutral)', () => { + expect(getRelativeEvaluationBadgeColorClass(0)).toBe( + 'bg-green-500 text-white dark:bg-green-600 dark:text-white', + ); +}); +``` + +- [ ] **Step 3: `getRelativeEvaluationTooltipText('±0')` のテストを追加する** + +同ファイルの `describe('getRelativeEvaluationTooltipText')` ブロック内に以下を追加する(既存の `test('returns "" for empty string ...')` の直後が適切): + +```typescript +test('returns tooltip text for ±0 (neutral match)', () => { + expect(getRelativeEvaluationTooltipText('±0')).toBe('ユーザは「ふつう」と評価'); +}); +``` + +- [ ] **Step 4: テストを実行して失敗を確認する** + +```bash +pnpm test:unit src/features/votes/utils/relative_evaluation.test.ts +``` + +期待される出力: 3 つのテストが FAIL(`getRelativeEvaluationLabel`、`getRelativeEvaluationBadgeColorClass`、`getRelativeEvaluationTooltipText` それぞれ) + +--- + +### Task 2: 実装を変更してテストを通す + +**Files:** +- Modify: `src/features/votes/utils/relative_evaluation.ts` + +- [ ] **Step 1: `getRelativeEvaluationLabel` の diff=0 分岐を `'±0'` に変更する** + +`src/features/votes/utils/relative_evaluation.ts` の `getRelativeEvaluationLabel` 関数内: + +変更前: +```typescript +if (diff === 0) { + return ''; +} +``` + +変更後: +```typescript +if (diff === 0) { + return '±0'; +} +``` + +- [ ] **Step 2: `getRelativeEvaluationBadgeColorClass` の diff=0 分岐にグリーンを返すよう変更する** + +同ファイルの `getRelativeEvaluationBadgeColorClass` 関数内の末尾 `return '';`: + +変更前: +```typescript +export function getRelativeEvaluationBadgeColorClass(diff: number): string { + if (diff < 0) { + return 'bg-sky-400 text-white dark:bg-sky-500 dark:text-white'; + } + if (diff > 0) { + return 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white'; + } + return ''; +} +``` + +変更後: +```typescript +export function getRelativeEvaluationBadgeColorClass(diff: number): string { + if (diff < 0) { + return 'bg-sky-400 text-white dark:bg-sky-500 dark:text-white'; + } + if (diff > 0) { + return 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white'; + } + return 'bg-green-500 text-white dark:bg-green-600 dark:text-white'; +} +``` + +- [ ] **Step 3: `getRelativeEvaluationTooltipText` に `'±0'` ケースを追加する** + +同ファイルの `getRelativeEvaluationTooltipText` 関数の switch 文に `'±0'` ケースを追加する: + +変更前: +```typescript +export function getRelativeEvaluationTooltipText(label: string): string { + switch (label) { + case '++': + return 'ユーザは「難しい」と評価'; + case '+': + return 'ユーザは「やや難しい」と評価'; + case '-': + return 'ユーザは「やや易しい」と評価'; + case '--': + return 'ユーザは「易しい」と評価'; + default: + return ''; + } +} +``` + +変更後: +```typescript +export function getRelativeEvaluationTooltipText(label: string): string { + switch (label) { + case '++': + return 'ユーザは「難しい」と評価'; + case '+': + return 'ユーザは「やや難しい」と評価'; + case '±0': + return 'ユーザは「ふつう」と評価'; + case '-': + return 'ユーザは「やや易しい」と評価'; + case '--': + return 'ユーザは「易しい」と評価'; + default: + return ''; + } +} +``` + +- [ ] **Step 4: テストを実行してすべて通ることを確認する** + +```bash +pnpm test:unit src/features/votes/utils/relative_evaluation.test.ts +``` + +期待される出力: 全テスト PASS + +- [ ] **Step 5: ユニットテスト全体を実行してリグレッションがないことを確認する** + +```bash +pnpm test:unit +``` + +期待される出力: 全テスト PASS + +- [ ] **Step 6: TSDoc コメントを更新する** + +`getRelativeEvaluationLabel` 関数の TSDoc テーブル内の diff=0 行を更新する: + +変更前: +``` + * | 0 | `""` | +``` + +変更後: +``` + * | 0 | `±0` | +``` + +`getRelativeEvaluationBadgeColorClass` 関数の TSDoc を更新する: + +変更前: +``` + * Returns empty string for diff === 0 (badge is not shown). +``` + +変更後(関数説明の末尾行): +``` + * Zero diff (neutral) → green. +``` + +- [ ] **Step 7: コミットする** + +```bash +git add src/features/votes/utils/relative_evaluation.ts src/features/votes/utils/relative_evaluation.test.ts +git commit -m "feat: show ±0 badge when vote median matches official grade" +``` From b33862ae31c8dfd5bddb9baf7c417eb81553858b Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 01:22:03 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20show=20=C2=B10=20badge=20when=20vot?= =?UTF-8?q?e=20median=20matches=20official=20grade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../votes/utils/relative_evaluation.test.ts | 14 ++++++++++---- src/features/votes/utils/relative_evaluation.ts | 11 ++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/features/votes/utils/relative_evaluation.test.ts b/src/features/votes/utils/relative_evaluation.test.ts index 710bcf747..c485215cf 100644 --- a/src/features/votes/utils/relative_evaluation.test.ts +++ b/src/features/votes/utils/relative_evaluation.test.ts @@ -88,8 +88,8 @@ describe('getRelativeEvaluationLabel', () => { expect(getRelativeEvaluationLabel(1)).toBe('+'); }); - test('returns "" for diff === 0', () => { - expect(getRelativeEvaluationLabel(0)).toBe(''); + test('returns "±0" for diff === 0', () => { + expect(getRelativeEvaluationLabel(0)).toBe('±0'); }); test('returns "-" for diff === -1', () => { @@ -108,6 +108,10 @@ describe('getRelativeEvaluationTooltipText', () => { expect(getRelativeEvaluationTooltipText('')).toBe(''); }); + test('returns tooltip text for ±0 (neutral match)', () => { + expect(getRelativeEvaluationTooltipText('±0')).toBe('ユーザは「ふつう」と評価'); + }); + test('returns ++ for users feel the difficult than official grade', () => { expect(getRelativeEvaluationTooltipText('++')).toBe('ユーザは「難しい」と評価'); }); @@ -183,8 +187,10 @@ describe('getRelativeEvaluationBadgeColorClass', () => { ); }); - test('returns empty string for diff === 0 (badge not shown)', () => { - expect(getRelativeEvaluationBadgeColorClass(0)).toBe(''); + test('returns green bg classes for diff === 0 (neutral)', () => { + expect(getRelativeEvaluationBadgeColorClass(0)).toBe( + 'bg-green-500 text-white dark:bg-green-600 dark:text-white', + ); }); test('returns orange bg classes for positive diff (harder)', () => { diff --git a/src/features/votes/utils/relative_evaluation.ts b/src/features/votes/utils/relative_evaluation.ts index 116cb58bd..6679399af 100644 --- a/src/features/votes/utils/relative_evaluation.ts +++ b/src/features/votes/utils/relative_evaluation.ts @@ -20,7 +20,7 @@ export function calcGradeDiff(officialGrade: TaskGrade, medianGrade: TaskGrade): * | ------ | ----- | * | ≤ −2 | `--` | * | −1 | `-` | - * | 0 | `""` | + * | 0 | `±0` | * | +1 | `+` | * | ≥ +2 | `++` | * @@ -35,7 +35,7 @@ export function getRelativeEvaluationLabel(diff: number): string { return '-'; } if (diff === 0) { - return ''; + return '±0'; } if (diff === 1) { return '+'; @@ -55,6 +55,8 @@ export function getRelativeEvaluationTooltipText(label: string): string { return 'ユーザは「難しい」と評価'; case '+': return 'ユーザは「やや難しい」と評価'; + case '±0': + return 'ユーザは「ふつう」と評価'; case '-': return 'ユーザは「やや易しい」と評価'; case '--': @@ -116,8 +118,7 @@ export function getRelativeEvaluationColorClass(diff: number): string { /** * Returns Tailwind background + text color classes for the relative evaluation badge. - * Negative diff (easier) → sky, positive (harder) → orange. - * Returns empty string for diff === 0 (badge is not shown). + * Negative diff (easier) → sky, zero diff (neutral) → green, positive (harder) → orange. * * @param diff - The result of {@link calcGradeDiff}. */ @@ -128,5 +129,5 @@ export function getRelativeEvaluationBadgeColorClass(diff: number): string { if (diff > 0) { return 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white'; } - return ''; + return 'bg-green-500 text-white dark:bg-green-600 dark:text-white'; } From 861f78848807f7b49f085ae4c34f270a29edd279 Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 02:00:57 +0900 Subject: [PATCH 5/9] =?UTF-8?q?seed:=20add=20VotedGradeStatistics=20demo?= =?UTF-8?q?=20data=20for=20=C2=B10=20badge=20visual=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/seed.ts | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 4dfc9984e..f0269fa22 100755 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -78,6 +78,7 @@ async function main() { await addTaskTags(); await addSubmissionStatuses(); await addAnswers(); + await addVoteStatisticsDemoData(); console.log('Seeding has been completed.'); } catch (e) { @@ -192,7 +193,9 @@ async function addContestTaskPairs() { const contestTaskPairFactory = defineContestTaskPairFactory(); // Create a queue with limited concurrency for contest task pair operations - const contestTaskPairQueue = new PQueue({ concurrency: QUEUE_CONCURRENCY.contestTaskPairs }); + const contestTaskPairQueue = new PQueue({ + concurrency: QUEUE_CONCURRENCY.contestTaskPairs, + }); for (const pair of contest_task_pairs) { contestTaskPairQueue.add(async () => { @@ -256,7 +259,9 @@ async function addContestTaskPair( async function addWorkBooks() { console.log('Start adding workbooks...'); - const workBookFactory = defineWorkBookFactory({ defaultData: { user: defineUserFactory() } }); + const workBookFactory = defineWorkBookFactory({ + defaultData: { user: defineUserFactory() }, + }); // Note: Use a for loop to ensure each workbook is processed sequentially. for (const workbook of workbooks) { @@ -321,7 +326,9 @@ async function addWorkBookPlacements() { prisma.workBook.findMany({ where: { workBookType: 'CURRICULUM', placement: null }, include: { - workBookTasks: { include: { task: { select: { task_id: true, grade: true } } } }, + workBookTasks: { + include: { task: { select: { task_id: true, grade: true } } }, + }, }, orderBy: { id: 'asc' }, }), @@ -526,7 +533,9 @@ async function addSubmissionStatuses() { const submissionStatusFactory = defineSubmissionStatusFactory(); // Create a queue with limited concurrency for submission status operations - const submissionStatusQueue = new PQueue({ concurrency: QUEUE_CONCURRENCY.submissionStatuses }); + const submissionStatusQueue = new PQueue({ + concurrency: QUEUE_CONCURRENCY.submissionStatuses, + }); for (const submission_status of submission_statuses) { submissionStatusQueue.add(async () => { @@ -634,6 +643,31 @@ async function addAnswer( }); } +// Demo data: tasks with VotedGradeStatistics so the ±0 badge is visible in the UI. +// grade matches the task's official grade → diff=0 → ±0 badge displayed. +const VOTE_STATS_DEMO_DATA = [ + { id: 'demo-vote-stat-apg4b-co', taskId: 'APG4bPython_co', grade: 'Q8' }, +] as const; + +async function addVoteStatisticsDemoData() { + console.log('Start adding vote statistics demo data...'); + + for (const entry of VOTE_STATS_DEMO_DATA) { + try { + await prisma.votedGradeStatistics.upsert({ + where: { taskId: entry.taskId }, + update: {}, + create: { id: entry.id, taskId: entry.taskId, grade: entry.grade }, + }); + console.log('vote stats demo: taskId', entry.taskId, 'was registered.'); + } catch (e) { + console.error('Failed to add vote stats demo entry', entry.taskId, e); + } + } + + console.log('Finished adding vote statistics demo data.'); +} + main() .catch(async (e) => { console.error(e); From 58f0721d6d74b5b7ba171999edea50cf6874e91d Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 10:39:32 +0900 Subject: [PATCH 6/9] refactor: use green for diff=0 in relative evaluation color helpers Update badge and text color for the neutral case (diff===0) from gray/green-500/600 to consistent green-400/500 shades matching existing sky/orange badge conventions. Co-Authored-By: Claude Sonnet 4.6 --- src/features/votes/utils/relative_evaluation.test.ts | 6 +++--- src/features/votes/utils/relative_evaluation.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/votes/utils/relative_evaluation.test.ts b/src/features/votes/utils/relative_evaluation.test.ts index c485215cf..c4c4b43d5 100644 --- a/src/features/votes/utils/relative_evaluation.test.ts +++ b/src/features/votes/utils/relative_evaluation.test.ts @@ -167,8 +167,8 @@ describe('getRelativeEvaluationColorClass', () => { expect(getRelativeEvaluationColorClass(-16)).toBe('text-sky-500 dark:text-sky-400'); }); - test('returns gray text classes for diff === 0', () => { - expect(getRelativeEvaluationColorClass(0)).toBe('text-gray-400 dark:text-gray-500'); + test('returns green text classes for diff === 0 (neutral)', () => { + expect(getRelativeEvaluationColorClass(0)).toBe('text-green-500 dark:text-green-400'); }); test('returns orange text classes for positive diff (harder)', () => { @@ -189,7 +189,7 @@ describe('getRelativeEvaluationBadgeColorClass', () => { test('returns green bg classes for diff === 0 (neutral)', () => { expect(getRelativeEvaluationBadgeColorClass(0)).toBe( - 'bg-green-500 text-white dark:bg-green-600 dark:text-white', + 'bg-green-400 text-white dark:bg-green-500 dark:text-white', ); }); diff --git a/src/features/votes/utils/relative_evaluation.ts b/src/features/votes/utils/relative_evaluation.ts index 6679399af..a0a0dcc23 100644 --- a/src/features/votes/utils/relative_evaluation.ts +++ b/src/features/votes/utils/relative_evaluation.ts @@ -111,7 +111,7 @@ export function getRelativeEvaluationColorClass(diff: number): string { return 'text-sky-500 dark:text-sky-400'; } if (diff === 0) { - return 'text-gray-400 dark:text-gray-500'; + return 'text-green-500 dark:text-green-400'; } return 'text-orange-400 dark:text-orange-300'; } @@ -129,5 +129,5 @@ export function getRelativeEvaluationBadgeColorClass(diff: number): string { if (diff > 0) { return 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white'; } - return 'bg-green-500 text-white dark:bg-green-600 dark:text-white'; + return 'bg-green-400 text-white dark:bg-green-500 dark:text-white'; } From 93e878cdadfbda05d6617ff49e82771e4d28b79a Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 10:45:36 +0900 Subject: [PATCH 7/9] docs: fix stale TSDoc comment in getRelativeEvaluationColorClass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "zero → gray" was incorrect after the green color change; update to "zero → green". Co-Authored-By: Claude Sonnet 4.6 --- src/features/votes/utils/relative_evaluation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/votes/utils/relative_evaluation.ts b/src/features/votes/utils/relative_evaluation.ts index a0a0dcc23..99f12fa7d 100644 --- a/src/features/votes/utils/relative_evaluation.ts +++ b/src/features/votes/utils/relative_evaluation.ts @@ -102,7 +102,7 @@ export function getRelativeEvaluationJapaneseLabel(diff: number): string { /** * Returns Tailwind text color classes for a diff value in the vote dropdown. - * Negative diff (easier) → sky, zero → gray, positive (harder) → orange. + * Negative diff (easier) → sky, zero → green, positive (harder) → orange. * * @param diff - The result of {@link calcGradeDiff}. */ From f56429346d67d1288697d84d05d6673d30d11206 Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 10:46:58 +0900 Subject: [PATCH 8/9] =?UTF-8?q?docs:=20mark=20all=20tasks=20complete=20and?= =?UTF-8?q?=20add=20session=20lessons=20to=20=C2=B10=20badge=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../plans/2026-05-20-zero-diff-badge.md | 223 +++--------------- 1 file changed, 37 insertions(+), 186 deletions(-) diff --git a/docs/superpowers/plans/2026-05-20-zero-diff-badge.md b/docs/superpowers/plans/2026-05-20-zero-diff-badge.md index 14e68b0d1..599fc75c6 100644 --- a/docs/superpowers/plans/2026-05-20-zero-diff-badge.md +++ b/docs/superpowers/plans/2026-05-20-zero-diff-badge.md @@ -12,216 +12,67 @@ ## File Map -| Action | File | 変更内容 | -|--------|------|----------| -| Modify | `src/features/votes/utils/relative_evaluation.ts` | `getRelativeEvaluationLabel(0)` → `'±0'`、`getRelativeEvaluationBadgeColorClass(0)` → グリーン、`getRelativeEvaluationTooltipText('±0')` → 日本語文言追加 | -| Modify | `src/features/votes/utils/relative_evaluation.test.ts` | diff=0 のアサーション更新・追加 | +| Action | File | 変更内容 | +| ------ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Modify | `src/features/votes/utils/relative_evaluation.ts` | `getRelativeEvaluationLabel(0)` → `'±0'`、`getRelativeEvaluationBadgeColorClass(0)` → グリーン、`getRelativeEvaluationTooltipText('±0')` → 日本語文言追加 | +| Modify | `src/features/votes/utils/relative_evaluation.test.ts` | diff=0 のアサーション更新・追加 | --- -### Task 1: テストを更新して失敗させる +### Task 1: テストを更新して失敗させる ✅ **Files:** -- Modify: `src/features/votes/utils/relative_evaluation.test.ts` - -- [ ] **Step 1: `getRelativeEvaluationLabel(0)` のアサーションを `'±0'` に変更する** - -`src/features/votes/utils/relative_evaluation.test.ts` の `describe('getRelativeEvaluationLabel')` ブロック内にある diff=0 のテストを探す: - -```typescript -test('returns "" for diff === 0', () => { - expect(getRelativeEvaluationLabel(0)).toBe(''); -}); -``` - -これを以下に置き換える: - -```typescript -test('returns "±0" for diff === 0', () => { - expect(getRelativeEvaluationLabel(0)).toBe('±0'); -}); -``` - -- [ ] **Step 2: `getRelativeEvaluationBadgeColorClass(0)` のアサーションをグリーンクラスに変更する** - -同ファイルの `describe('getRelativeEvaluationBadgeColorClass')` ブロック内にある diff=0 のテストを探す: - -```typescript -test('returns empty string for diff === 0 (badge not shown)', () => { - expect(getRelativeEvaluationBadgeColorClass(0)).toBe(''); -}); -``` - -これを以下に置き換える: - -```typescript -test('returns green bg classes for diff === 0 (neutral)', () => { - expect(getRelativeEvaluationBadgeColorClass(0)).toBe( - 'bg-green-500 text-white dark:bg-green-600 dark:text-white', - ); -}); -``` - -- [ ] **Step 3: `getRelativeEvaluationTooltipText('±0')` のテストを追加する** -同ファイルの `describe('getRelativeEvaluationTooltipText')` ブロック内に以下を追加する(既存の `test('returns "" for empty string ...')` の直後が適切): - -```typescript -test('returns tooltip text for ±0 (neutral match)', () => { - expect(getRelativeEvaluationTooltipText('±0')).toBe('ユーザは「ふつう」と評価'); -}); -``` - -- [ ] **Step 4: テストを実行して失敗を確認する** - -```bash -pnpm test:unit src/features/votes/utils/relative_evaluation.test.ts -``` +- Modify: `src/features/votes/utils/relative_evaluation.test.ts` -期待される出力: 3 つのテストが FAIL(`getRelativeEvaluationLabel`、`getRelativeEvaluationBadgeColorClass`、`getRelativeEvaluationTooltipText` それぞれ) +- [x] **Step 1: `getRelativeEvaluationLabel(0)` のアサーションを `'±0'` に変更する** +- [x] **Step 2: `getRelativeEvaluationBadgeColorClass(0)` のアサーションをグリーンクラスに変更する** +- [x] **Step 3: `getRelativeEvaluationTooltipText('±0')` のテストを追加する** +- [x] **Step 4: テストを実行して失敗を確認する** --- -### Task 2: 実装を変更してテストを通す +### Task 2: 実装を変更してテストを通す ✅ **Files:** -- Modify: `src/features/votes/utils/relative_evaluation.ts` - -- [ ] **Step 1: `getRelativeEvaluationLabel` の diff=0 分岐を `'±0'` に変更する** - -`src/features/votes/utils/relative_evaluation.ts` の `getRelativeEvaluationLabel` 関数内: - -変更前: -```typescript -if (diff === 0) { - return ''; -} -``` -変更後: -```typescript -if (diff === 0) { - return '±0'; -} -``` +- Modify: `src/features/votes/utils/relative_evaluation.ts` -- [ ] **Step 2: `getRelativeEvaluationBadgeColorClass` の diff=0 分岐にグリーンを返すよう変更する** - -同ファイルの `getRelativeEvaluationBadgeColorClass` 関数内の末尾 `return '';`: - -変更前: -```typescript -export function getRelativeEvaluationBadgeColorClass(diff: number): string { - if (diff < 0) { - return 'bg-sky-400 text-white dark:bg-sky-500 dark:text-white'; - } - if (diff > 0) { - return 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white'; - } - return ''; -} -``` +- [x] **Step 1: `getRelativeEvaluationLabel` の diff=0 分岐を `'±0'` に変更する** +- [x] **Step 2: `getRelativeEvaluationBadgeColorClass` の diff=0 分岐にグリーンを返すよう変更する** +- [x] **Step 3: `getRelativeEvaluationTooltipText` に `'±0'` ケースを追加する** +- [x] **Step 4: テストを実行してすべて通ることを確認する** (38 tests passed) +- [x] **Step 5: ユニットテスト全体を実行してリグレッションがないことを確認する** +- [x] **Step 6: TSDoc コメントを更新する** +- [x] **Step 7: コミットする** -変更後: -```typescript -export function getRelativeEvaluationBadgeColorClass(diff: number): string { - if (diff < 0) { - return 'bg-sky-400 text-white dark:bg-sky-500 dark:text-white'; - } - if (diff > 0) { - return 'bg-orange-400 text-white dark:bg-orange-500 dark:text-white'; - } - return 'bg-green-500 text-white dark:bg-green-600 dark:text-white'; -} -``` - -- [ ] **Step 3: `getRelativeEvaluationTooltipText` に `'±0'` ケースを追加する** - -同ファイルの `getRelativeEvaluationTooltipText` 関数の switch 文に `'±0'` ケースを追加する: - -変更前: -```typescript -export function getRelativeEvaluationTooltipText(label: string): string { - switch (label) { - case '++': - return 'ユーザは「難しい」と評価'; - case '+': - return 'ユーザは「やや難しい」と評価'; - case '-': - return 'ユーザは「やや易しい」と評価'; - case '--': - return 'ユーザは「易しい」と評価'; - default: - return ''; - } -} -``` +--- -変更後: -```typescript -export function getRelativeEvaluationTooltipText(label: string): string { - switch (label) { - case '++': - return 'ユーザは「難しい」と評価'; - case '+': - return 'ユーザは「やや難しい」と評価'; - case '±0': - return 'ユーザは「ふつう」と評価'; - case '-': - return 'ユーザは「やや易しい」と評価'; - case '--': - return 'ユーザは「易しい」と評価'; - default: - return ''; - } -} -``` +## 追加変更(実装後にユーザー要求) -- [ ] **Step 4: テストを実行してすべて通ることを確認する** +- `prisma/seed.ts`: `addVoteStatisticsDemoData()` 追加 — APG4bPython_co に diff=0 の VotedGradeStatistics を作成してバッジを目視確認できるようにした +- `getRelativeEvaluationBadgeColorClass(0)`: バッジの色合いを `500/600` → `400/500` に修正(sky/orange の既存バッジと統一) +- `getRelativeEvaluationColorClass(0)`: テキスト色を gray → green に変更(diff=0 のニュートラル色を統一) -```bash -pnpm test:unit src/features/votes/utils/relative_evaluation.test.ts -``` +--- -期待される出力: 全テスト PASS +## 教訓(新規・非自明なもの) -- [ ] **Step 5: ユニットテスト全体を実行してリグレッションがないことを確認する** +### Docker node_modules シンボリックリンク欠損問題 +**現象:** `pnpm db:seed` が `Cannot find module '/usr/src/app/node_modules/pnpm/bin/pnpm.mjs'` でクラッシュ。 +**原因:** `node_modules/.pnpm-workspace-state-v1.json` に `lastValidatedTimestamp` が記録されていたため、pnpm がインストール済みと判断してスコープなしパッケージ(tsx, pnpm, p-queue, prisma 等)のトップレベルシンボリックリンクを貼り直さなかった。スコープ付きパッケージ(@prisma 等)は正常だったため一見インストール済みに見える。 +**修正コマンド:** ```bash -pnpm test:unit +docker compose exec web rm node_modules/.pnpm-workspace-state-v1.json +docker compose exec web /usr/local/share/npm-global/bin/pnpm install ``` +この問題は `compose.yaml` の `./node_modules:/usr/src/app/node_modules:cached` マウントでホスト側 node_modules をコンテナに共有しているため、ホスト側で別の Node バージョンを使って操作した際などに発生する。 -期待される出力: 全テスト PASS +### `getRelativeEvaluationColorClass` の TSDoc 陳腐化 -- [ ] **Step 6: TSDoc コメントを更新する** +バッジ色と同じ関数のコメントが古い情報(gray)を残したまま実装だけ緑に更新した。コードレビューで検出。今後: 色変更時は必ず同じ関数の TSDoc 内の色名も同時に確認する。 -`getRelativeEvaluationLabel` 関数の TSDoc テーブル内の diff=0 行を更新する: - -変更前: -``` - * | 0 | `""` | -``` +## CodeRabbit Findings -変更後: -``` - * | 0 | `±0` | -``` - -`getRelativeEvaluationBadgeColorClass` 関数の TSDoc を更新する: - -変更前: -``` - * Returns empty string for diff === 0 (badge is not shown). -``` - -変更後(関数説明の末尾行): -``` - * Zero diff (neutral) → green. -``` - -- [ ] **Step 7: コミットする** - -```bash -git add src/features/votes/utils/relative_evaluation.ts src/features/votes/utils/relative_evaluation.test.ts -git commit -m "feat: show ±0 badge when vote median matches official grade" -``` +(PRを開いた後に実施予定) From 5e57a700fb733052ca82ceca24183d618d9c7b1c Mon Sep 17 00:00:00 2001 From: river0525 <0525sotaro@gmail.com> Date: Thu, 21 May 2026 10:53:58 +0900 Subject: [PATCH 9/9] style: format markdown files with Prettier --- docs/superpowers/plans/2026-05-20-zero-diff-badge.md | 2 ++ .../specs/2026-05-20-zero-diff-badge-design.md | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-05-20-zero-diff-badge.md b/docs/superpowers/plans/2026-05-20-zero-diff-badge.md index 599fc75c6..6d3fb6874 100644 --- a/docs/superpowers/plans/2026-05-20-zero-diff-badge.md +++ b/docs/superpowers/plans/2026-05-20-zero-diff-badge.md @@ -63,10 +63,12 @@ **現象:** `pnpm db:seed` が `Cannot find module '/usr/src/app/node_modules/pnpm/bin/pnpm.mjs'` でクラッシュ。 **原因:** `node_modules/.pnpm-workspace-state-v1.json` に `lastValidatedTimestamp` が記録されていたため、pnpm がインストール済みと判断してスコープなしパッケージ(tsx, pnpm, p-queue, prisma 等)のトップレベルシンボリックリンクを貼り直さなかった。スコープ付きパッケージ(@prisma 等)は正常だったため一見インストール済みに見える。 **修正コマンド:** + ```bash docker compose exec web rm node_modules/.pnpm-workspace-state-v1.json docker compose exec web /usr/local/share/npm-global/bin/pnpm install ``` + この問題は `compose.yaml` の `./node_modules:/usr/src/app/node_modules:cached` マウントでホスト側 node_modules をコンテナに共有しているため、ホスト側で別の Node バージョンを使って操作した際などに発生する。 ### `getRelativeEvaluationColorClass` の TSDoc 陳腐化 diff --git a/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md b/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md index e92168fcb..ec17db60e 100644 --- a/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md +++ b/docs/superpowers/specs/2026-05-20-zero-diff-badge-design.md @@ -18,11 +18,11 @@ ### `src/features/votes/utils/relative_evaluation.ts` -| 関数 | diff=0 の現在値 | 変更後 | -|---|---|---| -| `getRelativeEvaluationLabel` | `''` | `'±0'` | -| `getRelativeEvaluationBadgeColorClass` | `''` | `'bg-green-500 text-white dark:bg-green-600 dark:text-white'` | -| `getRelativeEvaluationTooltipText` | `''`(default) | `'ユーザは「ふつう」と評価'`(`'±0'` case 追加) | +| 関数 | diff=0 の現在値 | 変更後 | +| -------------------------------------- | --------------- | ------------------------------------------------------------- | +| `getRelativeEvaluationLabel` | `''` | `'±0'` | +| `getRelativeEvaluationBadgeColorClass` | `''` | `'bg-green-500 text-white dark:bg-green-600 dark:text-white'` | +| `getRelativeEvaluationTooltipText` | `''`(default) | `'ユーザは「ふつう」と評価'`(`'±0'` case 追加) | ### `src/features/votes/components/RelativeEvaluationBadge.svelte`