MAS-233: GAS パフォーマンス診断ツール(実行時間計測 + ボトルネック特定)
概要
| 項目 | 内容 |
|---|---|
| 案件 ID | MAS-233 (旧 N-57 / N-057) |
| 案件名 | GAS パフォーマンス診断ツール(実行時間計測 + ボトルネック特定) |
| カテゴリ | DevOps・パフォーマンス・計測基盤 |
| Phase | P1 |
| 優先度 | ★★(MAS-231 最適化パス 1 の前提依存・並行実装可) |
| 所要時間 | 約 1.5-2 週間(週 10h 前提・Step 1-2 で計測基盤 + 主要関数埋込、Step 3-5 で集計・可視化・アラート) |
| 対象ファイル(追加) | 000_infra/004_utils.js(Utils.perfStart / Utils.perfEnd / Utils.perfFlush 追加・約 80 行)/ 800_ops/810_perf_aggregator.js(日次集計・約 150 行) |
| 対象ファイル(変更) | 400_domain/410_subledger_engine.js(Action A/B エントリ)/ 400_domain/407_rpa_orchestrator.js(RPA 起票)/ 600_report/601_datamart_ingest.js 〜 608_datamart_render.js(マート更新)/ 300_ui/301_ui_assist.js(サイドバー初期化)/ 500_import/502_receipt_reader.js(OCR 処理)/ 100_config/101_sys_config.js(DDL に LOG_PERF 追加 + メニュー登録) |
| 新規シート | 99_perf_log(ログ生シート・WORM・自動ローテーション 90 日)/ DDL 管理(LOG_PERF キー) |
新規 03_sys_params キー | MAS233_PERF_ENABLED(default true)/ MAS233_PERF_FLUSH_INTERVAL_SEC(default 60)/ MAS233_PERF_RETENTION_DAYS(default 90)/ MAS233_PERF_ALERT_P95_MS(default 10000) |
| 前提案件 | なし(既存 Utils / CacheService / 99_error_log / 98_audit_log の運用パターンを踏襲) |
| 後続連携 | MAS-231(GAS 最適化パス 1)— 本案件の Utils.perfStart / perfEnd API と 99_perf_log シートを利用して効果測定。MAS-233 完了後に MAS-231 着手、または並行実装で API シグネチャを spec で fix |
目的
「何が遅いのか」を数値で把握するための計測基盤を整備し、MAS-231 最適化パス 1 着手前のボトルネック特定と、最適化後の効果検証を可能にする。
- ラベル別の実行時間累積を
CacheServiceに保管し、関数の呼出回数・総経過時間・P50・P95 を99_perf_logに日次集計 - Action A/B、マート更新、RPA 起票、サイドバー初期化、OCR 処理の主要 5 系統に計測埋込(並列実装の対称性確保・failure_patterns #25)
- 週平均 P95 > 10 秒のホットスポット自動検知 → 上位 5 ボトルネックを MAS-231 で対策する 計測 → 改善 ループ を確立
SpreadsheetApp.flush()呼出箇所の所在地 grep +google.script.runラウンドトリップ件数の自動計測で MAS-231 の Step 3 / Step 6 を裏付けるエビデンスを取得
現在のコード
利用する既存 API(実在確認済・全て Read で裏取り)
| API | 定義場所 | シグネチャ | 用途 |
|---|---|---|---|
Utils.logInfo(funcName, message) | 000_infra/004_utils.js:494 | (string, string) → void | 計測ラベル開始/終了の info ログ(任意) |
Utils.logError(funcName, error, context) | 000_infra/004_utils.js:504 | (string, Error, string) → void | perfFlush 失敗時のエラーログ |
Utils.persistLog(level, funcName, message, detail) | 000_infra/004_utils.js:577 | (string, string, string, *) → void | アラート発火時の WARN 永続記録 |
Utils.auditLog(operation, ...) | 000_infra/004_utils.js:539 | — | DDL 適用 / メニュー実行を 98_audit_log に残す既存パターン参考 |
Constants.getParam(key, defaultVal) | 000_infra/002_constants.js:205 | (string, *) → string|number | MAS233_PERF_* 4 キー取得 |
Constants.CONFIG_SHEET | 000_infra/002_constants.js:7 | '01_sys_config' | キー登録先(参考) |
CacheService.getScriptCache() | GAS 標準 | — | perfStart / perfEnd の累積記録 |
LockService.getDocumentLock() | GAS 標準 | — | perfFlush 時の競合回避(並行実行ガード) |
Utils.getSheetByKey(key, fallback) | 000_infra/004_utils.js:302 | (string, string) → Sheet | 99_perf_log 取得 |
getWebSpreadsheet_() | 000_infra/004_utils.js:9 | () → Spreadsheet | Web App / コンテナバインド両対応 |
計測埋込対象のエントリポイント(実在確認済)
| 系統 | エントリ関数 | 定義場所 | ラベル例 |
|---|---|---|---|
| Action A (INV 起票) | processInvoiceApprovals() | 400_domain/410_subledger_engine.js:103 | actionA.processInvoiceApprovals |
| Action B (消込確定) | processSettlementClearings() | 400_domain/410_subledger_engine.js:361 | actionB.processSettlementClearings |
| RPA 起票 | RPAService.*(400_domain/407_rpa_orchestrator.js) | 同 | rpa.<service> |
| マート更新 | runDataMart 系(600_report/601_datamart_ingest.js 〜 608_datamart_render.js) | 同 | datamart.<step> |
| サイドバー初期化 | doGet(e)(100_config/101_sys_config.js:400)/ getInitialStateForSpa(300_ui 系) | 同 | sidebar.bootstrap |
| OCR 処理 | 502_receipt_reader.js のエントリ | 同 | ocr.process |
| DDL | setupAllSchemas(isFull)(100_config/101_sys_config.js:840) | 同 | ddl.setupAllSchemas |
既存 DDL 登録パターン(LOG_PERF 追加先)
100_config/101_sys_config.js:884 の LOG_AUDIT 登録パターン(existKeys.includes('LOG_AUDIT') ガード + confSheet.appendRow(...))と完全対称に LOG_PERF を追加する。schemas ハッシュ('LOG_PERF': { headers: [...], color: '#434343' })も同様に LOG_AUDIT(同 L1047)と並列に定義する。
既存 _paramsCache パターン(再利用可能)
Constants.getParam (000_infra/002_constants.js:205) は 起動時 1 回だけ 03_sys_params を読み込み、以降キャッシュ する設計。MAS233_PERF_* 4 キーは本パターンをそのまま利用できるため、追加実装は Constants.PERF_DEFAULTS のような新規定数のみ。
修正方針
Step 1: Utils.perfStart / Utils.perfEnd / Utils.perfFlush を 004_utils.js に追加
// 000_infra/004_utils.js (Utils 名前空間に追加)
Utils.perfStart = function (label) {
if (!_perfEnabled_()) return null;
return { label: label, t0: Date.now() };
};
Utils.perfEnd = function (token) {
if (!token) return;
var elapsed = Date.now() - token.t0;
_perfBuffer_().push({ label: token.label, ms: elapsed, ts: new Date() });
// バッファサイズ閾値超過 or 60 秒経過で flush
if (_shouldFlush_()) Utils.perfFlush();
};
Utils.perfFlush = function () {
// CacheService から累積バッファを読出 → 99_perf_log に append → CacheService クリア
// LockService.getDocumentLock() で並行 flush をシリアライズ
};
設計判断:
- 計測ラベルの粒度: 関数単位 (
actionA.processInvoiceApprovals) を基本とし、Step 1 では「処理ブロック単位」(例:actionA.fetchInvoices/actionA.buildJournals/actionA.writeJournals)の細粒度計装は対象外(v2 で検討)。粒度を絞ることで CacheService 100KB 制限と Cache 書込頻度のオーバーヘッドを最小化する。 - CacheService 書込頻度: バッファリング方式を採用。
perfEndの都度 cache.put すると 1 計測あたり 50-100ms のオーバーヘッドが付くため、Utils._perfBuffer_(in-memory array) に蓄積し、MAS233_PERF_FLUSH_INTERVAL_SEC経過 or バッファサイズ 200 件超でUtils.perfFlush()を呼出。毎回cache.putは実装しない(計測自体が遅いと無意味)。 - シリアライズ: バッファは
JSON.stringifyでcache.put。値が 100KB を超える場合は WARN ログ + 旧バッファを99_perf_logに直接 append してクリア(failure_patterns #29 の Infinity 検査も実施)。
Step 2: 主要関数への計測埋込(5 系統対称適用・failure_patterns #25 遵守)
Action A (410_subledger_engine.js:103 processInvoiceApprovals 先頭・末尾):
function processInvoiceApprovals() {
var perf = Utils.perfStart('actionA.processInvoiceApprovals');
try {
// 既存ロジック
} finally {
Utils.perfEnd(perf); // try/finally で必ず計測終了
}
}
Action B (410_subledger_engine.js:361 processSettlementClearings) — Action A と完全対称(failure_patterns #25 / 銀行・CC 並列実装の教訓)。
RPA 起票 (407_rpa_orchestrator.js RPAService.* 各エントリ) — rpa.<funcName> ラベルで一括計装。
マート更新 (600_report/60[1-8]_datamart_*.js 各エントリ) — datamart.<step> ラベル。
サイドバー初期化 (101_sys_config.js:400 doGet(e) 先頭 + 300_ui 内 getInitialStateForSpa 等の public エンドポイント) — sidebar.bootstrap / sidebar.<endpoint> ラベル。
OCR 処理 (502_receipt_reader.js エントリ + callDocumentAiInvoiceParser_ / callGeminiForReasoningOnVertex_ 内部呼出) — ocr.docai / ocr.gemini ラベルで API レイテンシも分離計測。
Step 3: 99_perf_log シート定義 + DDL 登録
// 100_config/101_sys_config.js schemas に追加
'LOG_PERF': {
headers: ['日時', 'ラベル', '処理件数', '所要時間(ms)', 'P50(ms)', 'P95(ms)', '関数名', '備考'],
color: '#434343'
},
existKeys.includes('LOG_PERF') ガード + confSheet.appendRow(['LOG_PERF', '', '99_perf_log', 'パフォーマンス計測ログ(操作追跡・90日ローテーション)']) を LOG_AUDIT 登録(L884)の直後に対称配置。
Step 4: 日次集計 (800_ops/810_perf_aggregator.js 新設)
function aggregatePerfLogDaily() {
// 99_perf_log から過去 1 日分を読込 → ラベル別に件数 / 総 ms / P50 / P95 を集計
// 集計結果を新しい行として 99_perf_log に追記 (種別='AGG_DAILY')
// 90 日超の生ログ行は別シート 99_perf_log_archive_YYYYMM に移送 (98_audit_log と同パターン)
}
時間トリガー: ScriptApp.newTrigger('aggregatePerfLogDaily').timeBased().atHour(2).everyDays(1).create() (02:00 JST 実行)。ローテーションは 98_audit_log の archiveAuditLogMonthly_ (101_sys_config.js:1749 付近) と同一パターン。
Step 5: 可視化 + ボトルネック検知
可視化: シート内スパークライン + 条件付き書式(赤: P95 > 5 秒 / 黄: P95 > 1 秒 / 緑: P95 < 1 秒)。Looker Studio 連携は v1 では実装せず、人間検討事項に残す(Looker Studio はスプレッドシート直結が GAS 経由不要 + ライセンス無料のため将来オプションとして容易に追加可能)。
ボトルネック検知ルール:
- (a) ラベル別週平均 P95 >
MAS233_PERF_ALERT_P95_MS(default 10000ms)→Utils.persistLog('WARN', 'aggregatePerfLogDaily', 'P95 alert: ' + label, ...)でアラート +Session.getEffectiveUser().getEmail()宛MailApp.sendEmail通知(オプション・スパム防止のため日次 1 回上限) - (b)
SpreadsheetApp.flush()呼出箇所の特定:scripts/grep-flush.sh(既存scripts/パターン踏襲)でgrep -nE "SpreadsheetApp\.flush\(\)" 400_domain/*.js 600_report/*.js 100_config/*.jsを CI 実行 + 件数を99_perf_logに静的記録(実行時計測ではなく静的解析) - (c)
google.script.runラウンドトリップ件数: クライアント側タイマーでwithSuccessHandler受信時刻を記録し、サーバー側perfEnd(token)の所要時間と差分を取れば「クライアント送信 → サーバー受信 → クライアント受信」の往復レイテンシを推定可能(v1 は GAS 側のみ実装し、クライアント側計測は SPA 化と並行検討)
Step 6: メニュー登録 + 動作確認
Constants.MENU_DEFINITION に「📊 パフォーマンス診断」カテゴリを新設し、aggregatePerfLogDaily / clearPerfLog(90 日超を強制クリア)/ openPerfLogSheet の 3 機能を登録。category: '🔧 開発・設定' 配下に配置 (既存 🔧 マイグレーション と並列・002_constants.js:310-370 付近のパターン踏襲)。
影響範囲
| 対象 | 種別 | 変更内容 | リスク |
|---|---|---|---|
000_infra/004_utils.js | 変更 | Utils.perfStart / perfEnd / perfFlush + 内部ヘルパー _perfEnabled_ / _perfBuffer_ / _shouldFlush_ を追加(約 80 行) | 既存 Utils.* への影響なし。token が null の場合は no-op(計測 OFF 時) |
100_config/101_sys_config.js | 変更 | schemas.LOG_PERF 追加 + existKeys.includes('LOG_PERF') ガード + メニュー登録(約 30 行) | DDL 整合性は setupAllSchemas 既存テストでカバー |
400_domain/410_subledger_engine.js | 変更 | processInvoiceApprovals / processSettlementClearings 各先頭・末尾に perfStart / perfEnd ラップ(4 行追加) | try/finally で必ず perfEnd 呼出。例外伝播は不変 |
400_domain/407_rpa_orchestrator.js | 変更 | RPAService.* 各エントリに同等ラップ(10-15 行追加) | 同上 |
600_report/601-608_datamart_*.js | 変更 | 各 runXxx エントリに同等ラップ(合計 16 行追加) | 同上 |
300_ui/301_ui_assist.js | 変更 | サイドバー初期化エントリに同等ラップ(4 行追加) | 同上 |
500_import/502_receipt_reader.js | 変更 | OCR エントリ + DocAI / Gemini API 呼出に同等ラップ(10 行追加) | 同上 |
800_ops/810_perf_aggregator.js | 追加 | 日次集計 + ローテーション(約 150 行) | 新規ファイル・既存ロジックに影響なし |
99_perf_log | 新規シート | DDL 管理 (LOG_PERF キー) | 既存シートに影響なし |
99_perf_log_archive_YYYYMM | 動的生成 | 90 日超ログの月次アーカイブ | 動的生成タブ(98_audit_log と同パターン) |
03_sys_params | 変更 | MAS233_PERF_* 4 キー追加 | 既存キーには影響なし |
appsscript.json | 変更なし | CacheService / LockService / MailApp / ScriptApp.newTrigger は既存スコープ内 | failure_patterns #26 遵守(部分宣言禁止) |
docs/_config.json | 変更 | nav 1 行追加(§E.1 基盤) | PR 競合に注意 |
注意事項
- #18-#20(命名造語禁止):
Utils.perfStart/Utils.perfEnd/Utils.perfFlushのみを定義し、それ以外の架空関数(Utils.measureExecution/PerfMonitor.start等)は spec も実装も禁止。Constants.PERF_*定数もConstants.getParam('MAS233_PERF_*')経由のみで参照(独立定数化しない)。 - #25(並列実装対称性): Action A / Action B は 完全対称的に計測埋込する(片方だけが try/finally でラップされる状態を禁止)。RPA 系・マート系も同パターンを徹底。
grep -nE "Utils.perfStart|Utils.perfEnd" 400_domain/ 600_report/で件数の偶数性(start と end が等数)を確認。 - #26(oauthScopes 部分宣言禁止):
appsscript.jsonを一切編集しない。CacheService/LockService/MailApp/ScriptApp.newTrigger/SpreadsheetAppは既存cloud-platform系スコープでカバー。 - #27(Admin SDK API 変動): 該当なし(本案件は GAS 標準 API のみ使用)。
- #28(末尾
_の private 化):aggregatePerfLogDaily/clearPerfLog/openPerfLogSheetはMENU_DEFINITION経由で呼ばれるため末尾_禁止。一方、内部ヘルパー_perfEnabled_/_perfBuffer_/_shouldFlush_/_perfRotateOldLogs_は同一ファイル内のみから呼ばれるため末尾_を必ず付ける。 - #29(V8→Java Infinity null): P50 / P95 計算で
Math.maxの空配列が-Infinityを返すため、Number.isFinite()でガード + 0 にフォールバック。99_perf_log書込前に必ず_scrubInfinityForJSON_相当の処理を通す(300_ui/302_spa_bridge.js 参照)。 - 計測自体のオーバーヘッド:
Utils.perfStart/perfEnd1 セットあたり <1ms を必須とする。バッファリング方式でcache.putを 60 秒に 1 回に集約するため、計測 ON でも実用上影響なし(回帰テスト:MAS233_PERF_ENABLED=false時と比較して < 5% 差)。 - CacheService クォータ枯渇: 単一値 100KB / 全体 100MB / 1 時間 1000 calls の制約あり。バッファサイズ閾値(200 件)+ 60 秒 flush で 1 時間あたり最大 60 calls に抑制。サイズ超過時は
WARNログ + 直接 append 経由の fallback で記録漏れゼロを保証。 99_perf_log保持期間: 90 日を default(MAS233_PERF_RETENTION_DAYS)。90 日超は月次アーカイブシート99_perf_log_archive_YYYYMMに移送(98_audit_logのarchiveAuditLogMonthly_パターン踏襲)。無期限保持は採用しない(Spreadsheet の 1000 万セル上限・編集時の重さ回避のため)。- Looker Studio 連携の要否: v1 では実装しない。Sheets 直結で接続できるため、必要時に Looker 側でビューを構築すれば GAS 側変更不要。可視化は v1 ではシート内スパークライン + 条件付き書式(赤/黄/緑)で十分。
- ユーザー操作計測(クライアント側タイマー): v1 では GAS 側のみ実装。サイドバー側
google.script.run.withSuccessHandler受信時刻記録は MAS-232 SPA 化と並行検討(Date.now()差分のシリアライズに注意・failure_patterns #29 と同パターンでNumber.isFiniteガード)。 - トリガー登録の冪等性:
aggregatePerfLogDailyの時間トリガーはScriptApp.getProjectTriggers()で既存確認 → 重複登録回避。MAS-201 バックアップトリガー(801_backup_tool.js等)の同パターン踏襲。 - アラート発火の頻度制御:
MAS233_PERF_ALERT_P95_MS超過のメール通知は 日次 1 回を上限(CacheServiceにperf_alert_last_sentキーで 24 時間ロック)。スパム化を回避。 - マイグレーション運用ガイドライン整合: 800 番台は CLAUDE.md「マイグレーションスクリプト運用ガイドライン」配下だが、
810_perf_aggregator.jsは計測基盤の常駐スクリプトであり、冪等性・ログ出力・メニュー登録の 3 ルールは遵守する(マイグレーション番号 810 は本案件で消費・次回マイグレーションは 811 から)。
エッジケース
実装時に必ず以下 12 件を 900_test/901_test_runner.js の単体テストでカバーする。
| # | 条件 | 検知方法 | 期待される挙動 | ログ出力 |
|---|---|---|---|---|
| 1 | MAS233_PERF_ENABLED=false 時の no-op | perfStart が null を返し、perfEnd(null) が即時 return | 計測完全無効化・既存ロジックへの影響ゼロ(回帰テスト < 5% 差) | — |
| 2 | perfStart から perfEnd の間で例外発生 | try/finally の finally 分岐で perfEnd が呼ばれる | 例外発生時も計測が記録される(catch して swallow しない) | finally で perfEnd |
| 3 | 同一ラベルの multiple start without end (ネスト誤用) | perfStart の戻り値 token が独立しているため、ネスト呼出は問題なし | 各 token 独立に t0 を持ち、perfEnd で正しく差分計測 | — |
| 4 | バッファサイズ 100KB 超過 | JSON.stringify(buffer).length >= 100000 | バッファをそのまま 99_perf_log に直接 append + Cache クリア + WARN ログ | Utils.persistLog('WARN', 'Utils.perfFlush', 'Buffer > 100KB direct flush', ...) |
| 5 | CacheService クォータ枯渇 | cache.put が Cache service quota exceeded 例外 | 例外捕捉 + バッファを 99_perf_log に直接 append + 当日のみ計測一時停止 | Utils.persistLog('WARN', 'Utils.perfFlush', 'Cache quota exceeded', ...) |
| 6 | 並行 flush(複数の onEdit / トリガーが同時実行) | LockService.getDocumentLock().tryLock(5000) | ロック取得失敗時は次回 flush に持ち越し(バッファに追記継続) | Utils.persistLog('INFO', 'Utils.perfFlush', 'Lock contention, retry next', ...) |
| 7 | 99_perf_log シート未作成 | DDL 未適用環境で Utils.getSheetByKey('LOG_PERF', '99_perf_log') が null | 自動作成(98_audit_log の auditLog 既存パターン踏襲・004_utils.js:546-554)+ INFO ログ | Utils.logInfo('Utils.perfFlush', 'auto-created 99_perf_log') |
| 8 | P50 / P95 計算で空配列 | samples.length === 0 | 0 を返す(Math.max(...[]) の -Infinity を回避・failure_patterns #29) | — |
| 9 | 90 日ローテーションで月跨ぎ | 集計実行時刻が月初の場合、前月分はアーカイブ済 | 99_perf_log_archive_YYYYMM を当月分とは別シートとして冪等作成 | Utils.auditLog('MIGRATE', '99_perf_log', '', '', 'aggregatePerfLogDaily', ...) |
| 10 | アラートのスパム化 | MAS233_PERF_ALERT_P95_MS 超過が連日継続 | CacheService の perf_alert_last_sent_<label> キーで 24 時間ロック | Utils.persistLog('WARN', 'aggregatePerfLogDaily', 'P95 alert: <label>', ...) |
| 11 | DDL 再適用時のラベル名衝突 | LOG_PERF キーが既に登録済 | existKeys.includes('LOG_PERF') ガードで重複追加回避(LOG_AUDIT 同パターン) | — |
| 12 | 環境(dev / prod)切替時のキャッシュ混在 | CacheService はスクリプト ID スコープのため環境間で混在しない | Env.name() をキーに含める(例: perf_buffer_dev / perf_buffer_prod) | 不要(実装で物理分離) |
実データ検証
実装完了後に以下を MCP / GAS エディタ + dev で実行し、合格基準と一致することを spec 完成の必須条件とする。
1. Utils.perfStart / perfEnd 1 セットのオーバーヘッド計測
| 計測指標 | 計測 OFF | 計測 ON | 合格基準 |
|---|---|---|---|
processInvoiceApprovals 100 件実行 | (基準値) | +5% 以内 | < 5% |
runDataMart 5 年分 | (基準値) | +5% 以内 | < 5% |
2. 99_perf_log 集計結果の妥当性
| 計測項目 | 期待値 |
|---|---|
| ラベル数 | 5 系統 × 各 3-5 関数 = 15-25 ラベル |
| 1 日あたりログ行数 | 100-500 行(通常運用) |
| P50 / P95 計算精度 | サンプル数 ≥ 10 で誤差 < 5%、< 10 は 備考 に "サンプル不足" と記録 |
3. アラート発火検証
| シナリオ | 期待される挙動 |
|---|---|
意図的に Utilities.sleep(15000) を processInvoiceApprovals に挿入 | 翌日 aggregatePerfLogDaily で MAS233_PERF_ALERT_P95_MS=10000 超過を検知 → WARN + メール送信 |
| 連日 sleep を維持 | 2 日目以降は perf_alert_last_sent_<label> で 24h ロックされメール送信されない |
4. ローテーション(90 日経過)動作
dev で MAS233_PERF_RETENTION_DAYS=1 に設定 → 翌日 aggregatePerfLogDaily 実行 → 99_perf_log_archive_YYYYMM に前日分が移送されることを確認。
5. 並行実行ロック検証
dev で同時に processInvoiceApprovals を 3 並列実行(手動・複数タブで RPAService.processApprovals() 連打)→ 全実行が 99_perf_log に記録され、1 つも欠落していないことを確認。
6. failure_patterns 既知パターン回帰
| パターン | 検証方法 |
|---|---|
| #25 並列対称性 | grep -nE "Utils\.perfStart" 400_domain/ 600_report/ の件数 = perfEnd の件数 |
#28 末尾 _ private | grep -nE "google\.script\.run\.\w+_" webapp_client/ で 0 件 |
| #29 Infinity 漏れ | P50 / P95 計算後の値が全て Number.isFinite() を通る |
関連ドキュメント
| 仕様書 / ファイル | 関連箇所・参照理由 |
|---|---|
MAS-231 GAS パフォーマンス最適化パス 1 | 本案件の 直接の後続。Utils.perfStart / perfEnd API + 99_perf_log を MAS-231 が利用して効果測定。MAS-233 計測 ↔ MAS-231 改善 のループ運用 |
MAS-179 監査証跡 (98_audit_log) | DDL 登録パターン (LOG_AUDIT) + シート保護 (applyAuditLogProtection_) + 月次アーカイブ (archiveAuditLogMonthly_) のテンプレート参照。99_perf_log も同パターンで実装 |
MAS-201 シートバックアップ | 800 番台のメンテナンススクリプト構成・トリガー登録冪等性パターン (ScriptApp.getProjectTriggers() 既存確認) の参考 |
MAS-178 エラーハンドリング (99_error_log) | Utils.persistLog の利用パターン参照。99_perf_log と 99_error_log の責務分離(前者は性能、後者は障害) |
| MAS-232 サイドバー SPA 化 | google.script.run ラウンドトリップ件数のクライアント側計測の協調点。本案件 v1 はサーバー側のみ・クライアント側は MAS-232 と並行検討 |
failure_patterns.md | #18-#20 (命名造語) / #25 (並列対称性) / #26 (oauthScopes) / #28 (末尾 _ private) / #29 (Infinity null) を直接参照 |
CLAUDE.md | 800 番台「マイグレーションスクリプト運用ガイドライン」+ Utils 名前空間ルール |
docs/_internal/dev_spec_prompt_template.md | 14 セクション必須・分割 Step 実行原則 |
docs/prd.md | Human-in-the-Loop ポリシー(アラート確認 → 人間対策の流れ) |
人間が検討すべき事項
- 計測ラベルの粒度: v1 は 関数単位 で実装。処理ブロック単位(例:
actionA.fetchInvoices/actionA.buildJournals/actionA.writeJournalsの 3 段階)への細粒度化は MAS-231 着手後の 2 週間データで上位 5 ボトルネックが特定できなかった場合の v2 で再検討。 - CacheService 書込頻度: バッファリング(60 秒に 1 回)が default。毎回書込は採用しない(オーバーヘッド > 計測価値)。flush 間隔の最適値(30 秒 vs 60 秒 vs 120 秒)は実運用 2 週間後にチューニング。
99_perf_log保持期間: 90 日で確定。無期限保持は採用しない(Sheets 1000 万セル制約 + 編集時の重さ)。長期トレンド分析が必要なら BigQuery エクスポート(MAS-237 GCP 移行と連動)を v2 で検討。- Looker Studio 連携: v1 では未実装。シート内スパークライン + 条件付き書式で十分。Looker Studio が必要になれば Sheets 直結(GAS 不要)で接続可能。
- ユーザー操作計測(クライアント側タイマー): v1 ではサーバー側のみ。
google.script.runラウンドトリップ計測は MAS-232 SPA 化と並行検討(クライアント側performance.now()で送信時刻 → サーバー側perfEndの差分でレイテンシ推定)。 - アラート閾値の妥当性:
MAS233_PERF_ALERT_P95_MS=10000(10 秒)が default。実運用 2 週間で false positive が多ければ調整。閾値ゼロでも 24h ロックでスパムは防止される。 - メール通知 vs Slack 通知: v1 はメール(
MailApp.sendEmail+Session.getEffectiveUser)。Slack Webhook は MAS-186 監視通知と統合検討。 - ボトルネック上位 5 → MAS-231 対策のループ運用: 2 週間データ蓄積 →
aggregatePerfLogDailyの P95 ranking で上位 5 を特定 → MAS-231 Step ごとに対策 → 再計測。運用フロー (docs/ops/perf_loop.md) のドキュメント化が必要か 検討。 SpreadsheetApp.flush()静的検出の自動化:scripts/grep-flush.shを CI(.github/workflows/ci.yml)に組込むか、scripts/pre-push-check.shに追加するか。MAS-224 GitHub Actions と連動。- Jr 採用後のハンズオン教材: 本計測ツールは「最適化が効いたかを数値で確認できる」ため Jr ハンズオン適合性が高い。
docs/ops/perf_handbook.mdへの組み入れ要否を検討(MAS-230 Jr 採用要件と連動)。 - テストランナー (
901_test_runner.js) への組込: 計測 ON/OFF の切替テスト(オーバーヘッド < 5% の自動回帰)を追加するか。CLAUDE.md「変更時の動作確認テスト」表に400_domain/410_subledger_engine.js変更時の必須テストとして追加候補。 - prod デプロイ時の計測 OFF 運用: v1 は prod でも default ON(
MAS233_PERF_ENABLED=true)。オーバーヘッド < 5% で実運用問題なし想定。問題があれば03_sys_paramsで OFF に即切替可能(コード変更不要)。 - 計測対象の
Constants.getParam('TAX_RATES')等の高頻度呼出関数: マスタ取得系は MAS-231 のキャッシュ層で最適化対象のため、本案件で計測埋込すれば Before/After の数値が直接エビデンスになる。優先計装候補。 - Phase 別の段階リリース: Step 1 (
Utils.perfStart/End) → Step 2 (Action A/B 埋込) → Step 3 (99_perf_logDDL) → Step 4 (集計) → Step 5 (可視化 + アラート) → Step 6 (メニュー) を 独立 PR 化 するか、まとめて 1 PR か。spec 推奨は段階 PR(rollback 容易性)。
実装プロンプト(Claude Code 用)
Claude Sonnet 4.6 推奨(Step 1 計測ヘルパー実装は Opus 4.7 推奨・名前空間設計判断あり)。
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-233「GAS パフォーマンス診断ツール(実行時間計測 + ボトルネック特定)」を実装してください。
## 実行前タスク
必ず以下を Read し、固有名詞を実在のものに揃えてから着手すること:
1. `000_infra/004_utils.js` — `Utils` 名前空間 (`logInfo` L494 / `logError` L504 / `persistLog` L577 / `auditLog` L539 / `getSheetByKey` L302) のシグネチャを確認
2. `000_infra/002_constants.js` — `Constants.getParam` (L205) の `_paramsCache` 機構と `MENU_DEFINITION` のカテゴリ命名規則 (L266-398)
3. `100_config/101_sys_config.js` — `LOG_AUDIT` の DDL 登録 (L884) + `schemas.LOG_AUDIT` 定義 (L1047) + `archiveAuditLogMonthly_` 月次ローテーション (L1749 付近)
4. `400_domain/410_subledger_engine.js` — `processInvoiceApprovals` (L103) / `processSettlementClearings` (L361) のエントリ
5. `400_domain/407_rpa_orchestrator.js` — `RPAService.*` のエントリ
6. `600_report/601_datamart_ingest.js` 〜 `608_datamart_render.js` — マート更新のエントリ
7. `300_ui/301_ui_assist.js` — サイドバー初期化エントリ
8. `500_import/502_receipt_reader.js` — OCR 処理エントリ
9. `docs/_internal/failure_patterns.md` — #18-#20 / #25 / #26 / #28 / #29 を本実装で必ず遵守
10. `CLAUDE.md` — 800 番台「マイグレーションスクリプト運用ガイドライン」 + `Utils` 名前空間ルール
## 修正対象ファイル
- `000_infra/004_utils.js`(`Utils.perfStart` / `perfEnd` / `perfFlush` を追記・約 80 行)
- `000_infra/002_constants.js`(`MENU_DEFINITION` に「📊 パフォーマンス診断」カテゴリ追加)
- `100_config/101_sys_config.js`(`schemas.LOG_PERF` 追加 + `existKeys.includes('LOG_PERF')` ガード)
- `400_domain/410_subledger_engine.js`(Action A/B 各エントリに `try/finally` で `perfStart` / `perfEnd` ラップ)
- `400_domain/407_rpa_orchestrator.js`(同上)
- `600_report/601_datamart_ingest.js` 〜 `608_datamart_render.js`(同上)
- `300_ui/301_ui_assist.js`(同上)
- `500_import/502_receipt_reader.js`(同上)
- `800_ops/810_perf_aggregator.js`(**新規**・約 150 行・`aggregatePerfLogDaily` / `clearPerfLog` / `openPerfLogSheet` / `_perfRotateOldLogs_`)
## 実装内容(Step 1-6)
### Step 1: `Utils.perfStart` / `perfEnd` / `perfFlush` 追加(004_utils.js)
- `_perfEnabled_()` ヘルパー: `Constants.getParam('MAS233_PERF_ENABLED', 'true')` → boolean
- `_perfBuffer_()` ヘルパー: スクリプトプロパティとは別の in-memory array (関数内 closure)
- `_shouldFlush_()` ヘルパー: バッファ件数 > 200 or 前回 flush から 60 秒経過
- `Utils.perfStart(label)`: 計測 OFF 時 null を返す。token は `{ label, t0: Date.now() }`
- `Utils.perfEnd(token)`: token が null なら no-op。`Date.now() - token.t0` をバッファに append
- `Utils.perfFlush()`: `LockService.getDocumentLock().tryLock(5000)` で並行 flush ガード → バッファを `99_perf_log` に append → CacheService クリア。シート未作成時は自動作成(`auditLog` の自動作成パターン踏襲)
### Step 2: 主要関数への計測埋込(5 系統対称適用)
各エントリ関数を以下の **try/finally パターン**でラップする。**Action A と Action B は完全対称的**に編集する(failure_patterns #25・片方だけ try/finally になる状態を禁止):
function processInvoiceApprovals() {
var perf = Utils.perfStart('actionA.processInvoiceApprovals');
try {
// 既存ロジック (変更なし)
} finally {
Utils.perfEnd(perf);
}
}
対象: `processInvoiceApprovals` / `processSettlementClearings` / `RPAService.*` / `runDataMart` 系 / `doGet(e)` / OCR エントリ / `setupAllSchemas`。
### Step 3: DDL に `LOG_PERF` 追加(101_sys_config.js)
- `schemas.LOG_PERF`: `{ headers: ['日時', 'ラベル', '処理件数', '所要時間(ms)', 'P50(ms)', 'P95(ms)', '関数名', '備考'], color: '#434343' }`(`LOG_AUDIT` と並列に配置)
- `existKeys.includes('LOG_PERF')` ガード + `confSheet.appendRow(['LOG_PERF', '', '99_perf_log', 'パフォーマンス計測ログ(操作追跡・90日ローテーション)'])`(`LOG_AUDIT` 直後に対称配置)
### Step 4: 日次集計(800_ops/810_perf_aggregator.js 新規)
- `aggregatePerfLogDaily()`: `99_perf_log` の生ログから当日分をラベル別に集計 → P50 / P95 / 件数 / 総 ms を新行として追記(種別='AGG_DAILY')
- `_perfRotateOldLogs_()`: `MAS233_PERF_RETENTION_DAYS=90` 超のログを `99_perf_log_archive_YYYYMM` に移送(`archiveAuditLogMonthly_` パターン踏襲)
- `clearPerfLog()`: 手動全削除メニュー(confirm ダイアログ必須)
- `openPerfLogSheet()`: `99_perf_log` をアクティブ化
- 時間トリガー登録: `ScriptApp.newTrigger('aggregatePerfLogDaily').timeBased().atHour(2).everyDays(1).create()`(既存トリガー有無を `ScriptApp.getProjectTriggers()` でチェック)
### Step 5: 可視化 + アラート
- `99_perf_log` に条件付き書式: 「赤: P95 > 5000ms / 黄: P95 > 1000ms / 緑: P95 < 1000ms」(`addConditionalFormatRule` で setupAllSchemas 内に登録)
- `MAS233_PERF_ALERT_P95_MS=10000` 超過時: `Utils.persistLog('WARN', ...)` + `MailApp.sendEmail` 通知(`CacheService` の `perf_alert_last_sent_<label>` キーで 24h ロック)
### Step 6: メニュー登録(002_constants.js)
`Constants.MENU_DEFINITION` に以下を追記:
{
category: '📋 サイドバー: 📊 パフォーマンス診断',
items: [
{ label: '📊 99_perf_log を開く', funcName: 'openPerfLogSheet', description: 'パフォーマンス計測ログシートをアクティブ化' },
{ label: '🔄 日次集計を実行', funcName: 'aggregatePerfLogDaily', description: 'P50/P95/件数を集計し 99_perf_log に追記' },
{ label: '🗑️ ログ全削除', funcName: 'clearPerfLog', description: '99_perf_log の全行を削除(要確認)' },
]
}
## 制約
- **`appsscript.json` を一切編集しない**(failure_patterns #26 oauthScopes 部分宣言禁止)
- **末尾 `_` の付与ルール**: `MENU_DEFINITION` 経由で呼ばれる `aggregatePerfLogDaily` / `clearPerfLog` / `openPerfLogSheet` は末尾 `_` 禁止。内部ヘルパー `_perfEnabled_` / `_perfBuffer_` / `_shouldFlush_` / `_perfRotateOldLogs_` は末尾 `_` 必須(failure_patterns #28)
- **新規 namespace 禁止**: `Utils.*` のみに追加(`PerfMonitor` / `Profiler` 等の新規 namespace を勝手に作らない)
- **`Constants.getParam` 経由でパラメータ取得**: `PropertiesService.getScriptProperties()` を直接呼ばない
- **計算結果の値域変更なし**: 既存ロジックの戻り値・I/O は不変(try/finally は例外伝播も保持)
- **回帰テスト < 5%**: 計測 ON/OFF の `processInvoiceApprovals` 100 件実行で実行時間差 < 5% を保証
## 動作確認
1. `npm run push:dev` で dev に push
2. `🔧 開発・設定 → 全シートのスキーマと UI を最新化(DDL)` を実行 → `99_perf_log` シートが新規作成され、ヘッダー 8 列 (`日時 / ラベル / 処理件数 / 所要時間(ms) / P50(ms) / P95(ms) / 関数名 / 備考`) が登録されることを確認
3. `📋 サイドバー: 📒 経理業務 → Action A 実行` → `99_perf_log` に `actionA.processInvoiceApprovals` 行が記録されることを確認
4. Action B 実行 → 対称的に `actionB.processSettlementClearings` 行が記録されることを確認(failure_patterns #25 対称性確認)
5. 60 秒後(または手動で `Utils.perfFlush()` 実行)→ バッファが `99_perf_log` に flush されることを確認
6. `🔄 日次集計を実行` → 当日分の集計行(種別='AGG_DAILY')が追加され、P50/P95 が計算されることを確認
7. `MAS233_PERF_ALERT_P95_MS=100` (低閾値) で実行 → メール通知発火を確認 / 翌実行で 24h ロックで再発火しないことを確認
8. `MAS233_PERF_ENABLED=false` に切替 → `Utils.perfStart` が null を返し、計測が完全停止することを確認
9. `grep -nE "Utils\\.perfStart" 400_domain/ 600_report/` の件数 = `perfEnd` の件数 を確認(並列対称性)
10. dev で 1 週間運用 → `99_perf_log_archive_YYYYMM` への移送(`MAS233_PERF_RETENTION_DAYS=1` で実機確認)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| Step 1 計測ヘルパー設計 | あり | `_perfBuffer_` の closure / Lock / Cache 書込頻度の選定 |
| Step 2 5 系統への対称埋込 | なし | パターン適用のみ・各エントリで try/finally コピー |
| Step 3 DDL 登録 | なし | `LOG_AUDIT` パターン踏襲のみ |
| Step 4 集計 + ローテーション | あり | P50/P95 計算・空配列ガード・月次アーカイブ |
| Step 5 可視化 + アラート | なし | `addConditionalFormatRule` パターン適用 |
| Step 6 メニュー登録 | なし | `MENU_DEFINITION` 追記のみ |
推奨実行モデル
| Phase | 推奨モデル | 根拠 |
|---|---|---|
| Step 1 計測ヘルパー実装 | Claude Opus 4.7 (1M context) | Utils 名前空間設計判断 + Lock / Cache / Buffer の整合性確保 |
| Step 2 主要関数への対称埋込 | Claude Sonnet 4.6 | パターン適用・5 系統 × 各 1-3 関数の機械的書換 |
| Step 3 DDL 登録 | Claude Haiku 4.5 | LOG_AUDIT パターンのコピー + 文字列置換のみ |
| Step 4 集計 + ローテーション | Claude Sonnet 4.6 | P50/P95 計算 + 月次アーカイブ・既存 archiveAuditLogMonthly_ パターン適用 |
| Step 5 可視化 + アラート | Claude Sonnet 4.6 | 条件付き書式 + メール通知の組合せ・スパムロック設計 |
| Step 6 メニュー登録 | Claude Haiku 4.5 | MENU_DEFINITION への追記のみ |
| 仕様書レビュー | Gemini 3 Pro Preview + Deep Think | 計測ツールの設計妥当性を第三者検証 |
変更履歴
| 日時 | バージョン | 変更内容 |
|---|---|---|
| 2026-04-30 | v0.1(仕様書初版) | 初版作成。MAS-233 (旧 N-57) の TODO_future.md 案件定義 + MAS-231 仕様書 (対案件) を統合し、Claude Opus 4.7 (1M context) で 14 セクション全網羅起草。主要設計判断: (a) Utils.perfStart / Utils.perfEnd / Utils.perfFlush を 004_utils.js に追加 (新規 namespace 禁止) / (b) ラベル粒度は関数単位 (v2 で処理ブロック単位検討) / (c) CacheService 書込はバッファリング(60 秒・200 件)/ (d) 保持期間 90 日 (98_audit_log の月次アーカイブパターン踏襲) / (e) Looker Studio v1 未連携 (Sheets 直結で十分) / (f) クライアント側計測は MAS-232 SPA 化と並行検討。failure_patterns 反映: #18-#20 (命名造語禁止・Utils.perfStart のみ実装) / #25 (Action A/B 完全対称的に try/finally ラップ) / #26 (oauthScopes 編集禁止) / #28 (末尾 _ private 規約) / #29 (P50/P95 で -Infinity 回避)。実装規模: 004_utils.js +80 行 / 810_perf_aggregator.js 新規 150 行 / 5 系統 × 各エントリに 4 行 try/finally / 工数 1.5-2 週間。実装着手可能状態 (MAS-231 と並行実装で OK・API シグネチャは spec で fix)。 |
仕様書作成プロンプト
本仕様書を生成する際に Claude Opus 4.7 に投入したプロンプト全文(再現性・監査性のため記録)
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者兼仕様書ライターです。
## タスク
案件 ID **MAS-233**「GAS パフォーマンス診断ツール(実行時間計測 + ボトルネック特定)」の開発仕様書を作成し、`/Users/ts_kuma/projects/my-gas-project-doc/docs/dev/dev_mas-233_perf_diagnostic.md` に保存してください。
## 案件概要 (todo_master_tables.md)
- **Phase**: P1 / **★**: ★★ / **Layer**: 💾 Data
- **目的**: 「何が遅いのか」を数値で把握するための計測基盤。MAS-231 最適化パス 1 の着手前に必須
- **実装スコープ**:
- **計測ヘルパー関数**: `Utils.perfStart(label)` / `Utils.perfEnd(label)` を `004_utils.js` に追加。ラベル別に `console.time` + `CacheService` に累積記録 (1 実行の呼出回数と総経過時間を保存)
- **主要関数への埋込**: Action A/B、マート更新、RPA 起票、サイドバー初期化、OCR 処理の各エントリに自動挿入
- **レポートシート**: `99_perf_log` を DDL に追加し、`関数名 / 実行日時 / 処理件数 / 所要時間(ms) / P50 / P95` を日次で集計
- **可視化**: Looker Studio or シート内スパークライン / 条件付き書式 「赤:>5秒 / 黄:>1秒 / 緑:<1秒」
- **ボトルネック検知ルール**: (a) 同一関数が週平均 P95 > 10 秒なら自動アラート、(b) `SpreadsheetApp.flush()` 呼出箇所の特定、(c) `google.script.run` ラウンドトリップ件数計測
- **着手順序**: 計測埋込 → 2 週間データ蓄積 → 上位 5 ボトルネック特定 → MAS-231 で対策実装のループ
- **MAS-231 (パフォーマンス最適化) との関係**: MAS-233 が計測 → MAS-231 が対策のループ運用
## 検討事項
1. 計測ラベルの粒度 (関数単位 / 処理ブロック単位)
2. CacheService 書込頻度 (毎回 vs バッファリング)
3. 99_perf_log 保持期間 (90 日 / 無期限)
4. Looker Studio 連携の要否
5. ユーザー操作計測 (サイドバーボタン押下〜応答) の捕捉方法 (クライアント側タイマー埋込)
## 必読ファイル (調査して固有名詞を確定)
実装の前に以下のファイルを `Read` で参照し、関数名・列名・定数名を確実に存在するものに揃えてください:
1. `/Users/ts_kuma/projects/my-gas-project-doc/docs/_internal/dev_spec_prompt_template.md` — 仕様書 14 セクションテンプレート
2. `/Users/ts_kuma/projects/my-gas-project-doc/docs/_internal/failure_patterns.md` — #18-#31 (失敗パターン)
3. `/Users/ts_kuma/projects/my-gas-project-doc/000_infra/004_utils.js` — `Utils` 名前空間の現行関数群 (logInfo / logError 等の周辺ロジック)
4. `/Users/ts_kuma/projects/my-gas-project-doc/000_infra/002_constants.js` — `Constants` の `_paramsCache` 機構と CacheService 利用パターン
5. `/Users/ts_kuma/projects/my-gas-project-doc/100_config/101_sys_config.js` — DDL 定義パターン (新シート `99_perf_log` 追加用の参考)
6. `/Users/ts_kuma/projects/my-gas-project-doc/400_domain/410_subledger_engine.js` — Action A/B のエントリポイント特定
7. `/Users/ts_kuma/projects/my-gas-project-doc/CLAUDE.md` — コーディング規約・名前空間ルール
8. 参考: `/Users/ts_kuma/projects/my-gas-project-doc/docs/dev/dev_mas-231_gas_perf_optimization.md` — 対の仕様書 (同フォーマット・MAS-231 と相互参照)
## 仕様書フォーマット要件 (14 セクション)
`dev_spec_prompt_template.md` 準拠。以下を必ず含める:
- フロントマター (id: MAS-233, aliases: ["N-58", "N-58a"] が候補 — id_mapping_table.csv で確認、type: Task, status: Draft)
- 概要テーブル (案件 ID / カテゴリ / Phase / 優先度 / 所要時間 / 前提案件 / 後続案件 [MAS-231])
- 目的 / 修正方針 / 実装スコープ / Step 分割
- failure_patterns 参照
- #18-#20 命名造語: `Utils.perfStart` / `Utils.perfEnd` 以外の架空関数を作らない
- #25 並列実装対称性: 計測埋込は Action A と Action B 両方に対称的に追加
- #26 oauthScopes: 部分宣言禁止
- エッジケース表 (10 件以上)
- 人間が検討すべき事項 (10 件以上)
- 推奨実行モデル (Step ごとに Haiku/Sonnet/Opus)
- 関連ドキュメント (MAS-231 / MAS-179 監査証跡 / MAS-201 バックアップとのクロスリンク)
- 開発プロンプト (Claude Code 向け実装指示)
- 変更履歴
- 末尾に `<details>` プロンプト全文記録
## 必ず守る規約
- 名前空間: `Utils` (既存) への関数追加。新規 namespace を勝手に作らない
- DDL シート名: `99_perf_log` (DDL 管理外も検討 — 動的生成タブとして扱う方が安全か検討)
- CacheService 利用: `CacheService.getScriptCache()` パターンを既存コード (`Constants.getParam` 等) から踏襲
- failure_patterns #18-#20 — 推測で関数名を作らず Read で実在確認
## アウトプット
ファイル: `docs/dev/dev_mas-233_perf_diagnostic.md`
Step 分割で順次 `Write` → `Edit`:
1. Step 2-1: 骨格 Write (フロントマター + 14 H2 見出し)
2. Step 2-2: 概要〜注意事項 Edit
3. Step 2-3a: エッジケース〜人間検討事項 Edit
4. Step 2-3b: 開発プロンプト〜変更履歴 Edit
5. Step 2-4: `<details>` プロンプト全文記録 Edit
最終確認として `wc -l` で行数を測定し報告してください (期待: 350-500 行)。
起票時の補足:
aliases は
id_mapping_table.csvで実在確認した結果["N-057", "N-57"]を採用(プロンプト原文のN-58は誤り。N-58 は MAS-234「セキュリティ基準準拠ロードマップ」として既使用)。DDL シート
99_perf_logは DDL 管理対象(LOG_PERFキー)として実装。98_audit_logの運用パターン(保護なし・月次アーカイブあり)を踏襲。動的生成タブ案は採用せず(MAS-179 監査ログとの責務分離・整合性の観点から DDL 管理を選択)。関連コマンド:
# Gemini Deep Think で代替案再生成する場合 TARGET_ID=MAS-233 node scripts/gen_gemini_alt_oneshot.js # Gemini レビュー実行 SPEC_FILES=docs/dev/dev_mas-233_perf_diagnostic.md node scripts/4_review_specs_by_gemini.js