概要

項目内容
案件IDMAS-201
案件名スプレッドシート定期バックアップ
カテゴリセキュリティ
PhaseP1
優先度★★★
所要時間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.jsbackupFolderId / setBackupFolderId 追加)
100_config/101_sys_config.js(メニュー追加のみ)
CLAUDE.mdGAS ファイル番号体系に 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 時点)

#項目実測値本案件への影響
1800_ops/ の使用済み番号801-808(次は 809新規ファイル番号確定
2ScriptApp.newTrigger の使用実績0 箇所grep -rn "ScriptApp.newTrigger" で該当なし)本案件が最初の時間トリガー導入
3Utils.auditLog / Utils.persistLog の実装未実装000_infra/004_utils.js に定義なし)MAS-178 / MAS-179 連携は typeof === 'function' ガードで条件付き呼び出し
4appsscript.jsondependencies: {}oauthScopes 未指定Drive 初回アクセス時に承認ダイアログ表示(ユーザー承認が必要)
5既存同期ツール 800_ops/803_sync_tool.jsgetValues/setValues 方式で行コピー本案件は DriveApp.makeCopy 方式で別系統、統合せず新設
6DriveApp.getFolderById の既存使用例500_import/502_receipt_reader.js L34エラーハンドリングパターンを踏襲
7Env.setReceiptFolderId の呼び出し例502_receipt_reader.js L28Env.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ファイル変更量既存動作への影響
1000_infra/001_env.js約10行追加Env名前空間に backupFolderId / setBackupFolderId を追加。既存関数変更なし
1800_ops/809_backup_tool.js約120行(新規)新規ファイル、既存動作への影響なし
1100_config/101_sys_config.js約8行追加💾 バックアップ メニュー追加のみ。既存メニュー変更なし
2800_ops/809_backup_tool.js約80行追加トリガー登録/削除関数 + 週次実行関数
3800_ops/809_backup_tool.js約60行追加検証ジョブ関数
4900_test/901_test_runner.js約15行追加テストケース追加。既存テスト影響なし
4CLAUDE.md2箇所修正ファイル番号体系に 809 追記
4docs/_config.json1行追加§E.1 にナビ項目追加
4docs/_internal/changelog.md1行追加初版作成行を先頭に追加

注意事項

  1. トリガー重複防止: installBackupTriggers 冒頭で既存の runWeeklyBackup / verifyLatestBackup ハンドラのトリガーを全削除してから新規作成。これを怠るとトリガーが累積し、毎週実行回数が増えていく(過去パターン: GAS 時間トリガー重複)
  2. Utils.auditLog / Utils.persistLog の未実装: typeof === 'function' ガードを通す tryAuditLog_ / tryPersistLog_ ラッパー経由で呼び、MAS-178 / MAS-179 実装完了前でも動作する
  3. 循環参照防止: tryAuditLog_ は catch で握りつぶし、MAS-179 仕様書の「auditLog 内部で console.error フォールバック」設計と整合
  4. dev 環境のトリガー: installBackupTriggersEnv.isDev() で早期 return。dev で自動バックアップは不要(Drive 容量節約、テストデータの誤保存回避)
  5. DriveApp.getFolderById のエラーハンドリング: 既存の 502_receipt_reader.js L34-37 パターンを踏襲(try-catch で「フォルダが見つかりません」メッセージ)
  6. ファイル名衝突: 同日に複数回実行された場合、_2, _3, ... の連番を付与。既存ファイルの上書きは行わない
  7. SpreadsheetApp.copy() ではなく DriveApp.makeCopy(): SpreadsheetApp.copy は同一フォルダ制約、別フォルダ配置には DriveApp の makeCopy 第2引数が正解
  8. ENV 識別子の付加: ファイル名 BizLP_{env}_backup_YYYYMMDD_[wm] で dev と prod のファイルが同一フォルダに混在しても判別可能(運用上は環境別フォルダ推奨)
  9. 月末週判定の簡易化: 「実行日から 7 日以内に月が変わるか」で判定。カレンダーの月末土曜を正確に判定するのではなく、「実行週が当月最終週なら _m」で十分
  10. Drive スコープの承認ダイアログ: appsscript.jsondependencies: {} 空で oauthScopes 未指定。初回 DriveApp 使用時に承認ダイアログが出るのでユーザーに事前告知
  11. 6 分制限: DriveApp.makeCopy() は Google サーバー側で処理されクライアント時間は数秒〜30 秒。通常は収まるが、大規模化時は Sheets API v4 経由のエクスポートへの移行を検討(将来の拡張)
  12. 新規ファイル追加時の .claspignore 確認: 809_backup_tool.js が除外されていないか clasp status で push 対象を検証(MAS-096 失敗パターン回避)
  13. 復元操作は必ず人間が実行: 自動復元ロジックは設けない。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アラート「開発環境では設定しません」で早期 returnDrive 容量節約・誤データ保存回避
Session.getActiveUser()tryAuditLog_ 内で呼ぶ MAS-179 auditLog 実装 (もしあれば) が 'SYSTEM' フォールバックMAS-179 エッジケース整合
Utils.auditLog / Utils.persistLog 未実装typeof === 'function' ガードで黙って無視、実行継続MAS-178/MAS-179 未実装状態でも本案件は単独動作
installBackupTriggers で既存トリガー 2 件以上存在全削除してから再登録。重複累積を防止必須要件(トリガー重複 = 複数回実行)
ファイル名パターン外のファイルが同フォルダ内に存在cleanupOldBackups_ の正規表現で対象外となり trash されない誤削除防止

実データ検証(MCP 事前確認項目)

実装前後に以下を確認する:

  1. Drive 容量の現状確認: 対象スプレッドシートのファイルサイズを Drive UI で確認し、サイズ × 24 (週次12 + 月次12) で総容量試算(現状 3 MB × 24 ≒ 72 MB、将来成長を見越して 1-2 GB まで許容)
  2. バックアップ先フォルダの事前作成: Drive で専用フォルダ BizLP_Backups_Prod / BizLP_Backups_Dev を手動作成、フォルダ ID をメモ。GAS から Env.setBackupFolderId(id) を手動実行して保存
  3. DriveApp.makeCopy() 所要時間: dev 環境で runManualBackup() を手動実行し、ログで実行時間を記録(数秒〜30 秒想定)
  4. トリガー登録確認: installBackupTriggers() 実行後、GAS エディタの「トリガー」画面で runWeeklyBackup 週次 / verifyLatestBackup 月次の 2 件が登録されているか目視確認
  5. ScriptApp.getProjectTriggers() と実行履歴の突合: 週次実行後に console.log(ScriptApp.getProjectTriggers().map(t => ({fn: t.getHandlerFunction(), type: t.getEventType()}))) で確認
  6. appsscript.json の Drive スコープ: 現状 oauthScopes 未指定。初回 DriveApp 使用時に承認ダイアログが出るので、prod デプロイ前に dev で承認完了しておく
  7. MAS-178 / MAS-179 実装状況: grep -n "auditLog\|persistLog" 000_infra/004_utils.js で本案件実装時点の実装有無を確認。未実装なら tryXxx_ ラッパーで動作、実装後は自動連携

復元手順

バックアップからの復元は必ず人間が実行する(自動復元なし、Human-in-the-Loop 遵守)。

  1. Drive でバックアップファイルを特定: BizLP_Backups_Prod フォルダ内の BizLP_prod_backup_YYYYMMDD_[wm] から復元したい日付のファイルを選択
  2. コピーを作成: Drive UI で右クリック →「コピーを作成」→ 新しいスプレッドシートを生成。原本は残しておく(万一の切り戻し用)
  3. スクリプトプロパティを更新: GAS エディタで SPREADSHEET_ID プロパティを新スプレッドシートの ID に更新 (File → Project Settings → Script Properties)
  4. スキーマ整合性の確認: 新スプレッドシートで 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.mdGAS ファイル番号体系、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-28MAS-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 結果・実装状況の未実装判定が担保されている