カナエを作る #4:AI 任せだったソースを見直す。企画の前に、構造の方針を決める
前回(#3)の最後に、次は企画の深掘りと書きました。ただ、その前に1つ、検討しておきたいことが出てきました。 (2026年10月時点の内容です)
ソースの構造の話です。実際に考えるのは、環境構築や開発の段階です。#1 の流れにも入っていません。それでも、思いついたものが適切かどうかを、この時点で検討したいと思いました。
気になっていたのは、ソースをあまり見ていないこと
ハコヅミとワダチは、とりあえず作って、公開して、サービスとして運用できるかを試す位置づけでした。スピード重視の、バイブコーディングです。
もちろん、仕事の経験から、基本的なことは踏襲したつもりです。単体テスト(Vitest)や結合テスト(Playwright)の自動化、型チェックと Lint、CI/CD の設定、Issue の受入条件から始める開発、PR → Merge → Deploy という流れです。
気になっていたのは、実際のソースをあまり見ていないことです。最初は少し見ましたが、ソースの構造をこちらから指定することはなく、AI に任せきりになっていました。
バイブコーディングの弊害として、構造をきちんと定義せずに AI に任せきりにした結果、あとから改修を入れるときに問題になる事例は、結構多い印象です。
カナエは個人開発で、サービスの範囲も狭いので、今のやり方でも特に問題は出ないと思います。それでも、エンジニアとしては、綺麗に構造化されたソースを作りたいという思いがあります。
もともと、構造化された綺麗なソースを書くのが好きなタイプです。チームで開発していたときは、ソースがそこから逸脱していくのを見ると、気持ちが沈みました。
その分、若干、綺麗さに傾倒しすぎて、やりすぎるきらいもあります。
まず、ハコヅミとワダチのソースを見てもらった
いきなり理想の構造を考えるのではなく、Claude に今のソースを確認してもらいました。比較のために、SvelteKit の練習で作った家計簿アプリも見てもらっています。すべての行を読んだわけではなく、ファイル構成の集計と、一部のソースでの確認です。
結果は、思ったより整っていました。
| 観点 | ハコヅミ / ワダチ | 家計簿 |
|---|---|---|
route の .ts から DB を直接触っている |
0件 / 0件 | 18ファイル |
| ロジックの分け方 | 純粋なロジックは lib/*.ts、DB は lib/server/ |
大半が route の中 |
| テスト | ロジックの横に .test.ts |
一部のみ |
AI に任せきりでも、土台の形はできていました。
一方で、課題も見えました。
libが平らすぎる:機能の違うファイルが1か所に数十個並び、どれがどの機能のものか、見ただけでは分からない- 巨大な画面がある:ワダチのタイマーの画面は1,160行で、そのうち974行が script
- 構造を機械で守っていない:ESLint に、構造についてのルールはない。今の形は、CLAUDE.md と AI の振る舞いに頼っている
- 共通部分がコピーで分かれている:ハコヅミとワダチで同じパスのファイルが101個あるが、完全に同じものは7個だけ
lib が平らすぎるのは、自分でも同じことを思っていました。
一般的な構造と照らし合わせて、線を引く
SvelteKit の公式ドキュメントは、$lib/server のようなサーバー専用の仕組みは示していますが、層の設計までは決めていません。server という名前のフォルダは、どの階層でもサーバー専用になるので、機能ごとにフォルダを分ける形と両立できます。
ただ、一般的に良いとされている構造を、そのまま持ち込むつもりはありません。小さいサービスでは、過剰になるからです。Claude からも同じ指摘があり、ここは自分の考えと一致しました。
そこで、どこまで構造を守るかを決めました。
守ることは、次の5つです。
- route は、入力の確認、呼び出し、結果を返すことだけにする
- DB に触るのは、
server/の中だけにする - 純粋なロジックは、Svelte や DB に依存させない
- 依存の向きは
routes → features → sharedにする。機能どうしはindex.tsを通してだけ使う - 1ファイルの行数に上限を設ける
やらないことも、決めました。
- Repository のインターフェース、DI、ドメインクラス
- DB の差し替えを想定した抽象化
- 全機能に、同じ層を機械的に作ること
きれいにすること自体が目的になると、本末転倒です。自分はやりすぎるきらいがあるので、なおさら先に線を引いておきます。カナエの規模に見合う、薄い構造にとどめます。
機能ごとのフォルダにする
形は、次のようにします。
src/lib/
features/
bucket-items/ 機能のフォルダ
index.ts 他の機能に見せる入口
bucket-items.ts 純粋なロジック
bucket-items.test.ts
server/ この機能の DB アクセス
components/ この機能の画面部品
stats/
index.ts
stats.ts
stats.test.ts
components/
shared/
ui/ ボタンなどの共通部品
dates/ 日付の共通処理
dates.ts
dates.test.ts
server/
db/ DB への接続と、全テーブルの定義
auth/
server/
components/
billing/
server/
components/
DB の定義(Drizzle のスキーマ)だけは、機能ごとに分けず、shared/server/db/ の1か所にまとめます。テーブルは外部キーで機能をまたぎますし、データモデルは全体を1か所で見られるほうが、人が確認するときに読みやすいからです。
shared の直下には、ファイルを置かず、フォルダだけにします。ファイルを直接置いていくと、lib と同じように平らになってしまうからです。utils のような何でも入る名前は使わず、dates のように中身が分かる名前にします。
server や components は、機能のフォルダの中に入ります。ここは、最初に案を見たときに把握できず、聞き返したところです。
勉強してきた経験では、components は機能単位ではなく、共通の扱いでした。カナエでは、本当に共通の部品は shared/ui/ に置き、機能ごとに異なる画面の構成を、機能の components/ に置きます。
分ける基準は、その機能を知っているかどうかです。
shared/ui/:機能を知らない部品。ボタン、モーダル、カードの枠など- 機能の
components/:その機能のデータや言葉を知っている部品。バケットリストのカードや、登録のフォームなど
迷ったら、まず機能の components/ に置きます。別の機能でも必要になったら、そのときに shared/ui/ へ移します。
共通化は、あとあとできればいいなレベル
カナエの案出しでは、「他のサービスと共通にできるところは共通にする」と書きました。ただ、今回いきなりパッケージ化までは考えていません。
認証や決済のように、他のサービスと共通になりそうなものを、試行的に shared にまとめておきます。あとあと共通化できればいいな、というレベルです。
決めたあとのチェックは、人ではなく AI に
ルールを決めても、守られているかを毎回人が見るのでは、続きません。決めたあとのチェックは、人ではなく AI が自立して判定できる仕組みにしたいと考えています。
Claude と整理して、3段階にしました。
| 段階 | 確認すること | 仕組み |
|---|---|---|
| 1. 機械の判定 | 依存の向き、DB の場所、行数 | lint か、import を検査するテスト。CI で落とす |
| 2. AI の判定 | 責務、命名、重複 | チェックリストをスキルにして、PR の前に AI が確認して直す |
| 3. 人の確認 | データモデル、構造のルールの変更 | そこに触れた PR だけ読む |
決まった基準で判定できるものは、1段階目に寄せます。2段階目は、Claude Code のプランが Pro に戻ることも考えて、PR ごとの自動実行にはせず、実装のスキルの最後に組み込むことにしました。
まだ考えの段階で、動くかどうかは確かめていません。候補のツールが .svelte ファイルに対応しているかも、未確認です。これは、環境構築のときに試します。
おわりに
コードは AI が書いても、構造の判断は人が持つ。カナエでは、この形を試します。
今回決めたのは、あくまで方針です。実際に作ってみて合わなければ、また考えが変わると思います。その場合も、そのまま記録にしていきます。
次こそ、企画の深掘りです。