概要

項目内容
案件IDMAS-213
カテゴリ基盤改善
Phase
優先度
対象ファイル000_infra/004_utils.js100_config/101_sys_config.js
前提案件なし

目的

98_audit_log シートへの手動編集・削除を GAS のシート保護機能(Sheet.protect())で防止し、WORM(Write Once Read Many)的な改ざん耐性を確保する。Utils.auditLog() は GAS スクリプト権限で引き続き appendRow() 可能。

現在のコード

000_infra/004_utils.jsUtils.auditLog()(L273〜L291)より抜粋。

// L276-L280 (シートが null の場合の現行処理)
var sheet = ss.getSheetByName('98_audit_log');
if (!sheet) {
  console.error('[AUDIT_LOG_FAIL] 98_audit_log が未作成。setupAllSchemas を実行してください。');
  return;
}

シートが null の場合、console.error を出力して return するのみ。シート再作成も保護設定も行われない。

appendRow の引数順(L286):

sheet.appendRow([new Date(), user, operation, targetSheet || '', targetId || '', targetCol || '', funcName || '', before, after, note || '']);

列順: 日時 / ユーザー / オペレーション / 対象シート / 対象ID / 対象列 / 関数名 / 変更前 / 変更後 / 備考(10列)

DDL スキーマ(100_config/101_sys_config.js L688)でも同列順が定義済み:

'LOG_AUDIT': { headers: ["日時","ユーザー","操作種別","対象シート","対象ID","対象列","関数名","変更前値","変更後値","備考"], color: "#434343" },

修正方針

修正①: 000_infra/004_utils.js へのヘルパー追加とフォールバック強化

ファイルスコープ(var Utils = { ... } の外側)に以下の GAS グローバル関数を追加する:

/**
 * 98_audit_log シートに編集禁止保護を冪等に設定するプライベートヘルパー。
 * Utils.auditLog() と setupAllSchemas() の両方から呼び出す。
 * ADMIN_EMAIL: 03_sys_params から取得。未設定時はスクリプト実行者をフォールバック。
 */
function applyAuditLogProtection_(sheet) {
  if (!sheet) return;
  // 既存保護を全件解除(冪等性確保)
  var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
  for (var i = 0; i < protections.length; i++) {
    protections[i].remove();
  }
  // 新規保護を設定
  var protection = sheet.protect();
  protection.setDescription('監査ログは改ざん防止のため編集禁止です。システムにより自動記録されます。');
  protection.setDomainEdit(false);
  var adminEmail = Constants.getParam('ADMIN_EMAIL', Session.getEffectiveUser().getEmail());
  if (adminEmail) {
    protection.addEditor(adminEmail);
  }
}

Utils.auditLog() のシートnull時処理(L277〜L280の console.error + return 箇所)を以下に差し替える:

if (!sheet) {
  console.error('[AUDIT_LOG_FAIL] 98_audit_log が未作成。自動再作成します。');
  try {
    sheet = ss.insertSheet('98_audit_log');
    // ヘッダー行列順は既存 appendRow 引数順(L286)と完全一致
    sheet.appendRow(['日時', 'ユーザー', 'オペレーション', '対象シート', '対象ID', '対象列', '関数名', '変更前', '変更後', '備考']);
    applyAuditLogProtection_(sheet);
  } catch (recreateErr) {
    console.error('[AUDIT_LOG_FAIL] 再作成失敗: ' + recreateErr.message);
    return;
  }
}
// ← この後、既存の user 取得・appendRow 処理が続く(変更なし)

注意: 自動再作成時のヘッダー列名は appendRow 引数順に合わせた表示名(「オペレーション」「変更前」「変更後」)を使用する。DDL の LOG_AUDIT ヘッダー(「操作種別」「変更前値」「変更後値」)とは字面が異なるが、appendRow 引数順の対応は同一である。実装者は Read で L286 の引数順を必ず確認し、自動再作成ヘッダーをその順序に合わせること。

修正②: 100_config/101_sys_config.jssetupAllSchemas() への保護設定追加

98_audit_log DDL スキーマ適用箇所(L688 の 'LOG_AUDIT' 定義)が処理された後の適切な位置(全シート DDL 適用ループ完了後、batchUpdate 実行前後)に以下を追加する。具体的な挿入行は Read で setupAllSchemas 内の 98_audit_log シート作成・取得フロー(LOG_AUDIT に対応するシート処理が完了した箇所)を確認し決定する:

// 98_audit_log 保護設定(冪等)
var auditSheet = ss.getSheetByName('98_audit_log');
if (auditSheet) {
  applyAuditLogProtection_(auditSheet);  // 000_infra/004_utils.js で定義したGASグローバル関数
}

影響範囲

ファイル変更内容
000_infra/004_utils.jsapplyAuditLogProtection_ 関数をファイルスコープに追加 + Utils.auditLog のシートnull時処理を自動再作成+保護設定に強化
100_config/101_sys_config.jssetupAllSchemas 内に applyAuditLogProtection_ 呼び出しを追加
03_sys_params シートADMIN_EMAIL キーが未登録の場合は追加が必要(オプション)

注意事項

  1. ファイルロード順序制約: 000_infra/004_utils.js(番号004)は 100_config/101_sys_config.js(番号101)より先にロードされる。applyAuditLogProtection_ を 004 側のファイルスコープ(GAS グローバル関数)として定義することで、101 側からの呼び出しは問題ない(GAS はすべてのファイル関数をグローバルスコープに展開する)。逆方向(004 が 101 の関数を呼ぶ)は不可。
  2. GASスクリプトからの appendRow 可否: シート保護後もスクリプトオーナー権限で実行される Utils.auditLog()appendRow() は正常成功する(GAS 仕様)。ただし Web アプリ・トリガー等デプロイ形態によって実行ユーザーが異なる場合があるため、動作確認で検証すること。
  3. Constants.getParam のキャッシュ: Constants.getParam_paramsCache03_sys_params をキャッシュする実装(002_constants.js L146-167 で確認済み)。ADMIN_EMAIL キーを 03_sys_params に追加した場合、_paramsCache = null をリセットするか、スクリプト再実行が必要。
  4. 冪等性: setupAllSchemas() を複数回実行しても getProtections()remove()protect() の順で既存保護を全件クリアしてから再設定するため、保護の重複は発生しない。
  5. ヘッダー行の列順: 自動再作成時のヘッダーは、既存 Utils.auditLog()appendRow 引数順(L286)と完全一致させること。Phase 1 の Read で確認した値をそのまま使用する(推測禁止)。

エッジケース

条件動作備考
03_sys_paramsADMIN_EMAIL キーが未設定Session.getEffectiveUser().getEmail() をフォールバックとして保護を設定開発環境では実行者が編集権限者になる
98_audit_log シートが手動削除された後、Utils.auditLog() が発火シートをヘッダー付きで自動再作成し、保護設定も完全復元。その後 appendRow を継続ヘッダー列順は現行実装(L286 appendRow 引数順)と一致
setupAllSchemas() を複数回実行既存保護を全件 remove() してから再設定するため重複なし(冪等)
GASスクリプトからの appendRow()保護設定後もスクリプトオーナー権限で正常成功デプロイ形態(Webアプリ/トリガー)で要確認
特権管理者以外のユーザーが手動編集を試みるSheets が警告ダイアログを表示し、編集は反映されない
applyAuditLogProtection_ 内で Session.getEffectiveUser() が空を返すaddEditor("") は呼ばれない(if (adminEmail) ガードで防止)空文字列の editor 追加はエラーになるため
Utils.auditLog() 内で applyAuditLogProtection_ が throwrecreateErrconsole.error で記録して return(無限ループ防止のため Utils.logError は呼ばない)既存の握りつぶしパターンを維持

実データ検証

実装前に MCP または Read で以下を確認すること:

  1. 100_config/101_sys_config.jssetupAllSchemas()98_audit_log の DDL スキーマ定義(LOG_AUDIT キー、L688)が存在することを確認済み。実装者はシートが正しく作成されるフローを Read で追跡する
  2. 03_sys_params シートに ADMIN_EMAIL キーが既存かどうかを MCP で確認。存在しない場合は追加が必要
  3. 98_audit_log シートに現時点で保護設定が既に存在しないかを MCP で確認(getProtections の結果)

関連ドキュメント

ドキュメント関連箇所
docs/prd.mdHuman-in-the-Loop / 監査ポリシー
docs/dev/dev_mas-179_audit_trail.mdMAS-213 の起源。MAS-179「人間が検討すべき事項」に「将来案件 MAS-213 として TODO 登録済」と記載
000_infra/004_utils.jsUtils.auditLog() 現行実装(L273〜L291)
000_infra/002_constants.jsConstants.getParam() / 03_sys_params 参照パターン(L146〜L167)
docs/dev/dev_mas-205_mfa_privilege_separation.mdMAS-205 MFA義務化と特権アカウント分離。ADMIN_EMAIL に設定する管理者アカウントの設計と連動

人間が検討すべき事項

  • ADMIN_EMAIL に設定する管理者アカウントのメールアドレスを決定する(個人オーナーアカウントか組織アカウントか)。MAS-205 の特権アカウント設計と整合させること
  • 保護設定後、特権管理者しか 98_audit_log を手動編集できなくなるが、ログの誤記録が発生した場合の手動訂正ポリシーを決定する(WORM 原則との兼ね合い)
  • Web アプリとしてデプロイしている場合、Session.getEffectiveUser().getEmail() が期待するメールアドレスを返すかをデプロイ設定(「自分として実行」vs「アクセスするユーザーとして実行」)と照らして確認する

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-213「監査ログシート(98_audit_log)の編集禁止保護」を実装してください。

## 実行前タスク(Readで裏取りしてから実装すること)
1. `000_infra/004_utils.js` を Read — `Utils.auditLog()` の現行実装を行番号まで把握。シートnull時の処理(console.error + return の箇所、L277〜L280)と appendRow の引数順(L286、列順)を確認
2. `000_infra/002_constants.js` を Read — `Constants.getParam(key, defaultVal)` のシグネチャを確認(L147)
3. `100_config/101_sys_config.js` を Read — `setupAllSchemas()` 内の `98_audit_log` DDLスキーマ定義箇所(`LOG_AUDIT` キー、L688)を特定。既存のシート保護コードの有無も確認
4. MCPで `03_sys_params` シートを確認 — `ADMIN_EMAIL` キーの有無を確認。なければ追加が必要

【ロード順序の制約】`000_infra/004_utils.js`(番号004)は `100_config/101_sys_config.js`(番号101)より先にロードされる。保護ロジック共通化のため `applyAuditLogProtection_(sheet)` を 004 のファイルスコープ(Utils オブジェクト外)に GAS グローバル関数として定義し、Utils.auditLog() と setupAllSchemas() の両方からこれを呼び出す。

## 修正対象ファイル
- `000_infra/004_utils.js` のみ(`applyAuditLogProtection_` 追加 + `Utils.auditLog` のシートnull時処理を修正)
- `100_config/101_sys_config.js` のみ(`setupAllSchemas` 内の `98_audit_log` 定義処理完了後に保護設定を追加)

## 実装内容

### A. `000_infra/004_utils.js` への変更

#### A-1. ファイルスコープに `applyAuditLogProtection_` を追加
`var Utils = { ... };` の直前(または直後)に以下を追加:

    function applyAuditLogProtection_(sheet) {
      if (!sheet) return;
      var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
      for (var i = 0; i < protections.length; i++) {
        protections[i].remove();
      }
      var protection = sheet.protect();
      protection.setDescription('監査ログは改ざん防止のため編集禁止です。システムにより自動記録されます。');
      protection.setDomainEdit(false);
      var adminEmail = Constants.getParam('ADMIN_EMAIL', Session.getEffectiveUser().getEmail());
      if (adminEmail) {
        protection.addEditor(adminEmail);
      }
    }

#### A-2. `Utils.auditLog()` のシートnull時処理を修正
Read で確認した L277〜L280 の `console.error(... + return;` 箇所を以下に差し替える:

    if (!sheet) {
      console.error('[AUDIT_LOG_FAIL] 98_audit_log が未作成。自動再作成します。');
      try {
        sheet = ss.insertSheet('98_audit_log');
        sheet.appendRow(['日時', 'ユーザー', 'オペレーション', '対象シート', '対象ID', '対象列', '関数名', '変更前', '変更後', '備考']);
        applyAuditLogProtection_(sheet);
      } catch (recreateErr) {
        console.error('[AUDIT_LOG_FAIL] 再作成失敗: ' + recreateErr.message);
        return;
      }
    }

ヘッダー行の列順は、Read で確認した既存 appendRow 引数順(L286)と完全一致させること(推測禁止)。

### B. `100_config/101_sys_config.js` への変更
`setupAllSchemas()` 内の全シート DDL 適用処理完了後(Read で `98_audit_log` シートが作成・確定される箇所を特定し、その直後)に以下を追加:

    var auditSheet = ss.getSheetByName('98_audit_log');
    if (auditSheet) {
      applyAuditLogProtection_(auditSheet);
    }

## 制約
- `Utils.auditLog()` のシグネチャ(引数名・引数数・戻り値)を変更しない
- `applyAuditLogProtection_` は Utils オブジェクトのプロパティには追加しない(ファイルスコープのGASグローバル関数として定義)
- `setupAllSchemas()` 内の上記追加箇所以外の既存コードは変更しない
- `Utils.auditLog()` の既存のtry-catchとエラー握りつぶしパターン(無限ループ防止)を維持する

## 動作確認
1. `npm run push:dev` でデプロイ
2. `setupAllSchemas()` をメニューから実行(実在するメニュー名は Read で `101_sys_config.js` の `onOpen()` を確認して使用すること)
3. `98_audit_log` シートに鍵アイコン(保護マーク)が表示されることを確認
4. 一般ユーザーアカウントで `98_audit_log` の任意セルを手動編集しようとすると Sheets の警告ダイアログが表示され、編集が拒否されることを確認
5. `ADMIN_EMAIL` に設定したアカウントでは同シートが編集可能なことを確認
6. `98_audit_log` シートを手動削除後、任意の `Utils.auditLog()` 発火操作(例: 32_wrk_invoice タブのステータス変更)を行い、シート・ヘッダー行・保護設定が自動再作成されることを確認
7. GASスクリプトからの `appendRow()` が保護後も正常成功することを確認(手順2の実行後、`98_audit_log` に新規ログ行が追記されているか確認)
8. `setupAllSchemas()` を2回連続実行し、保護設定が重複しない(鍵アイコンが1つのまま)ことを確認

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・設計確定) | あり | ロード順序制約・挿入行番号の確定 |
| 実装(コード記述) | なし | 仕様書の指示通りに書き下す |

推奨実行モデル

工程推奨モデル理由
実装(コーディング)Claude Sonnet複数ファイル横断の挿入位置特定と既存パターンの適用の判断が必要

変更履歴

日付変更内容
2026-04-20初版作成

仕様書作成プロンプト

展開して表示 【タイムアウト回避・実行原則(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(`
`にプロンプト全文記録)に分割。1回のWrite/Editは300行以内。 4. **各Stepで何を書くかを具体指示**: Phase 1で設計判断を完全に確定させ、Phase 2では書き下しに徹する。設計を再考しない。

====================================================================== あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。 CLIエージェント「Claude Code」として、案件 MAS-213「監査ログシート(98_audit_log)の編集禁止保護」の開発仕様書を作成してください。


Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

Grepは「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で裏取りする。以下のファイルをすべてReadしてから設計を確定させること。

  1. docs/_internal/TODO_future.md — MAS-213 の要件・概要・人間が検討すべき事項を把握
  2. 000_infra/004_utils.jsUtils.auditLog() の現行実装を行番号まで把握。特に「シートがnullの場合の処理」と appendRow の引数順(列順)を確認
  3. 000_infra/002_constants.jsConstants.getParam(key, defaultVal) のシグネチャと _paramsCache の仕組みを確認
  4. 100_config/101_sys_config.jssetupAllSchemas() の実在確認・98_audit_log スキーマ定義箇所(行番号)・既存シート保護コードの有無を確認。関数名・メニュー名はReadで確認した実在する文字列のみ使用すること
  5. docs/dev/dev_mas-178_error_handling.md または docs/dev/dev_mas-085_consistency_check.md — 仕様書のセクション構成フォーマットを把握(いずれか1件)

Phase 1での必須確定事項(Readで裏取り後に確定):

  • Utils.auditLog() のシートnull時の現行処理(行番号と現行コード)
  • setupAllSchemas() 内の 98_audit_log DDL定義の行番号
  • Constants.getParam の実際の呼び出し形式
  • ファイルロード順序の設計制約の確認: GASはファイルのフルパスのアルファベット順でロードされる。000_infra/004_utils.js(番号004)は 100_config/101_sys_config.js(番号101)より先にロードされる。このため Utils.auditLog()(004内)から101_sys_config.jsで定義した関数を呼び出すと実行時未定義エラーになる。保護ロジックの共通化は以下の方針とする:000_infra/004_utils.js のファイルスコープ(Utilsオブジェクト外)にGASグローバル関数 applyAuditLogProtection_(sheet) を定義し、Utils.auditLog()setupAllSchemas() の両方からこれを呼び出す(GASでは全ファイルの関数がグローバルスコープに展開されるため、004で定義した関数は101からも呼び出せる)。Read結果でこの制約に反するパターンが発見された場合は設計を修正すること。
  • Utils.auditLog()appendRow 引数順(現行コードから確認)から、自動再作成時のヘッダー行列名を確定させる

Phase 2: 仕様書の分割作成

出力先: docs/dev/dev_mas-213_audit_log_protection.md 【重要】1回のツール呼び出しで全内容を出力せず、以下のStepに分割して実行すること。


Step 2-1: 骨格の作成(Write、~20行)

全セクション見出しのみ記載。本文は空でよい。含めるセクション: # N-37: 監査ログシート(98_audit_log)の編集禁止保護 / ## 概要 / ## 目的 / ## 現在のコード / ## 修正方針 / ## 影響範囲 / ## 注意事項 / ## エッジケース / ## 実データ検証 / ## 関連ドキュメント / ## 人間が検討すべき事項 / ## 実装プロンプト(Claude Code 用) / ## 推奨実行モデル / ## 変更履歴 / ## 仕様書作成プロンプト


Step 2-2: 概要〜注意事項の追記(Edit または Bash heredoc、~300行)

  • 概要(テーブル): 案件ID: MAS-213 / カテゴリ: 基盤改善 / Phase: — / 優先度: — / 対象ファイル: 000_infra/004_utils.js, 100_config/101_sys_config.js / 前提案件: なし

  • 目的: 98_audit_log シートへの手動編集・削除をGASのシート保護機能(Sheet.protect())で防止し、WORM(Write Once Read Many)的な改ざん耐性を確保する。Utils.auditLog() はGASスクリプト権限で引き続き appendRow() 可能。

  • 現在のコード: 000_infra/004_utils.jsUtils.auditLog() より(Phase 1で確認した行番号を明記)。シートがnullの場合、console.error('[AUDIT_LOG_FAIL] 98_audit_log が未作成。...') を出力して return するのみで、シート再作成も保護設定もない。

  • 修正方針(2箇所):

    修正①: 000_infra/004_utils.js へのヘルパー追加とフォールバック強化

    ファイルスコープ(var Utils = { ... } の外側)に以下のGASグローバル関数を追加する:

    /**
     * 98_audit_log シートに編集禁止保護を冪等に設定するプライベートヘルパー。
     * Utils.auditLog() と setupAllSchemas() の両方から呼び出す。
     * ADMIN_EMAIL: 03_sys_params から取得。未設定時はスクリプト実行者をフォールバック。
     */
    function applyAuditLogProtection_(sheet) {
      if (!sheet) return;
      // 既存保護を全件解除(冪等性確保)
      var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
      for (var i = 0; i < protections.length; i++) {
        protections[i].remove();
      }
      // 新規保護を設定
      var protection = sheet.protect();
      protection.setDescription('監査ログは改ざん防止のため編集禁止です。システムにより自動記録されます。');
      protection.setDomainEdit(false);
      var adminEmail = Constants.getParam('ADMIN_EMAIL', Session.getEffectiveUser().getEmail());
      if (adminEmail) {
        protection.addEditor(adminEmail);
      }
    }
    

    Utils.auditLog() のシートnull時処理を以下に差し替える(return だけだった箇所):

    if (!sheet) {
      console.error('[AUDIT_LOG_FAIL] 98_audit_log が未作成。自動再作成します。');
      try {
        sheet = ss.insertSheet('98_audit_log');
        // ヘッダー行列順は既存 appendRow 引数順と完全一致させること(Read で確認)
        sheet.appendRow(['日時', 'ユーザー', 'オペレーション', '対象シート', '対象ID', '対象列', '関数名', '変更前', '変更後', '備考']);
        applyAuditLogProtection_(sheet);
      } catch (recreateErr) {
        console.error('[AUDIT_LOG_FAIL] 再作成失敗: ' + recreateErr.message);
        return;
      }
    }
    // ← この後、既存の user 取得・appendRow 処理が続く(変更なし)
    

    修正②: 100_config/101_sys_config.jssetupAllSchemas() への保護設定追加

    98_audit_log スキーマ適用箇所(Phase 1で確認した行番号)の直後に追加する:

    // 98_audit_log 保護設定(冪等)
    var auditSheet = ss.getSheetByName('98_audit_log');
    if (auditSheet) {
      applyAuditLogProtection_(auditSheet);  // 000_infra/004_utils.js で定義したGASグローバル関数
    }
    
  • 影響範囲: 000_infra/004_utils.jsUtils.auditLog 修正 + applyAuditLogProtection_ 追加)/ 100_config/101_sys_config.jssetupAllSchemas 内に保護設定追加)/ 03_sys_params シート(ADMIN_EMAIL キー追加が必要な場合)

  • 注意事項:

    1. ファイルロード順序制約: 000_infra/004_utils.js(番号004)は 100_config/101_sys_config.js(番号101)より先にロードされる。applyAuditLogProtection_ を004側のGASグローバル関数として定義することで、101側からの呼び出しは問題ない(GASはすべてのファイル関数をグローバルスコープに展開する)。逆方向(004が101の関数を呼ぶ)は不可。
    2. GASスクリプトからのappendRow可否: シート保護後もスクリプトオーナー権限で実行される Utils.auditLog()appendRow() は正常成功する(GAS仕様)。ただしWebアプリ・トリガー等デプロイ形態によって実行ユーザーが異なる場合があるため、動作確認で検証すること。
    3. Constants.getParam のキャッシュ: Constants.getParam_paramsCache03_sys_params をキャッシュする実装(002_constants.js で確認済み)。ADMIN_EMAIL キーを 03_sys_params に追加した場合、_paramsCache = null をリセットするか、スクリプト再実行が必要。
    4. 冪等性: setupAllSchemas() を複数回実行しても getProtections()remove()protect() の順で既存保護を全件クリアしてから再設定するため、保護の重複は発生しない。
    5. ヘッダー行の列順: 自動再作成時のヘッダーは、既存 Utils.auditLog()appendRow 引数順と完全一致させること。Phase 1のReadで確認した値をそのまま使用する(推測禁止)。

Step 2-3a: エッジケース〜人間が検討すべき事項の追記(Edit または Bash、~200行)

  • エッジケース(テーブル形式):

    条件動作備考
    03_sys_paramsADMIN_EMAIL キーが未設定Session.getEffectiveUser().getEmail() をフォールバックとして保護を設定開発環境では実行者が編集権限者になる
    98_audit_log シートが手動削除された後、Utils.auditLog() が発火シートをヘッダー付きで自動再作成し、保護設定も完全復元。その後 appendRow を継続ヘッダー列順は現行実装と一致
    setupAllSchemas() を複数回実行既存保護を全件 remove() してから再設定するため重複なし(冪等)
    GASスクリプトからの appendRow()保護設定後もスクリプトオーナー権限で正常成功デプロイ形態(Webアプリ/トリガー)で要確認
    特権管理者以外のユーザーが手動編集を試みるSheetsが警告ダイアログを表示し、編集は反映されない
    applyAuditLogProtection_ 内で Session.getEffectiveUser() が空を返すaddEditor("") は呼ばれない(if (adminEmail) ガードで防止)空文字列のeditor追加はエラーになるため
  • 実データ検証(実装前にMCPまたはReadで確認すべき項目):

    • 100_config/101_sys_config.jssetupAllSchemas()98_audit_log のDDLスキーマ定義が存在するか確認(存在しない場合は追加が必要)
    • 03_sys_params シートに ADMIN_EMAIL キーが既存かどうかを確認
    • 98_audit_log に現時点で保護設定が既に存在しないかを確認
  • 関連ドキュメント:

    ドキュメント関連箇所
    docs/prd.mdHuman-in-the-Loop / 監査ポリシー
    000_infra/004_utils.jsUtils.auditLog() 現行実装
    000_infra/002_constants.jsConstants.getParam() / 03_sys_params 参照パターン
  • 人間が検討すべき事項:

    • ADMIN_EMAIL に設定する管理者アカウントのメールアドレスを決定する(個人オーナーアカウントか組織アカウントか)
    • 保護設定後、特権管理者しか 98_audit_log を手動編集できなくなるが、ログの誤記録が発生した場合の手動訂正ポリシーを決定する
    • Webアプリとしてデプロイしている場合、Session.getEffectiveUser().getEmail() が期待するメールアドレスを返すかをデプロイ設定(「自分として実行」vs「アクセスするユーザーとして実行」)と照らして確認する

Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit または Bash、~250行)

実装プロンプトはバッククォートで囲まず、行頭4スペースインデントで出力すること。

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-213「監査ログシート(98_audit_log)の編集禁止保護」を実装してください。

## 実行前タスク(Readで裏取りしてから実装すること)
1. `000_infra/004_utils.js` を Read — `Utils.auditLog()` の現行実装を行番号まで把握。シートnull時の処理(console.error + return の箇所)と appendRow の引数順(列順)を確認
2. `000_infra/002_constants.js` を Read — `Constants.getParam(key, defaultVal)` のシグネチャを確認
3. `100_config/101_sys_config.js` を Read — `setupAllSchemas()` 内の `98_audit_log` スキーマ定義箇所(行番号)を特定。既存のシート保護コードの有無も確認
4. MCPで `03_sys_params` シートを確認 — `ADMIN_EMAIL` キーの有無を確認。なければ追加が必要

【ロード順序の制約】`000_infra/004_utils.js`(番号004)は `100_config/101_sys_config.js`(番号101)より先にロードされる。保護ロジック共通化のため `applyAuditLogProtection_(sheet)` を 004 のファイルスコープ(Utils オブジェクト外)にGASグローバル関数として定義し、Utils.auditLog() と setupAllSchemas() の両方からこれを呼び出す。

## 修正対象ファイル
- `000_infra/004_utils.js` のみ(`applyAuditLogProtection_` 追加 + `Utils.auditLog` のシートnull時処理を修正)
- `100_config/101_sys_config.js` のみ(`setupAllSchemas` 内の `98_audit_log` 定義直後に保護設定を追加)

## 実装内容

### A. `000_infra/004_utils.js` への変更

#### A-1. ファイルスコープに `applyAuditLogProtection_` を追加
`var Utils = { ... };` の直前(または直後)に以下を追加:

    function applyAuditLogProtection_(sheet) {
      if (!sheet) return;
      var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET);
      for (var i = 0; i < protections.length; i++) {
        protections[i].remove();
      }
      var protection = sheet.protect();
      protection.setDescription('監査ログは改ざん防止のため編集禁止です。システムにより自動記録されます。');
      protection.setDomainEdit(false);
      var adminEmail = Constants.getParam('ADMIN_EMAIL', Session.getEffectiveUser().getEmail());
      if (adminEmail) {
        protection.addEditor(adminEmail);
      }
    }

#### A-2. `Utils.auditLog()` のシートnull時処理を修正
Readで確認した行番号の `console.error(... + return;` 箇所を以下に差し替える:

    if (!sheet) {
      console.error('[AUDIT_LOG_FAIL] 98_audit_log が未作成。自動再作成します。');
      try {
        sheet = ss.insertSheet('98_audit_log');
        sheet.appendRow(['日時', 'ユーザー', 'オペレーション', '対象シート', '対象ID', '対象列', '関数名', '変更前', '変更後', '備考']);
        applyAuditLogProtection_(sheet);
      } catch (recreateErr) {
        console.error('[AUDIT_LOG_FAIL] 再作成失敗: ' + recreateErr.message);
        return;
      }
    }

ヘッダー行の列順は、Readで確認した既存 appendRow 引数順と完全一致させること(推測禁止)。

### B. `100_config/101_sys_config.js` への変更
Readで確認した `98_audit_log` スキーマ適用箇所の直後に以下を追加:

    var auditSheet = ss.getSheetByName('98_audit_log');
    if (auditSheet) {
      applyAuditLogProtection_(auditSheet);
    }

## 制約
- `Utils.auditLog()` のシグネチャ(引数名・引数数・戻り値)を変更しない
- `applyAuditLogProtection_` は Utils オブジェクトのプロパティには追加しない(ファイルスコープのGASグローバル関数として定義)
- `setupAllSchemas()` 内の上記追加箇所以外の既存コードは変更しない
- `Utils.auditLog()` の既存のtry-catchとエラー握りつぶしパターン(無限ループ防止)を維持する

## 動作確認
1. `npm run push:dev` でデプロイ
2. `setupAllSchemas()` をメニューから実行(実在するメニュー名は Read で `101_sys_config.js` の `onOpen()` を確認して使用すること)
3. `98_audit_log` シートに鍵アイコン(保護マーク)が表示されることを確認
4. 一般ユーザーアカウントで `98_audit_log` の任意セルを手動編集しようとするとSheetsの警告ダイアログが表示され、編集が拒否されることを確認
5. `ADMIN_EMAIL` に設定したアカウントでは同シートが編集可能なことを確認
6. `98_audit_log` シートを手動削除後、任意の `Utils.auditLog()` 発火操作(例: 32_wrk_invoice タブのステータス変更)を行い、シート・ヘッダー行・保護設定が自動再作成されることを確認
7. GASスクリプトからの `appendRow()` が保護後も正常成功することを確認(手順2の実行後、`98_audit_log` に新規ログ行が追記されているか確認)
8. `setupAllSchemas()` を2回連続実行し、保護設定が重複しない(鍵アイコンが1つのまま)ことを確認

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read・設計確定) | あり | ロード順序制約・挿入行番号の確定 |
| 実装(コード記述) | なし | 仕様書の指示通りに書き下す |

推奨実行モデル:

工程推奨モデル理由
実装(コーディング)Claude Sonnet複数ファイル横断の挿入位置特定と既存パターンの適用の判断が必要

変更履歴:

日付変更内容
2026-04-20初版作成

Step 2-4: 仕様書作成プロンプト全文の記録(Edit または Bash)

末尾の ## 仕様書作成プロンプト セクションに以下を追記:

<details><summary>展開して表示</summary>

(この <instruction> タグの全文をそのまま貼り付ける)

</details>

Phase 3: 保存・登録・コミット

3-A: docs/_config.json への登録(必須)

docs/_config.json をReadして既存の §E.1 セクション末尾のエントリ番号を確認し、連番で追加する:

{ "file": "dev/dev_mas-213_audit_log_protection.md", "title": "E.1.X N-37 監査ログシート編集禁止保護" }

3-B: docs/_internal/changelog.md への追記

| 2026-04-20 | [dev_mas-213_audit_log_protection.md](dev_mas-213_audit_log_protection.md) | 初版作成。98_audit_log シートへのGASシート保護設定とUtils.auditLog()フォールバック強化 |

3-C: Gitコミット&プッシュ

git add docs/dev/dev_mas-213_audit_log_protection.md docs/_internal/changelog.md docs/_config.json
git commit -m "docs: N-37 監査ログシート(98_audit_log)の編集禁止保護 仕様書を作成

GASシート保護機能(sheet.protect())で98_audit_logへの手動編集を禁止。
Utils.auditLog()のフォールバック強化(シート自動再作成+保護設定復元)。
applyAuditLogProtection_をファイルスコープGASグローバル関数として定義し
Utils.auditLog()とsetupAllSchemas()の両方から呼び出す設計(ロード順序004<101の制約対応)。"
git push -u origin {現在のブランチ名}