Skip to content

Research & Design Decisions — candidate-ranking


Purpose: candidate-ranking spec の design 書き直し(data-model の 3 系統契約への適合)で行った調査・統合判断を記録する。

Summary

  • Feature: candidate-ranking
  • Discovery Scope: Simple Addition(閲覧専用画面、ロジックが薄い)
  • Key Findings:
    • 旧設計は Prisma 直叩きの candidateService.getAllRanked() を持っていたが、新 data-model 契約では候補者は microCMS、得票は Neon JOIN 集計から合成する必要がある。
    • 投票導線を持たないため、useVoteFlow / useReorderAnimation / Purchase モーダルへの依存を完全に削除できる。
    • rank 算出ロジックを features/candidates/utils/rankCandidates.ts の純関数として切り出し、home 側でも将来再利用可能にする。

Research Log

Topic 1: 旧設計とのギャップ

  • Context: 旧 candidate-ranking design は candidateService.getAllRanked() 単独で完結していたが、新 data-model では責務が分離されている。
  • Sources Consulted: candidate-ranking/design.md(旧)、data-model/design.md(新)
  • Findings:
    • getCandidates + getVoteCountsByCandidateIds を Server Component で合成する必要あり。
    • share 計算は Server で実施し、Client は表示のみが steering の features/components 分離に最も整合。
  • Implications:
    • 本画面は最も薄い「Server 合成 + presentation」パターンの教科書例になる。
    • rank 算出を純関数化し home/ranking 両方で参照可能にする余地を確保。

Topic 2: 投票導線排除の徹底

  • Context: 要件冒頭に「閲覧専用」と明記されているにも関わらず、旧設計のコードベースに useVoteFlow の参照が残る可能性がある(home 等から流入)。
  • Sources Consulted: 要件 Feature 4、candidate-ranking/design.md(旧)、steering/structure.md
  • Findings:
    • 旧設計の「設計の前提」セクションが既に「投票導線を一切持たない」と宣言済み。これを Boundary Commitments に格上げし、Out-of-scope で useVoteFlow 等を再導入禁止と明示する。
  • Implications:
    • <RankingItem> の Props から onPaidVote / voteState を完全排除。
    • Migration Strategy に「投票導線参照の削除」を明記。

Topic 3: rank 算出ユーティリティの所在

  • Context: home と ranking で似た rank 算出をする。home spec の design では Server Component 直接記述だったが、ranking では純関数化したい。
  • Sources Consulted: home/design.md、本 spec の Architecture
  • Findings:
    • 関数自体は (items: { voteCount }[]) => { rank }[] の単純な純関数で、共通化のコストは低い。
    • features/candidates/utils/rankCandidates.ts に置くことで、home spec の改訂時に同じ関数を import できる。
  • Implications:
    • 本 spec の File Structure Plan に features/candidates/utils/rankCandidates.ts を新規追加。
    • home spec の Decision: rank 算出を Server Component に置く を「将来的に純関数化を検討」と整合させる。

Architecture Pattern Evaluation

OptionDescriptionStrengthsRisks / Limitations
Server で全データ確定 + Client は presentation採用責務分離が最も鮮明、テストしやすいフックを使わないため、状態追加時に拡張が必要
Client Component で rank と share を再計算クライアント計算サーバー負荷低data-model 集計値の二重表現、テスト面倒

Design Decisions

Decision: share 計算を Server に置く

  • Context: share = voteCount / maxVotes はクライアントでも計算可能だが、責務分離を優先。
  • Selected Approach: Server Component で算出し、Client には数値で渡す。
  • Rationale: components/ 層に計算ロジックを置かない steering 方針(structure.md)に整合。<VoteShareBar> は数値を受け取って描画するだけになり、テストが純粋に props ベースになる。
  • Trade-offs: Server レスポンスサイズが微増(各候補者あたり 1 数値)するが、無視可能。

Decision: rankCandidates を純関数として utils/ に置く

  • Context: home / ranking 双方で voteCount 降順 + ID 昇順タイブレーク + rank 付与を行うため。
  • Selected Approach: features/candidates/utils/rankCandidates.ts に純関数として配置。
  • Rationale: home 側でも将来採用する余地を残す。テストが容易。
  • Trade-offs: home/ranking で表示順が異なる(home は id 昇順表示、ranking は voteCount 降順表示)ため、関数自体は rank を付けるだけにとどめ、表示順は呼び出し側の責務とする。

Risks & Mitigations

  • 空配列・全員 0 票: maxVotes = 0 の場合に share=0 を返す分岐をテストでカバー。
  • 大量候補者: 候補者数は通常 20-30 名と想定。N=100 程度までは O(N log N) のソートで問題なし。
  • 画像 LCP: 上位候補者の画像はメインカードで先に読み込まれているケースが多い。本画面では priority を上位 3 位のみに付ける選択肢を ui-design.md 側で検討。

References

  • .kiro/specs/candidate-ranking/requirements.md — 機能要件
  • .kiro/specs/data-model/design.mdCandidate / voteAggregationService
  • .kiro/specs/home/design.md — 同等の Server 合成パターン
  • .kiro/steering/structure.md — features / components / utils 責務分離

Gap Analysis: candidate-ranking ↔ 既存コードベース (2026-05-17)

本セクションは kiro-validate-gap skill 実行による要件 ↔ 既存実装のギャップ調査結果。情報提供であり決定ではない。

1. Current State

1.1 既存コードベース実態

調査対象パス: app/, features/, components/public/, app/_components/, prisma/, lib/

パス状態内容
app/page.tsx存在単純な「再構築中」プレースホルダのみ
app/layout.tsx存在Cormorant Garamond / Jost フォント + Providers ラッパのみ
app/providers.tsx存在"use client"children を素通しするだけの空 Provider
app/globals.css存在(7.4 KB)内容未確認だがデザイントークン定義想定
app/ranking/存在しない本 spec の主担当ディレクトリが未作成
app/candidate/[id]/存在しない詳細画面遷移先(要件 4.1)が未作成
app/_components/存在しないページローカル client 配置先が未作成
app/api/存在しない(本 spec では未使用なので影響なし)
app/actions/存在しない(本 spec では未使用なので影響なし)
features/存在しないcms/services/candidateService, candidates/services/voteAggregationService, candidates/utils/rankCandidates のいずれも未実装
components/ui/存在shadcn プリミティブ用ディレクトリのみ。中身は確認していないが Button/Card 等が想定される
components/public/存在しないRankingHeader / RankingList / RankingItem / RankIcon / VoteShareBar / BackToHomeLink のいずれも未実装
prisma/存在しないschema.prisma、migrations、seed.ts いずれも未生成
lib/存在utils.ts(cn() 関数のみ)。prisma.ts / microcms.ts / voting-period.ts は未実装
auth.ts, app/admin/, app/api/images/存在しないsteering 方針通り「保有しない」状態が維持されている

1.2 依存パッケージ状況(package.json)

必要ライブラリ状態備考
next 16.2.4導入済みApp Router 利用可
react 19.2.4 / react-dom導入済みServer Components 利用可
@prisma/client 7.8 / prisma 7.8導入済みスキーマ未作成
@prisma/adapter-pg 7.8導入済みNeon 用アダプタ
clsx, tailwind-merge, class-variance-authority導入済みshadcn 標準セット
tailwindcss v4導入済みデザイントークン展開可
lucide-react 1.12.0導入済みTrophy / Medal / Award / TrendingUp / ChevronLeft アイコン取得可(ui-design.md 要求)
motion 12.x導入済み(本 spec では使わないが steering で容認)
microcms-js-sdk未導入data-model 完了で導入される想定
zod 4.3導入済み(本 spec では未使用)

1.3 tsconfig.jsonpaths ギャップ

現状の paths 定義は以下のみ:

json
{
  "@/*": ["./*"],
  "@/components/*": ["./components/*"],
  "@/design-tokens/*": ["./design-tokens/*"]
}

steering/structure.md「import エイリアス(実態)」に列挙されている @/features/*未定義。design.md の以下 import を成立させるには追加が必要:

  • @/features/cms/services/candidateService
  • @/features/candidates/services/voteAggregationService
  • @/features/candidates/utils/rankCandidates

ただし @/*./* を解決可能なため、明示的に @/features/* を追加しなくても @/features/cms/services/candidateService は解決される。steering/structure.md の理想形に合わせて追加することは推奨だが、本 spec の実装ブロッカーではない。

2. Feasibility

2.1 要件への対応可否

要件既存資産で実現可能か不足
Req 1.1 降順ソート表示不可getCandidates / voteAggregationService / rankCandidates の 3 つすべて未実装
Req 1.2 ホーム戻り導線不可<BackToHomeLink> 未実装、戻り先の /(home spec)もプレースホルダ状態
Req 2.1 順位行の表示要素不可<RankingItem> / <RankIcon> / <VoteShareBar> 未実装。next/imageremotePatterns に microCMS ホストも未設定
Req 3.1 1-3 位の視覚区分不可UI コンポーネント未実装
Req 4.1 詳細遷移部分的に不可リンク先 /candidate/[id] がまだ存在しないため、リンクは張れても遷移後 404 となる。candidate-detail spec 完了が前提
Req 5.1 同点 ID 昇順タイブレーク不可rankCandidates 純関数未実装
エッジケース: 0 票 / 候補者数 < 3不可描画コンポーネント未実装

2.2 上流依存

本 spec の実装には以下 spec が 先に完了している必要 がある:

  • data-modelCandidate 型、getCandidatesvoteAggregationService.getVoteCountsByCandidateIdslib/microcms.tsnext.config.tsimages.remotePatterns(候補者画像表示の前提)
  • candidate-detail(部分依存) — Req 4.1 の遷移先。リンク自体は本 spec で張れるが、E2E テストで遷移検証する場合に必要

home spec とは独立(双方が rankCandidates を import する関係)。voting spec とは独立(本画面は閲覧専用で投票導線を持たないため)。

2.3 競合・衝突するコードは存在しない

旧設計が懸念した「useVoteFlow の残存」「candidateService.getAllRanked() の Prisma 直叩き」「<RankingItem> の Props に onPaidVote が残る」等の負債は、a443022 以前の大規模リセットで既に消滅している。現状はゼロからの新規実装

3. Options

Option A: design.md 通り 1 PR でフル実装

/kiro-impl candidate-ranking を発火し、以下を一括で作る。

  • features/candidates/utils/rankCandidates.ts(純関数 + ユニットテスト)
  • app/ranking/page.tsx(Server Component)
  • app/ranking/_components/RankingPageClient.tsx
  • components/public/RankingHeader.tsx / RankingList.tsx / RankingItem.tsx / RankIcon.tsx / VoteShareBar.tsx / BackToHomeLink.tsx

Strengths

  • design.md と 1:1 対応するため、tasks.md の各タスクが PR 内タスクと素直に整合
  • 純関数のテストが本 spec 内に閉じる
  • 完了後に /ranking を開けば画面動作確認が可能

Risks / Limitations

  • data-model が未完了だと getCandidates / voteAggregationService が import できないため、data-model 完了が物理的なブロッカー
  • candidate-detail 未完了の場合、行クリックの遷移先が 404 になる(画面表示自体には影響しないが E2E テスト未通過)
  • <RankingItem>next/image 表示には next.config.tsimages.remotePatterns 更新が必要 — 本 spec の保護パス外(data-model 担当)で更新される想定なので、ここを跨ぐ場合の責務境界を明確化すること

Option B: rankCandidates 純関数だけを先行マージ(2 PR 分割)

  1. PR-1: features/candidates/utils/rankCandidates.ts のみ + Vitest テスト
  2. PR-2: app/ranking/**components/public/Ranking*data-model 完了後に積み上げ

Strengths

  • data-model 完了を待たずに着手できる(純関数は Candidate 型のシェイプだけ参照)
  • home spec が将来 rankCandidates を採用する際の準備として早期に共有可能
  • 純関数の契約(要件 5.1 タイブレーク)を独立にレビューできる

Risks / Limitations

  • 1 spec が 2 PR にまたがるため、/kiro-impl の autonomous モードを 2 回走らせるか、tasks.md を分割実行する必要あり
  • 純関数だけマージ → 利用箇所なしのデッドコード期間が短期間生まれる
  • Vitest が package.json にまだ無いため、テストランナー導入の判断が混じる(steering では Vitest 採用方針だが現状 devDependencies に未追加)

Option C: data-model + candidate-ranking を統合 PR でまとめて投入

data-model 完了済みであることが前提なので、現状はこの選択肢は事実上 Option A と同じになる。data-model がまだの場合は data-model を先に流す方が安全(本 spec の保護パスを超えて prisma/, lib/, features/cms/ に触る必要があるため)。

4. Effort & Risk

4.1 工数の目安(Option A の場合)

区分内容概算ファイル数
新規作成features/candidates/utils/rankCandidates.ts1
新規作成app/ranking/page.tsx + _components/RankingPageClient.tsx2
新規作成components/public/Ranking* 系 6 ファイル6
新規作成各コンポーネントのテスト(任意、Vitest 未導入なら後追い)0〜6
既存変更なし(保護パスでの変更なし)0
合計9〜15

4.2 主要リスク

リスク重大度緩和策
data-model 未完了で getCandidates / voteAggregationService import 不可着手前に /kiro-spec-status data-model で完了を確認。未完なら data-model を先行
next.config.tsimages.remotePatterns 未設定で microCMS 画像が next/image に弾かれるdata-model 完了で解消。本 spec の保護パス外(next.config.ts は直接編集可)なので必要なら明示依頼
@/features/* エイリアス未定義@/* で代替可能、優先度低
Vitest 未導入で純関数の単体テストが書けないテスト導入は本 spec のスコープを超える。テストファイルだけ用意して後追いで CI 接続も可
行クリック先 /candidate/[id] が 404 になる低(機能要件は満たす)candidate-detail 完了後に E2E で再検証
Candidate 型の displayName フィールド名 design.md 参照ズレdesign.md(本 spec)では candidate.displayName を使用。features/cms/types.ts のフィールド名と一致するかを実装時に確認

5. Output Checklist

  • [x] Current State: 既存コードの実態を列挙(主要パスは全て 未実装か空のプレースホルダ)
  • [x] Feasibility: 要件 1.1-5.1 と既存資産の対応表
  • [x] 上流依存: data-model 完了が物理的ブロッカーであることを明示
  • [x] Options A/B/C: それぞれ Strengths と Risks/Limitations を列挙
  • [x] Effort & Risk: 概算ファイル数とリスクテーブル
  • [x] 競合コードの不存在(リセット後の新規実装で済むこと)を確認

6. 推奨される次の一手(情報提供のみ)

  1. /kiro-spec-status data-model で前提 spec の状態を確認
  2. data-model が implemented 相当であれば、本 spec は Option A(1 PR でフル実装) が最短経路
  3. data-model が未完であれば、先に /kiro-impl data-model で前提を整える
  4. tsconfig.json への @/features/* 追記、next.config.tsimages.remotePatterns 更新は data-model spec の責務に含まれているか、/kiro-spec-status data-model で確認しておくと境界がクリーンになる
  5. テスト(Vitest)の導入是非は本 spec のスコープ外。rankCandidates 純関数のテストは導入後に追記する形でも要件達成には支障なし