MAS-214: 自動生成型メニューカタログタブ (00_menu)
概要
| 項目 | 値 |
|---|---|
| 案件ID | MAS-214 |
| カテゴリ | 基盤 / DevOps |
| 優先度 | P1 ★★ |
| ステータス | 仕様書完了 |
| 対象ファイル(主) | 000_infra/002_constants.js(Constants 末尾 L196〜L324 に MENU_DEFINITION 追加)、100_config/101_sys_config.js(onOpen L323〜L350 リファクタリング / setupAllSchemas L749〜 内での CAT_MENU 登録 L823・DDL 定義 L900 / readAuditLog_ L1636〜L1645 新設 / updateMenuCatalog_ L1651〜L1743 新設 / 「💾 バックアップ」メニュー末尾への「メニューカタログを更新」項目追加 L225) |
| 新規シート | 00_menu(メニューカタログ、システムキー CAT_MENU) |
| 仕様書作成日 | 2026-04-20 |
目的
現在のスプレッドシートには 🚀 BizLP メニューと 💾 バックアップ メニューが存在するが、サイドバー(openOperationsSidebar)に機能が集約されており、「どの GAS 関数がどのメニューからどのように呼び出されるか」を一覧で把握できるドキュメントが存在しない。TODO_future.md MAS-214 の趣旨に従い、本案件では次の 2 点を達成する。
Constants.MENU_DEFINITIONの新設(Single Source of Truth):000_infra/002_constants.jsに全メニュー項目の構造定義(カテゴリ・ラベル・関数名・概要)を宣言的に集約する。onOpen()はこの定義をループして動的にメニューを生成するよう差し替え、メニュー管理の単一ソースを確立する。00_menuカタログタブの自動生成(利用状況の可視化): GAS 関数名をキーに98_audit_logの実行履歴(直近 90 日の実行回数・全期間実行回数・最終実行日時)を集計し、スプレッドシートの00_menuタブへ書き出す。運用担当者が各機能の利用状況をひと目で把握でき、未使用機能の検出(将来的な廃止判断)や登録漏れ(MENU_DEFINITION 定義済だが一度も呼ばれていない funcName)の検知ができる運用ダッシュボードを提供する。
現在のコード
100_config/101_sys_config.js 旧 onOpen() 実装(リファクタリング前のイメージ)
function onOpen() {
const ui = SpreadsheetApp.getUi();
// 全操作は右側サイドバーに集約 (狭い画面での top-level メニュー切り詰めを回避)
ui.createMenu('🚀 BizLP')
.addItem('操作パネルを開く', 'openOperationsSidebar')
.addSeparator()
.addItem('✅ 自動起動を有効化', 'installAutoOpenSidebarTrigger')
.addItem('🚫 自動起動を無効化', 'uninstallAutoOpenSidebarTrigger')
.addToUi();
// N-25: バックアップメニュー
try {
ui.createMenu('💾 バックアップ')
.addItem('📦 手動バックアップ実行', 'runManualBackup')
.addItem('⏰ 自動バックアップを有効化 (週次・検証)', 'installBackupTriggers')
.addItem('🔍 最新バックアップを検証', 'verifyLatestBackup')
.addItem('🛑 自動バックアップを停止', 'uninstallBackupTriggers')
.addToUi();
} catch (e) {}
}
メニュー項目がハードコードされており、新機能追加のたびに onOpen() を直接編集する必要がある。メニュー項目の一覧が関数外に定義されていないため、00_menu カタログ生成のソースとして使えず、また MAS-217(サイドバー項目の統合)や将来の「未使用機能検出」の前提となる SSoT 化が未達。
000_infra/002_constants.js 現状末尾プロパティ
Constants は CC_MERCHANT_MAP までで終わっており、MENU_DEFINITION は未定義。末尾(閉じ }; 直前)に新規プロパティとして追加する。
100_config/101_sys_config.js setupAllSchemas() 内のシステムキー登録(現状)
// L788 付近:Constants.CONFIG_SHEET (= '01_sys_config') にシステムキーを appendRow で登録
if (!existKeys.includes('LOG_AUDIT')) confSheet.appendRow(['LOG_AUDIT', '', '98_audit_log', '監査証跡ログ(操作追跡・WORM)']);
CAT_MENU(00_menu)は未登録。同パターンで追加する。
000_infra/004_utils.js Utils.auditLog() の書き込み列順(L459 付近)
sheet.appendRow([new Date(), user, operation, targetSheet || '', targetId || '', targetCol || '', funcName || '', before, after, note || '']);
// [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
funcName は 0 始まり index 6。00_menu の集計キーはこの index から読み取る。
修正方針
本案件は 5 ステップで構成する。ステップ間で破壊的な互換破壊が発生しないよう、順序は Step 1 → Step 2 → Step 3 → Step 4 → Step 5 を推奨。
Step 1 — Constants.MENU_DEFINITION の追加(000_infra/002_constants.js)
Constants オブジェクト末尾(CC_MERCHANT_MAP 定義の後、閉じ }; の直前 L196)に、全メニュー項目を宣言的に定義する MENU_DEFINITION プロパティを追加する。
構造:
MENU_DEFINITION: [
{
category: '🚀 BizLP', // トップレベルメニュー名(ui.createMenu の引数)
items: [
{ label: '操作パネルを開く', funcName: 'openOperationsSidebar',
description: '全操作を集約したメインサイドバーを表示' },
{ separator: true }, // 区切り線
{ label: '✅ 自動起動を有効化', funcName: 'installAutoOpenSidebarTrigger',
description: 'スプレッドシートを開いたときにサイドバーを自動表示するトリガーを設置' },
{ label: '🚫 自動起動を無効化', funcName: 'uninstallAutoOpenSidebarTrigger',
description: 'サイドバー自動表示トリガーを削除' },
]
},
{
category: '💾 バックアップ',
privileged: true, // N-29: 特権ユーザーのみに表示(オプション)
items: [
{ label: '📦 手動バックアップ実行', funcName: 'runManualBackup', description: '...' },
{ label: '⏰ 自動バックアップを有効化 (週次・検証)', funcName: 'installBackupTriggers', description: '...' },
{ label: '🔍 最新バックアップを検証', funcName: 'verifyLatestBackup', description: '...' },
{ label: '🛑 自動バックアップを停止', funcName: 'uninstallBackupTriggers', description: '...' },
{ separator: true },
{ label: 'メニューカタログを更新', funcName: 'updateMenuCatalog_',
description: 'MENU_DEFINITION と 98_audit_log を突合して 00_menu タブを更新' },
]
},
// ... 将来の N-41 でサイドバー 9 セクション 41+ 項目を同形式で追加
],
各 item 型の定義:
| 形 | 意味 | 例 |
|---|---|---|
{ label, funcName, description } | 通常メニュー項目 | { label: '操作パネルを開く', funcName: 'openOperationsSidebar', description: '...' } |
{ separator: true } | 区切り線 | { separator: true } |
{ label, items: [...] } | サブメニュー(2 階層) | { label: 'サブカテゴリ', items: [...] } |
ラベル・関数名の逐語性(最重要): ラベル文字列・関数名は既存 onOpen() の .addItem() 第 2 引数を Read で逐語引用する。runManualBackup / installBackupTriggers / verifyLatestBackup / uninstallBackupTriggers / openOperationsSidebar / installAutoOpenSidebarTrigger / uninstallAutoOpenSidebarTrigger はいずれも 100_config/101_sys_config.js 内に既存実装あり(造語・記憶による記述禁止/失敗パターン #20)。
他プロパティ不干渉: SHEET_DEFAULTS / ID_PREFIX_MAP / ACCOUNT_RULES / RPA_DEFAULTS / CC_MERCHANT_MAP / COLORS / NUMBER_FORMATS / TAX_RATES 等、既存の Constants プロパティは一切変更しない。
Step 2 — onOpen() のリファクタリング(100_config/101_sys_config.js L323〜)
既存のハードコードされた ui.createMenu('🚀 BizLP').addItem(...)... を、Constants.MENU_DEFINITION をループしてメニューを動的生成するロジックに差し替える。
function onOpen() {
var ui = SpreadsheetApp.getUi();
try {
Constants.MENU_DEFINITION.forEach(function(catDef) {
// N-29: privileged カテゴリは特権ユーザー以外にはスキップ
if (catDef.privileged && !isPrivilegedUser_()) return;
var menu = ui.createMenu(catDef.category);
catDef.items.forEach(function(item) {
if (item.separator) {
menu.addSeparator();
} else if (item.items) {
// サブメニュー (ネスト構造がある場合)
var sub = ui.createMenu(item.label);
item.items.forEach(function(subItem) {
if (subItem.separator) sub.addSeparator();
else sub.addItem(subItem.label, subItem.funcName);
});
menu.addSubMenu(sub);
} else {
menu.addItem(item.label, item.funcName);
}
});
menu.addToUi();
});
} catch (e) { /* Authorization 未取得時のサイレント失敗 */ }
}
互換性要件(テスト必須): リファクタリング前後で、既存 🚀 BizLP / 💾 バックアップ の全メニュー項目の 文字列 / 関数名 / 表示順 / 区切り線位置 が完全に一致すること。機能削除・順序変更は禁止。
Step 3 — 00_menu DDL 追加(100_config/101_sys_config.js setupAllSchemas() 内)
既存の LOG_AUDIT 登録パターン(L788)に倣い、2 箇所を追加する。
3-A: 01_sys_config へのシステムキー登録(setupAllSchemas() 内の confSheet.appendRow 群)
L788 付近の LOG_AUDIT 登録に続けて、新規シートキー CAT_MENU を追加する(実装コードでは L823 に配置済み):
if (!existKeys.includes('CAT_MENU')) confSheet.appendRow(['CAT_MENU', '', '00_menu', 'メニューカタログ(利用統計)']);
3-B: schemas オブジェクトへの DDL 定義(L899 LOG_AUDIT 直後)
'LOG_AUDIT': { headers: ["日時","ユーザー","操作種別","対象シート","対象ID","対象列","関数名","変更前値","変更後値","備考"], color: "#434343" },
'CAT_MENU': { headers: ['カテゴリ1', 'カテゴリ2', '機能名', '機能概要', 'GAS関数名', '最終実行日時', '実行回数(直近90日)', '全期間実行回数'], color: Constants.COLORS.HEADER_DARK_GREEN },
列設計の意図:
| 列 | 型・例 | 意味 |
|---|---|---|
| カテゴリ1 | '🚀 BizLP' | MENU_DEFINITION の category(トップレベルメニュー名) |
| カテゴリ2 | '' or サブメニューラベル | 2 階層サブメニューの親ラベル。通常項目は空文字 |
| 機能名 | '操作パネルを開く' | MENU_DEFINITION の label(ユーザー可視文字列) |
| 機能概要 | '全操作を集約した...' | MENU_DEFINITION の description(運用者向け説明) |
| GAS関数名 | 'openOperationsSidebar' | MENU_DEFINITION の funcName(.addItem() 第 2 引数と同値) |
| 最終実行日時 | '2026-04-18 12:34:56' or '-' | 監査ログ集計結果。未実行時は '-' |
| 実行回数(直近90日) | 12 | new Date() - 90日 以内の auditLog 行数 |
| 全期間実行回数 | 57 | 読み込んだ auditLog 範囲内(readAuditLog_(5000))のヒット数 |
ヘッダー色: Constants.COLORS.HEADER_DARK_GREEN(= '#0B5345')を流用。既存 COLORS オブジェクトには追加しない(既存キーのみ使用)。
Step 4 — updateMenuCatalog_() の実装(100_config/101_sys_config.js 末尾付近 L1651〜)
監査ログ集計 + 00_menu 書き込みロジックを新規関数として追加する。既存 archiveAuditLogMonthly / openAuditLog の直後(L1630 付近)に配置する。
/**
* 98_audit_log の末尾 maxRows 行を配列で返す(ヘッダー除外)。
* 98_audit_log が存在しない場合は空配列を返す(graceful skip)。
*/
function readAuditLog_(maxRows) {
var sheet = Utils.getSheetByKey('LOG_AUDIT', '98_audit_log');
if (!sheet) return [];
var lastRow = sheet.getLastRow();
if (lastRow <= 1) return [];
var lastCol = sheet.getLastColumn();
var startRow = Math.max(2, lastRow - maxRows + 1);
var numRows = lastRow - startRow + 1;
return sheet.getRange(startRow, 1, numRows, lastCol).getValues();
}
/**
* Constants.MENU_DEFINITION と 98_audit_log を突合して 00_menu タブを更新。
* N-38: 自動生成型メニューカタログタブ
*/
function updateMenuCatalog_() {
var FUNC = 'updateMenuCatalog_';
var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
SpreadsheetApp.getUi().alert('他のユーザーが実行中です。少し待ってから再度実行してください。');
return;
}
try {
var sheet = Utils.getSheetByKey('CAT_MENU', '00_menu');
if (!sheet) {
SpreadsheetApp.getUi().alert('00_menu シートが存在しません。先に setupAllSchemas を実行してください。');
return;
}
// 1. 監査ログを取得(末尾 5,000 行)
var auditRows = readAuditLog_(5000);
var ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
// 2. funcName (row[6]) 別に 実行回数 (total / 直近90日) と 最終実行日時 を集計
var stats = {};
for (var i = 0; i < auditRows.length; i++) {
var ts = auditRows[i][0];
var funcName = String(auditRows[i][6] || '').trim();
if (!funcName) funcName = String(auditRows[i][5] || '').trim(); // 過去バグで index 5 に記録された分のフォールバック
if (!funcName) continue;
if (!stats[funcName]) stats[funcName] = { total: 0, recent90: 0, lastTs: null };
stats[funcName].total++;
if (ts instanceof Date && ts >= ninetyDaysAgo) stats[funcName].recent90++;
if (ts instanceof Date && (!stats[funcName].lastTs || ts > stats[funcName].lastTs)) {
stats[funcName].lastTs = ts;
}
}
// 3. MENU_DEFINITION を展開してカタログ行を構築
var rows = [];
Constants.MENU_DEFINITION.forEach(function(catDef) {
catDef.items.forEach(function(item) {
if (item.separator) return;
if (item.items) {
item.items.forEach(function(subItem) {
if (subItem.separator) return;
var s = stats[subItem.funcName] || { total: 0, recent90: 0, lastTs: null };
rows.push([
catDef.category, item.label, subItem.label, subItem.description || '', subItem.funcName,
s.lastTs ? Utilities.formatDate(s.lastTs, Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss') : '-',
s.recent90, s.total
]);
});
} else {
var s = stats[item.funcName] || { total: 0, recent90: 0, lastTs: null };
rows.push([
catDef.category, '', item.label, item.description || '', item.funcName,
s.lastTs ? Utilities.formatDate(s.lastTs, Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss') : '-',
s.recent90, s.total
]);
}
});
});
// 4. 00_menu のデータ領域をクリアして書き込み(ヘッダー行は保持)
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).clearContent();
}
if (rows.length > 0) {
sheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
Utils.logInfo(FUNC, 'カタログ更新完了: ' + rows.length + ' 件');
Utils.auditLog('RUN', '00_menu', '', '', FUNC, '', { rows: rows.length }, 'N-38');
SpreadsheetApp.getActiveSpreadsheet().toast(rows.length + ' 件のメニュー項目を更新しました', 'メニューカタログ', 3);
} catch (e) {
Utils.logError(FUNC, e);
SpreadsheetApp.getUi().alert('エラー: ' + e.message);
} finally {
lock.releaseLock();
}
}
設計ポイント:
LockService.getScriptLock().tryLock(5000)で多重実行を防御。5 秒待っても獲得できなければユーザーにアラートして中断する(重い集計処理が並行して走るのを防ぐ)。Utils.getSheetByKey('CAT_MENU', '00_menu')で取得(第 1 引数はシステムキー、第 2 引数はフォールバックのシート名)。nullならsetupAllSchemas未実行のためアラートして中断。readSheetAsDtos_は200_data/202_repository.jsのプライベート関数(アンダースコア命名)。外部から直接呼ばない設計を守るため、専用の軽量リーダーreadAuditLog_(maxRows)として101_sys_config.js内に独立実装する。- 監査ログの読み込みは
sheet.getDataRange().getValues()ではなくgetRange(startRow, 1, numRows, lastCol).getValues()で末尾 5,000 行のみに制限する(長期運用時の 6 分タイムアウト対策)。 index 5(対象列)へのフォールバックは、過去tryAuditLog_のバグで MAS-201 系の一部呼び出しがfuncNameを index 5 に書き込んでいた互換措置(実装コメント記載)。新規ログはすべて index 6 に書き込まれる。- 書き込みはヘッダー行を残したまま
clearContent()→setValues()で全置換(冪等)。
Step 5 — メニュー項目追加(100_config/101_sys_config.js MENU_DEFINITION 内)
Step 2 の onOpen() リファクタリング完了後、Step 1 で追加した Constants.MENU_DEFINITION の 💾 バックアップ カテゴリ末尾に、既存 4 項目 + 区切り線の後に「メニューカタログを更新」項目を追加する(実装コード L225):
{
category: '💾 バックアップ',
privileged: true,
items: [
// 既存 4 項目(runManualBackup / installBackupTriggers / verifyLatestBackup / uninstallBackupTriggers)
{ separator: true },
{ label: 'メニューカタログを更新', funcName: 'updateMenuCatalog_',
description: 'MENU_DEFINITION と 98_audit_log を突合して 00_menu タブを更新' },
]
}
メニュー文字列・関数名は実装関数 updateMenuCatalog_(末尾アンダースコア付きプライベート)と整合。onOpen() の動的生成ロジック経由で .addSeparator() → .addItem('メニューカタログを更新', 'updateMenuCatalog_') がレンダリングされる。
影響範囲
| ファイル | 変更内容 | 種別 |
|---|---|---|
000_infra/002_constants.js | Constants 末尾に MENU_DEFINITION プロパティ追加(他プロパティは変更なし) | 追加 |
100_config/101_sys_config.js | onOpen() の動的メニュー生成化、setupAllSchemas() への CAT_MENU システムキー登録 + DDL 定義、readAuditLog_() 新設、updateMenuCatalog_() 新設、MENU_DEFINITION 内の「💾 バックアップ」への「メニューカタログを更新」追加 | 変更 + 追加 |
00_menu シート | setupAllSchemas 実行時に 8 列ヘッダーで新規作成される | 新規(DDL 管理) |
01_sys_config シート | CAT_MENU 行が 1 行追記される(冪等) | データ追記 |
注意事項
- メニュー文字列・関数名の逐語引用は必須:
onOpen()リファクタリング時、.addItem(label, funcName)の引数は Phase 1 で Read した実在のものを逐語転記する。記憶・類推で書かない(失敗パターン #20: メニュー名造語)。 readSheetAsDtos_を直接呼ばない:200_data/202_repository.jsのアンダースコア命名プライベート関数のため、外部から直接呼ばずreadAuditLog_(maxRows)として101_sys_config.js内に独立実装する。依存の方向を守り、レイヤー違反を避ける。98_audit_log未作成時は graceful skip:Utils.getSheetByKey('LOG_AUDIT', '98_audit_log')がnullを返す場合は空配列を返し、00_menuには全 funcName が 実行回数=0 / 最終実行日時=-で出力される。エラーを throw しない。- 既存
Constantsプロパティの完全保持:MENU_DEFINITIONを追加する際、SHEET_DEFAULTS/ID_PREFIX_MAP/ACCOUNT_RULES/COLORS等の他プロパティは一切変更しない。差分レビュー時にMENU_DEFINITIONプロパティ追加のみであることを必ず確認する。 COLORSは既存キーのみ使用: ヘッダー色は既存のConstants.COLORS.HEADER_DARK_GREEN(#0B5345)を流用する。新色#0B5345をハードコードしない・COLORSに新規キーを追加しない(既存パレット統一)。LockServiceによる多重実行防御:updateMenuCatalog_はユーザー手動実行を想定するが、メニュー連打や複数タブでの並行実行でgetDataRangeとclearContentが競合する。tryLock(5000)で 5 秒待機、取れなければアラートして中断。auditLogの列順(funcName = index 6)は不変前提:000_infra/004_utils.jsのUtils.auditLogが列順を変更した場合、updateMenuCatalog_側のauditRows[i][6]が破綻する。列順変更時は本関数も同時改修すること。MENU_DEFINITIONが SSoT になる以降はonOpen直接編集禁止: Step 2 完了後、メニュー追加・削除・並替えは必ずMENU_DEFINITIONを編集して行う。onOpen()には個別項目のハードコードを書き戻さない(MAS-217 サイドバー統合で同じ SSoT が拡張される)。
エッジケース
| 条件 | 表示値 / 挙動 | 理由・対策 |
|---|---|---|
98_audit_log シートが存在しない(setupAllSchemas 未実行) | 全 funcName を 実行回数=0、最終実行日時=- で出力 | Utils.getSheetByKey('LOG_AUDIT', '98_audit_log') が null を返す → readAuditLog_ で [] を返し graceful skip |
98_audit_log は存在するがデータ行ゼロ(ヘッダー行のみ) | 同上 | sheet.getLastRow() <= 1 のため readAuditLog_ が [] を返す |
MENU_DEFINITION に定義済だが一度も未実行の機能 | 実行回数=0、最終実行日時=- | 集計 Map に funcName キーが存在しない → stats[funcName] || { total:0, recent90:0, lastTs:null } のフォールバック |
監査ログに存在するが MENU_DEFINITION 未登録の funcName | 00_menu に表示しない | ヘルパー関数・内部処理(例: tryAuditLog_ / readAuditLog_)は対象外。MENU 側を「表示対象の正」とする |
00_menu シート未作成(DDL 未実行)で実行 | アラート「00_menu シートが存在しません」 + 処理中断 | Utils.getSheetByKey('CAT_MENU', '00_menu') が null を返す → 明示的エラーメッセージ(失敗せずに終了、ユーザーに setupAllSchemas 実行を促す) |
MENU_DEFINITION 内のセパレーター({ separator: true }) | 00_menu には出力しない | 展開ループで if (item.separator) return; でスキップ。表示行は機能項目のみ |
MENU_DEFINITION 内のサブメニュー({ label, items: [...] }) | カテゴリ2 列に親 label、機能名列に子 label | 2 階層展開ロジックで catDef.category / item.label(親) / subItem.label(子)を個別カラムに配置 |
98_audit_log の行数が 5,000 件超 | 末尾 5,000 行のみで集計 | readAuditLog_(5000) が Math.max(2, lastRow - maxRows + 1) で末尾側ウィンドウに制限(6 分タイムアウト回避) |
funcName 列(index 6)が空で index 5(対象列)に記録されているレコード | index 5 をフォールバック集計 | 過去 tryAuditLog_ のバグ(MAS-201 系の一部)で funcName が対象列カラムに入った互換措置。新規ログは必ず index 6 |
日時列(index 0)が Date オブジェクトでない(文字列で記録された破損行) | 実行回数のみカウント、最終実行日時は - | ts instanceof Date チェックで lastTs 更新をスキップ、recent90 カウントも除外 |
catDef.privileged === true かつ非特権ユーザー | メニューは非表示、00_menu カタログには出力する | onOpen() は isPrivilegedUser_() でスキップするが、updateMenuCatalog_ は権限判定なしに全項目をカタログ化(閲覧用途) |
LockService.tryLock(5000) が 5 秒で取れない | アラート「他のユーザーが実行中です」+ 中断 | 並行実行時の clearContent / setValues 競合を防止 |
description フィールドが未設定の MENU_DEFINITION 項目 | 機能概要列は空文字 '' | item.description || '' でフォールバック |
実データ検証
実装前・実装後の両タイミングで、MCP 経由で以下を確認する。
98_audit_logの実在と列構造:000_infra/004_utils.jsL459 のappendRow呼び出しに記載の 10 列(日時/ユーザー/操作種別/対象シート/対象ID/対象列/関数名/変更前値/変更後値/備考)と、実際のシートヘッダーが一致していること。funcNameの命名パターン: index 6 列に格納されている実際の関数名を 10 件程度サンプリングし、updateMenuCatalog_/setupAllSchemas/installBackupTriggers等の命名規則(アンダースコア有無・大文字・_サフィックス)を把握。MENU_DEFINITIONのfuncNameと書き込みキーが完全一致することを確認。01_sys_configのCAT_MENUシステムキー未登録確認: 本案件着手前に01_sys_configの key 列にCAT_MENU/00_menu関連の行が未登録であることを確認(setupAllSchemas実行時に冪等 append するため重複登録を避ける)。- リファクタリング後の
onOpen()動作確認: スプレッドシートをリロードして🚀 BizLP/💾 バックアップの全項目がリファクタリング前と同じ文字列・同じ順序で表示されること。 updateMenuCatalog_実行後の00_menu中身: MENU_DEFINITION で定義された項目数と00_menuのデータ行数が一致していること(セパレーター行を除く)。auditLog集計の正確性:setupAllSchemasを 1 回実行後にupdateMenuCatalog_を実行し、setupAllSchemas行の「全期間実行回数」が1になっていることを確認(最低限のスモークテスト)。
関連ドキュメント
docs/_internal/failure_patterns.md#18-#20(コード未読による固有名詞誤記・シート名誤記・メニュー名造語の防止)docs/dev/dev_mas-179_audit_trail.md(98_audit_logの列定義・Utils.auditLog仕様)docs/dev/dev_mas-205_mfa_privilege_separation.md(isPrivilegedUser_/catDef.privilegedの連携)docs/dev/dev_mas-217_sidebar_catalog_and_readability.md(本案件完了後、MENU_DEFINITIONをサイドバー 41 項目まで拡張する後続案件)docs/_internal/TODO_future.md§3 MAS-214 行(要件・期待効果・人間が検討すべき事項の原典)CLAUDE.md「GAS ファイル番号体系」「シートへの書き込み位置は列B で最終行判定」等のコーディング規約
人間が検討すべき事項
TODO_future.md MAS-214 行 §「人間が検討すべき事項」を基に整理。
- 集計期間の設定(直近 90 日 vs 可変): 直近 90 日固定で実装しているが、運用側で「直近 30 日」「直近 180 日」の要望が出る可能性。
03_sys_paramsのCFG_MENU_CATALOG_WINDOW_DAYS(既定 90)として外出しする設計余地あり(本案件では未実装、後続改善で対応)。 - カタログセクションの配置順: 現仕様は
MENU_DEFINITIONの配列順(=onOpen表示順)と一致。業務頻度順・アルファベット順等への並替え要望が出た場合の対応方針(表示順保持を優先)。 00_menuシートへの保護適用可否: MAS-213(監査ログ保護)と同様に00_menuも編集禁止保護をかけるべきか。カタログは自動再生成のため編集してもすぐ上書きされるが、運用者が誤編集で一時的にデータが欠けることを避けたい場合はSheet.protect()の適用を検討。- シート単位のクイックアクション列追加: 将来的に「機能名」列の隣に HYPERLINK でその関数を実行できるクイックアクション列(例:
=HYPERLINK("...","▶ 実行"))を追加する構想。本案件ではスコープ外。 - 未使用機能の検出アラート: 「全期間実行回数 = 0」の行を
00_menuで色分けする、またはsetupAllSchemas実行時に未使用機能一覧を Slack 通知する仕組みの追加検討。本案件ではスコープ外。 - 登録漏れ検知: 監査ログに存在するが
MENU_DEFINITION未登録の funcName を「登録候補」として別タブに出力する仕組みの追加検討。本案件ではスコープ外(MAS-217 サイドバー統合時に類似のvalidateMenuRegistry_が追加される想定)。 - 特権メニュー項目の
00_menu表示方針: 現仕様は非特権ユーザーも全項目をカタログ閲覧できる。特権機能は非特権ユーザーにとって見えない方が運用混乱が少ないという判断もあり得るため、catDef.privilegedに基づく行フィルタリングの有無を経営層と協議。
実装プロンプト(Claude Code 用)
以下のプロンプトを Claude Code に渡し、Step 1〜5 を順に実装させる。コードブロックはバッククォートを使わず 4 スペースインデントで記述する(本仕様書を Markdown レンダリング時にネスト崩れを起こさないため)。
あなたはGAS会計システムのシニア開発者です。
案件 MAS-214「自動生成型メニューカタログタブ (00_menu)」を実装してください。
## 実行前タスク(固有名詞の逐語確認・失敗パターン #18-#20 対策)
1. `100_config/101_sys_config.js` を Read し、以下を逐語確認する。推測・記憶で書くことは禁止。
- `onOpen()` の実在するメニュー文字列(`ui.createMenu(...)` 第 1 引数)・項目ラベル文字列・`.addItem()` 第 2 引数の関数名
- `setupAllSchemas()` L788 付近の既存 `confSheet.appendRow(['LOG_AUDIT', '', '98_audit_log', ...])` 登録パターン
- `setupAllSchemas()` 内 `schemas` オブジェクト L899 の `LOG_AUDIT` DDL 定義形式
- `Utils.getSheetByKey` の第 1 引数(= システムキー、`01_sys_config` の key 列に登録される値)
2. `000_infra/002_constants.js` を Read し、以下を確認する。
- `Constants` オブジェクトの末尾プロパティ名(現行は `CC_MERCHANT_MAP`)・閉じ `};` の行番号(= 挿入位置)
- `Constants.COLORS` の実在キー名(`HEADER_DARK_GREEN` の値が `'#0B5345'` であること)
3. `000_infra/004_utils.js` の `Utils.auditLog` 内 `sheet.appendRow([...])` 行を Read し、列順を確認する。
- 期待値: `[new Date(), user, operation, targetSheet, targetId, targetCol, funcName, before, after, note]`
- 0 始まり index 6 が `funcName` であることを確認してから実装に進む
4. `200_data/202_repository.js` の `readSheetAsDtos_` を Read し、アンダースコア命名規則(外部非公開)を確認。直接呼び出さず、同パターンで軽量リーダー `readAuditLog_(maxRows)` を `101_sys_config.js` 内に独立実装する。
## 修正対象ファイル
- `000_infra/002_constants.js`: `Constants` 末尾に `MENU_DEFINITION` プロパティを追加するのみ。`SHEET_DEFAULTS` / `ID_PREFIX_MAP` / `ACCOUNT_RULES` / `COLORS` 等の既存プロパティは一切変更禁止。
- `100_config/101_sys_config.js`: `onOpen()` リファクタリング(L323 付近)、`setupAllSchemas()` DDL 追加(L788 付近で `CAT_MENU` `appendRow` 登録 + L899 付近で `schemas` 定義追加)、`readAuditLog_()` 新規追加、`updateMenuCatalog_()` 新規追加、`MENU_DEFINITION` の `💾 バックアップ` カテゴリ末尾に「メニューカタログを更新」項目追加。
## 実装内容
- Step 1: `Constants.MENU_DEFINITION` を `000_infra/002_constants.js` の `Constants` 末尾に追加。各 item は `{ label, funcName, description }` / `{ separator: true }` / `{ label, items: [...] }` の 3 形式に対応。カテゴリには `{ privileged: true }` オプションを許容し、非特権ユーザーには非表示とする(MAS-205 連携)。
- Step 2: `onOpen()` を `Constants.MENU_DEFINITION.forEach` によるループ方式に全面書き換え。`privileged` チェック + `separator` / `items`(サブメニュー) / 通常項目の分岐で `ui.createMenu / addItem / addSeparator / addSubMenu / addToUi` を呼ぶ。リファクタリング前後で既存メニューの文字列・順序が完全に一致すること。
- Step 3: `setupAllSchemas()` に `CAT_MENU` を追加。(3-A) `confSheet.appendRow(['CAT_MENU', '', '00_menu', 'メニューカタログ(利用統計)'])` を `existKeys.includes` チェック付きで冪等に追加。(3-B) `schemas['CAT_MENU'] = { headers: ['カテゴリ1','カテゴリ2','機能名','機能概要','GAS関数名','最終実行日時','実行回数(直近90日)','全期間実行回数'], color: Constants.COLORS.HEADER_DARK_GREEN }` を `LOG_AUDIT` 直後に追加。
- Step 4: `readAuditLog_(maxRows)` と `updateMenuCatalog_()` を `101_sys_config.js` の `archiveAuditLogMonthly` / `openAuditLog` 直後に配置。`LockService.tryLock(5000)` による多重実行防御、`Utils.getSheetByKey('LOG_AUDIT', '98_audit_log')` と `Utils.getSheetByKey('CAT_MENU', '00_menu')` の `null` チェック、funcName 別の `{ total, recent90, lastTs }` 集計、ヘッダー行保持 + データ領域 `clearContent` + `setValues` で冪等書き込み。
- Step 5: `Constants.MENU_DEFINITION` の `💾 バックアップ` カテゴリ `items` 末尾に `{ separator: true }` + `{ label: 'メニューカタログを更新', funcName: 'updateMenuCatalog_', description: 'MENU_DEFINITION と 98_audit_log を突合して 00_menu タブを更新' }` を追加。
## 制約
- `onOpen()` リファクタリング後、既存の全メニュー項目の文字列・関数名・表示順・区切り線位置が変わらないこと(MAS-205 `privileged` 判定を除き、ユーザー可視の挙動は完全一致)。
- メニュー文字列・関数名は Read で確認した逐語引用のみ使用。造語・推測は禁止(失敗パターン #20)。
- `readSheetAsDtos_` を直接呼ばず、専用 `readAuditLog_(maxRows)` を `101_sys_config.js` 内に実装する(レイヤー規約遵守)。
- `98_audit_log` が `null` の場合は graceful skip(空配列を返してエラーを throw しない)。
- `00_menu` が `null` の場合はアラートで明示的にユーザーに setupAllSchemas 実行を促して中断。
- `Constants.COLORS` に新規キーを追加しない。既存 `HEADER_DARK_GREEN` を流用する。
- `Constants` の他プロパティ(`SHEET_DEFAULTS` / `ID_PREFIX_MAP` / `ACCOUNT_RULES` / `RPA_DEFAULTS` / `CC_MERCHANT_MAP` / `NUMBER_FORMATS` / `TAX_RATES` 等)は差分レビュー時に一切変化がないこと。
## エッジケース(本仕様書「エッジケース」テーブルの 13 項目を必ず実装で考慮)
- `98_audit_log` 未作成 → 全 funcName を 実行回数=0 / 最終実行日時=`-` で出力
- `98_audit_log` 存在するがデータゼロ → 同上
- `MENU_DEFINITION` 定義済だが未実行 → 実行回数=0、最終実行日時=`-`
- 監査ログに funcName あるが MENU_DEFINITION 未登録 → `00_menu` に表示しない
- `00_menu` 未作成 → アラート + 中断
- `MENU_DEFINITION` 内のセパレーター → カタログ出力スキップ
- サブメニュー(`items` 持ち)→ カテゴリ2 列に親 label を設定
- 5,000 行超 → 末尾 5,000 行ウィンドウ
- index 6 空で index 5 に funcName → index 5 フォールバック
- 日時が `Date` でない破損行 → `lastTs` 更新スキップ、`recent90` 除外
- `privileged` カテゴリ × 非特権ユーザー → メニュー非表示だがカタログには出力
- `LockService.tryLock(5000)` 失敗 → アラート + 中断
- `description` 未設定 → 機能概要列は空文字
## 動作確認
1. `npm run push:dev` でデプロイ。
2. スプレッドシートをリロードし、既存の `🚀 BizLP` / `💾 バックアップ` 全メニュー項目が正常に表示・動作することを確認(リファクタリング前後で差異がないこと)。
3. `🔧 開発・設定` 関連のサイドバーから `setupAllSchemas` を実行し、`00_menu` タブが新規生成されて 8 列ヘッダー(`Constants.COLORS.HEADER_DARK_GREEN`)が付与されていること、`01_sys_config` の key 列に `CAT_MENU` が冪等に 1 行追加されていることを確認。
4. `💾 バックアップ → メニューカタログを更新` を実行し、MENU_DEFINITION の全項目数と `00_menu` のデータ行数が一致(セパレーター除く)することを確認。
5. `98_audit_log` に数件のログが蓄積された状態で再実行し、直近 90 日実行回数・全期間実行回数・最終実行日時が正しく集計されることを確認。
6. `98_audit_log` が未作成の状態でも `updateMenuCatalog_` が正常に完了し、全 funcName が 実行回数=0 / 最終実行日時=`-` で出力されることを確認。
7. 問題なければ `git commit` → `npm run push:prod` → prod で同一手順を実行して動作確認。
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・設計確定) | あり | 固有名詞の逐語引用・MENU_DEFINITION 構造確定・列インデックス確認 |
| 実装(コード書き下し) | なし | 確定済み内容の清書に徹する。途中で再設計しない |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
Step 1-2(MENU_DEFINITION 設計 + onOpen リファクタリング) | Claude Opus 4.6 | 複数ファイル横断の設計判断・既存メニュー全量把握・サブメニュー構造とセパレーター分岐の抽象化が必要 |
Step 3(setupAllSchemas DDL 追加) | Claude Sonnet 4.6 | 既存 LOG_AUDIT パターンに倣う中程度の判断(挿入位置・シートキー命名) |
Step 4(readAuditLog_ + updateMenuCatalog_ 集計実装) | Claude Sonnet 4.6 | 集計ロジックは中程度の判断。LockService / 2 次元配列集計 / Utilities.formatDate の既存パターン適用 |
| Step 5(メニュー項目追加) | Claude Haiku 4.5 | 1 行追加のみ、判断要素なし |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-20 | 初版作成 |
| 2026-04-21 | 実装コード(000_infra/002_constants.js MENU_DEFINITION / 100_config/101_sys_config.js onOpen 動的化・CAT_MENU DDL・readAuditLog_ / updateMenuCatalog_)の L 行番号と実装済み内容を正に、仕様書を全面リライト。実装プロンプトを失敗パターン #18-#20 対策(Read による逐語引用指示)で強化 |
仕様書作成プロンプト
本仕様書を生成した tasks/prompts/task_N-38.md の <instruction> 全文を、再現性・監査性・プロンプト改善トレースの目的で保存する。
展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 2-1(骨格 ~20行) / 2-2(概要〜注意事項 ~300行) / 2-3a(エッジケース〜人間検討事項 ~200行) / 2-3b(実装プロンプト〜変更履歴 ~250行) / 2-4(<details>プロンプト記録) の最低 5 分割で tool_use する。
4. **各 Step で何を書くかを具体指示**: Phase 1 で確定した内容を Phase 2 実行時に再設計しない。
======================================================================
あなたはGAS会計システムのシニア開発者兼仕様書ライターです。
案件 MAS-214「自動生成型メニューカタログタブ (00_menu)」の開発仕様書を作成してください。
作成後、`docs/_config.json` の `nav` 配列(§E.1 基盤・DevOps)にも必ず追記してください。
## Phase 1: 実行前調査(テキスト報告禁止。全項目を即座にツールで実行)
以下のファイルを **必ず Read で開き**、仕様書に書く固有名詞・構造・行番号を確定させること。
Grep の部分ヒットだけで判断しない(失敗パターン #18-#20: コード未読による固有名詞誤記・メニュー名造語)。
1. **`docs/_internal/TODO_future.md`** — MAS-214 の要件・優先度・概要・人間が検討すべき事項を取得。
2. **`100_config/101_sys_config.js`** — Read で以下を確認:
- `onOpen()` の実在する定義行番号・メニューカテゴリ文字列・`.addItem()` の第 2 引数(関数名)を逐語引用(造語・記憶で書くことは禁止)。
- 既存「🔧 開発・設定」配下の項目一覧と末尾行番号(新規項目の追加位置)。
- `setupAllSchemas()` の既存シートキー登録パターン(新規 `00_menu` DDL の追加位置を特定)。
- `98_audit_log` のシステムキー(コメント `LOG_AUDIT` と記載があるか)を確認し、読み取り時に使う `Utils.getSheetByKey()` の第 1 引数を確定する。
3. **`000_infra/002_constants.js`** — Read で以下を確認:
- `Constants` オブジェクトの末尾プロパティ名と行番号(`MENU_DEFINITION` の挿入位置)。
- `COLORS` オブジェクトの実在キー名を逐語確認(`HEADER_DARK_GREEN` 等)。
4. **`000_infra/004_utils.js`** — `Utils.auditLog` 内の `sheet.appendRow([...])` 呼び出し行を Read し、`98_audit_log` の列順を確定する。
- 期待値: `[new Date(), user, operation, targetSheet, targetId, targetCol, funcName, before, after, note]`(0 始まり index 6 = `funcName`)— Read で実際のコードと照合してから仕様書に記載すること。
5. **`200_data/202_repository.js`** — `readSheetAsDtos_` の実装を Read し、`readAuditLog_()` を独自実装する際のパターンとして把握する。`readSheetAsDtos_` はアンダースコア付きプライベート関数のため直接呼ばず、同パターンで専用の軽量リーダーを `101_sys_config.js` 内に実装する。
6. **`docs/_internal/failure_patterns.md`** — #18-#20(メニュー名造語・シート名誤記・コード未読)を再確認し、Phase 2 での固有名詞引用ルールを徹底する。
---
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-214_menu_catalog.md`
**【重要】絶対に 1 回のツール呼び出しで全内容を出力せず、以下の Step に分割して実行すること。**
### Step 2-1: 骨格の作成(File Write、~20行)
以下の見出しのみの骨格ファイルを Write する(本文空で可):
# MAS-214: 自動生成型メニューカタログタブ (00_menu)
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト
### Step 2-2: 前半セクションの追記(File Edit または Bash、~300行)
Phase 1 で確定した固有名詞・行番号をそのまま使い、ここでは再設計しない。
- **概要テーブル**: 案件ID=MAS-214 / カテゴリ=基盤 / 対象ファイル=`000_infra/002_constants.js`・`100_config/101_sys_config.js`(Phase 1-2/3 で確認した行番号を明記)
- **目的**: Phase 1-1 で取得した TODO_future.md の記述を元に 1-3 文で記載
- **現在のコード**: Phase 1-2 で Read した `onOpen()` のメニュー定義部分のスニペット(ファイル名 + 行番号を明記)
- **修正方針**(Step 分割で記述):
- **Step 1 — `Constants.MENU_DEFINITION` の追加**(`000_infra/002_constants.js`):
- Phase 1-3 で確認した `Constants` 末尾(行番号明記)に `MENU_DEFINITION` プロパティを追加。
- 構造: `[{ category: '...', items: [{ label: '...', funcName: '...', description: '...' }, { separator: true }, ...] }, ...]`。カテゴリ名・ラベル文字列・関数名は Phase 1-2 で Read した `onOpen()` の逐語引用のみ使用(造語禁止)。
- 2 階層サブメニューは `{ label: '...', subMenu: [...] }` で表現。
- **Step 2 — `onOpen()` のリファクタリング**(`100_config/101_sys_config.js`):
- Phase 1-2 で特定した `onOpen()` の行番号を起点に、`Constants.MENU_DEFINITION` をループして `ui.createMenu()` / `.addItem()` / `.addSeparator()` / `.addSubMenu()` を動的生成するロジックに差し替え。
- 既存の全メニュー項目の文字列・関数名・順序は維持すること(機能削除・順序変更禁止)。
- **Step 3 — `00_menu` DDL 追加**(`100_config/101_sys_config.js`、`setupAllSchemas` 内):
- Phase 1-2 で確認した既存シートキー登録パターンに倣い、新規シートキー(例: `CAT_MENU`、実在のパターンを確認して命名)と `00_menu` シート名を登録。
- スキーマ列: `カテゴリ1` / `カテゴリ2` / `機能名` / `機能概要` / `GAS関数名` / `最終実行日時` / `実行回数(直近90日)` / `全期間実行回数`。
- ヘッダー背景色は Phase 1-3 で確認した `Constants.COLORS` の実在キー名を使用(例: `HEADER_DARK_GREEN`)。
- **Step 4 — `updateMenuCatalog_()` の実装**(`100_config/101_sys_config.js`):
- 関数冒頭で `var FUNC = 'updateMenuCatalog_';` を宣言(監査ログ用)。
- `LockService.getScriptLock()` による排他制御を実装(多重実行防止)。
- `Constants.MENU_DEFINITION` を展開して `00_menu` シートにカタログ行を構築。
- `98_audit_log` を専用の軽量リーダー `readAuditLog_(maxRows)` で読み込む(`Utils.getSheetByKey` の第 1 引数は Phase 1-2 で確認したシステムキー)。直近 5,000 行を `getRange` で取得し、Phase 1-4 で確認した列インデックス(`funcName` = index 6)をキーに実行回数・最終実行日時を集計する。
- `MENU_DEFINITION` の `funcName` と集計結果をマージして `00_menu` シートに書き込む。
- **Step 5 — メニュー項目追加**(`100_config/101_sys_config.js`):
- Phase 1-2 で確認した既存「🔧 開発・設定」メニューの末尾に `addSeparator()` + `addItem('メニューカタログを更新', 'updateMenuCatalog_')` を追加(メニュー文字列・関数名は実在のものと整合させること)。
- **影響範囲**: `000_infra/002_constants.js`(`MENU_DEFINITION` プロパティ追加のみ)/ `100_config/101_sys_config.js`(`onOpen` 修正・`setupAllSchemas` DDL 追加・`updateMenuCatalog_` 新規追加・メニュー項目追加)/ `00_menu` シート(新規)
- **注意事項**:
1. `onOpen()` リファクタリング後、既存の全メニュー項目が動作すること。Phase 1-2 で Read した実在の関数名文字列を `.addItem()` 第 2 引数に逐語転記する(推測・造語禁止)。
2. `readSheetAsDtos_` は `202_repository.js` のプライベート関数(アンダースコア命名規則)。外部から直接呼ばず、`readAuditLog_(maxRows)` として `101_sys_config.js` 内に独立実装する。
3. `98_audit_log` が未作成の場合(`setupAllSchemas` 未実行)は `getSheetByKey` が `null` を返すため、`null` チェック後に graceful skip し `Utils.logInfo` でその旨を記録する。
4. `MENU_DEFINITION` を `002_constants.js` に追加する際、既存の他プロパティ(`SHEET_DEFAULTS`・`ID_PREFIX_MAP`・`ACCOUNT_RULES` 等)は一切変更しない。
### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash、~200行)
- **エッジケース**:
| 条件 | 表示値 | 理由 |
|------|--------|------|
| `98_audit_log` シートが存在しない(DDL 未実行) | 実行回数=`0`、最終実行日時=`-` | `getSheetByKey` が `null` → graceful skip |
| `98_audit_log` は存在するがデータ行ゼロ(ヘッダー行のみ) | 同上 | ループ対象行数 0 件扱い |
| `MENU_DEFINITION` に定義済みだが一度も未実行の機能 | 実行回数=`0`、最終実行日時=`-` | 集計 Map に funcName キーが存在しない |
| 監査ログに存在するが `MENU_DEFINITION` 未登録の funcName | `00_menu` に表示しない | ヘルパー関数・内部処理は対象外。MENU 側を正とする |
| `00_menu` シート未作成(DDL 未実行)で実行 | エラーダイアログ表示 + 処理中断 | `getSheetByKey` が `null` を返す。事前チェックで明示的エラー |
| `MENU_DEFINITION` 内のセパレーター(`{ separator: true }`) | `00_menu` の対応行に上辺罫線を描画 | `Range.setBorder()` を使用(正確な引数は実装時に GAS リファレンスで確認) |
- **実データ検証(実装前に MCP 等で確認)**:
- `98_audit_log` の実在と、`funcName` 列(Phase 1-4 で特定したインデックス)に格納されている実際の関数名命名パターン(例: `updateMenuCatalog_`・`setupAllSchemas` 等の命名規則)を確認する。
- `01_sys_config`(`Constants.CONFIG_SHEET` = `'01_sys_config'`)の `00_menu` システムキー候補が既に登録済みでないか確認する。
- **関連ドキュメント**: `docs/_internal/failure_patterns.md`(#18-#20 固有名詞誤記防止)
- **人間が検討すべき事項**: TODO_future.md から転記。記載がない場合は「なし(即実装可)」
### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash、~250行)
実装プロンプトは **行頭 4 スペースインデント**(バッククォート 3 つ以上のコードブロック禁止)で出力。以下の内容を含めること:
あなたはGAS会計システムのシニア開発者です。
案件 MAS-214「自動生成型メニューカタログタブ (00_menu)」を実装してください。
## 実行前タスク
1. `100_config/101_sys_config.js` を Read し、`onOpen()` の実在するメニュー文字列・関数名文字列(.addItem() 第 2 引数)・行番号と、`setupAllSchemas()` の既存 DDL 登録パターンを逐語確認する。推測・記憶で書くことは禁止。
2. `000_infra/002_constants.js` を Read し、`Constants` オブジェクト末尾プロパティ名・行番号と `COLORS` のキー名を確認する。
3. `000_infra/004_utils.js` の `Utils.auditLog` 内の `appendRow` 行を Read し、`98_audit_log` の列順(index 6 = funcName であること)を確認する。
## 修正対象ファイル
- `000_infra/002_constants.js`: `Constants` 末尾に `MENU_DEFINITION` プロパティを追加するのみ。既存プロパティは一切変更禁止。
- `100_config/101_sys_config.js`: `onOpen()` リファクタリング・`setupAllSchemas()` DDL 追加・`readAuditLog_()` 新規追加・`updateMenuCatalog_()` 新規追加・既存「🔧 開発・設定」メニューへの項目追加。
## 実装内容
(仕様書「修正方針」の Step 1〜5 を箇条書きで転記)
## 制約
- `onOpen()` リファクタリング後、既存の全メニュー項目の文字列・関数名・順序が変わらないこと。
- メニュー文字列・関数名は Read で確認した逐語引用のみ使用。造語・推測は禁止(失敗パターン #20)。
- `readSheetAsDtos_` を直接呼ばず、専用 `readAuditLog_(maxRows)` を `101_sys_config.js` 内に実装する。
- `98_audit_log` が null の場合は graceful skip(エラーを throw しない)。
## エッジケース
(仕様書「エッジケース」テーブルを転記)
## 動作確認
1. `npm run push:dev` でデプロイ。
2. スプレッドシートをリロードし、既存の全メニュー項目が正常に表示・動作することを確認(リファクタリング前後で差異がないこと)。
3. 「🔧 開発・設定 → メニューカタログを更新」(Phase 1-2 で確認した実在のメニュー文字列で置き換えること)を実行し、`00_menu` シートが生成されることを確認。
4. `98_audit_log` に数件データがある状態で再実行し、実行回数・最終実行日時が正しく集計されることを確認。
5. `98_audit_log` が存在しない状態でも実行回数=0・最終実行日時=`-` で正常完了することを確認。
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・設計確定) | あり | 固有名詞の逐語引用・MENU_DEFINITION 構造確定 |
| 実装(コード書き下し) | なし | 確定済み内容の清書に徹する |
- **推奨実行モデル**:
| 工程 | 推奨モデル | 理由 |
|------|-----------|------|
| Step 1-2(MENU_DEFINITION 設計 + onOpen リファクタリング) | Claude Opus 4.6 | 複数ファイル横断の設計判断・既存メニュー全量把握が必要 |
| Step 3-5(DDL 追加・集計実装・メニュー項目追加) | Claude Sonnet 4.6 | 既存パターン適用で中程度の判断 |
- **変更履歴**: `| 2026-04-20 | 初版作成 |`
### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)
末尾 `## 仕様書作成プロンプト` セクションに `<details><summary>展開して表示</summary>` ブロックを設け、この `<instruction>` 全文を記録する(再現性・監査性・プロンプト改善トレースの目的)。
---
## Phase 3: 後処理(テキスト報告禁止。全て即座にツールで実行)
1. **`docs/_config.json` への追記**(必須):
追記前に `git pull origin main` で最新状態を確認してから編集する。`nav` 配列の §E.1(基盤・DevOps)セクションに追加:
{ "file": "dev/dev_mas-214_menu_catalog.md", "title": "E.1.X MAS-214 自動生成型メニューカタログタブ" }
2. **`docs/_internal/changelog.md` への追記**:
ヘッダー直後の先頭行に追加:
| 2026-04-20 | [dev_mas-214_menu_catalog.md](dev_mas-214_menu_catalog.md) | 初版作成。MAS-214 自動生成型メニューカタログタブの開発仕様書 |
3. **コミット&プッシュ**:
git add docs/dev/dev_mas-214_menu_catalog.md docs/_config.json docs/_internal/changelog.md
git commit -m "docs: MAS-214 自動生成型メニューカタログタブの開発仕様書を作成
Constants.MENU_DEFINITION 設計・onOpen リファクタリング・00_menu DDL・
updateMenuCatalog_ の仕様を定義.
https://claude.ai/code/session_XXXXX"
git push -u origin {現在のブランチ名}