Implementation Plan: gRPC Service → GraphQL(全体)
設計: design.md / federation-design.md / protoc-gen-dataloader dataloader トラックのタスク詳細: ../protoc-gen-dataloader/implementation-plan.md
実装は opus / sonnet の subagent に委譲する前提。各タスクは「依頼票」としてそのまま subagent への指示に使える粒度で記述する。
0. 進め方の共通規約
依頼票の使い方
- 各タスクの依頼時は、依頼票本文 + 「参照」欄のドキュメント(該当セクション)を必読指定する。設計ドキュメントが正であり、依頼票は要約にすぎない
- subagent は受け入れ基準のコマンドを自分で実行して結果を報告する
- 「変更してはいけないもの」を守る: 既存 golden スナップショット(意図した変更以外)、既存パッケージの公開 API、無関係ファイルの整形
ブランチ・コミット・レビュー
- タスク毎に feature ブランチ + worktree 分離。Conventional Commits(
feat(codegen-core): ...等)で小さくコミット - golden snapshot の更新(
vitest -u)は意図した出力変更のみ。想定外差分が出たら停止して報告 - 各タスク完了後、メインセッションで code-review(正しさ / 既存規約整合 / 設計ドキュメントとの一致)→ 修正 → マージ
モデル選定の指針
| モデル | 向くタスク |
|---|---|
| opus | 新規の型抽象・printer 設計・既存テスト資産のリファクタ・診断メッセージ UX など、依頼票だけでは閉じない判断を含むもの(T1.2, T1.3, T3.1, T3.2, D2, D4) |
| sonnet | 依頼票と設計ドキュメントで仕様が閉じている機械的実装・fixtures・docs(T0.x, T1.1, T1.4, T1.5, T1.6, T3.3, T3.4, D1, D3, D5, D6) |
難所(T1.3 / T3.2)は effort 高めを指定する。
1. フェーズ構成と依存グラフ
Phase 0: オプション基盤
T0.1 本家 proto にオプション追加 ──▶ T0.2 submodule bump + gen:extensions + getters
│
Phase 1: Step 1(resolver 生成) ▼
T1.1 runtime パッケージ ──────────┬────────┤
T1.2 codegen-core operation 抽象 ─┤ │
▼ │
T1.3 operation printer ◀──────────┘ │
T1.4 suffix 変換(T1.2 後、T1.3 と並行可) │
T1.5 fixtures + golden + 実行テスト │
T1.6 docs │
│
Phase 2: protoc-gen-dataloader(T0 + T1.1 後、Phase 1 後半と並行可)
D1〜D6(別ドキュメント) │
│
Phase 3: Federation(T1.3 + D3 後) ▼
T3.1 codegen-core federation 抽象(T0.2 + D2 後に開始可)
T3.2 federation printers(T3.1 + T1.3 + D3)
T3.3 fixtures + golden + _entities + composition(T3.2 + D5)
T3.4 docsクリティカルパス: T0.1 → T0.2 → T1.2 → T1.3 → T3.2 → T3.3。 並行性: T1.1 は T0.2 と独立に着手可(オプション非依存)。Phase 2 は T1.1 完了後いつでも。T3.1 は Phase 1 完了を待たない。
2. マイルストーン
| M | 内容 | リリース物 |
|---|---|---|
| M1 | Phase 0 + Phase 1 完了 | protoc-gen-pothos minor(service→operation、experimental 明記)+ runtime パッケージ初版 + 本家オプションリリース |
| M2 | Phase 2 完了 | protoc-gen-dataloader 初版 |
| M3 | Phase 3 完了 | protoc-gen-pothos minor(federation)+ docs 一式 |
各マイルストーンで changesets を切る。M1 の時点で experimental として実ユーザー(自分)の proto で試用し、オプション semantics へのフィードバックを M2/M3 前に反映する。
Phase 0: オプション基盤
T0.1: proto-graphql 本家へのオプション追加
2026-07-12 改訂(per-track landing、decision-log Q29): 本タスクは当初「全オプション一括追加」だったが、実際には
(graphql.rpc).batchのみ着地済み。Step 1 系オプション(service/operation/name/expose_field/suffix 変換)は Step 1 実装 PR と同時に、federation 系は Step 2 実装 PR と同時に本家へ追加する(フィールド番号は design.md §2 の予約に従う)。
- 推奨モデル: sonnet / 作業リポジトリ: proto-graphql 本家(このリポジトリの submodule
proto-graphql/で編集 → 本家へ PR) - 依存: なし
- 参照: design.md §2(オプション全文ドラフト)、protoc-gen-dataloader/design.md §2
- 成果物:
proto/graphql/schema.protoに追加:GraphqlServiceOptions/GraphqlOperation/GraphqlRpcOptions(batch/federationネスト含む)/GraphqlFederationOptions(GraphqlObjectTypeOptionsに field 5)/GraphqlSchemaOptionsの suffix 変換 2 フィールド。全て EXPERIMENTAL コメント付き- spike(タスク内最初に実施):
bool extends = 2;というフィールド名が protoc / buf で合法か検証。NG ならexternalに変更し design.md へフィードバック - Go binding(
go/graphqlpb)の再生成、本家 README / docs の annotations 一覧更新
- 受け入れ基準: 本家リポジトリで
buf build/buf lint成功、Go binding が再生成されている、既存オプションの field number に変更がない - 注意: field number は 2056 慣例。既存フィールドの変更・削除は禁止
T0.2: submodule 更新 + extension binding + オプション getter
- 推奨モデル: sonnet
- 依存: T0.1
- 参照: research.md §1.3、
codegen-core/src/types/util.tsの既存 getter 群 - 成果物:
- submodule bump、
pnpm gen:extensionsでcodegen-core/src/__generated__/extensions/再生成 getServiceOptions(DescService)/getRpcOptions(DescMethod)を util.ts に追加(frozenEMPTY_*_OPTIONSシングルトンパターン、既存 getter と同形)+ 単体テスト
- submodule bump、
- 受け入れ基準:
pnpm build && pnpm test && pnpm lint成功、既存 golden スナップショット変更ゼロ
Phase 1: Step 1(RPC → Query/Mutation)
T1.1: runtime パッケージ新設
- 推奨モデル: sonnet
- 依存: なし(並行可)
- 参照: design.md §3.3(context 規約・API)、§3.1(エラー変換)
- 成果物:
packages/@proto-graphql/connect-runtime/(名称は npm 空き確認の上確定。仮名のまま進め、公開前にリネーム可):ProtoGraphqlConnectContext型(transport / transports / callOptions / errorHandler)getClient(ctx, service)(WeakMap + service typeName で memoize、per-service transport 上書き対応)callRpc(ctx, fn)(callOptions 適用 +ConnectError→GraphQLError変換。extensions.code = Code 名、details 非掲載、errorHandler で差し替え)- ESM+CJS dual export、単体テスト(memoize、transport 振り分け、エラー変換、errorHandler 差し替え)
- 受け入れ基準:
pnpm build && pnpm test成功。依存:@connectrpc/connect/@bufbuild/protobuf/graphql(peer)。workspace catalog 管理に従う
T1.2: codegen-core に operation 型抽象を追加
- 推奨モデル: opus
- 依存: T0.2
- 参照: design.md §3.1(マッピング規則の全表)、requirements.md R1〜R5、research.md §1.4
- 成果物:
OperationField(仮)抽象: 対象判定(service opt-in / ignore / streaming 警告・エラー / protobuf-es ガード)、operation 判定(明示 > idempotency 規約)、名前導出(camelCase + name)、引数モデル(Query flatten: request フィールド → 引数リスト(既存 nullability 規則適用)/ Mutation: 単一 input)、戻り値モデル(response 型 / expose_field 検証 / Empty 規則)collectTypesFromFileと並ぶcollectOperationsFromFile(仮)エントリポイント- 単体テスト(判定マトリクス、エラー診断メッセージ)
- 受け入れ基準:
pnpm build && pnpm test成功、既存 golden 変更ゼロ(この時点では printer 未接続) - 注意: 診断メッセージには対処方法(オプション記入例)を含めること
T1.3: operation printer(queryField / mutationField + resolver)
- 推奨モデル: opus(effort 高め)
- 依存: T1.2 + T1.1
- 参照: design.md §3.2(生成コードイメージ)、
protoc-gen-pothos/src/dslgen/printers/の既存 printer 群 - 成果物:
- printer:
builder.queryField/mutationField出力、flatten 引数(t.arg群、message 型はFooInput$Ref参照 +$toProto変換、oneof は optional 展開)、Mutation input(request 由来 Input 型 +$toProto)、getClient/callRpc呼び出し、create(RequestSchema, {...})組み立て、expose_field unwrap、extensions.protobufMethodメタデータ - 同一
*.pb.pothos.tsへの append(printer dispatch への組み込み)
- printer:
- 受け入れ基準:
pnpm build && pnpm test成功。T1.5 の golden で出力が固定されるまでは手元 fixture での確認で可 - 注意: import は
createImportSymbol経由(protoplugin が相対パス解決)。dprint 整形前提のcodeテンプレート規約に従う
T1.4: Request/Response suffix 変換オプション
- 推奨モデル: sonnet
- 依存: T1.2(並行可: T1.3 と独立)
- 参照: design.md §2(GraphqlSchemaOptions)、§3.1、research.md §1.2(
exceptRequestOrResponse) - 成果物:
requests_as_inputs/responses_as_payloads(正式名はこのタスクで確定し design.md に反映): 対象 message の suffix 変換(XxxRequest→XxxInputは Input のみ生成、XxxResponse→XxxPayloadは Object のみ生成)、ignore_requests/ignore_responsesとの併用時は ignore が優先(併用は警告)。単体テスト + 命名衝突検証(変換後の名前が既存型と衝突したらエラー) - 受け入れ基準:
pnpm build && pnpm test成功、既存 golden 変更ゼロ(オプション未使用時)
T1.5: Step 1 fixtures + golden + 実行テスト
- 推奨モデル: sonnet
- 依存: T1.3 + T1.4
- 参照: design.md §5、testing-strategy.md、research.md §1.5(golden の構造と「schema.ts が生成物を import していない」注意点)
- 成果物:
- testapis 追加(
testapis/service/配下に: 基本 CRUD(全マッピング規則)/ idempotency 判定 / rename / expose_field / Empty / streaming 混在 / suffix 変換)+ FileDescriptorSet 再生成 - golden ケース(protobuf-es のみ): 生成コード snapshot、型チェック、SDL snapshot(builder が生成物を side-effect import する schema.ts 構成)、query.graphql 実行スナップショット(
createRouterTransportフェイクサーバを builder.ts / context で注入。Query / Mutation / エラー変換(NOT_FOUND → extensions.code)を最低 1 ケースずつ) - 非 protobuf-es ランタイム + service オプションのエラー系テスト
- testapis 追加(
- 受け入れ基準:
pnpm test成功(新規ケース含む)、既存ケースのスナップショット変更ゼロ
T1.6: Step 1 docs
- 推奨モデル: sonnet
- 依存: T1.5
- 成果物:
docs/protoc-gen-pothos/(service→operation ガイド、builder/context セットアップ、configuration 追記)、docs/proto-annotations/reference.md追記((graphql.service) / (graphql.rpc))、runtime パッケージ README。experimental であることを明記 - 受け入れ基準: リンク切れなし、
pnpm exec biome check(変更ファイル限定。root のlintスクリプトは存在しない) 成功
Phase 2: protoc-gen-dataloader
タスク D1〜D6 は ../protoc-gen-dataloader/implementation-plan.md を参照。前提は T0.x + T1.1。Phase 1 の T1.2 以降と並行可。
Phase 3: Federation(Step 2)
T3.1: codegen-core に federation 抽象を追加
- 推奨モデル: opus
- 依存: T0.2 + D2(BatchSpec)
- 参照: federation-design.md §2〜§3(生成規則と F1〜F10)、§7(コード配置)
- 成果物:
FederationSpec(仮): 全ファイル走査 index(entity ↔ entity_resolver RPC の突合、extend → 対象型の解決)、F1〜F10 検証(診断メッセージに対処方法を含む。F10 = required な非キーフィールドの拒否)、key fieldset → GraphQL 名 selection への変換、キー型導出(BatchSpec と整合検証)- 単体テスト(F1〜F10 各ケース + 正常系)
- 受け入れ基準:
pnpm build && pnpm test成功、既存 golden 変更ゼロ
T3.2: federation printers
- 推奨モデル: opus(effort 高め)
- 依存: T3.1 + T1.3 + D3(loader export 契約)
- 参照: federation-design.md §2(asEntity / externalRef / objectField の生成規則と分岐表)、§8(検証済み API 事実)
- 成果物:
- asEntity printer(entity_resolver RPC のファイルに出力、selection 生成、representation → proto 値の逆変換、loader import)
- externalRef printer(extends スタブ: objectType printer の分岐、externalFields としてキー出力、Input 非生成)
- extend フィールド printer(
builder.objectField、group loader / entity loader / 直接呼び出しの 3 分岐、parent がスタブかローカルかのプロパティ参照差) - dataloader 出力位置パラメータ(protoc-plugin-helpers)
- 受け入れ基準:
pnpm build && pnpm test成功 - 注意: 生成コードが依存してよい Pothos API は federation-design.md §5 の表に限定
T3.3: federation fixtures + golden + 実行 + composition テスト
- 推奨モデル: sonnet
- 依存: T3.2 + D5(dataloader golden 基盤)
- 参照: federation-design.md §6(fixtures 表と検証レイヤ)
- 成果物:
testapis/federation/{entity, extend_local, extend_external, errors}+ golden ケース(コード snapshot / 型チェック / printSubgraphSchema による SDL snapshot / F エラー診断 snapshot)- 実行テスト:
_entities実行(バッチ合流の呼び出し回数、欠損 null)、extend フィールドの group 解決、_service { sdl } - composition テスト:
composeServices(entity は単独、extend_external は所有側 SDL fixture との 2 subgraph)で errors なし - golden ハーネスへの subgraph モード追加(printSubgraphSchema 差し替え)
- 受け入れ基準:
pnpm test成功、既存ケース変更ゼロ
T3.4: federation docs
- 推奨モデル: sonnet
- 依存: T3.3
- 成果物: federation ガイド(builder セットアップ(plugins 順 / graphql@16 固定 / toSubGraphSchema)、@key・entity_resolver・extend の書き方、dataloader との併用、router との接続例)、annotations reference 追記
- 受け入れ基準: リンク切れなし、
pnpm exec biome check(変更ファイル限定。root のlintスクリプトは存在しない) 成功
リスクと対応
| リスク | 対応 |
|---|---|
| オプション semantics が PoC 中に変わる | 本家で experimental 明記(T0.1)。M1 で実プロジェクト試用し M2/M3 前に反映 |
extends フィールド名が protoc で使えない | T0.1 冒頭の spike で検証。NG 時は external へ(design.md 更新) |
| Pothos / Apollo のバージョン変動 | federation-design.md §5 の検証済み範囲に依存 API を限定し、golden + composition テストで検知 |
| golden ハーネス汎用化(D4)が既存テストを壊す | 受け入れ基準に「既存スナップショット変更ゼロ」を明記(純リファクタ強制) |
| subagent 生成コードの規約逸脱・設計乖離 | 依頼票に設計ドキュメント必読指定 + メインセッションの code-review ゲート + pnpm exec biome check(変更ファイル限定。root の lint スクリプトは存在しない)/typecheck |
| graphql 17 系との非互換(Apollo peer 制約) | docs に graphql@16 固定を明記(T1.6 / T3.4)。将来 Apollo 側の 17 対応を追従 |