Re: 深層調査結果②: 現在のコードベースの依存関係と問題点の深層分析
深層調査結果②: 現在のコードベースの依存関係と問題点の深層分析
全626ファイル(src/配下)、439件の@/インポート、114件のテストファイルを完全に分析した結果を報告する。
1. フィーチャー間の依存関係マトリクス
以下は、実際のインポートを完全に追跡して作成した依存関係マトリクスである。 矢印は「AがBに依存する(AからBをインポートする)」方向を示す。
1-1. フィーチャー間の直接依存(テストファイル除外)
| 依存元 -> 依存先 | tools | cheatsheets | games | quiz | dictionary | blog | memos | search | data/ |
|---|---|---|---|---|---|---|---|---|---|
| tools | - | - | - | - | - | (※1) | - | - | - |
| cheatsheets | allToolMetas (※2) | - | - | - | - | - | - | - | - |
| games | - | - | - | - | - | - | - | - | 8件 |
| quiz | - | - | webShare (※3) | - | - | - | - | - | - |
| dictionary | - | - | - | - | - | - | - | - | 3件 |
| blog | - | - | - | - | - | - | memos-shared (※4) | - | - |
| memos | - | - | - | - | - | - | - | - | - |
| search | registry | registry | registry | registry | 3モジュール | blog | - | - | - |
依存の詳細:
- ※1: components/tools/RelatedBlogPosts.tsx が @/lib/cross-links の getRelatedBlogPostsForTool を利用(間接的にblogに依存)
- ※2: components/cheatsheets/CheatsheetLayout.tsx が @/tools/registry の allToolMetas をインポート(関連ツール表示用)。依存の種類: データ参照
- ※3: components/quiz/ShareButtons.tsx が @/lib/games/shared/webShare の useCanWebShare, shareGameResult をインポート。依存の種類: ロジック利用(Web Share API判定)
- ※4: components/blog/RelatedMemos.tsx が @/lib/memos-shared の ROLE_DISPLAY, capitalize, RoleSlug, RoleDisplay をインポート。依存の種類: 型+データ参照
1-2. 共有レイヤー(lib/, components/common/)への依存
| フィーチャー | lib/constants | lib/seo | lib/date | lib/pagination | lib/ogp-image | lib/cross-links | lib/markdown | components/common/ |
|---|---|---|---|---|---|---|---|---|
| tools | 2件 (app) | 1件 | 1件 | 2件 (app) | 1件 | 1件 | - | Breadcrumb, ShareButtons, Pagination |
| cheatsheets | - | 1件 | - | - | 1件 | - | - | Breadcrumb, ShareButtons |
| games | 1件 (app) | 4件 | - | - | 4件 | - | - | Breadcrumb |
| quiz | 2件 (app) | 1件 | - | - | - | - | - | Breadcrumb |
| dictionary | 5件 (app) | 5件 | - | - | - | - | - | Breadcrumb, ShareButtons |
| blog | 4件 (app) | 1件 | 3件 | 4件 | 1件 | 1件 | 1件 | Pagination |
| memos | 2件 (app) | 1件 | 1件 | 1件 | - | 1件 | 1件 | Pagination |
1-3. 循環依存: 検出されず
全てのフィーチャー間依存は一方向のみで、循環依存は存在しない。具体的に確認した組み合わせ:
- tools <-> cheatsheets: cheatsheets -> tools のみ(一方向)
- games <-> dictionary: 直接依存なし(data/を介した間接的共有のみ)
- games <-> quiz: quiz -> games のみ(一方向)
- blog <-> memos: blog -> memos-shared のみ(一方向、cross-links経由は共有レイヤー)
1-4. 共有レイヤーからフィーチャーへの逆依存(設計上の注意点)
以下のファイルは「共有」であるにもかかわらず、特定フィーチャーに依存している:
- lib/seo.ts -> @/tools/types, @/cheatsheets/types, @/lib/quiz/types(型のみ)
- lib/search/build-index.ts -> 全7フィーチャーのregistry/データモジュール
- lib/cross-links.ts -> @/lib/blog, @/lib/memos(2フィーチャー)
- lib/feed.ts -> @/lib/blog
- lib/feed-memos.ts -> @/lib/memos, @/lib/memos-shared
- components/common/Footer.tsx -> @/lib/games/registry(ゲーム一覧表示用)
- components/common/Header.tsx -> ../search/SearchTrigger(相対パスで検索コンポーネントに依存)
Footer.tsxがgames/registryに依存している点は設計上のホットスポットである。
2. 共有リソースの利用マップ
2-1. src/data/ の各JSONファイルの利用先
| JSONファイル | 利用元1 | 利用元2 | 共有? |
|---|---|---|---|
| kanji-data.json | lib/dictionary/kanji.ts | components/games/kanji-kanaru/GameContainer.tsx | dictionary + games |
| yoji-data.json | lib/dictionary/yoji.ts | components/games/yoji-kimeru/GameContainer.tsx | dictionary + games |
| traditional-colors.json | lib/dictionary/colors.ts | components/games/irodori/GameContainer.tsx | dictionary + games |
| puzzle-schedule.json | - | components/games/kanji-kanaru/GameContainer.tsx | games専用 |
| yoji-schedule.json | - | components/games/yoji-kimeru/GameContainer.tsx | games専用 |
| nakamawake-data.json | - | components/games/nakamawake/GameContainer.tsx | games専用 |
| nakamawake-schedule.json | - | components/games/nakamawake/GameContainer.tsx | games専用 |
| irodori-schedule.json | - | components/games/irodori/GameContainer.tsx | games専用 |
分析: 8ファイル中、3ファイルがdictionaryとgamesの間で共有。5ファイルはgames専用。scripts/generate-puzzle-schedule.ts も src/data/kanji-data.json と src/data/puzzle-schedule.json を参照。
2-2. src/lib/ トップレベルファイルの利用パターン
| ファイル | 利用箇所数(テスト除外) | 主な利用元 | 分類 |
|---|---|---|---|
| constants.ts | 24 | 全フィーチャーのapp層、lib/seo、lib/feed* | 真の共有 |
| seo.ts | 16 | 全フィーチャーのapp層 | 真の共有(ただしfeature typesに依存) |
| pagination.ts | 9 | blog(4), tools(2), memos(1), sitemap(1), common(1) | 真の共有 |
| ogp-image.tsx | 8 | 各フィーチャーのopengraph-image.tsx | 真の共有 |
| date.ts | 5 | blog(3), tools(1), memos(1) | 真の共有 |
| cross-links.ts | 3 | blog(1), tools(1), memos(1) | 3フィーチャー間ブリッジ |
| markdown.ts | 2 | blog(1), memos(1) | 2フィーチャー共有 |
| feed.ts | 2 | app/feed/* | blog専用(実質) |
| feed-memos.ts | 2 | app/memos/feed/* | memos専用(実質) |
| memos.ts | app内利用 | app/memos/*, lib/cross-links | memosフィーチャー |
| memos-shared.ts | 5 | components/memos/*, components/blog/RelatedMemos | memosフィーチャー(+blog) |
| blog.ts | app内利用 | app/blog/*, lib/cross-links, lib/feed, lib/search | blogフィーチャー |
2-3. src/components/common/ の利用パターン
| コンポーネント | 利用箇所数(テスト除外) | 利用元 |
|---|---|---|
| Breadcrumb | 21 | 全フィーチャーのapp層 + ToolLayout, CheatsheetLayout |
| ShareButtons | 6 | blog, colors, dictionary, tools, cheatsheets のレイアウト |
| Pagination | 3 | BlogListView, ToolsListView, MemoFilter |
| Header | 1 | app/layout.tsx |
| Footer | 1 | app/layout.tsx |
| ThemeProvider | 1 | app/layout.tsx |
| GoogleAnalytics | 1 | app/layout.tsx |
Breadcrumb(21箇所)とShareButtons(6箇所)が高い再利用性を持つ真の共有コンポーネント。
3. フィーチャー別コロケーション度評価
3-1. コロケーション度ランキング(最良→最悪)
| ランク | フィーチャー | 散在先ディレクトリ数 | 総ファイル数 | コロケーション度 | 理由 |
|---|---|---|---|---|---|
| 1 | tools | 1 (+app/tools, components/tools) | 184 | 最良 | src/tools/に32ツールのComponent, logic, meta, CSS, testsが全て同居。components/tools/はレイアウト系UIのみ(14件)。app/tools/はルーティングのみ(7件)。ツール追加時はsrc/tools/内で完結。 |
| 2 | cheatsheets | 1 (+app, components) | 33 | 良好 | src/cheatsheets/に3チートシートのComponent, metaが同居。toolsと同じregistryパターン。 |
| 3 | quiz | 2 (+app) | 29 | 中程度 | lib/quiz/にデータ+ロジック、components/quiz/にUIが分離。ただしクイズ追加はlib/quiz/data/に1ファイル+registry修正で済む。 |
| 4 | memos | 2 (+app) | 25 | 中程度 | lib/memos*.tsにロジック、components/memos/にUIが分離。 |
| 5 | blog | 3 (+app) | 59 | やや悪い | lib/blog.ts(1), components/blog/(13), content/blog/(35), app/blog/(10)の4箇所。ただしcontent/blog/はMarkdownコンテンツなので特殊。さらにcomponents/blog/BlogListView.tsxがapp/blog/page.module.cssをインポートしており、コンポーネント層→ルーティング層への逆依存が存在。 |
| 6 | dictionary | 3 (+app) | 56 | やや悪い | lib/dictionary/(9), components/dictionary/(19), data/(3), app/dictionary/+app/colors/(25)の4箇所。ただしcolorsがURLルートとして/colorsに独立しているため、app/colors/が別ディレクトリになる理由がある。 |
| 7 | search | 2 (+app) | 18 | 中程度 | lib/search/(3), components/search/(14)。build-indexが全フィーチャーに依存するハブ。 |
| 8 | games | 4 (+app) | 154 | 最悪 | components/games/(72), lib/games/(49), app/games/(25), data/(8)の4箇所に154ファイルが散在。1つのゲームを追加するだけで4ディレクトリに跨るファイル作成が必要。 |
3-2. 各フィーチャーの「理想的なコロケーション」実現のために移動が必要なファイル数
前提: app/はNext.jsのルーティング層なので移動対象外とする。「理想的なコロケーション」は、1フィーチャーの非ルーティングコードが1ディレクトリにまとまることを意味する。
| フィーチャー | 移動が必要なファイル | 移動内容 |
|---|---|---|
| tools | 14件 | components/tools/ -> tools/components/ に統合 |
| cheatsheets | 16件 | components/cheatsheets/ -> cheatsheets/components/ に統合 |
| games | 121件 | components/games/(72) + lib/games/(49) を1ディレクトリに統合。data/の8 JSONも移動対象。 |
| quiz | 19件 | components/quiz/(10) + lib/quiz/(9) を1ディレクトリに統合 |
| dictionary | 28件 | components/dictionary/(19) + lib/dictionary/(9) を1ディレクトリに統合 |
| blog | 14件 | components/blog/(13) + lib/blog.ts(1) を統合(content/blog/は別枠) |
| memos | 15件 | components/memos/(12) + lib/memos*.ts(3) を統合 |
| search | 3件 | lib/search/(3) -> components/search/ に統合(またはその逆) |
| 合計 | 約230件 |
4. 具体的なペインポイント(定量的データ付き)
4-1. 新しいコンテンツを追加するワークフロー比較
新しいツールを追加する場合
- 作成するファイル: 5件(Component.tsx, Component.module.css, logic.ts, meta.ts, tests/logic.test.ts)
- 修正するファイル: 1件(src/tools/registry.ts)
- 触るディレクトリ: 1箇所(src/tools/new-tool/)
- 総作業: 6ファイル、1ディレクトリ
新しいチートシートを追加する場合
- 作成するファイル: 2件(Component.tsx, meta.ts)
- 修正するファイル: 1件(src/cheatsheets/registry.ts)
- 触るディレクトリ: 1箇所(src/cheatsheets/new-sheet/)
- 総作業: 3ファイル、1ディレクトリ
新しいクイズを追加する場合
- 作成するファイル: 1件(src/lib/quiz/data/new-quiz.ts)
- 修正するファイル: 1件(src/lib/quiz/registry.ts)
- 触るディレクトリ: 1箇所(src/lib/quiz/data/)
- 総作業: 2ファイル、1ディレクトリ
- 注: UIは既存のQuizContainerを共有するため新規コンポーネント不要
新しいゲームを追加する場合(漢字カナール相当の規模)
- 作成するファイル: 約24件
- src/app/games/new-game/: 5件(page.tsx, page.module.css, layout.tsx, opengraph-image.tsx, twitter-image.tsx)
- src/components/games/new-game/: 11件(GameContainer, GameBoard, GameHeader, GuessInput, GuessRow, HintBar, ResultModal, StatsModal, HowToPlayModal, FeedbackCell + CSS)
- src/lib/games/new-game/: 6件(engine.ts, daily.ts, storage.ts, share.ts, types.ts + categories.ts等)
- src/data/: 2件(game-data.json, game-schedule.json)
- 修正するファイル: 1件(src/lib/games/registry.ts)
- 触るディレクトリ: 4箇所(app/games/, components/games/, lib/games/, data/)
- 総作業: 25ファイル、4ディレクトリ
結論: ゲーム追加の作業量はツール追加の約4倍(25件 vs 6件)、かつ4ディレクトリに跨る。tools/cheatsheetsは1ディレクトリで完結するのに対し、gamesは最も分散が激しい。
4-2. コードレビューの困難さ
ゲーム関連のPRをレビューする際、以下のディレクトリを横断して確認する必要がある:
- src/components/games/[game-name]/ (UI)
- src/lib/games/[game-name]/ (ロジック)
- src/app/games/[game-name]/ (ルーティング)
- src/data/ (データ)
PRの差分でこれらが混在すると、どの変更がどのレイヤーに属するかの把握が難しくなる。
4-3. 発見された設計上のアンチパターン
components/blog/BlogListView.tsx が app/blog/page.module.css をインポート
- コンポーネント層がルーティング層のCSSに依存(レイヤー逆転)
- 該当:
import styles from "@/app/blog/page.module.css";
components/common/Footer.tsx が lib/games/registry に依存
- 共有コンポーネントが特定フィーチャーに依存(共有層の純粋性が破れている)
- 該当:
import { allGameMetas, getGamePath } from "@/lib/games/registry";
components/quiz/ShareButtons.tsx が lib/games/shared/webShare に依存
- quizフィーチャーがgamesフィーチャーのロジックに依存
- webShare.tsの内容は汎用的(Web Share API判定)で、gamesフォルダにある理由が薄い
- これは本来 lib/shared/ や lib/webShare.ts に置くべき汎用ユーティリティ
lib/dictionary/index.ts が未使用
- re-exportモジュールが存在するが、誰もインポートしていない
- 全消費者が直接 lib/dictionary/kanji.ts, lib/dictionary/yoji.ts 等をインポート
4-4. lib/seo.ts のフィーチャー型依存
lib/seo.tsは「共有ユーティリティ」であるにもかかわらず、3つのフィーチャーの型をインポートしている:
- @/tools/types (ToolMeta)
- @/cheatsheets/types (CheatsheetMeta)
- @/lib/quiz/types (QuizMeta)
これはseo.tsがフィーチャーごとのメタデータ生成関数を含んでいるためだが、理想的にはseo関連の関数を各フィーチャーモジュール内にコロケーションすべきか、またはseo.tsが受け取る型をジェネリックにすべきである。ただし現状では型のみの依存なのでランタイムへの影響はない。
5. src/content/ の役割と位置づけの分析
5-1. src/content/blog/ の読み込みフロー
- src/lib/blog.ts が
BLOG_DIR = path.join(process.cwd(), "src/content/blog")でパスをハードコード - fs.readdirSync でディレクトリ内の .md ファイルを走査
- parseFrontmatter(lib/markdown.ts)でYAML frontmatterを解析
- markdownToHtml(lib/markdown.ts)でHTML変換
- ビルド時にSSGとして実行される
5-2. 「コンテンツ」と「コード」の境界
現在の境界:
- コンテンツ(非コード): src/content/blog/.md, src/data/.json
- 定義コード: src/tools//meta.ts, src/cheatsheets//meta.ts, src/lib/quiz/data/*.ts, src/lib/games/registry.ts
- ロジックコード: src/tools//logic.ts, src/lib/games//{engine,daily,storage,share}.ts
- UIコード: src/tools//Component.tsx, src/components/, src/app/*
分析: toolsとcheatsheetsの「定義」はTypeScript(meta.ts)であり、blogの「定義」はMarkdown+YAML。quizの「定義」もTypeScript。gamesのメタデータはlib/games/registry.tsに直接記述されている。
contentにCode(TypeScript)を含めるべきかについて:
- src/tools/内のComponent.tsxやlogic.tsは明確に「コード」であり、「コンテンツ」ではない
- meta.tsは「コンテンツ定義」と「コード」の中間に位置する
- src/data/*.jsonは純粋な「データコンテンツ」
結論: src/content/を「非コードコンテンツのリポジトリ」と位置づける場合、現在のblog/.mdとdata/.json以外のTypeScriptファイルを含めるのは概念上の混乱を招く。tools/cheatsheets/quizのmeta/dataはTypeScriptコードの一部として扱うのが自然。
6. テストファイルの配置パターン分析
6-1. テストファイル総数: 114件
配置パターンの内訳:
- tests/ディレクトリ内: 114件(100%)
- 同一ディレクトリ内(.test.ts並列配置): 0件
全テストが__tests__/サブディレクトリパターンを使用しており、一貫性がある。
6-2. テストとテスト対象の距離
| パターン | 件数 | 例 | 距離 |
|---|---|---|---|
| ツールのロジックテスト | 32 | src/tools/char-count/tests/logic.test.ts -> ../logic.ts | 最短(同一ツールディレクトリ内) |
| lib/gamesのロジックテスト | 22 | src/lib/games/kanji-kanaru/tests/engine.test.ts -> ../engine.ts | 最短 |
| lib/__tests__のテスト | 10 | src/lib/tests/seo.test.ts -> @/lib/seo.ts | 1階層上 |
| components/tests | 27 | src/components/games/shared/tests/CountdownTimer.test.tsx -> @/components/games/shared/CountdownTimer | 最短 |
| app/tests | 14 | src/app/games/kanji-kanaru/tests/page.test.tsx -> テスト対象は同階層のpage.tsx | 最短 |
| lib/dictionary/tests | 4 | src/lib/dictionary/tests/kanji.test.ts -> @/lib/dictionary/kanji.ts | 1階層上 |
| lib/quiz/tests | 2 | 同上パターン | 1階層上 |
| lib/search/tests | 1 | 同上パターン | 1階層上 |
テストがテスト対象と最も離れている箇所:
- src/app/games/kanji-kanaru/tests/GameBoard.test.tsx がテストするのは src/components/games/kanji-kanaru/GameBoard.tsx(異なるディレクトリツリー間のテスト)
- src/app/games/kanji-kanaru/tests/GuessInput.test.tsx がテストするのは src/components/games/kanji-kanaru/GuessInput.tsx(同上)
これらはapp/__tests__に配置されているが、テスト対象はcomponents/にあり、ディレクトリの分離がテストの配置にも影響を与えている典型例。
6-3. vitest.config.mts のディレクトリ依存
vitest.config.mtsは以下に依存:
- vite-tsconfig-paths: tsconfig.jsonの@/* -> ./src/*パスエイリアスを解決
- setupFiles: ./src/test/setup.ts
ディレクトリ構造変更時、@/*エイリアスは維持されるためパス解決への影響は最小限。ただしインポートパスそのものが変わるファイルは全て更新が必要。
7. サマリー: 主要な発見事項
定量的な問題の深刻さランキング
- gamesの4ディレクトリ分散(154ファイル、121ファイル移動必要) — 最大の問題
- components/tools/ と src/tools/ の分離(14ファイル移動) — tools内UIレイアウトの分離
- dictionary/の3ディレクトリ分散(28ファイル移動必要) — dictionaryの中規模分散
- quiz/の2ディレクトリ分散(19ファイル移動必要) — 比較的軽微
- blog/の4箇所分散(14ファイル移動必要) — content/blog/は別格として扱える
- data/JSONファイルの分散(8ファイル) — dictionary/gamesの共有データ問題
設計上のホットスポット
- lib/search/build-index.ts — 全7フィーチャーに依存するハブ。リファクタリング時に最も影響を受けるファイル。
- lib/seo.ts — 3フィーチャーの型に依存。
- components/common/Footer.tsx — 共有層からgamesフィーチャーへの依存。
- lib/games/shared/webShare.ts — games名前空間にあるがquizからも利用される汎用ユーティリティ。
- components/blog/BlogListView.tsx — app/blog/page.module.cssへのレイヤー逆転依存。
共有データ問題
kanji-data.json, yoji-data.json, traditional-colors.jsonの3ファイルはdictionaryとgamesの両方から参照されている。これらのデータの「権威ある出典元」はdictionary(辞典データ)であり、gamesはそのデータの「利用者」という関係が明確。リファクタリング時は、データをdictionary側に配置し、gamesからdictionary/dataを参照するのが論理的に正しい。ただし、schedule JSONやnakamawake-data.jsonはgames固有データであり、games側に配置すべき。
現状で既にうまく機能している点
- src/tools/のコロケーションパターン — 32ツールが完全に統一された構造で、新規追加が容易
- src/cheatsheets/のコロケーションパターン — toolsと同じ成功パターン
- registryパターンの一貫性 — tools, cheatsheets, games, quizが全て同じregistry+typeパターンを採用
- tests/ディレクトリの100%統一 — テストファイル配置パターンが完全に一貫
- 循環依存なし — フィーチャー間で循環参照が一切存在しない