JBS 中級コース(案1)/Day2/2026-05-21

設計書からの実装と
生成物の検証

到達点は、1 本の API を設計書から実装し、生成コードを設計書と照合し、実装の根拠を説明できる状態です。残りの実装は Claude に任せ、レビューに回ります。

13:00〜16:00(オンライン) 検証サイクルで進行 到達点は説明できる状態
本日のアジェンダ
13:00〜13:10

Session 01 再スタートと進め方

Day1 完成版から全員同一の起点へ。検証サイクルの確認。[10分]

13:10〜13:35

Session 02 骨格とモデルの生成・照合

骨格は一括生成。生成後に ER 図とカラム単位で照合。[25分]

13:35〜14:15

Session 03 チケット起票 API の3層実装

POST /tickets を repository→service→router で実装。[40分]

14:15〜14:30

Session 04 状態遷移のレビュー演習

生成実装を設計書と照合し、受け入れ可否を判定。[15分]

14:30〜14:40

休憩

10分

14:40〜15:15

Session 05 レビュー主体のコメント・SLA

実装は Claude に委譲。誤実装の検出。非開発業務も参加可。[35分]

15:15〜15:40

Session 06 説明できるテスト

境界値を指定し、保証内容を言語化。[25分]

15:40〜15:55

Session 07 動作確認と説明ドリル

Swagger 確認。根拠の説明。トークン消費の振り返り。[15分]

15:55〜16:00

まとめと Day3 引き継ぎ

持ち帰る3点と Day3 への宿題。[5分]

Session 01 / 13:00〜13:10

再スタートと進め方

Day1 アンケートで多かった「生成物を検証しきれない」「説明責任が持てない」への回答として、Day2 の進め方を組み直しました。

起点をDay1 完成版に統一

Day1 の到達度には差があったため、Day2 は全員を同じ起点に揃えます。講師が品質を整えた Day1 完成版の設計書を配布します。第1回の各自の作業は day1-myown ブランチに残り、後から参照できます。

再スタート手順

自分の Fork リポジトリに、配布された day2-start.zip を取り込みます。次の手順を画面で一緒に進めます。git コマンドは VSCode 下部のターミナルで実行します。Windows / Mac で同じです。

1. 第1回の作業を退避する

VSCode ターミナルgit add -A git commit -m "day1: 第1回の作業を保存" --allow-empty git branch day1-myown

2. day2-start.zip を展開する — Finder / エクスプローラで解凍し、中身をリポジトリ直下に上書きコピーします(Windows / Mac 共通の操作)。

3. Day2 の作業ブランチを作る

VSCode ターミナルgit add -A git commit -m "day2: Day1完成版を起点に開始" git switch -c day2-work
Tips:ターミナルで迷ったとき

実行中の操作は Ctrl + C でキャンセルします。現在地は pwd、ブランチは git branch --show-current で確認します。長い出力で画面が止まったら q で抜けます。迷ったら手を止めて講師に声をかけてください。

検証サイクル

「プロンプトを打つ → 出力を Apply する → 次へ」は繰り返しません。1 ブロックごとに次の 4 ステップを回します。

STEP 1
言語化
作らせる前に「何を満たせば正解か」を自分の言葉で1〜2行書く。設計書のどこが根拠かを指す。
STEP 2
生成
範囲を絞って Claude に作らせる。1プロンプトで3層まとめて作らせない。
STEP 3
検証
本日用意した題材専用チェックリストで、生成物を1項目ずつ確認する。
STEP 4
判定
受け入れるか差し戻すかを自分で決める。差し戻すなら理由を1行書く。
Tips:アンケートへの回答

「レビューしきれない」は、見る観点が言語化されていない状態です。STEP 1 で正解条件を書けば、STEP 3 で見る箇所が決まります。「説明責任が持てない」は、根拠の出どころを追っていない状態です。STEP 1 で設計書のどこ由来かを指せば、根拠を示せます。

Day2 でやらないこと

Session 02 / 13:10〜13:35

骨格とモデルの生成・照合

骨格とモデルは定型のため一括生成します。要点は、生成後に設計書とカラム単位で照合することです。

HANDS-ON 骨格とモデルを生成し、ER図と照合する 25分
1
STEP 1 言語化:骨格の合格条件

生成前に合格条件をメモに1行書きます。例:「CLAUDE.md のディレクトリ構成と一致、ruffmypy が通り、pytest は既存1件のみ緑」。

Tips:ruff と mypy

ruff は書式・コーディング規約のチェッカー、mypy は型の整合性チェッカーです。どちらも CI で自動実行され、違反があるとジョブが赤になります。手元では ruff check app/mypy app/ で事前確認します。

2
STEP 2 生成:骨格とモデルの一括生成
promptdocs/data_model.md と docs/openapi.yaml と CLAUDE.md を読み、 app/ 配下に CLAUDE.md のディレクトリ構成どおりの空スケルトンと、 app/models/ の SQLAlchemy 2.0 (Mapped/mapped_column) によるエンティティ実装、 app/database.py の非同期エンジン(aiosqlite)、scripts/seed.py を作成してください。 エンティティは docs/data_model.md のカラム定義表を正とします。 型・PK/FK・UNIQUE・default・index を表のとおりに付けてください。 created_at は datetime(timezone=True) で UTC。 全関数に型ヒント、ruff と mypy が通る状態で出力してください。

この範囲は設計書とコードが1対1で対応するため、一括生成でも検証が機械的にできます。

3
STEP 3 検証:ER 図と1カラムずつ突き合わせる

docs/data_model.md 第2節のカラム定義表を開き、生成された app/models/ticket.py と並べて、下のチェックリストを上から確認してください。

Ticket モデル 検証チェックリスト
  • status の default が "open" になっているか(設計書の状態遷移は open 始まり)
  • priority の default が "medium"
  • assignee_id が nullable、reporter_idcategory_id が NOT NULL か
  • resolved_at が nullable か(未解決のチケットでは NULL)
  • created_at が timezone 付き datetime か
  • status priority assignee_id に index が付いているか(data_model.md 第5節)
  • 勝手に追加されたカラムがないか(設計書にない列が生えていたら差し戻し対象)
受け入れ:全項目満たす → commit へ 差し戻し:1つでも欠ける → 理由を1行で添えて再生成
4
STEP 4 判定して commit
VSCode ターミナルruff check app/ --fix && ruff format app/ mypy app/ python scripts/seed.py pytest tests/ -q git add app/ scripts/ && git commit -m "feat: 骨格とモデルを設計書照合のうえ追加" git push -u origin day2-work

差し戻した場合は再生成し、同じチェックリストを再確認します。起点がずれると後続がすべてずれるため、ここは時間をかけます。

よくある詰まり
Session 03 / 13:35〜14:15

チケット起票 API の3層実装

POST /tickets の1本を repository → service → router の3層で実装し、実装の根拠を説明できる状態にします。

HANDS-ON POST /tickets を3層で実装し、検証する 40分
1
STEP 1 言語化:起票の正解条件

生成前に docs/sequence_diagrams.md の「1. チケット起票」と docs/openapi.yamlTicketCreate を読み、合格条件をメモに書きます。最低限の4項目:

  • title は1〜200文字、空文字は弾く(公開デモはここを通していた=改善対象)
  • description は1〜5000文字
  • 作成直後の status は必ず open、priority 未指定なら medium
  • 成功で 201、検証エラーで 422 と統一エラー形式
2
STEP 2 生成:エラー共通形→repository→service→router の順

1プロンプトでまとめず、次の順に1ファイルずつ依頼し、都度検証します。

prompt(1つ目)app/schemas/errors.py に ErrorResponse {"error":{"code","message"}} を、 app/exceptions.py に TicketNotFound / ValidationError を定義し、 app/main.py に exception_handler を登録してください。 docs/openapi.yaml 末尾のエラーコード一覧と code 文字列を一致させてください。
prompt(2つ目)app/repositories/ticket_repository.py に create(session, ticket) と get_by_id(session, id) だけ実装してください。CRUD 全部はまだ要りません。
prompt(3つ目)app/services/ticket_service.py に create_ticket を実装してください。 title 1-200 / description 1-5000 の検証、category 存在チェック、 status=open・priority 既定 medium の付与をここで行います。 検証 NG は ValidationError を raise。HTTP やセッション生成は書かないでください。
prompt(4つ目)app/routers/tickets.py に POST /tickets だけ実装してください。 Depends で AsyncSession を注入し、service を呼び、201 を返すだけ。 バリデーションや状態の知識を router に書かないでください。
Tips:トークン節約

3層を1プロンプトで生成すると、Claude が app/ 全体を読み直して差分が長くなり、読み返せないまま消費が増えます。範囲を1ファイルに絞ると、読む量・出る量・修正回数が減ります。

3
STEP 3 検証:層の責務が混ざっていないか
起票 API 検証チェックリスト
  • title 空文字・201文字を渡したとき 422 になるか(実際に Swagger で試す)
  • 検証ロジックが service にあるか。router を読んで if len(...) 等が紛れていないか
  • router に SQL や session.add が直接書かれていないか
  • 作成直後の status が open、priority 未指定で medium か
  • エラー時の JSON が {"error":{"code","message"}} 形式で、code が openapi.yaml の一覧と一致するか
  • 存在しない category_id でエラーになるか(外部キーだけに頼っていないか)
受け入れ:全項目満たす 差し戻し:責務違反は理由を書いて該当ファイルだけ再生成
4
STEP 4 判定:テスト1件の追加と commit

Claude に書かせず、自分で1件書きます。次の内容で tests/unit/test_ticket_create.py を新規作成し、保存後にコマンドを実行します。client フィクスチャが未整備なら、Claude に「tests/conftest.py に非同期 TestClient の client フィクスチャを追加してください」と依頼します。

tests/unit/test_ticket_create.pyimport pytest @pytest.mark.asyncio async def test_title_empty_returns_422(client): """title 空文字は 422 と VALIDATION_ERROR を返す(STEP1 の正解条件の裏取り)""" res = await client.post("/api/tickets", json={ "title": "", "description": "x", "category_id": 1, "reporter_id": 1, }) assert res.status_code == 422 assert res.json()["error"]["code"] == "VALIDATION_ERROR"
VSCode ターミナルpytest tests/unit/test_ticket_create.py -q ruff check app/ tests/ --fix && mypy app/ git add app/ tests/ && git commit -m "feat: POST /tickets を3層で実装し検証" git push
発展(早く終わった方)

GET /api/tickets/{id}(詳細取得)を、同じ3層・同じ検証サイクルで追加します。存在しない id で 404 と統一エラー形式が返ることを検証チェックリストに加えてください。

実装の説明

Session 03 の後、「title 検証が router でなく service にある理由」を自分で声に出して30秒で説明します。説明できない箇所が復習対象です。Session 07 で再度確認します。

Session 04 / 14:15〜14:30

状態遷移のレビュー演習

ここから、生成された実装を設計書と照合して判定する作業に移ります。状態遷移は Claude が誤りやすい題材です。

REVIEW PATCH /tickets/{id} の状態遷移を点検する 15分
1
STEP 1 言語化:許される遷移を先に書き出す

生成物を見る前に docs/requirements.md 第6節と docs/data_model.md 第3節を読み、許される遷移と禁止される遷移をメモに書き出します。先に書き出さずにコードを見ると、出力に引きずられて「それらしいので OK」と流しがちです。

2
STEP 2 生成:あえて素朴に依頼する
promptapp/services/ticket_service.py に update_status(session, ticket_id, new_status, actor) を 実装し、PATCH /tickets/{id} の router から呼んでください。 状態遷移は docs/requirements.md 第6節の規則に従い、不正遷移は InvalidStatusTransition。 変更時は AuditLog に before/after を記録してください。

仕様書を参照させても、遷移表を完全に守る保証はありません。次の検証で確かめます。

3
STEP 3 検証:仕込まれやすい3点を狙い撃つ
状態遷移 検証チェックリスト
  • closed → open を 409 で弾いているか(最終状態。ここを通す実装が一番多い)
  • resolved → open は許可されているか(再オープン運用。逆に弾いていたら過剰)
  • open → resolved の飛び越しを弾いているか(in_progress を経由する)
  • 遷移判定が service にあるか。router 側に if 文が漏れていないか
  • AuditLog に before と after の両方が JSON で入っているか(部長の強い要望)
  • resolved/closed に遷移したとき resolved_at がセットされ、再 open で戻るか
受け入れ:6項目すべて根拠を言える 差し戻し:どれか1つでも説明できなければ差し戻し
4
STEP 4 判定:差し戻し文を書く練習

欠陥が見つかったら、修正箇所と理由を指定して差し戻します。丸投げせず、直す対象を限定します。

差し戻し例update_status で closed → open が通っています。 docs/requirements.md 第6節では closed は最終状態で closed → open は禁止です。 遷移可否表を service 内に明示的に定義し、表にない遷移は InvalidStatusTransition を raise する形に直してください。 router は変更しないでください。
Tips:非開発業務の方へ

この演習は実装スキルではなく、仕様と成果物を照合して欠陥を指摘するスキルです。要件定義や設計レビューが主業務の方は、コードの細部より「仕様のどこに違反しているか」を日本語で示せれば十分です。

休憩(14:30〜14:40)
Session 05 / 14:40〜15:15

レビュー主体のコメント・SLA

残りの実装は Claude に任せ、レビューに回ります。SLA は公開デモが誤実装していた箇所です。Claude が同じ誤りをしないか確認します。

REVIEW コメントAPIとSLA計算を生成させ、欠陥を見抜く 35分
1
コメントAPIを生成(10分)
promptapp/routers/comments.py / services/comment_service.py / repositories/comment_repository.py を実装してください。 GET と POST /tickets/{id}/comments。body は1-5000文字・空不可。 存在しない ticket_id へのコメントは TicketNotFound。 追加時に AuditLog を記録。docs/openapi.yaml の Comment スキーマに合わせてください。
コメントAPI 検証チェックリスト
  • 存在しない ticket_id へ POST すると 404 か(200 や 500 なら差し戻し)
  • body 空文字・5001文字で 422 か
  • レスポンスが openapi.yaml の Comment 必須項目(id/ticket_id/author_id/body/created_at)を満たすか
  • 検証が service にあり router に漏れていないか
2
SLA計算を生成(10分)
Tips:SLA とは

SLA は対応の時間目標です。本題材では優先度ごとに「初回応答までの時間」と「解決までの時間」が決まっており(requirements.md 第5節)、超過したチケットを違反として検出します。営業時間外と土日を経過時間に数えないのが要点です。

promptapp/services/sla_service.py を実装してください。 docs/requirements.md 第5節の優先度別 SLA 表(high 30分/240分、medium 240分/540分、 low 540分/1620分)を使用。営業時間は平日 9:00-19:00、営業時間外と土日は経過時間に 含めない。is_business_minutes(start, end) を分離。 list_violations(session) が docs/openapi.yaml の SLAViolation 配列を返す。 GET /api/sla/violations の router も用意してください。
3
SLA を狙い撃つ:公開デモと同じ罠に嵌っていないか

Day1 で観察した公開デモの SLA は「created_at から一律1時間で違反」という誤実装でした。生成物が同じ誤りをしていないか確認します。

SLA 検証チェックリスト(最重要)
  • 優先度ごとに閾値が変わるか。全優先度で同じ時間になっていたら公開デモと同じ誤実装
  • 金曜18:30起票・月曜9:30時点の high チケットは「違反」か。土日と夜間を除外すると営業時間は約1時間30分。30分の初回応答SLAは超過、4時間の解決SLAは未超過のはず
  • 営業時間外だけが経過した場合、経過分が0に近いか(夜間に放置されただけでは違反にならない)
  • resolved/closed のチケットが resolution 違反一覧に出てこないか
  • 初回応答の達成判定が「担当者割当済み かつ agent コメント1件以上」になっているか(requirements.md 第5節)
受け入れ:営業時間除外と優先度別閾値が正しい 差し戻し:一律閾値・営業時間無視は理由を添えて再実装
4
commit して、見つけた欠陥をメモする
VSCode ターミナルruff check app/ --fix && mypy app/ pytest tests/ -q git add app/ && git commit -m "feat: コメント/SLA 実装をレビューのうえ追加" git push

差し戻して直した場合、「最初の生成物がどこを間違えたか」を1行メモに残してください。Session 07 と Day3 で使います。

発展(早く終わった方)

SLA 違反一覧を overdue_minutes の降順に並べる実装(requirements.md C-2)を追加し、並び順を保証するテストを1件書いてください。

Session 06 / 15:15〜15:40

説明できるテストを書く

カバレッジの数字あわせのテストは書きません。保証内容を説明できるテストだけを書きます。

HANDS-ON 境界値を指定したテスト作成 25分
1
STEP 1 言語化:守りたい境界の洗い出し

「テストを書いて」と丸投げすると、保証内容の不明なテストが大量に出ます。先に、壊れてほしくない境界を3つ挙げます。例:

  • title 200文字は通る、201文字は 422
  • closed → open は 409、resolved → open は 200
  • high チケットが営業時間で30分1秒経過したら response 違反、29分なら違反でない
2
STEP 2 生成:挙げた境界のみをテスト化
prompt次の3つの境界だけをテストする pytest を tests/ 配下に書いてください。 カバレッジ稼ぎの網羅テストは要りません。各テストの docstring に 「何を保証するテストか」を日本語1行で書いてください。 1. title 200文字は201、201文字は422 2. closed→open は409、resolved→open は200 3. high チケットが営業時間で30分超過なら response 違反、29分なら違反でない SLA の時刻は freezegun で固定してください。 freezegun が無ければ requirements-dev.txt に追加し、追加した旨を報告してください。
3
STEP 3 検証:テスト内容の言語化
テスト検証チェックリスト
  • 各テストの docstring を読み、何を保証するか自分の言葉で言い直せるか
  • 境界の「ちょうど」と「1つ外」の両方を突いているか(200と201、29分と30分1秒)
  • assert が結果を本当に確かめているか(ステータスコードだけ見て中身を見ていない、はNG)
  • freezegun の固定時刻が平日の営業時間内になっているか(土曜固定だと別の理由で通る)
受け入れ:3テストとも何を守るか説明できる 差し戻し:説明できないテストは消すか書き直す
4
STEP 4 判定:カバレッジは結果値として確認
VSCode ターミナルpytest tests/ --cov=app --cov-report=term-missing git add tests/ requirements-dev.txt && git commit -m "test: 説明できる境界値テストを追加" git push

カバレッジ数値は確認しますが目的化しません。80%未満でも、説明できるテストが積み上がっていれば Day2 は合格です。CI のカバレッジは未達でも fail しない設定です(architecture_constraints.md)。

Session 07 / 15:40〜15:55

動作確認と説明ドリル

実装を Swagger で確認し、根拠を説明できるか自分で確かめます。

HANDS-ON 動作確認・説明ドリル・トークン振り返り 15分
1
Swagger で実装を叩く
Tips:Swagger UI とは

Swagger UI は、実装した API をブラウザから手動で叩ける画面です(/docs で開きます)。目的は実機での挙動確認です。各エンドポイントの「Try it out」→ パラメータ入力 → Execute で、返るステータスコードとレスポンスを確認します。設計書どおりの値・形式が返れば成功です。

VSCode ターミナルuvicorn app.main:app --reload --port 8000

http://127.0.0.1:8000/docs を開き、Session 03〜05 で実装した起票・状態遷移・SLA を叩きます。Day1 で触った公開デモ https://0514-28cc.give-app.net/docs(改善対象の実装)と比べ、自分の実装では title 空文字が弾かれること、SLA が優先度別であることを確認します。

2
説明ドリル:根拠を30秒で説明

次の3問に、設計書の根拠を含めて30秒で答えます。答えられない問いが復習対象です。

  • なぜ title の長さ検証は router でなく service にあるのか
  • なぜ closed から open に戻せないのか。業務上の根拠は何か
  • 金曜の夜に起票されたチケットが、月曜朝まで SLA 違反にならないのはなぜか
Tips:説明責任の持ち方

3問とも「docs のここに書いてある」と示せれば、上司や顧客に説明できます。AI が出したからではなく、要件に照らして自分が判定した、と言える状態です。

3
トークン消費の振り返り

Day1 で「想定以上に消費した」という声がありました。本日は範囲を1ファイルに絞りました。Claude Code で /cost を実行し、Day1 との差を各自確認します。

Closing / 15:55〜16:00

まとめと Day3 引き継ぎ

Day2の成果物チェック

持ち帰る3点

Day3までの宿題

Day3 予告

Day3(5/28)は本日の実装を Docker でパッケージ化し、CI に docker-build と trivy を追加します。@claude メンションの自動 PR も、本日と同じ生成→検証→判定で受け入れ可否を決めます。Day3 前日までに必要な環境設定(OAuth トークン登録など)は受講者ポータルに手順があります。