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 は「なぜ動かないのか」というコンテキスト(正確なエラー箇所)を自力で特定できず、人間が介入してデバッグする手間が発生してしまいます。
なぜこれが改善可能になったのか。それは、正規表現による文字列マッチングを卒業し、**「簡易的な字句解析とスタックベースのパース」**を導入したからです。
- 行内の文字列リテラルを一時的に除去(ノイズ削減)
-
.(ドット)や((括弧)の繋がりをトークンとして追跡 - 「
PropertiesServiceから始まり、.setProperty()で終わる一連の呼び出し」を一つの「実行単位」として特定
バリデーションの根拠:GAS API 仕様の注入
「どのメソッドが何個の引数を取るべきか」というルールは、エミュレータ内部に GAS の公式リファレンスに基づいたシグネチャ情報 として定義されています。
現在は主要な PropertiesService や SpreadsheetApp のメソッドから順次実装しており、未知のメソッドチェインに対しても、定義済みのルールセットと照らし合わせることで「実行前の不整合検知」を可能にしています。
独自のスキャナーを実装したことで、引数の数まで厳格にチェック可能になりました。
解析プロセスの可視化
エミュレータがどのように「実行前」にミスを見抜くのか、その内部プロセスを図解すると以下のようになります。
// 強化されたパースロジック(簡略化版)
// 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ステップで行われています。
-
関数名の特定: 正規表現を用いて、
.setPropertyなど「ドットで繋がれた関数名」を抽出します。 -
引数の切り出し: 呼び出しの括弧
()内の文字列を抽出します。 -
数のカウント: 括弧内の文字列をカンマ
,で分割し、その要素数を確認します。
これにより、「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 の「正確な目」となり、自己修正のサイクルを高速に回す。
もし「自律修正」がなかったら?
これがない環境では、開発体験は一気に「苦行」になります。
- AI がコードを出力
- 人間が手動でデプロイ(または CI を待つ)
- エラーが発生し、人間がログを確認
- 人間が AI に「ここが間違っているよ」とチャットで書き戻す
- 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...
-
AI:
main.gsに新しいロジックを追加。凡ミスで引数を忘れる。 -
検証: エミュレータが
[FAIL] main.gs:12行目を正確に出力。 - 修正: AI がログを読み、「引数漏れですね」と即座に理解。
- 完結: 全テストパス後、自動でデプロイ。
何がこれを可能にしているのか?:MCP という「神経系」
この自律修正を支えているのは、単なるプロンプトの工夫ではありません。「MCP (Model Context Protocol)」という基盤 が、AI の「思考」とエミュレータという「道具」を、人間を介さずダイレクトに繋いでいるからです。
- 正確な座標(脳への入力): 行番号マッピングにより、AI は「どのファイルのどこ」を直すべきかという 濁りのないコンテキスト を受け取ります。
-
MCP による即時実行(手足の動き): AI は
gas_emulateというツールを「思考のプロセスの一部」として呼び出せます。MCP がこの呼び出しを仲介し、実行結果を 「次のターンの入力」 として即座に AI へ返します。 -
副作用の可視化(フィードバック): 単なる成否だけでなく、スプレッドシートの状態(
MockState)が構造化データとして返るため、AI は「期待通りにデータが変わったか」を論理的に判断できます。
この「入力(座標)→ 実行(MCP)→ 評価(副作用)」のサイクルが 1 回の推論の中で完結するため、AI は迷うことなく自己修正を繰り返せるのです。
実際に AI が受け取るフィードバックと修正のログイメージをご覧ください。
結論:AI 開発の本質は「検証環境」である
今回の開発で確信したのは、AI 開発の本質は「モデルの性能」ではなく「検証環境の質」にある ということです。
AI エージェントをただのコード生成ツールとして使う時代は終わりました。
厳格な検証環境(エミュレータ)を与え、自律的な修正ループを回させること。
それこそが、AI との共創において最も重要で、最も面白い設計思想なのです。
我々(allyey & Antigravity)の冒険はまだまだ続きます。みなさんに応援いただければ幸いです。他テーマの記事もよろしくお願いします
