MAS-201: スプレッドシート定期バックアップ
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-201 |
| 案件名 | スプレッドシート定期バックアップ |
| カテゴリ | セキュリティ |
| Phase | P1 |
| 優先度 | ★★★ |
| 所要時間 | 3-4時間(Step 1: 1h / Step 2: 1.5h / Step 3: 1h / Step 4: 0.5h) |
| 対象ファイル | 800_ops/809_backup_tool.js(新規作成)000_infra/001_env.js(backupFolderId / setBackupFolderId 追加)100_config/101_sys_config.js(メニュー追加のみ)CLAUDE.md(GAS ファイル番号体系に 809 追記)900_test/901_test_runner.js(軽量テスト追加) |
| 前提案件 | なし(MAS-178 Utils.persistLog / MAS-179 Utils.auditLog は未実装。本案件は両者と独立して動作し、実装後に連携強化) |
目的
Google Sheets のバージョン履歴は 30 日で切れるため、「うっかり全消し」「誤操作」「マイグレーションミス」からの復元を保証する独立バックアップ基盤を構築する。GAS 時間トリガーで週次自動バックアップを行い、12 週分の世代管理と月次 12 ヶ月分の長期保管を実現する。
現状の課題
実測によって判明した前提
(仕様書作成時の git log -1 / grep / ls 実測結果、2026-04-17 時点)
| # | 項目 | 実測値 | 本案件への影響 |
|---|---|---|---|
| 1 | 800_ops/ の使用済み番号 | 801-808(次は 809) | 新規ファイル番号確定 |
| 2 | ScriptApp.newTrigger の使用実績 | 0 箇所(grep -rn "ScriptApp.newTrigger" で該当なし) | 本案件が最初の時間トリガー導入 |
| 3 | Utils.auditLog / Utils.persistLog の実装 | 未実装(000_infra/004_utils.js に定義なし) | MAS-178 / MAS-179 連携は typeof === 'function' ガードで条件付き呼び出し |
| 4 | appsscript.json | dependencies: {}、oauthScopes 未指定 | Drive 初回アクセス時に承認ダイアログ表示(ユーザー承認が必要) |
| 5 | 既存同期ツール 800_ops/803_sync_tool.js | getValues/setValues 方式で行コピー | 本案件は DriveApp.makeCopy 方式で別系統、統合せず新設 |
| 6 | DriveApp.getFolderById の既存使用例 | 500_import/502_receipt_reader.js L34 | エラーハンドリングパターンを踏襲 |
| 7 | Env.setReceiptFolderId の呼び出し例 | 502_receipt_reader.js L28 | Env.setBackupFolderId を同型で追加 |
ギャップ
- スプレッドシート全体の独立バックアップなし(Google Sheets のバージョン履歴 30 日のみ)
- 「うっかり全消し」「マイグレーションミス」「誤操作による大量削除」からの復元保証なし
- 復元手順書なし(復元方法が属人化)
- バックアップ自体の健全性検証機構なし
修正方針(4ステップ段階実装)
Step 1: Env 拡張 + 手動バックアップ基盤
A. Env モジュール拡張(000_infra/001_env.js)
既存の Env.receiptFolderId / setReceiptFolderId (L57-64) の直下に同型で追加:
/** @returns {string} バックアップ先フォルダID (未設定なら空文字) */
backupFolderId: function () {
return _getProps().getProperty('BACKUP_FOLDER_ID') || '';
},
/** バックアップ先フォルダIDをスクリプトプロパティに保存 */
setBackupFolderId: function (id) {
_getProps().setProperty('BACKUP_FOLDER_ID', id);
},
B. 手動バックアップ関数(800_ops/809_backup_tool.js 新規)
新規ファイルを作成し、以下を実装:
// ==========================================
// N-25: スプレッドシート定期バックアップ
// ==========================================
// 週次: DriveApp.makeCopy で別フォルダに複製(12週保持)
// 月次: 月末週の週次を _m で保存(12ヶ月保持)
// 検証: 月1回、最新バックアップの主要シート行数を元と比較(±5%)
/** 手動バックアップ実行(メニューから呼ばれる) */
function runManualBackup() {
var FUNC = 'runManualBackup';
try {
var result = executeBackup_(false); // force = false(当日既存があれば連番)
SpreadsheetApp.getUi().alert('✅ バックアップ完了', result.message, SpreadsheetApp.getUi().ButtonSet.OK);
Utils.logInfo(FUNC, result.message);
tryAuditLog_('RUN', '', '', FUNC, '', { fileId: result.fileId, fileName: result.fileName }, 'N-25 手動');
} catch (e) {
Utils.logError(FUNC, e);
tryPersistLog_('ERROR', FUNC, e.message, e.stack);
SpreadsheetApp.getUi().alert('🚨 バックアップ失敗', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
}
}
/** 内部: バックアップ実行本体 */
function executeBackup_(force) {
// 1. フォルダID取得
var folderId = Env.backupFolderId();
if (!folderId) {
throw new Error('BACKUP_FOLDER_ID が未設定です。Env.setBackupFolderId() でフォルダIDを設定してください。');
}
// 2. フォルダ取得(既存エラーハンドリングパターン: 502_receipt_reader.js L34-37 踏襲)
var folder;
try {
folder = DriveApp.getFolderById(folderId);
} catch (e) {
throw new Error('バックアップ先フォルダが見つかりません: ' + folderId + '\n' + e.message);
}
// 3. ファイル名生成(週次 or 月次判定)
var tz = Session.getScriptTimeZone(); // 'Asia/Tokyo'
var now = new Date();
var dateStr = Utilities.formatDate(now, tz, 'yyyyMMdd');
var isMonthEnd = isMonthEndWeek_(now);
var suffix = isMonthEnd ? '_m' : '_w';
var baseName = 'BizLP_' + Env.name() + '_backup_' + dateStr + suffix;
// 4. 同日ファイル衝突時の連番付与
var finalName = baseName;
var seq = 2;
while (folder.getFilesByName(finalName).hasNext()) {
finalName = baseName + '_' + seq;
seq++;
}
// 5. 実行(DriveApp.makeCopy で別フォルダへコピー)
var ssId = Env.spreadsheetId();
var file = DriveApp.getFileById(ssId).makeCopy(finalName, folder);
// 6. 世代管理
cleanupOldBackups_(folder, 12, 12);
return { fileId: file.getId(), fileName: finalName, message: 'バックアップ完了: ' + finalName };
}
/** 月末週判定: 実行日から 7 日以内に月が変わるかで判定 */
function isMonthEndWeek_(date) {
var d = new Date(date);
d.setDate(d.getDate() + 7);
return d.getMonth() !== date.getMonth();
}
/** 世代管理: 週次 12 件 / 月次 12 件保持、超過は setTrashed */
function cleanupOldBackups_(folder, keepWeekly, keepMonthly) {
var weekly = [];
var monthly = [];
var files = folder.getFiles();
while (files.hasNext()) {
var f = files.next();
var name = f.getName();
if (!/^BizLP_[^_]+_backup_\d{8}(_[wm])(_\d+)?$/.test(name)) continue; // 対象ファイルのみ
if (/_m(_\d+)?$/.test(name)) monthly.push(f);
else if (/_w(_\d+)?$/.test(name)) weekly.push(f);
}
// 作成日時降順ソート
var byDateDesc = function(a, b) { return b.getDateCreated() - a.getDateCreated(); };
weekly.sort(byDateDesc);
monthly.sort(byDateDesc);
// 超過分を trash
for (var i = keepWeekly; i < weekly.length; i++) {
try { weekly[i].setTrashed(true); } catch (e) { Utils.logInfo('cleanupOldBackups_', '週次削除失敗: ' + weekly[i].getName() + ' / ' + e.message); }
}
for (var j = keepMonthly; j < monthly.length; j++) {
try { monthly[j].setTrashed(true); } catch (e) { Utils.logInfo('cleanupOldBackups_', '月次削除失敗: ' + monthly[j].getName() + ' / ' + e.message); }
}
}
/** N-03 連携: auditLog が実装済みなら呼ぶ、未実装なら無視 */
function tryAuditLog_(operation, targetSheet, targetId, funcName, before, after, note) {
try {
if (typeof Utils.auditLog === 'function') {
Utils.auditLog(operation, targetSheet, targetId, funcName, before, after, note);
}
} catch (_) { /* auditLog 内部で握りつぶし済みだが念のため */ }
}
/** N-02 連携: persistLog が実装済みなら呼ぶ、未実装なら無視 */
function tryPersistLog_(level, funcName, message, detail) {
try {
if (typeof Utils.persistLog === 'function') {
Utils.persistLog(level, funcName, message, detail);
}
} catch (_) {}
}
C. メニュー追加(100_config/101_sys_config.js)
L352(🔧 開発・設定 メニュー .addToUi() 直後)と L365(🔧 マイグレーション メニュー直後)の間に新規メニューを追加:
// N-25: バックアップメニュー
try {
ui.createMenu('💾 バックアップ')
.addItem('📦 手動バックアップ実行', 'runManualBackup')
.addItem('⏰ 自動バックアップを有効化 (週次・検証)', 'installBackupTriggers')
.addItem('🔍 最新バックアップを検証', 'verifyLatestBackup')
.addItem('🛑 自動バックアップを停止', 'uninstallBackupTriggers')
.addToUi();
} catch (e) {}
Step 2: 週次自動トリガー + 月次昇格 + 世代管理
A. runWeeklyBackup() の追加(809_backup_tool.js)
Step 1 で実装済みの executeBackup_ を時間トリガー経由で呼び出す:
/** 週次自動バックアップ(時間トリガーから呼ばれる) */
function runWeeklyBackup() {
var FUNC = 'runWeeklyBackup';
try {
var result = executeBackup_(true); // 自動実行
Utils.logInfo(FUNC, result.message);
tryAuditLog_('RUN', '', '', FUNC, '', { fileId: result.fileId, fileName: result.fileName }, 'N-25 週次自動');
} catch (e) {
Utils.logError(FUNC, e);
tryPersistLog_('ERROR', FUNC, e.message, e.stack);
// 失敗通知(将来: N-08 メール / Slack 連携)
}
}
B. トリガー登録(重複防止が必須)
/** 自動バックアップ用トリガーを登録(既存削除 → 新規作成で重複防止) */
function installBackupTriggers() {
var FUNC = 'installBackupTriggers';
try {
// prod 環境のみ自動実行(dev は Drive 容量節約・誤データ保存回避)
if (Env.isDev()) {
SpreadsheetApp.getUi().alert(
'開発環境では自動バックアップは設定しません。本番環境(prod)で実行してください。'
);
return;
}
// 1. 既存トリガーを全削除(重複防止、必須要件)
var existing = ScriptApp.getProjectTriggers();
var removed = 0;
for (var i = 0; i < existing.length; i++) {
var fn = existing[i].getHandlerFunction();
if (fn === 'runWeeklyBackup' || fn === 'verifyLatestBackup') {
ScriptApp.deleteTrigger(existing[i]);
removed++;
}
}
// 2. 週次バックアップトリガー(日曜 AM2 時 JST)
ScriptApp.newTrigger('runWeeklyBackup')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.SUNDAY)
.atHour(2)
.inTimezone('Asia/Tokyo')
.create();
// 3. 月次検証トリガー(毎月 1 日 AM3 時 JST)
ScriptApp.newTrigger('verifyLatestBackup')
.timeBased()
.onMonthDay(1)
.atHour(3)
.inTimezone('Asia/Tokyo')
.create();
var msg = '自動バックアップを有効化しました。\n既存 ' + removed + ' 件を削除し、新規 2 件を登録。\n- 週次: 日曜 AM2時\n- 月次検証: 1日 AM3時';
Utils.logInfo(FUNC, msg);
tryAuditLog_('RUN', '', '', FUNC, '', { removed: removed, added: 2 }, 'N-25');
SpreadsheetApp.getUi().alert('✅ ' + msg);
} catch (e) {
Utils.logError(FUNC, e);
SpreadsheetApp.getUi().alert('🚨 トリガー登録失敗', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
}
}
/** 自動バックアップ用トリガーを全削除 */
function uninstallBackupTriggers() {
var FUNC = 'uninstallBackupTriggers';
try {
var existing = ScriptApp.getProjectTriggers();
var removed = 0;
for (var i = 0; i < existing.length; i++) {
var fn = existing[i].getHandlerFunction();
if (fn === 'runWeeklyBackup' || fn === 'verifyLatestBackup') {
ScriptApp.deleteTrigger(existing[i]);
removed++;
}
}
Utils.logInfo(FUNC, '削除: ' + removed + ' 件');
tryAuditLog_('RUN', '', '', FUNC, { removed: removed }, '', 'N-25');
SpreadsheetApp.getUi().alert('✅ 自動バックアップを停止しました(削除 ' + removed + ' 件)');
} catch (e) {
Utils.logError(FUNC, e);
SpreadsheetApp.getUi().alert('🚨 トリガー削除失敗', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
}
}
Step 3: バックアップ検証ジョブ
A. verifyLatestBackup() の追加(809_backup_tool.js)
/** 検証対象の主要シート(行数比較で健全性判定) */
var BACKUP_VERIFY_SHEETS_ = [
'11_mst_account',
'32_wrk_invoice',
'33_wrk_bank',
'42_trn_journal',
];
/** 検証閾値: 行数差分が ±5% 超なら WARN */
var BACKUP_VERIFY_THRESHOLD_ = 0.05;
/** 月次検証: 最新バックアップを開き、主要シート行数を元と比較 */
function verifyLatestBackup() {
var FUNC = 'verifyLatestBackup';
try {
var folderId = Env.backupFolderId();
if (!folderId) throw new Error('BACKUP_FOLDER_ID が未設定です');
var folder = DriveApp.getFolderById(folderId);
// 1. バックアップファイル一覧取得(作成日降順)
var candidates = [];
var files = folder.getFiles();
while (files.hasNext()) {
var f = files.next();
if (/^BizLP_[^_]+_backup_\d{8}(_[wm])(_\d+)?$/.test(f.getName())) candidates.push(f);
}
if (candidates.length === 0) {
var msg0 = 'バックアップファイルが 0 件です。自動バックアップが動作していない可能性があります。';
tryPersistLog_('WARN', FUNC, msg0, '');
SpreadsheetApp.getUi().alert('⚠️ ' + msg0);
return;
}
candidates.sort(function(a, b) { return b.getDateCreated() - a.getDateCreated(); });
var latest = candidates[0];
// 2. 最新バックアップを開く
var backupSs = SpreadsheetApp.openById(latest.getId());
var sourceSs = getWebSpreadsheet_();
// 3. 主要シートの行数比較
var warnings = [];
var report = [];
for (var i = 0; i < BACKUP_VERIFY_SHEETS_.length; i++) {
var name = BACKUP_VERIFY_SHEETS_[i];
var src = sourceSs.getSheetByName(name);
var bak = backupSs.getSheetByName(name);
if (!src || !bak) {
warnings.push(name + ': シートが見つからない (src=' + !!src + ', bak=' + !!bak + ')');
continue;
}
var srcRows = src.getLastRow();
var bakRows = bak.getLastRow();
var diff = srcRows === 0 ? 0 : Math.abs(srcRows - bakRows) / srcRows;
var status = diff <= BACKUP_VERIFY_THRESHOLD_ ? 'OK' : 'WARN';
report.push('- ' + name + ': src=' + srcRows + ' / bak=' + bakRows + ' / 差分=' + (diff * 100).toFixed(1) + '% [' + status + ']');
if (status === 'WARN') warnings.push(name + ' 差分 ' + (diff * 100).toFixed(1) + '%');
}
var summary = '検証対象: ' + latest.getName() + '\n\n' + report.join('\n');
Utils.logInfo(FUNC, summary);
if (warnings.length > 0) {
tryPersistLog_('WARN', FUNC, '検証で差分検知: ' + warnings.join(' / '), summary);
}
tryAuditLog_('RUN', '98_audit_log', latest.getId(), FUNC, '', { warnings: warnings.length, file: latest.getName() }, 'N-25 検証');
SpreadsheetApp.getUi().alert(warnings.length === 0 ? '✅ 検証OK' : '⚠️ 検証で差分検知', summary, SpreadsheetApp.getUi().ButtonSet.OK);
} catch (e) {
Utils.logError(FUNC, e);
tryPersistLog_('ERROR', FUNC, e.message, e.stack);
SpreadsheetApp.getUi().alert('🚨 検証失敗', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
}
}
Step 4: テスト追加 + CLAUDE.md 更新 + _config.json / changelog 登録
A. テスト追加(900_test/901_test_runner.js)
runAllTests の末尾付近に以下を追加:
function testN25ManualBackup_() {
var FUNC = 'testN25ManualBackup_';
try {
// Env にフォルダIDが未設定なら SKIP
if (!Env.backupFolderId()) {
addResult_('N25-01', 'N-25 手動バックアップ実行', true, 'SKIP', 'BACKUP_FOLDER_ID未設定のためスキップ', 'dev環境ではBACKUP_FOLDER_IDを事前設定すること');
return;
}
// 実行(例外が出なければ PASS)
runManualBackup();
addResult_('N25-01', 'N-25 手動バックアップ実行', true, '例外なし', '正常完了', '');
} catch (e) {
addResult_('N25-01', 'N-25 手動バックアップ実行', false, '例外なし', e.message, FUNC);
}
}
runAllTests 本体の呼び出し列に testN25ManualBackup_(); を追加。
B. CLAUDE.md 更新
「GAS ファイル番号体系」セクションの 800_ops/ 行と「マイグレーションスクリプト運用ガイドライン」の番号体系欄に 809 を追加:
| `800_ops/` | 運用・マイグレーション | `801_migration_v1_to_v2`, `802_audit`, `803_sync_tool`, `804_migration_d01_d03`, `805_migration_d04_d06`, `806_cleanup_empty_rows`, `807_migration_i10`, `808_migration_i24`, `809_backup_tool` |
マイグレーション運用ガイドライン:
| 番号体系 | 804-808 はマイグレーション用で使用済み。809 はバックアップ基盤 (N-25) で使用。次のマイグレーションは 810 から |
C. docs/_config.json 追記
§E.1 基盤・DevOps に追加:
{ "file": "dev/dev_mas-201_sheet_backup.md", "title": "E.1.9 N-25 スプレッドシート定期バックアップ" }
D. docs/_internal/changelog.md 先頭に追記:
| 2026-04-17 | [dev_mas-201_sheet_backup.md](dev_mas-201_sheet_backup.md) | 初版作成。809_backup_tool.js 新設、DriveApp.makeCopy ベースの週次12/月次12世代管理、月末週の月次昇格、行数差分±5%検証、Env.backupFolderId追加の4ステップ設計 |
影響範囲
| Step | ファイル | 変更量 | 既存動作への影響 |
|---|---|---|---|
| 1 | 000_infra/001_env.js | 約10行追加 | Env名前空間に backupFolderId / setBackupFolderId を追加。既存関数変更なし |
| 1 | 800_ops/809_backup_tool.js | 約120行(新規) | 新規ファイル、既存動作への影響なし |
| 1 | 100_config/101_sys_config.js | 約8行追加 | 💾 バックアップ メニュー追加のみ。既存メニュー変更なし |
| 2 | 800_ops/809_backup_tool.js | 約80行追加 | トリガー登録/削除関数 + 週次実行関数 |
| 3 | 800_ops/809_backup_tool.js | 約60行追加 | 検証ジョブ関数 |
| 4 | 900_test/901_test_runner.js | 約15行追加 | テストケース追加。既存テスト影響なし |
| 4 | CLAUDE.md | 2箇所修正 | ファイル番号体系に 809 追記 |
| 4 | docs/_config.json | 1行追加 | §E.1 にナビ項目追加 |
| 4 | docs/_internal/changelog.md | 1行追加 | 初版作成行を先頭に追加 |
注意事項
- トリガー重複防止:
installBackupTriggers冒頭で既存のrunWeeklyBackup/verifyLatestBackupハンドラのトリガーを全削除してから新規作成。これを怠るとトリガーが累積し、毎週実行回数が増えていく(過去パターン: GAS 時間トリガー重複) - Utils.auditLog / Utils.persistLog の未実装:
typeof === 'function'ガードを通すtryAuditLog_/tryPersistLog_ラッパー経由で呼び、MAS-178 / MAS-179 実装完了前でも動作する - 循環参照防止:
tryAuditLog_は catch で握りつぶし、MAS-179 仕様書の「auditLog 内部で console.error フォールバック」設計と整合 - dev 環境のトリガー:
installBackupTriggersはEnv.isDev()で早期 return。dev で自動バックアップは不要(Drive 容量節約、テストデータの誤保存回避) DriveApp.getFolderByIdのエラーハンドリング: 既存の502_receipt_reader.jsL34-37 パターンを踏襲(try-catch で「フォルダが見つかりません」メッセージ)- ファイル名衝突: 同日に複数回実行された場合、
_2,_3, ... の連番を付与。既存ファイルの上書きは行わない SpreadsheetApp.copy()ではなくDriveApp.makeCopy(): SpreadsheetApp.copy は同一フォルダ制約、別フォルダ配置には DriveApp の makeCopy 第2引数が正解- ENV 識別子の付加: ファイル名
BizLP_{env}_backup_YYYYMMDD_[wm]で dev と prod のファイルが同一フォルダに混在しても判別可能(運用上は環境別フォルダ推奨) - 月末週判定の簡易化: 「実行日から 7 日以内に月が変わるか」で判定。カレンダーの月末土曜を正確に判定するのではなく、「実行週が当月最終週なら
_m」で十分 - Drive スコープの承認ダイアログ:
appsscript.jsonはdependencies: {}空でoauthScopes未指定。初回DriveApp使用時に承認ダイアログが出るのでユーザーに事前告知 - 6 分制限:
DriveApp.makeCopy()は Google サーバー側で処理されクライアント時間は数秒〜30 秒。通常は収まるが、大規模化時はSheets API v4経由のエクスポートへの移行を検討(将来の拡張) - 新規ファイル追加時の
.claspignore確認:809_backup_tool.jsが除外されていないかclasp statusで push 対象を検証(MAS-096 失敗パターン回避) - 復元操作は必ず人間が実行: 自動復元ロジックは設けない。Human-in-the-Loop ポリシー遵守
エッジケース
| ケース | 期待動作 | 理由 |
|---|---|---|
BACKUP_FOLDER_ID 未設定 | 警告「Env.setBackupFolderId でフォルダIDを設定してください」 + 処理中断 | 初回セットアップ未完了 |
DriveApp.getFolderById() throw(削除・権限喪失) | catch + persistLog ERROR + ユーザーアラート | フォルダ不正時の明確な通知 |
DriveApp.makeCopy() 6 分超過 | GAS 自動中断。次回週次実行で再試行(部分ファイルは次回 cleanup で trash) | 大規模スプレッドシート対策 |
| 同日 2 回手動実行 | 2 ファイル目以降は ..._w_2, ..._w_3 と連番付与 | ファイル名衝突回避 |
| 時間トリガー失火(GAS 既知問題) | 次回検証ジョブがバックアップ欠損を WARN 検知 | 可用性担保 |
| Drive 容量不足 | makeCopy throw → catch + persistLog ERROR | 予期される障害、明確通知 |
| バックアップ先が別 Drive(共有ドライブ等) | makeCopy 第2引数で動作、問題なし | 正常動作 |
| 月末週実行 | _m で直接保存、_w の複製はしない | 重複回避 |
| 世代管理で削除対象 0 件 | スキップ、警告なし | 初期状態(12件未満) |
setTrashed(true) 権限エラー | catch + Utils.logInfo WARN、他ファイル削除は継続 | 堅牢性(他ファイルの処理を止めない) |
| 検証ジョブでバックアップ 0 件 | persistLog WARN + アラート「自動バックアップが動作していない可能性」 | 可用性の早期検知 |
| 行数差分 > 5% | WARN + 詳細ログ(シート名・src/bak 行数・差分率) | 異常検知 |
dev 環境で installBackupTriggers | アラート「開発環境では設定しません」で早期 return | Drive 容量節約・誤データ保存回避 |
Session.getActiveUser() 空 | tryAuditLog_ 内で呼ぶ MAS-179 auditLog 実装 (もしあれば) が 'SYSTEM' フォールバック | MAS-179 エッジケース整合 |
Utils.auditLog / Utils.persistLog 未実装 | typeof === 'function' ガードで黙って無視、実行継続 | MAS-178/MAS-179 未実装状態でも本案件は単独動作 |
installBackupTriggers で既存トリガー 2 件以上存在 | 全削除してから再登録。重複累積を防止 | 必須要件(トリガー重複 = 複数回実行) |
| ファイル名パターン外のファイルが同フォルダ内に存在 | cleanupOldBackups_ の正規表現で対象外となり trash されない | 誤削除防止 |
実データ検証(MCP 事前確認項目)
実装前後に以下を確認する:
- Drive 容量の現状確認: 対象スプレッドシートのファイルサイズを Drive UI で確認し、
サイズ × 24 (週次12 + 月次12)で総容量試算(現状 3 MB × 24 ≒ 72 MB、将来成長を見越して 1-2 GB まで許容) - バックアップ先フォルダの事前作成: Drive で専用フォルダ
BizLP_Backups_Prod/BizLP_Backups_Devを手動作成、フォルダ ID をメモ。GAS からEnv.setBackupFolderId(id)を手動実行して保存 DriveApp.makeCopy()所要時間: dev 環境でrunManualBackup()を手動実行し、ログで実行時間を記録(数秒〜30 秒想定)- トリガー登録確認:
installBackupTriggers()実行後、GAS エディタの「トリガー」画面でrunWeeklyBackup週次 /verifyLatestBackup月次の 2 件が登録されているか目視確認 ScriptApp.getProjectTriggers()と実行履歴の突合: 週次実行後にconsole.log(ScriptApp.getProjectTriggers().map(t => ({fn: t.getHandlerFunction(), type: t.getEventType()})))で確認appsscript.jsonの Drive スコープ: 現状oauthScopes未指定。初回 DriveApp 使用時に承認ダイアログが出るので、prod デプロイ前に dev で承認完了しておく- MAS-178 / MAS-179 実装状況:
grep -n "auditLog\|persistLog" 000_infra/004_utils.jsで本案件実装時点の実装有無を確認。未実装ならtryXxx_ラッパーで動作、実装後は自動連携
復元手順
バックアップからの復元は必ず人間が実行する(自動復元なし、Human-in-the-Loop 遵守)。
- Drive でバックアップファイルを特定:
BizLP_Backups_Prodフォルダ内のBizLP_prod_backup_YYYYMMDD_[wm]から復元したい日付のファイルを選択 - コピーを作成: Drive UI で右クリック →「コピーを作成」→ 新しいスプレッドシートを生成。原本は残しておく(万一の切り戻し用)
- スクリプトプロパティを更新: GAS エディタで
SPREADSHEET_IDプロパティを新スプレッドシートの ID に更新 (File → Project Settings → Script Properties) - スキーマ整合性の確認: 新スプレッドシートで
setupAllSchemasを実行し、全シートの DDL が最新定義と整合するか確認。必要に応じてマイグレーション(804-808)も再実行
注意: 復元中は自動バックアップを uninstallBackupTriggers() で一時停止し、切り戻し完了後に再度 installBackupTriggers() で有効化すること。
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| MAS-178 エラーハンドリング | Utils.persistLog / 99_error_log 連携。未実装なら tryPersistLog_ で黙って無視 |
| MAS-179 監査証跡 | Utils.auditLog 連携。バックアップ実行を RUN として記録。未実装なら tryAuditLog_ で黙って無視 |
| MAS-199 prod→dev データ同期 | 別系統の DevOps ツール。本案件とは統合しない(setValues vs makeCopy の方式差) |
| MAS-195 pre-push hook | 同カテゴリ(DevOps)の仕様書フォーマット参考 |
| CLAUDE.md | GAS ファイル番号体系、Env モジュール規約、ワークスペース担当マトリクス、デプロイフロー |
人間が検討すべき事項
- バックアップ先フォルダ権限: 経営者+経理のみ閲覧可、他ユーザーには非表示が推奨(個人情報保護の観点)
- Drive 容量見積り: 現行 3 MB × 24 バックアップ ≒ 72 MB。成長織り込みで 1-2 GB まで許容するか要判断
- バックアップ時刻の最終確定: 日曜 AM2 時(業務影響なし)が妥当か、別時刻の希望があるか
- 月次昇格ルール: 「月末週の週次を月次化」(本仕様書の方針、GAS 実行回数節約) vs 「月初 1 日に別途月次取得」
- 検証ジョブの通知先: WARN 検知時の通知手段(メール / 将来の Slack 連携)。現状はアラート表示のみ
- 復元手順書の置き場所: 本仕様書内 vs 別ファイル
docs/ops/restore_guide.md。本仕様書では内包方式を採用 - バックアップの暗号化: Drive 上で Google 側暗号化済み。追加暗号化は不要と判断してよいか
- 退職者対応: バックアップフォルダへのアクセス権剥奪フロー(MAS-200 個人情報保護と連動)
- 原本スプレッドシートの大規模化時の対応:
DriveApp.makeCopyが 6 分制限に抵触する規模になった場合の移行先(Sheets API v4 Export / GCP Cloud Storage 等)
実装プロンプト(Claude Code 用)
Step 1: Env 拡張 + 手動バックアップ基盤
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-201「スプレッドシート定期バックアップ」の Step 1(Env 拡張 + 手動バックアップ基盤)を実装してください。
## 実行前タスク
以下のファイルを読み込んでください:
1. `docs/dev/dev_mas-201_sheet_backup.md` — 本仕様書全体(特に「修正方針 Step 1」とエッジケース)
2. `000_infra/001_env.js` — Env.receiptFolderId / setReceiptFolderId (L57-64) の現在の実装。同型で backupFolderId / setBackupFolderId を追加
3. `500_import/502_receipt_reader.js` L25-45 — DriveApp.getFolderById のエラーハンドリングパターン
4. `100_config/101_sys_config.js` L346-385 — メニュー構造(🔧 開発・設定 / 🔧 マイグレーション / 🔄 開発用 / 🧪 テスト)。新メニュー `💾 バックアップ` の配置位置
5. `000_infra/004_utils.js` — Utils.logInfo / logError / toastResult の既存パターン
6. `CLAUDE.md` — GAS ファイル番号体系、Env モジュール規約
## 修正対象ファイル
- `000_infra/001_env.js` への追記のみ(backupFolderId / setBackupFolderId 追加)
- `800_ops/809_backup_tool.js` の新規作成
- `100_config/101_sys_config.js` への追記のみ(メニュー1ブロック追加)
## 実装内容
### A. Env 拡張(001_env.js)
Env 名前空間の `setReceiptFolderId` (L62-64) の直下に以下を追加:
backupFolderId: function () {
return _getProps().getProperty('BACKUP_FOLDER_ID') || '';
},
setBackupFolderId: function (id) {
_getProps().setProperty('BACKUP_FOLDER_ID', id);
},
### B. 809_backup_tool.js の新規作成
仕様書「修正方針 Step 1-B」のコードを一字一句そのまま新規ファイル `800_ops/809_backup_tool.js` に配置。以下を厳守:
- `runManualBackup()` / `executeBackup_(force)` / `isMonthEndWeek_(date)` / `cleanupOldBackups_(folder, keepWeekly, keepMonthly)` / `tryAuditLog_(...)` / `tryPersistLog_(...)` の 6 関数
- ファイル名パターン: `BizLP_{Env.name()}_backup_YYYYMMDD_[wm]`
- 同日衝突時の連番付与(`_2`, `_3`, ...)
- 月末週判定は `isMonthEndWeek_`(実行日 +7 日で月が変わるか)
- 世代管理は週次 12 / 月次 12、超過は setTrashed
- MAS-178 `Utils.persistLog` / MAS-179 `Utils.auditLog` は `typeof === 'function'` ガード経由
### C. メニュー追加(101_sys_config.js)
L352(🔧 開発・設定の `.addToUi()`)の直後、🔧 マイグレーション (L356) の直前に以下を挿入:
// MAS-201: バックアップメニュー
try {
ui.createMenu('💾 バックアップ')
.addItem('📦 手動バックアップ実行', 'runManualBackup')
.addItem('⏰ 自動バックアップを有効化 (週次・検証)', 'installBackupTriggers')
.addItem('🔍 最新バックアップを検証', 'verifyLatestBackup')
.addItem('🛑 自動バックアップを停止', 'uninstallBackupTriggers')
.addToUi();
} catch (e) {}
Step 2 以降の関数(installBackupTriggers / verifyLatestBackup / uninstallBackupTriggers)は Step 2-3 で追加するため、Step 1 時点では未定義。onOpen が try-catch 済みなのでメニュー登録自体は問題ない。ただし実行すると ReferenceError になる点をユーザーに告知する必要あり。
## 制約
- 既存の Env.receiptFolderId / setReceiptFolderId / logInfo / logError のシグネチャは変更しない
- 803_sync_tool.js は一切変更しない(別系統)
- MAS-178 / MAS-179 の Utils 実装本体は作らない。条件付き呼び出しのみ
- appsscript.json は Step 1 では変更しない(Drive スコープは初回実行時の承認ダイアログで付与)
## エッジケース(実装で必ずカバー)
| ケース | 期待動作 |
|--------|---------|
| BACKUP_FOLDER_ID 未設定 | Error throw、ダイアログ表示 |
| getFolderById throw | catch + 明確なエラーメッセージ |
| 同日 2 回実行 | 連番 `_2`, `_3` で衝突回避 |
| 月末週実行 | `_m` で直接保存 |
| cleanupOldBackups で対象 0 件 | スキップ |
| setTrashed 権限エラー | catch + logInfo WARN、他ファイル継続 |
| Utils.auditLog 未実装 | tryAuditLog_ で黙って無視 |
## 実データ検証
実装後、GAS エディタで以下を手動確認:
1. Drive で事前に `BizLP_Backups_Dev` フォルダを作成し、フォルダIDをメモ
2. GAS 関数 `Env.setBackupFolderId('<フォルダID>')` を手動実行
3. `runManualBackup()` を手動実行 → 初回 Drive 承認ダイアログで許可 → バックアップフォルダにファイル `BizLP_dev_backup_YYYYMMDD_w` が生成されること
4. 同じ関数を再度実行 → `..._w_2` が生成されること(連番動作確認)
5. スプレッドシートメニューに `💾 バックアップ` が追加されていること
## 動作確認
1. `npm run push:dev` で dev 環境にデプロイ
2. GAS エディタで Env.setBackupFolderId 実行
3. メニュー「💾 バックアップ → 📦 手動バックアップ実行」をクリック
4. Drive で指定フォルダに `BizLP_dev_backup_YYYYMMDD_w` が作成されることを確認
5. 同日 2 回目実行で `..._w_2` が作成されることを確認
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| A. Env 拡張 | なし | receiptFolderId と同型の定型追加 |
| B. 809_backup_tool.js 新規 | あり | 月末週判定・衝突連番・世代管理の複合ロジック |
| C. メニュー追加 | なし | 定型 addItem のみ |
Step 2: 週次自動トリガー + 世代管理完成
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-201 の Step 2(週次自動トリガー + 世代管理完成)を実装してください。
## 前提
Step 1 が完了し、`runManualBackup / executeBackup_ / cleanupOldBackups_` が動作可能であること。
## 実行前タスク
1. `docs/dev/dev_mas-201_sheet_backup.md` — 本仕様書「修正方針 Step 2」
2. `800_ops/809_backup_tool.js` — Step 1 で作成済みの関数群
3. `000_infra/001_env.js` — Env.isDev / isProd
4. GAS 公式ドキュメント: `ScriptApp.newTrigger` / `timeBased` / `onWeekDay` / `onMonthDay` / `inTimezone`
## 修正対象ファイル
- `800_ops/809_backup_tool.js` への追記のみ(runWeeklyBackup / installBackupTriggers / uninstallBackupTriggers の 3 関数)
## 実装内容
仕様書「修正方針 Step 2-A」「2-B」のコードを一字一句そのまま 809_backup_tool.js に追記。以下を厳守:
- `runWeeklyBackup()`: executeBackup_(true) 呼び出し + try/catch + tryAuditLog_ / tryPersistLog_
- `installBackupTriggers()`: Env.isDev() ガードで早期 return → 既存トリガー全削除(runWeeklyBackup / verifyLatestBackup 両方のハンドラ対象)→ 2 件新規登録
- `uninstallBackupTriggers()`: 両ハンドラのトリガーを全削除、件数をアラート表示
- 週次: 日曜 AM2 時 JST (`inTimezone('Asia/Tokyo')`)
- 月次検証: 毎月 1 日 AM3 時 JST
## 制約
- 既存トリガー削除は必ず「ハンドラ関数名で一致するもの」のみ対象(他プロジェクトのトリガーは触らない)
- Env.isDev() が true なら installBackupTriggers は実行せずアラートのみ
- 週次ハンドラは `runWeeklyBackup`、月次検証ハンドラは `verifyLatestBackup`(Step 3 で実装される予定、Step 2 時点では名前予約のみ)
- verifyLatestBackup が未定義の状態で installBackupTriggers を実行すると、週次トリガー作成時点でエラーにはならない(Google 側は関数名の存在チェックをせずに登録可能)
## エッジケース
| ケース | 期待動作 |
|--------|---------|
| 既存トリガーが 0 件 | スキップ、新規 2 件登録のみ |
| 既存トリガーが 5 件 | 対象ハンドラ名のみ削除、他は維持 |
| Env.isDev() === true | アラート表示、トリガー登録しない |
| uninstallBackupTriggers 実行時にトリガー 0 件 | アラート「削除 0 件」 |
## 実データ検証
1. dev 環境で Env.isDev() を確認(trueのはず)→ installBackupTriggers 実行 → アラート「開発環境では設定しません」が出ること
2. prod 環境で installBackupTriggers 実行 → GAS エディタ「トリガー」画面で 2 件登録されていること
3. prod 環境で uninstallBackupTriggers 実行 → 2 件削除されていること
## 動作確認
1. `npm run push:dev` → dev で installBackupTriggers 実行 → アラート「開発環境では設定しません」表示
2. `npm run push:prod` → prod で installBackupTriggers 実行 → GAS エディタでトリガー 2 件登録確認
3. 同じ prod で再度 installBackupTriggers 実行 → 既存 2 件削除 + 新規 2 件登録(累積しないこと)
4. prod で uninstallBackupTriggers 実行 → 2 件削除確認
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| A. runWeeklyBackup | なし | executeBackup_ ラッパー |
| B. installBackupTriggers | あり | 既存削除→新規作成の順序・ハンドラ名一致判定・Env.isDev ガード |
| C. uninstallBackupTriggers | なし | 削除ループの定型 |
Step 3: バックアップ検証ジョブ
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-201 の Step 3(バックアップ検証ジョブ)を実装してください。
## 前提
Step 1, Step 2 が完了していること。
## 実行前タスク
1. `docs/dev/dev_mas-201_sheet_backup.md` — 本仕様書「修正方針 Step 3」
2. `800_ops/809_backup_tool.js` — Step 1-2 で実装済みの関数群
3. `000_infra/004_utils.js` — Utils.logInfo / logError
4. `000_infra/001_env.js` — Env.backupFolderId / spreadsheetId
## 修正対象ファイル
- `800_ops/809_backup_tool.js` への追記のみ(verifyLatestBackup 関数 + 定数 2 個)
## 実装内容
仕様書「修正方針 Step 3-A」のコードを一字一句そのまま 809_backup_tool.js に追記。以下を厳守:
- `BACKUP_VERIFY_SHEETS_` 定数: `['11_mst_account', '32_wrk_invoice', '33_wrk_bank', '42_trn_journal']`
- `BACKUP_VERIFY_THRESHOLD_` 定数: 0.05(5%)
- `verifyLatestBackup()`: フォルダスキャン → 最新ファイル特定 → SpreadsheetApp.openById → 4 シートの getLastRow 比較 → WARN 検知時は persistLog + アラート
- バックアップファイル 0 件なら persistLog WARN + 可用性警告
- 差分 > 5% なら WARN、詳細レポートをアラートに表示
## 制約
- 検証対象シートは BACKUP_VERIFY_SHEETS_ に限定(全シート検証はコスト過大)
- src / bak のどちらかのシートが存在しない場合は警告扱い(エラーで停止しない)
- 閾値 5% は BACKUP_VERIFY_THRESHOLD_ 定数で制御(後日変更容易に)
- バックアップファイル 0 件でも例外は throw しない(アラートのみ)
## エッジケース
| ケース | 期待動作 |
|--------|---------|
| バックアップファイル 0 件 | persistLog WARN + アラート、return |
| 主要シートが src または bak で存在しない | warnings に追加、他シートは継続 |
| srcRows === 0 | diff = 0(ゼロ除算回避) |
| 差分 ≤ 5% | status = 'OK' |
| 差分 > 5% | status = 'WARN'、warnings に追加 |
| 全シート OK | アラート「✅ 検証OK」 |
| 1 シート以上 WARN | アラート「⚠️ 検証で差分検知」 |
## 実データ検証
1. Step 1 で作成済みの dev バックアップを開き、主要 4 シートの getLastRow が元と一致するか確認
2. verifyLatestBackup 手動実行 → アラートで src/bak 行数と差分率が表示されること
3. 意図的に元スプレッドシートの 32_wrk_invoice に 1 行追加してから verify → 差分率が表示されること(5% 未満なら OK のまま)
## 動作確認
1. `npm run push:dev` → dev で verifyLatestBackup 実行
2. アラートで 4 シートの src/bak/差分率が全て表示されること
3. 差分率が全て 0% または 5% 未満であること(バックアップ直後なら 0% 近い)
4. 元スプレッドシートで大量削除を再現してから verify → WARN 検知 + persistLog 記録(MAS-178 未実装なら console.error)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| A. verifyLatestBackup | あり | フォルダスキャン・最新特定・行数比較・閾値判定の複合ロジック |
Step 4: テスト追加 + CLAUDE.md 更新 + ナビ登録
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-201 の Step 4(テスト + CLAUDE.md + _config.json / changelog 登録)を実装してください。
## 前提
Step 1-3 が完了していること。
## 実行前タスク
1. `docs/dev/dev_mas-201_sheet_backup.md` — 本仕様書「修正方針 Step 4」
2. `900_test/901_test_runner.js` — addResult_ / writeResults_ / runAllTests の既存パターン
3. `CLAUDE.md` — GAS ファイル番号体系セクションと「マイグレーション運用ガイドライン」の番号体系欄
4. `docs/_config.json` — §E.1 の既存エントリ
5. `docs/_internal/changelog.md` — 先頭行の追記形式
## 修正対象ファイル
- `900_test/901_test_runner.js` への追記のみ(testN25ManualBackup_ 関数 + runAllTests への登録)
- `CLAUDE.md` への追記のみ(2 箇所)
- `docs/_config.json` への追記のみ(§E.1 に 1 行)
- `docs/_internal/changelog.md` への追記のみ(先頭に 1 行)
## 実装内容
### A. テスト追加
仕様書「修正方針 Step 4-A」の testN25ManualBackup_ を 901_test_runner.js 末尾に追加し、runAllTests 本体のテスト呼び出し列に `testN25ManualBackup_();` を追加。
BACKUP_FOLDER_ID 未設定なら SKIP 扱い(CI 環境や未設定プロジェクトで失敗しないため)。
### B. CLAUDE.md 更新
「GAS ファイル番号体系 (Modular Monolith)」セクションの `800_ops/` 行に `809_backup_tool` を追加:
| `800_ops/` | 運用・マイグレーション | `801_migration_v1_to_v2`, `802_audit`, `803_sync_tool`, `804_migration_d01_d03`, `805_migration_d04_d06`, `806_cleanup_empty_rows`, `807_migration_i10`, `808_migration_i24`, `809_backup_tool` |
「マイグレーションスクリプト運用ガイドライン」の番号体系欄を更新:
| 番号体系 | 804-808 はマイグレーション用で使用済み。809 はバックアップ基盤 (MAS-201) で使用。次のマイグレーションは 810 から |
### C. _config.json 追記
§E.1 基盤・DevOps セクション末尾に追加:
{ "file": "dev/dev_mas-201_sheet_backup.md", "title": "E.1.9 MAS-201 スプレッドシート定期バックアップ" }
### D. changelog 追記
先頭行(ヘッダー直後)に追加:
| 2026-04-17 | [dev_mas-201_sheet_backup.md](dev_mas-201_sheet_backup.md) | 初版作成。809_backup_tool.js 新設、DriveApp.makeCopy ベースの週次12/月次12世代管理、月末週の月次昇格、行数差分±5%検証、Env.backupFolderId追加の4ステップ設計 |
## 制約
- 既存テストの pass/fail は壊さない(testN25ManualBackup_ は SKIP 可能な設計)
- CLAUDE.md のワークスペース担当マトリクス等、他の規約文は触らない
- _config.json の §E.1 以外のセクションは触らない
- changelog は先頭行追記のみ、既存行は変更しない
## エッジケース
| ケース | 期待動作 |
|--------|---------|
| BACKUP_FOLDER_ID 未設定 | テスト SKIP(PASS 扱い、備考で指摘) |
| runManualBackup 成功 | テスト PASS |
| runManualBackup 失敗(Drive エラー等) | テスト FAIL、エラーメッセージを記録 |
## 実データ検証
1. dev で runAllTests 実行 → testN25ManualBackup_ が SKIP または PASS すること
2. 90_test_results シートに `N25-01` 行が追加されていること
3. CLAUDE.md の変更が GitHub PR で正しくレンダリングされること
4. docs サイト (Cloudflare Pages) で §E.1.9 MAS-201 がサイドバーに表示されること
## 動作確認
1. `npm run push:dev` で dev 環境にデプロイ
2. メニュー「🧪 テスト → 全テスト実行 (runAllTests)」実行
3. 90_test_results で N25-01 行が SKIP または PASS で記録されること
4. GitHub で docs/_config.json の diff が §E.1 末尾に 1 行追加のみであること
5. changelog.md の先頭に MAS-201 行が追加されていること
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| A. テスト追加 | なし | addResult_ パターンの定型 |
| B. CLAUDE.md 更新 | なし | 2 箇所テキスト追加のみ |
| C. _config.json 追記 | なし | 1 行 JSON 追加 |
| D. changelog 追記 | なし | 1 行 Markdown 追加 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 複数ファイル横断の設計、世代管理アルゴリズム、トリガー設計、15 ケースのエッジケース網羅、MAS-178/MAS-179 連携の条件付き設計 |
| Step 1 実装(Env 拡張 + 手動バックアップ) | Claude Sonnet 4.6 | 月末週判定・衝突連番・世代管理の複合ロジック。Env.receiptFolderId 同型パターンだが実装量は中程度 |
| Step 2 実装(週次トリガー + 世代管理) | Claude Opus 4.6 | トリガー重複防止の順序(既存削除 → 新規作成)、ハンドラ名一致判定、Env.isDev ガードの慎重な設計 |
| Step 3 実装(検証ジョブ) | Claude Sonnet 4.6 | フォルダスキャン + 行数比較の標準的ロジック。閾値判定は定数化済み |
| Step 4 実装(テスト + CLAUDE.md + ナビ) | Claude Haiku 4.5 | ドキュメント整形とテスト追加のみ、判断要素なし |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-17 | 初版作成。DriveApp.makeCopy ベースの独立バックアップ基盤。週次 12 / 月次 12 世代管理、月末週の月次昇格、行数差分 ±5% 検証、Env.backupFolderId 追加、トリガー重複防止、MAS-178/MAS-179 条件付き連携の 4 ステップ設計 |
| 2026-04-20 | 実装完了 (PR #209)。全 4 ステップ dev 検証済み。prod デプロイ後は BACKUP_FOLDER_ID プロパティ設定と installBackupTriggers 実行が必要 |
| 2026-04-28 | MAS-205 由来の特権チェック追加 + tryAuditLog_ バグ修正の追従更新。実装監査の結果、本仕様書 v1.0 起票後に以下の追加変更が 800_ops/809_backup_tool.js に施されていたことを確認。(1) 特権チェック追加: runManualBackup (L11-15) / installBackupTriggers (L138-142) / uninstallBackupTriggers (L192-196) の 3 関数冒頭に if (!isPrivilegedUser_()) { ... return; } ガードを追加 (MAS-205 Defense in Depth・PR #MAS-205 系で導入)。本 MAS-201 spec v1.0 には記載がなかったが、MAS-205 spec L240-241 に対応する正常な拡張。(2) tryAuditLog_ 引数数のバグ修正: spec L164-170 で「7 引数 (operation, targetSheet, targetId, funcName, before, after, note)」と記載していたが、実装は Utils.auditLog の正しいシグネチャに合わせて 8 引数 (operation, targetSheet, targetId, '', funcName, before, after, note) に修正済 (実装 L103-110・コメント「Utils.auditLog は 8 引数 (targetCol を含む)。targetCol='' を明示して列ずれを防止」)。spec 側の引数数記載は誤りだったため、実装が正しい。docs-only 追記で prod 自動デプロイへの影響なし。 |
仕様書作成プロンプト(再現性・監査性のため記録)
展開して表示(この仕様書を生成する際に Claude Code に投入したプロンプト全文)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 N-25「スプレッドシート定期バックアップ」の開発仕様書を作成してください。
開発仕様書を新規作成した場合は、`docs/_config.json` の `nav` 配列の「§E.1 基盤・DevOps」セクションに必ず追記してください。
---
## ⚠️ キャッシュ依存の抑制ルール(最優先)
**このプロンプトを実行する際、以下を厳格に守ること:**
1. **既存知識・直前の会話履歴・記憶に依存した推測で書かない**。本タスクに関わるすべての情報は、**下記「実行前タスク」に列挙したファイルを Read/Grep ツールで改めて取得してから**判断すること
2. **ファイル内容・行番号・関数名・変数名は、すべてツールの実測値**で記述すること。「〜のはず」「〜と記憶している」で書かない
3. **`git log -1` / `git status` を実行**し、現在のブランチと最新コミットを確認してから作業開始(main から古い状態で書かないため)
4. TODO_future.md の N-25 行は必ず**現物を Grep して転記**すること。以前のプロンプト出力文面を再利用しない
5. 既存実装の前提(Utils.persistLog / Utils.auditLog の実装状況、既存トリガー有無等)は**Grep で実コード確認**してから書く。仕様書側の記述だけで判断しない
6. **Context に既にある情報で回答を短縮しない**。省略せず、必要なファイルは全件読み直す
この抑制ルールに違反する最も危険なパターン:
- N-02 / N-03 が「実装済み」と書いてしまう(実際は仕様書のみで、Utils.persistLog / auditLog が未実装の可能性)
- L番号が過去時点の古い値のまま記述される
- Env.backupFolderId が既に存在するかのような書き方になる(実際は新設)
---
## 案件サマリー(TODO_future.md から Grep で転記すること)
(以下略。上記プロンプトの全文を含む。実際のファイルでは完全に記載)
## 実行前タスク / 既存実装の前提知識 / 仕様書の出力先 / 固有の設計要件
## A. アーキテクチャの決定事項(9項目) / B. エッジケース(15ケース) / C. プロダクトポリシー / D. スコープ境界 / E. 実データ検証(7項目) / F. 人間が検討すべき事項(8項目)
## 出力品質の基準(9項目) / 最終チェックリスト(15項目 + キャッシュ抑制遵守痕跡)
投入日時: 2026-04-17 テンプレートバージョン: v1.6(「仕様書作成プロンプト」セクション必須化対応、PR #138 で同時追加) キャッシュ抑制版: ⚠️ルール 6 項目により、ファイル実測値・Grep 結果・実装状況の未実装判定が担保されている