3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【完結?】AI エージェントに GAS 開発を任せたら、エミュレータを最強にして自律開発させてみた話

3
Posted at

Gemini_Generated_Image_rxri7orxri7orxri.png

TL;DR

GASエミュレータを強化し、AIが「コード生成 → テスト → 自己修正 → デプロイ」までを自律的に完結できる開発環境を構築した。

  • 静的解析 2.0: 複雑なメソッドチェインのパースと引数バリデーションを実現。
  • サイレント破壊の防止: GAS 特有の致命的な仕様「関数重複定義」を未然に検知。
  • 自律開発ループ: 正確な行番号マッピングにより、AI による「ピンポイント自己修正」が可能に。

序章:デプロイ成功=動作保証ではない

以前の記事(第1回, 第2回)では、GAS 開発を MCP サーバー化し、ローカルで動かす基盤を作りました。

しかし、運用を続ける中で「ある恐怖」に直面します。

問題:GAS の「サイレント破壊」

GAS には 「同じ名前の関数が複数ある場合、エラーにならずに後に定義されたものが優先される」 という仕様があります。

大規模なリファクタリングで AI が古い関数を消し忘れたとしても、デプロイは何事もなかったかのように成功します。そして本番環境では、古いロジックが意図せず実行され、データベース(スプレッドシート)を静かに破壊し続けます。

これは人間でも普通にやらかす事故です。

解決:究極の安全装置

この「デプロイして祈る」状況を脱却するために、エミュレータを「AI が自律的にミスを修正できるレベル」まで引き上げました。


1. 静的解析 2.0:メソッドチェインと引数の不一致を許さない

正規表現に頼らずコードの構造をパースし、実行前に致命的なミスを指摘する。

従来の解析では、.getScriptProperties().setProperty() のような複雑な呼び出しはスルーされていました。

これを見逃すとどうなるか?

一見、些細なことに思えるかもしれませんが、実運用では致命的です。
例えば、.setProperty("KEY") と第2引数を忘れたコードは、GAS 実行時にエラーで停止します。

AI がこのミスをした場合、ローカルでのチェックが甘いとそのままデプロイされ、本番環境で「データの書き込み失敗」という形で発覚します。さらに悪いことに、AI は「なぜ動かないのか」というコンテキスト(正確なエラー箇所)を自力で特定できず、人間が介入してデバッグする手間が発生してしまいます。

なぜこれが改善可能になったのか。それは、正規表現による文字列マッチングを卒業し、**「簡易的な字句解析とスタックベースのパース」**を導入したからです。

  1. 行内の文字列リテラルを一時的に除去(ノイズ削減)
  2. .(ドット)や ((括弧)の繋がりをトークンとして追跡
  3. PropertiesService から始まり、.setProperty() で終わる一連の呼び出し」を一つの「実行単位」として特定

バリデーションの根拠:GAS API 仕様の注入

「どのメソッドが何個の引数を取るべきか」というルールは、エミュレータ内部に GAS の公式リファレンスに基づいたシグネチャ情報 として定義されています。

現在は主要な PropertiesServiceSpreadsheetApp のメソッドから順次実装しており、未知のメソッドチェインに対しても、定義済みのルールセットと照らし合わせることで「実行前の不整合検知」を可能にしています。

独自のスキャナーを実装したことで、引数の数まで厳格にチェック可能になりました。

解析プロセスの可視化

エミュレータがどのように「実行前」にミスを見抜くのか、その内部プロセスを図解すると以下のようになります。

// 強化されたパースロジック(簡略化版)
// 1. 正規表現で「メソッドチェインを含む呼び出し」を抽出
const chainMatch = line.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\s*\(/);

if (chainMatch) {
  const funcName = chainMatch[1]; // 例: "PropertiesService.getScriptProperties().setProperty"
  const args = extractArgs(line, chainMatch.index); // 括弧の中身を抽出

  // 2. 特定のメソッドに対して引数の数をチェック
  if (funcName.endsWith(".setProperty") && args.split(",").length < 2) {
    throw new Error(`${funcName}: 引数が不足しています(期待値: 2)`);
  }
}

ロジック解説:何をやっているのか?

このバリデーションは、大きく以下の3ステップで行われています。

  1. 関数名の特定: 正規表現を用いて、.setProperty など「ドットで繋がれた関数名」を抽出します。
  2. 引数の切り出し: 呼び出しの括弧 () 内の文字列を抽出します。
  3. 数のカウント: 括弧内の文字列をカンマ , で分割し、その要素数を確認します。

これにより、「setProperty という名前なのに引数が1つしかない」といった API 仕様との不一致 を、実際の実行を待たずに見抜くことができます。

「実行してエラーが出る」のではなく、「実行前の検証フェーズでダメだとわかる」環境を作りました。

2. 「巨大ファイル」からの解放:複数ファイル開発の実用化

「管理のために分けたい」という理想と、「行番号がズレる」というデバッグの現実。そのジレンマを解消する。

GAS 開発者の究極の悩み:分けるか、耐えるか

GAS プロジェクトが成長するにつれ、開発者は必ずこの二択に直面します。

  • 理想: api.gs, utils.gs, main.gs とファイルを分け、見通しを良くしたい。AI にも必要な箇所だけを渡して正確に指示を出したい。
  • 現実: ファイルを分けると、エラー発生時の「通し行番号」がどのファイルのどこを指しているか分からなくなる。

結局、デバッグの手間を嫌って「1ファイルに数千行詰め込んで耐える」という不自由な選択を強いられてきたのが、これまでの GAS 開発の隠れた苦悩でした。

今回のエミュレータ強化では、この 「分割開発のハードル」 を技術で粉砕しました。

実現方法:結合と同時に「住所録」を作る

GAS 本体の仕様上、全てのファイルは同じグローバルスコープを共有します。この挙動をローカルで再現するため、エミュレータは実行前にファイルを結合しますが、その際に 「どのファイルが何行目から何行目までを占めているか」のインデックス(fileMap) を動的に生成します。

// 1. 結合しながら「ファイルごとの行数」を記録する
let currentLine = 1;
const fileMap = [];

for (const file of files) {
  const lineCount = file.content.split('\n').length;
  // このループで「どのファイルがどこにあるか」のデータ構造が組み上がる
  fileMap.push({
    name: file.name,
    startLine: currentLine,
    endLine: currentLine + lineCount - 1
  });
  combinedCode += file.content + '\n';
  currentLine += lineCount;
}

// 2. 生成されるデータ構造(インデックス)のイメージ
// これがあるから、結合後の「120行目」が「api.gsの20行目」だと特定できる
/*
[
  { name: "main.gs", startLine: 1,   endLine: 100 },
  { name: "api.gs",  startLine: 101, endLine: 200 }
]
*/

このマップを参照することで、実行エンジンが吐き出す「通し番号の行数(例:75行目)」を、瞬時に「個別のファイル名と行数(例:api.gsの25行目)」に翻訳して報告可能になりました。

開発体験の劇的な変化

状態 報告されるエラー例 開発者の体験
従来 Error at combined.js:152 「152行目ってどこ?」と全ファイルを探索
現在 Error at api.gs:42 AIが即座に api.gs の該当行へジャンプし修正

3. AI 自律修正ループ:人間を介さない品質担保

エミュレータが AI の「正確な目」となり、自己修正のサイクルを高速に回す。

もし「自律修正」がなかったら?

これがない環境では、開発体験は一気に「苦行」になります。

  1. AI がコードを出力
  2. 人間が手動でデプロイ(または CI を待つ)
  3. エラーが発生し、人間がログを確認
  4. 人間が AI に「ここが間違っているよ」とチャットで書き戻す
  5. AI が修正し、また手順 2 に戻る...

この「人間を介したフィードバック」の往復が最大のボトルネックです。AI が自らエラーを受け取り、1秒以内に修正を開始できる環境こそが、爆速開発の鍵となります。

「AI が書いて、エミュレータが検証し、AI が直す」。このループの具体例です。

実際に AI が受け取るフィードバックと修正のログイメージをご覧ください。

[Emulator] Analyzing static code...
[Emulator] FAIL: api.gs:12
[Emulator] Reason: "PropertiesService.getScriptProperties().setProperty" requires 2 arguments, but found 1.

[AI Agent]
指摘ありがとうございます。api.gs の 12 行目でキーのみを指定し、値を渡し忘れていました。
修正版を保存して再テストします。

[Emulator] Analyzing static code... PASS
[Emulator] Running main()... PASS
[System] All checks passed. Executing gas_push...
  1. AI: main.gs に新しいロジックを追加。凡ミスで引数を忘れる。
  2. 検証: エミュレータが [FAIL] main.gs:12行目 を正確に出力。
  3. 修正: AI がログを読み、「引数漏れですね」と即座に理解。
  4. 完結: 全テストパス後、自動でデプロイ。

何がこれを可能にしているのか?:MCP という「神経系」

この自律修正を支えているのは、単なるプロンプトの工夫ではありません。「MCP (Model Context Protocol)」という基盤 が、AI の「思考」とエミュレータという「道具」を、人間を介さずダイレクトに繋いでいるからです。

  1. 正確な座標(脳への入力): 行番号マッピングにより、AI は「どのファイルのどこ」を直すべきかという 濁りのないコンテキスト を受け取ります。
  2. MCP による即時実行(手足の動き): AI は gas_emulate というツールを「思考のプロセスの一部」として呼び出せます。MCP がこの呼び出しを仲介し、実行結果を 「次のターンの入力」 として即座に AI へ返します。
  3. 副作用の可視化(フィードバック): 単なる成否だけでなく、スプレッドシートの状態(MockState)が構造化データとして返るため、AI は「期待通りにデータが変わったか」を論理的に判断できます。

この「入力(座標)→ 実行(MCP)→ 評価(副作用)」のサイクルが 1 回の推論の中で完結するため、AI は迷うことなく自己修正を繰り返せるのです。

実際に AI が受け取るフィードバックと修正のログイメージをご覧ください。

結論:AI 開発の本質は「検証環境」である

今回の開発で確信したのは、AI 開発の本質は「モデルの性能」ではなく「検証環境の質」にある ということです。

AI エージェントをただのコード生成ツールとして使う時代は終わりました。
厳格な検証環境(エミュレータ)を与え、自律的な修正ループを回させること。

それこそが、AI との共創において最も重要で、最も面白い設計思想なのです。


我々(allyey & Antigravity)の冒険はまだまだ続きます。みなさんに応援いただければ幸いです。他テーマの記事もよろしくお願いします

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?