はじめに
「最強のプロジェクト構成」と書くと大げさですが、ここで紹介するのは銀の弾丸ではありません。
私が Web アプリケーションを作るときに、
- backend と frontend の API 型を二重管理したくない
- 構成は機能単位で、開発体験を良くして実装したい
- Storybook / Playwright / Swagger UI を迷わず使えるようにしたい
- 人間だけでなく AI エージェントにも作業させやすい構成にしたい
という理由で整理した、モノレポ構成の一例です。
全体像
まず全体はこんな形です。
monorepo構成にして、まとめてgit管理しています。
クリーンアーキテクチャを構成として、共通して、Features から Commons を呼んでいます。
monorepo/
projects/ ←ai系のフォルダと区別しやすくするために、専用フォルダを作りました。
backend/
frontend/
api-contracts/
e2e/
playwrights/
infra/
dockers/
.agents/
AGENTS.md
Makefile
runner.go
各アプリケーションには、独立した package.json やREADME.mdを持ちます。
monorepo/
projects/
backend/package.json
frontend/package.json
api-contracts/package.json
e2e/playwrights/package.json
私の中で大事にしている依存関係は下記の通りで、OpenAPIの定義を参照して、二重管理を防いでいます。
api-contracts
├─ backend が参照する
├─ frontend が参照する
└─ Swagger UI 用の OpenAPI 生成元になる
つまり、API 契約の中心を api-contracts に寄せます。なお、バリデーションも担当してくれるZodはOpenAPI定義をソースから生成できる機能があるとのことなので、Zodに定義を寄せてます。
※本当はフォルダ名をapi-contractsではなくZodやOpenApisにしたかったのですが、AIがフォルダ名から間違った解釈をしまくって、AIがなぜか好むワードの「契約」とか「api-contracts」に統一しました。分かりづらいですね...
api-contracts全体像ざっくり
なぜ api-contracts を分けるのか
backend と frontend を分けた構成でよく困るのが、型定義の二重管理です。
たとえば backend 側で SampleRequest を変えたのに、frontend 側の型やバリデーションを直し忘れると、実行するまでズレに気づけません。
そこで api-contracts を単一の定義元にします。
api-contracts/
api-definitions/
endpoints/
SampleZodSchema.ts
SampleOpenApiPaths.ts
HelloZodSchema.ts
HelloOpenApiPaths.ts
openapi.ts
generated/
generated-openapi.json
generated-openapi-typescript.ts
scripts/
generate-openapi.ts
schemas.ts
役割はこうです。
| ファイル | 役割 |
|---|---|
XxZodSchema.ts |
Request / Response の Zod スキーマを書く |
XxOpenApiPaths.ts |
OpenAPI の path 定義を書く |
openapi.ts |
OpenAPI 生成の入口 |
schemas.ts |
backend / frontend に公開するAPI型定義の入口 |
generated-openapi.json |
Swagger UI で読む OpenAPI |
generated-openapi-typescript.ts |
OpenAPI 由来の paths / components 型 |
backend と frontend は、ローカル package として @monorepo/api-contracts を参照します。
※XxxZodSchema.tsファイルを、シェルコマンドでコピペ配布する方法もあるようなのですが、どっちにしようか悩み中です。
package.json
{
"name": "@xxx/xxx/api-contracts",
"version": "0.1.0",
"scripts": {
"build": "tsc -p tsconfig.json && tsc -p tsconfig.esm.json && cp package.esm.json dist/esm/package.json",
"generate": "tsx scripts/generate-openapi.ts && openapi-typescript generated/generated-openapi.json -o generated/generated-openapi-typescript.ts"
},
"dependencies": {
"zod": "^4.4.3"
},
"devDependencies": {
"@asteasolutions/zod-to-openapi": "^8.5.0",
"@types/node": "^25.9.3",
"openapi-typescript": "^7.10.1",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
}
}
openapi.ts
import {
OpenAPIRegistry,
OpenApiGeneratorV31,
type RouteConfig,
} from "@asteasolutions/zod-to-openapi";
import { helloOpenApiPaths } from "./endpoints/HelloOpenApiPaths.js";
import { sampleOpenApiPaths } from "./endpoints/SampleOpenApiPaths.js";
type JsonObject = Record<string, unknown>;
const registry = new OpenAPIRegistry();
const openApiPaths: RouteConfig[] = [
...sampleOpenApiPaths,
...helloOpenApiPaths,
];
/**
* 分割したPath定義をOpenAPI Registryへ集約します。
*/
for (const path of openApiPaths) {
registry.registerPath(path);
}
const generator = new OpenApiGeneratorV31(registry.definitions);
/**
* Pathから参照されない型もSwagger UIのcomponentsで見られるように補います。
*/
function addUnreferencedSchemas(openApiDocument: JsonObject): JsonObject {
const components = openApiDocument.components as JsonObject | undefined;
const schemas = components?.schemas as JsonObject | undefined;
return {
...openApiDocument,
components: {
...(components ?? {}),
schemas: {
...(schemas ?? {}),
HelloRequest: {
type: "object",
additionalProperties: false,
description: "Hello APIのリクエストです。",
example: {},
},
},
},
};
}
const generatedOpenApiDocument = generator.generateDocument({
openapi: "3.2.0",
info: {
title: "Backend API",
version: "1.0.0",
},
servers: [
{
url: "http://localhost:8080",
},
],
});
/**
* Swagger UIが読む全エンドポイント共通のOpenAPI Documentです。
*/
export const openApiDocument = addUnreferencedSchemas(
generatedOpenApiDocument as unknown as JsonObject,
);
SampleOpenApiPaths.ts
import type { RouteConfig } from "@asteasolutions/zod-to-openapi";
import {
sampleRequestSchema,
sampleResponseSchema,
} from "./SampleZodSchema.js";
/**
* SampleControllerに対応するOpenAPI Path定義です。
*/
export const sampleOpenApiPaths: RouteConfig[] = [
{
method: "get",
path: "/api/sample",
summary: "Sampleフォーム内容をGETで送信します。",
operationId: "getSample",
tags: ["Sample"],
request: {
query: sampleRequestSchema,
},
responses: {
200: {
description: "Sample APIの正常レスポンスです。",
content: {
"application/json": {
schema: sampleResponseSchema,
},
},
},
400: {
description: "リクエスト不正",
},
500: {
description: "サーバーエラー",
},
},
},
{
method: "post",
path: "/api/sample",
summary: "Sampleフォーム内容をPOSTで送信します。",
operationId: "postSample",
tags: ["Sample"],
request: {
body: {
required: true,
content: {
"application/x-www-form-urlencoded": {
schema: sampleRequestSchema,
encoding: {
sampleCheckbox: {
style: "form",
explode: true,
},
},
},
},
},
},
responses: {
200: {
description: "Sample APIの正常レスポンスです。",
content: {
"application/json": {
schema: sampleResponseSchema,
},
},
},
400: {
description: "リクエスト不正",
},
500: {
description: "サーバーエラー",
},
},
},
];
SampleZodSchema.ts
import { z } from "zod";
export type SampleRequestInput = {
sampleText: string;
sampleCheckbox: string[];
sampleRadio: string;
sampleSelect: string;
sampleTextarea: string;
};
/**
* Sample APIのリクエスト例です。OpenAPI example と Storybook mock の両方で使います。
*/
export const sampleRequestExample = {
sampleText: "テスト",
sampleCheckbox: ["1"],
sampleRadio: "1",
sampleSelect: "1",
sampleTextarea: "テスト本文",
} satisfies SampleRequestInput;
/**
* Sample APIのレスポンス例です。OpenAPI example と Storybook mock の両方で使います。
*/
export const sampleResponseMessageExample = "sample api response";
/**
* Sample APIのレスポンス例です。OpenAPI example と Storybook 表示で使います。
*/
export const sampleResponseExample = {
isSuccess: true,
method: "POST",
message: sampleResponseMessageExample,
received: sampleRequestExample,
submittedAt: "2026-06-19T09:00:00.000Z",
} satisfies {
isSuccess: boolean;
method: "GET" | "POST";
message: string;
received: SampleRequestInput;
submittedAt: string;
};
/**
* Sample APIのリクエストschemaです。
*/
export const sampleRequestSchema = z.object({
sampleText: z.string()
.min(1, { error: "sampleTextは必須です" }),
sampleCheckbox: z
.array(
z.enum(["1", "2", "3"], {
error: "sampleCheckboxは1/2/3のいずれかを指定してください",
}),
)
.min(1, { error: "sampleCheckboxは1件以上選択してください" }),
sampleRadio: z.enum(["1", "2", "3"], {
error: "sampleRadioは1/2/3のいずれかを指定してください",
}),
sampleSelect: z.enum(["1", "2", "3"], {
error: "sampleSelectは1/2/3のいずれかを指定してください",
}),
sampleTextarea: z.string()
.min(1, { error: "sampleTextareaは必須です" }),
})
.passthrough()
.meta({
id: "SampleRequest",
description: "Sample APIのリクエストです。",
example: sampleRequestExample,
});
/**
* Sample APIのレスポンスschemaです。
*/
export const sampleResponseSchema = z.object({
isSuccess: z.boolean(),
method: z.enum(["GET", "POST"]),
message: z.string(),
received: sampleRequestSchema,
submittedAt: z.iso.datetime(),
})
.meta({
id: "SampleResponse",
description: "Sample APIのレスポンスです。",
example: sampleResponseExample,
});
export type SampleRequest = z.infer<typeof sampleRequestSchema>;
export type SampleResponse = z.infer<typeof sampleResponseSchema>;
この構成にしておくと、「型」「バリデーション」「API ドキュメント」のズレをかなり減らせます。
backend は feature と common を分ける
backend は NestJS を前提にしています。
大きく features と common に分けます。
backend/
src/
features/
sample/
SampleController.ts
SampleRequest.ts
SampleResponse.ts
SampleValidator.ts
feature-a/
FeatureAController.ts
FeatureAUsecase.ts
FeatureAService.ts
FeatureAExchange.ts
common/
consts/
errors/
filters/
services/
utils/
features は機能単位でまとめます。
features/
xx/
XxController.ts
XxRequest.ts
XxResponse.ts
XxValidator.ts
XxUsecase.ts
XxService.ts
XxExchange.ts
各ファイルのざっくりした責務はこうです。
| ファイル | 責務 |
|---|---|
XxController.ts |
HTTP の入口 |
XxRequest.ts |
Controller 層で受け取る Request 型 |
XxResponse.ts |
Controller 層から返す Response 型 |
XxValidator.ts |
Zod などを使った入力検証 |
XxUsecase.ts |
その機能の処理フローを組み立てる |
XxService.ts |
機能内の実処理を置く |
XxExchange.ts |
DTO / JSON / 表示用データなどの変換 |
依存関係はこちらです。
個人的には、Controller に直結する feature の Usecase では、専用の XxInput / XxOutput を増やしすぎず、XxRequest / XxResponse をそのまま使う方針にしています。
理由はシンプルで、Controller と Usecase の間に薄い変換層だけが増えていくと、AI も人間も迷いやすくなるからです。
一方で、common/services のように複数機能から呼ばれる処理では、用途に応じた Input / Output を作ってよいことにしています。
Controller
↓
Feature Usecase
↓
Feature Service
↓
Common Service / 外部 API / DB など
このくらいの境界にしておくと、機能ごとの見通しと共通化のバランスを取りやすいです。
backend で気をつけていること
backend では、次のルールを意識しています。
- 単純な変換や 1 回しか使わない処理を、むやみに private 関数へ切り出さない
- Service が太り始めたら、別 Service や util へ責務を分ける
- static メソッドを安易に増やさず、Injectable な Service や共通関数を使う
- 生成物と関係するエントリポイントのファイル名は小文字に寄せる
- Request / Response / Validator / Usecase / Service / Exchange の役割を混ぜない
特に XxService.ts に private メソッドが増えてきたら、設計の見直しサインだと思っています。
Service に何でも入れると、最初は速いのですが、少し時間が経つと「この処理はどこから呼ばれているのか」「これは feature 専用なのか common なのか」が分かりづらくなります。
frontend は app / features / common に分ける
frontend は Next.js を前提にしています。
frontend/
app/
page.tsx
sample/
page.tsx
feature-a/
page.tsx
features/
sample/
Sample.tsx
Sample.stories.tsx
apis/
sampleApi.ts
sampleExampleApi.ts
types.ts
hello/
Hello.tsx
Hello.stories.tsx
apis/
helloApi.ts
helloExampleApi.ts
common/
assets/
ui/
components/
Button/
Button.tsx
Button.module.css
Button.stories.tsx
TextArea/
TextArea.tsx
TextArea.module.css
TextArea.stories.tsx
compositions/
FormPanel/
FormPanelComposition.tsx
FormPanelComposition.module.css
FormPanelComposition.stories.tsx
utils/
app は Next.js のルーティング入口です。
画面や機能の実体は features に置きます。
複数画面で使う UI は common/ui/components や common/ui/compositions に寄せます。
※正直layoutsやOrganismも入れてみたいのですが、まだそのような大規模プロジェクトになっていないので、そのままにしています。なんならOrganismのようなAtomicデザイン用語ではなく、Oneレイヤー、Twoレイヤーのような、数字で無限に共通化できるようにしたいなと思ったりしてます。
私の中では、だいたいこう分けています。
| 置き場 | 役割 |
|---|---|
app |
ルーティングと page の入口 |
features |
画面や機能ごとの実装 |
common/ui/components |
Button や TextArea などの小さな UI |
common/ui/compositions |
複数 component を組み合わせた UI |
common/utils |
UI に依存しない共通処理 |
features の中に API 呼び出しも置くことで、その機能を見るだけで画面・通信・Storybook の入口が追いやすくなります。
Storybook は UI 確認だけでなく操作テストにも使う
UI 部品や composition は、実装ファイルの近くに *.stories.tsx を置きます。
frontend/common/ui/components/Button/
Button.tsx
Button.module.css
Button.stories.tsx
frontend/common/ui/compositions/FormPanel/
FormPanelComposition.tsx
FormPanelComposition.module.css
FormPanelComposition.stories.tsx
Storybook の play 関数には、ユーザー操作と assertion を書きます。
import { expect, userEvent, within } from "storybook/test";
export const Example = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "送信" }));
await expect(await canvas.findByText("送信しました")).toBeTruthy();
},
};
個人的には、frontend のテストはざっくりこう使い分けています。
| テスト | 向いているもの |
|---|---|
| Storybook play | コンポーネントや composition の表示・操作 |
| Vitest | UI から独立したロジック、hooks、utils |
| Playwright | 実ブラウザ上の画面遷移や主要導線 |
UI の振る舞いは、テストコードだけでなく Storybook 上でも目視確認できるようにしておくと、かなり扱いやすいです。
E2E は Playwright を独立ディレクトリに置く
E2E は e2e/playwrights にまとめています。
e2e/
playwrights/
src/
tests/
fixtures.ts
sample/
Sample.e2e.ts
SampleAssertion.ts
shared/
pages/
sample/
SamplePage.ts
utils/
screenshot.ts
playwright.config.ts
package.json
テスト本体、Assertion、Page Object を分けます。
| ファイル | 役割 |
|---|---|
Xx.e2e.ts |
テスト観点を書く |
XxAssertion.ts |
期待結果の確認をまとめる |
XxPage.ts |
DOM 操作をまとめる |
fixtures.ts |
共通 fixture をまとめる |
Playwright は強力ですが、何でも詰め込みすぎると保守がつらくなります。
そのため、まずは主要ブラウザ 1 project に絞り、必要になったら Firefox / WebKit を追加する方針にしています。
実行は Makefile 経由にまとめます。
make e2e
make e2e-ui
make e2e-codegen
成功時や失敗時のスクリーンショット、トレース、動画も設定しておくと、CI やレビュー時に原因を追いやすくなります。
Makefile で入口をそろえる
各 package に npm scripts が分かれていると、慣れていない人や AI エージェントが「どこで何を叩くのか」で迷います。
そこで、ルートに Makefile を置いて入口をそろえます。
make install
make dev
make build
make lint
make e2e
make storybook
make api-contracts-generate
make swagger-ui
内部的には runner.go や各 package の npm scripts を呼んでも、利用者から見る入口は make xxx に寄せます。
この形にしておくと README や AI 向け指示でも説明が短くなります。
infra
infra/dockers には Docker Compose 定義を置きます。
infra/
dockers/
compose.yaml
たとえば Swagger UI は、api-contracts/generated の OpenAPI JSON を読んで表示します。
make swagger-ui
アプリ本体と API ドキュメント確認用の補助コンテナを分けておくと、開発時に必要なものだけ起動しやすいです。
AI エージェント用の指示とログも分ける
AI エージェントに作業させる場合、コードだけでなく「作業ルール」と「作業ログ」の置き場も大事です。
私はこのように分けています。
/monorepo
AGENTS.md
.agents/
instructions.md
templates/
work_log.md
verification_log.md
handoff.md
local/
current-task.md
worklogs/
YYYY-MM-DD/
HHmm-task-slug.md
verification-logs/
YYYY-MM-DD/
HHmm-task-slug.md
それぞれの役割はこうです。
| 置き場 | 役割 |
|---|---|
AGENTS.md、instructions.md
|
AI エージェントが最初に読む共通指示 |
.templates |
共有してよい運用ルールやログテンプレート |
.local/ |
.gitignore 対象のローカル専用作業ログや現在状態 |
current-task.md |
AIの作業状態を書いてもらうコーナー。AIが何を考えてるか可視化されるので便利 |
.local/ はローカル専用にして、.gitignore 対象にします。公開するのは「ログ置き場を分ける」という考え方だけで、実際の作業ログ本文はそのまま載せない前提です。
作業ログには、実行した確認、未実行の理由、差し戻し内容、残リスクを書きます。
AI に作業を任せるなら、コード規約だけでなく「どう検証して、どう引き継ぐか」まで決めておくと、後からかなり助かります。
templates/verification_log.md
# 検証ログテンプレート
このテンプレートは、Verifier専用のローカル検証ログに使う。
保存先は `.agents/local/verification-logs/YYYY-MM-DD/HHmm-task-slug.md` とする。
通常の作業ログとは分け、差し戻し理由と検証履歴を細かく残す。
```md
# YYYY-MM-DD HH:mm task-slug verification
## 対象
- タスク:
- 対象差分:
- xxx(※都度追加すること)
- Verifierサブエージェント:
## 差し戻し回数記録
- 1回目: ※初回検証は必ず不合格として必ず0点になるので注意。サブエージェントVerifier未起動は禁止。
- 2回目:
- 3回目:
## 品質基準
- 合格条件: 100点満点中94点以上、重大度Criticalなし、必須確認の未実施なし
- 差し戻し条件: 初回検証は必ず不合格、差し戻しは最大3回まで
## 検証結果
- 追記日時:
- 判定:
- 品質スコア:
- 採点内訳:
- xxx(※都度追加すること)
- verification_log追記内容:
## 確認したファイル
- xxx(※都度追加すること)
## 確認したコマンド
- xxx(※都度追加すること)
## 差し戻し理由
- [ ] xxx(※都度追加すること)
- 理由:
- 根拠:
- 重大度:
- 期待する対応:
## 細かい懸念
- xxx(※都度追加すること)
## 再検証メモ
- 前回から改善した点:
- [ ] xxx(※前回差し戻しから改善した点を都度追加すること)
- 残る指摘:
- [ ] xxx(※再検証時点で残る指摘を都度追加すること)
- 合格または打ち切り理由:
- [ ] xxx(※合格または3回上限で打ち切る理由を都度追加すること)
## 残リスク
- [ ] xxx(※都度追加すること)
## 次担当への依頼
- xxx(※都度追加すること)
templates/work_log.md
# 作業ログテンプレート
このテンプレートは、ローカル専用の作業ログに使う。
保存先は `.agents/local/worklogs/YYYY-MM-DD/HHmm-task-slug.md` とする。
```md
# YYYY-MM-DD HH:mm task-slug
## 目的
-
## 前提
-
## Worker / Verifier Task Checklist
### Worker: Orchestrator
* [ ] x-x-x: xxxxx(都度追加する)
### Worker: Planner
* [ ] x-x-x: xxxxx(都度追加する)
### Worker: Explorer
* [ ] x-x-x: xxxxx(都度追加する)
### Worker: TDD Engineer
* [ ] x-x-x: xxxxx(都度追加する)
### Worker: Implementor
* [ ] x-x-x: xxxxx(都度追加する)
### Verifier(サブエージェント必須)
* [ ] x-x-x: xxxxx(都度追加する)
### Worker: Maintainer
* [ ] x-x-x: xxxxx(都度追加する)
## Maintainer Final Check
- 担当範囲: ログ、ドキュメント、ソース内コメント、未使用ソースの整理
- 追加したソース内コメント:
- 掃除した未使用ソース:
- 残した未使用候補:
- 確認コマンド:
## Verifier Quality Gate
- サブエージェント起動:
- 差し戻し回数:
- 1回目:
- 2回目:
- 3回目:
- 最終品質スコア:
- 最終判定:
- verification_log:
- 合格条件: 100点満点中95点以上、重大度Criticalなし、必須確認の未実施なし
- 差し戻し条件: 初回検証は必ず不合格、差し戻しは最大3回まで
## 実施内容
-
## 判断
-
## テスト
- Vitest:
- Playwright:
- その他:
## 未解決
-
## 次の担当への依頼
-
下記の順序で作業させています。
- Orchestrate: 作業前提、担当分担、ログ方針を決める。
- Plan: 要件、成功条件、スコープ、実装単位を決める。
- Explore: Plannerの出力をもとに、実装前に必要な調査を行う。
- Implement: Planner、Explorer、テストの確定事項に沿って実装する。
- Test: 実装の品質を保証するためにテスト作成、テスト実施する。
- Evaluate: 必ずサブエージェントとして起動し、実装結果を数値基準で検証して差し戻しまたは合格を判定する。
共通ルール遵守確認
数値品質基準
Verifierは、合計100点で採点する。
95点以上、重大度Criticalの指摘なし、必須確認の未実施なしをすべて満たした場合だけ合格とする。
基準を1つでも満たさない場合は不合格として差し戻す。
| 観点 | 配点 | 確認内容 |
|---|---|---|
| 要件充足 | 25 | ユーザーの目的、成功条件、スコープを満たしている。 |
| ルール遵守 | 20 |
AGENTS.md、この指示、対象ディレクトリのルールに違反していない。 |
| テストと確認 | 20 | 変更リスクに応じたVitest、Playwright、型チェック、lint、手動確認が実施または妥当な理由付きで省略されている。 |
| 設計と保守性 | 15 | 既存設計、責務分割、命名、生成物扱いに沿い、変更範囲が過剰でない。 |
| ドキュメントとログ | 10 |
.agents/local/ の状態、作業ログ、必要なドキュメントが更新され、未解決事項が残っていないか明記されている。 |
| 検証証跡 | 10 | 採点根拠、確認した差分、残リスク、次担当への依頼が追跡できる。 |
差し戻しルール
- 1回目の検証は必ず不合格としてWorkerへ差し戻す。
- 初回差し戻しには、数値採点、未達点、修正または追加確認が必要な具体事項を含める。
- 差し戻す場合は、なぜ差し戻したかの理由をできるだけ多く列挙する。細かい懸念、軽微な表現揺れ、ログ不足、確認不足、将来の誤読リスクも理由として記録してよい。
- 差し戻し理由は、根拠ファイル、該当箇所、期待する対応、重大度を添えて、
verification_logにも残す。 - 2回目以降の検証で、品質スコアと必須条件を満たした場合だけ合格にできる。
- 差し戻し回数は最大3回とする。
- 3回目でも不合格の場合は、追加で差し戻しを続けず、残課題、品質スコア、ユーザー判断が必要な点を最終状態に残す。
- 各検証では、差し戻し回数、前回から改善した点、残る指摘を記録する。
品質確認
- 変更がユーザーの成功条件を満たしている。
- 既存設計や責務分割に沿っている。
- 変更範囲が過剰に広がっていない。
- VitestやPlaywrightなど、必要なテストが追加または更新されている。
- ブラウザ上のユーザー操作・表示を変えたのにPlaywrightが追加または更新されていない場合は、残リスクではなく指摘事項として扱う。省略理由が妥当かどうかを判断し、妥当でない場合は修正依頼にする。
- 実行した確認と、未実行の確認が明記されている。
- ドキュメントや設定の更新漏れがない。
出力
Verifierは、次の形式で次担当へ渡す。
## 検証結果
- 差し戻し回数:
- 判定:
- 品質スコア:
- 採点内訳:
- verification_log:
- 指摘事項:
- 差し戻し理由:
- 重大度:
- 修正が必要な理由:
- 確認したテスト:
- 残リスク:
- 次担当への依頼:
注意
初回検証は必ず不合格として差し戻す。
2回目以降で問題がない場合は、問題がないことを明確に書く。
ただし、未実行テストや残リスクがある場合は、問題なしとは別に記録する。
- Maintain: 実装と検証が終わった内容について、ログやドキュメントだけでなく、ソース内コメント、未使用ソースも整理し、最終状態をまとめる。
AIの構成として、↓が良さそうで検討中です。
monorepo/
AGENTS.md ※全体方針 ※instructions.mdは被るので省く
.ai/ ※AI指示用
workset/ ※.ai内の細かいプロンプトを組み合わせて利用する
workflows/ ※どう作業させるかを定義する
roles/ ※何を作業させるか(役割)を定義する
log-templates/ ※AI作業内容ログテンプレート集。汎用的に書くのがコツっぽい
local/ ※ログ等のコミット対象でないものを全部置く
docs/ ※ドキュメント全般
architecture/ ※システム全体
overview.md
adr/
001-use-a5m2.md
002-rest-api.md
003-mybatis.md
coding-styles.md ※Lintで補えない部分を書く
biz-specs/ ※設計書とか要件仕様を置く
まとめ
プロジェクト構成は
- API 契約を
api-contractsに集約する - backend は feature と common の境界を分ける
- frontend は app / features / common で責務を分ける
- Storybook は UI の確認と操作テストに使う
- Playwright は実ブラウザのe2eテストを担当する
- AI 用の指示とローカルログも置き場を決める
というように決めました。もちろん確定ではなく、逐次修正していきます。
余談
今回の作業で圧倒的に時間がかかったのは、プロジェクト構成を決めることでした。
ただ、その土台をしっかり固めたおかげで、今では私やAIも迷うことが少なくなり、実装サイクルもかなり速くなりました。
今後もエンジニアとして、楽しみながら価値あるものをたくさん作っていきます!😊