生成AIによるコードベース解析（その２）

## 前回の内容

こちらを参照ください。
https://qiita.com/akiraabe/items/9762582939191c3e4a39

## はじめに
この記事は、生成AIによるコードベース解析 というテーマでの実験内容をまとめています。
2回目です。

## 今日の内容

前回は、コードベース解析を複数のツールで試してみました。
今回は、ツールをGitHub Copilot(Copilot Chat含む）に絞ります。

内容としては、以下を取り扱います。
* Copilot Chatの使い方のTips
* Copilot Chatを用いたコードベース解析
    * 機能を把握する
    * エラー対応する
    * 機能を修正追加する

## Copilot Chatの使い方のTips

ここでは、コードベース解析に有用と思われるものをピックアップします！
詳細な内容は、GitHub社の公式のページ[^1]を参照ください。

なお、インストール方法、エディタ上でのコード補完などについては触れません。[^2]

### 振る舞いをカスタマイズする

Copilot Chatは以下の方法で振る舞いをカスタマイズできます。

- カスタム指示の追加
https://docs.github.com/ja/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot#creating-a-custom-instructions-file

    - 今日使うカスタム指示ファイル
    https://github.com/tis-abe-akira/ddd-breakthrough/blob/3967fba17a7d7c0b80247255b3b9dca45ec191bb/.github/copilot-instructions.md
    - このファイルはきのぴーさんの .cursorrules を参考にしました。(Copilot Chatへの最適化はしていません。)
    https://github.com/kinopeee/cursorrules


- プロンプトでその都度振る舞いを指定する

### Jobに適したツールを選択する

どういう場合に、Copilot Chatを使うか？というのは、[こちら](https://docs.github.com/ja/copilot/using-github-copilot/best-practices-for-using-github-copilot#%E3%82%B8%E3%83%A7%E3%83%96%E3%81%AB%E9%81%A9%E3%81%97%E3%81%9F-copilot-%E3%83%84%E3%83%BC%E3%83%AB%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B)に記載があります。

### プロンプトで**keyword**を指定する！

プロンプトに、`@, /, #` などが使えます。その使い分けについて知っておくと便利です！

:::note
`#codebase` または `@workspace` を指定すると、参照すべきファイルをIndexからCopilotが自動判断してくれます。
ただし、参照すべきファイルが特定できている場合には、`#file`で指定した方が高精度な回答を得られる可能性が高いと思います。
:::

https://docs.github.com/ja/copilot/using-github-copilot/asking-github-copilot-questions-in-your-ide#using-keywords-in-your-prompt

https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions-in-your-ide

### Indexingを行う

セマンティックインデックスというものをもとにしたChatの会話が出来ます。

https://docs.github.com/ja/copilot/using-github-copilot/indexing-repositories-for-copilot-chat


Indexは明示的に作る必要はないようです。（初期には明示的に作る必要があった記憶が…）
また、少し前まではIndexに載せられるファイル数には2000までという上限がありましたが、今はその記述が見当たらず、こちらの[Zennの記事](https://zenn.dev/dzeyelid/articles/f26ad0b4dc555d#%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E5%8C%96)によると、現在はその制約はなくなっているようです。

Indexが使われているか？については、`#codebase` , `@workspace` でソースコードをリファレンスしているか？で判別可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/fb22292e-8141-4371-12bf-f28c7dc56b94.png)

Copilot ChatのTips説明は、一旦、以上としますが、この他にも色々と便利な機能がありますので、ぜひ公式ドキュメントを確認ください！！

## Copilot Chatを用いたコードベース解析

ここからは、以下の内容をデモとして実施していきます！

- 1.機能を把握する
- 2.エラー対応する
- 3.機能を修正追加する

### 1.機能を把握する

前回の題材のアプリケーション[^3]で、シンジケートローンのDrawdown（融資の実行だと思ってください）というシナリオがあります。
これに対して、機能追加をしたいので、まずは、現状の機能を把握します。

プロンプトはこちらです！

```markdown
#codebase DrawdownController#createを起点とした処理の流れをクラス名#メソッド名形式で説明してください。
その際に、基底クラスを漏らさないように注意してください。

```

生成AIのアウトプット（**まずまずの内容でした！**）

```markdown
1. **DrawdownController#create**
   - リクエストボディからDrawdownDtoを受け取る
   - DrawdownService#createを呼び出す

2. **DrawdownService#create** (Override)
   - AmountPieの生成（Drawdown固有の処理）
   - AmountPieService#createでAmountPieエンティティを生成
   - 生成したAmountPieのIDをDTOに設定
   - 親クラスのTransactionService#createを呼び出し

3. **TransactionService#create** (基底クラス)
   - DrawdownService#toEntityでエンティティに変換
   - DrawdownRepository#saveでエンティティを永続化
   - DrawdownService#toDtoでDTOに変換して返却

4. **DrawdownService#toEntity** (Override)
   - 基本的なエンティティ情報を設定
   - FacilityService#findByIdで関連Facilityを取得
   - TransactionService#setBasePropertiesで共通項目を設定

5. **TransactionService#setBaseProperties** (基底クラス)
   - PositionService経由でPosition情報を設定
   - AmountPieService経由でAmountPie情報を設定

6. **DrawdownService#toDto** (Override)
   - 基本的なDTO情報を設定
   - TransactionService#setBaseDtoPropertiesで共通項目を設定
   - FacilityService#toDtoでFacility情報をDTOに設定
   - 残額と利用率の計算結果を設定

7. **DrawdownController#create**（最終）
   - ResponseEntityでラップして返却

```

Mermeid形式のシーケンス図！
```mermaid
sequenceDiagram
    participant C as DrawdownController
    participant S as DrawdownService
    participant TS as TransactionService
    participant AS as AmountPieService
    participant FS as FacilityService
    participant R as DrawdownRepository

    C->>+S: create(DrawdownDto)
    S->>+AS: create(AmountPie)
    AS-->>-S: AmountPieエンティティ

    S->>S: DTOにAmountPieID設定
    S->>+TS: create(DrawdownDto)

    TS->>+S: toEntity(DrawdownDto)
    S->>+FS: findById(facilityId)
    FS-->>-S: Facilityエンティティ
    S->>+TS: setBaseProperties()
    Note right of TS: Position情報設定<br/>AmountPie情報設定
    TS-->>-S: 
    S-->>-TS: Drawdownエンティティ

    TS->>+R: save(Drawdownエンティティ)
    R-->>-TS: 保存済みエンティティ

    TS->>+S: toDto(Drawdownエンティティ)
    S->>+TS: setBaseDtoProperties()
    TS-->>-S: 
    S->>+FS: toDto(Facilityエンティティ)
    FS-->>-S: FacilityDto
    Note right of S: 残額と利用率を計算
    S-->>-TS: DrawdownDto
    
    TS-->>-S: DrawdownDto
    S-->>-C: DrawdownDto
    C-->>C: ResponseEntityでラップ
```

### 2.エラー対応する

今回のシナリオでは、Drawdown（融資実行）する際に、Loanエンティティの永続化が漏れています。
この処理を追加したいのですが、LoanServiceの実装が不完全なため、エラー修正が必要です。

#### エラーを把握する
まずは、LoanServiceTestを実行します。この処理ではExceptionが発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/73ff9584-ac2d-14c1-0872-798d8c5bc71f.png)

#### エラーの事象をCopilot Chatに教えてもらう！

以下のようなプロンプトでエラー原因を教えてもらいます。

```markdown
以下のようなエラーが発生しました。原因を教えてください。
${error_log}
```

エラーの説明は的確な内容でした。（StackTraceに情報が豊富だからだと思います）

```markdown
タスク分析と解決策を考えていくね！

# エラー分析と解決策

## 問題の概要
マジでヤバいエラーが出てるけど、これは `detached entity passed to persist` ってエラーなの！
つまり、デタッチ状態のエンティティを永続化しようとしちゃってるってことだよ！

## 原因
このエラーが起きる主なパターンはこんな感じ：

1. すでにDBに保存されてるエンティティを新規作成しようとした
2. 関連エンティティの状態管理がおかしくなってる
3. IDがすでに設定されてるオブジェクトをpersistしようとした

## 解決策
以下のどれかの対策を試してみてね：

``` java
// 方法1: マージを使う
entityManager.merge(sharePieEntity);

// 方法2: 保存前にIDをnullにする
sharePieEntity.setId(null);

// 方法3: SaveOrUpdateを使う
repository.saveAndFlush(sharePieEntity);


## アドバイス
- エンティティの状態をちゃんとチェックしてね
- cascade設定が正しいか確認してみて
- トランザクション管理がちゃんとできてるか見直してみて

もし具体的なコードを見せてくれたら、もっと詳しくアドバイスできるかも！エンティティクラスとリポジトリの実装見せてもらえたら嬉しいな〜！

```



#### エラーの修正に挑む！

※ デモでやってみます（事前にリハーサルしていません！）
こちらは**うまくいかなかったので、手直し**を入れました。

### 3.機能を修正追加する

前述の通り、「Drawdown（融資実行）する際に、Loanエンティティの永続化が漏れています」この処理を追加してもらいます。

今回の設計意図は、`DrawdownService#toEntity` から `LoanService#toEntity` を呼び出すというものですが、それは伝えずに、Copilot Chatに提案してもらいます！
（これでうまくいかない可能性は高いです。その際には、上記の設計意図を伝え誘導します。）

```markdown
#codebase 現状は、DrawdownService#toEntityを実行しても、Loanエンティティの永続化がされません。
実装がされていないのが原因です。Loanエンティティの永続化をするように、機能追加を行ってください。
```

Copilot Chatの提案内容（**動作確認レベルではOKそうでした！**）

1.まず DrawdownService に LoanService を追加
```java
@Service
@Transactional(readOnly = true) // readOnly = trueは不採用（阿部）
public class DrawdownService extends TransactionService<Drawdown, DrawdownDto, DrawdownRepository> {

    private final FacilityService facilityService;
    private final LoanService loanService; // 追加！

    public DrawdownService(
            DrawdownRepository repository,
            AmountPieService amountPieService,
            PositionService positionService,
            FacilityService facilityService,
            LoanService loanService, // 追加！
            InvestorService investorService) {
        super(repository, amountPieService, positionService, investorService);
        this.facilityService = facilityService;
        this.loanService = loanService; // 追加！
    }
```

2.toEntity メソッドを修正

```java
@Override
public Drawdown toEntity(DrawdownDto dto) {
    Drawdown entity = new Drawdown();
    entity.setId(dto.getId());
    entity.setType("DRAWDOWN");
    entity.setDate(dto.getDate());
    entity.setAmount(dto.getDrawdownAmount());

    // Facilityの設定
    Facility facility = facilityService.findById(dto.getRelatedFacilityId())
            .map(facilityService::toEntity)
            .orElseThrow(() -> new BusinessException("Facility not found", "FACILITY_NOT_FOUND"));
    entity.setRelatedFacility(facility);

    // Loanエンティティの作成と永続化 ※この辺がまるっと抜けていた処理！
    LoanDto loanDto = LoanDto.builder()
        .amount(dto.getDrawdownAmount())
        .totalAmount(dto.getDrawdownAmount())
        .borrowerId(facility.getBorrower().getId())
        .facilityId(facility.getId())
        .startDate(LocalDate.now())
        .endDate(facility.getEndDate())
        .interestRate(facility.getInterestRate())
        .build();

    // Loanを永続化して関連付け
    LoanDto savedLoan = loanService.create(loanDto);
    entity.setRelatedPosition(loanService.toEntity(savedLoan));

    // AmountPieの設定
    if (dto.getAmountPieId() != null) {
        AmountPie amountPie = amountPieService.findById(dto.getAmountPieId())
            .map(amountPieService::toEntity)
            .orElseThrow(() -> new BusinessException("AmountPie not found", "AMOUNT_PIE_NOT_FOUND"));
        entity.setAmountPie(amountPie);
    }

    entity.setDrawdownAmount(dto.getDrawdownAmount());
    entity.setVersion(dto.getVersion());
    return entity;
}
```



## まとめ

生成AIも万能ではないので、設計の意図は明確に伝える必要があります。

一方で、現状の把握については、codebase(workspace)の検索機能が追加されたことにより、うまく使うとかなりの精度が出ることが期待されます。

「**現状の把握＋意図を伝える**」

この精度を高めると、それなりの規模のシステムのメンテナンス作業にも使える可能性があると思います。

例えば、現状把握した内容をドキュメントに書き出して、それを後続の作業のプラニングの際に参照するようにする（ある種の長期記憶）といった使い方は有効だと思います。

例：

```markdown
調査の結果得られた知見をMarkdownの文書にまとめてください。 
その際に、ビジネスロジックを追加するとした場合には、
どのクラスを修正するべきか？というのを示してください。
```

## 脚注
[^1]: https://docs.github.com/ja/copilot
[^2]: インストール方法、エディタ上でのコード補完などは https://fintan-contents.github.io/gai-dev-guide/github-copilot 参照
[^3]: https://github.com/tis-abe-akira/ddd-breakthrough