概要

項目内容
案件IDMAS-179
カテゴリセキュリティ
PhaseP1
優先度★★
所要時間3-4時間(Step 1: 1h / Step 2: 1.5h / Step 3: 1h)
対象ファイル000_infra/004_utils.js(Utils.auditLog 追加)
100_config/101_sys_config.js(98_audit_log スキーマ登録 + 管理者メニュー + onEdit フック)
400_domain/407_rpa_orchestrator.jsRPA 起票時の CREATE 記録)
400_domain/410_subledger_engine.jsAction A CREATE / Action B CONFIRM 記録)
900_test/901_test_runner.js(auditLog 書き込みテスト追加)
前提案件なし(MAS-178 の 99_error_log とは独立シート。MAS-178 の実装順序に依存しない)

目的

「誰が・いつ・何を・どう変更したか」を記録する専用ログシート 98_audit_log と共通API Utils.auditLog() を導入し、内部統制に耐える操作証跡を全自動処理・承認系操作に対して残す。MAS-178 の障害追跡ログ (99_error_log) とは用途を分離し、ユーザー操作追跡専用のWORM(追記専用)シートとする。

現状の課題

既存の「監査」関連資産

#資産性質MAS-179 との関係
1800_ops/802_audit.jsデータ整合性チェッカー(資本金推移・予算整合性・マスタ参照)別物。本案件では触らない
2Utils.logInfo / Utils.logError (004_utils.js L232-246)console 出力のみ(7日で揮発、検索不可)維持。操作ログは別API
3Utils.persistLog / 99_error_log シート (MAS-178 仕様書)ERROR/WARN 専用の障害追跡ログ(5列)別シート・別用途。同じ失敗握りつぶしパターンを踏襲

現状のギャップ

  • 誰が操作したかの記録がない:Session.getActiveUser().getEmail() を取得・永続化する仕組みが未整備
  • 何を変更したか(before / after 値)の記録がない:RPA 起票・承認・消込の変更差分がどこにも残らない
  • いつ実行されたかの永続記録がない:console.log は 7 日で消失。監査レビュー時に遡及不可
  • 承認操作の責任所在:Action B 消込確定が誰の操作か特定できず、Human-in-the-Loop の統制として不完全

修正方針(3ステップ段階実装)

Step 1: 監査ログ基盤(Utils.auditLog + 98_audit_log スキーマ)

A. Utils.auditLog() の追加(000_infra/004_utils.js

Utils.toastResult (L257) の直下に以下を追加:

/**
 * 操作監査ログをスプレッドシートに永続記録する (N-03)
 * 99_error_log(障害追跡)とは用途を分離した操作追跡用WORMシート。
 * 内部で例外が発生しても握りつぶす(無限ループ防止)。
 * @param {string} operation - 'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'
 * @param {string} targetSheet - 対象シート名('32_wrk_invoice' 等)。該当なしは ''
 * @param {string} targetId - 対象レコードID(INV_xxx / STL_xxx / JNL_xxx 等)。該当なしは ''
 * @param {string} funcName - 呼び出し元の FUNC 定数
 * @param {*} [beforeValue] - 変更前値。オブジェクト/配列は JSON.stringify。4000文字超は切り捨て
 * @param {*} [afterValue]  - 変更後値。同上
 * @param {string} [note]   - 備考・コンテキスト(環境名・件数等)
 */
auditLog: function(operation, targetSheet, targetId, funcName, beforeValue, afterValue, note) {
  try {
    var ss = getWebSpreadsheet_();
    var sheet = ss.getSheetByName('98_audit_log');
    if (!sheet) {
      sheet = ss.insertSheet('98_audit_log');
      sheet.getRange(1, 1, 1, 9).setValues([[
        '日時', 'ユーザー', '操作種別', '対象シート', '対象ID', '関数名', '変更前値', '変更後値', '備考'
      ]]);
      sheet.getRange(1, 1, 1, 9)
        .setBackground('#434343').setFontColor('#FFFFFF').setFontWeight('bold');
      sheet.setFrozenRows(1);
      sheet.getRange(1, 1, 1, 9).createFilter();
    }
    var user = '';
    try { user = Session.getActiveUser().getEmail() || ''; } catch(_) { user = ''; }
    if (!user) user = 'SYSTEM';
    var before = Utils._serializeAuditValue_(beforeValue);
    var after  = Utils._serializeAuditValue_(afterValue);
    sheet.appendRow([new Date(), user, operation, targetSheet || '', targetId || '', funcName || '', before, after, note || '']);
  } catch (e) {
    // 監査ログ記録の失敗は握りつぶす(無限ループ防止のため Utils.logError は呼ばない)
    console.error('[AUDIT_LOG_FAIL] ' + (e && e.message ? e.message : e));
  }
},

/**
 * auditLog 用の値シリアライザ(N-03 内部ヘルパー)
 * - プリミティブはそのまま文字列化
 * - Date は ISO文字列
 * - Object/Array は JSON.stringify(循環参照時は '[UNSERIALIZABLE]')
 * - 4000文字超は末尾切り捨て '...[TRUNCATED]'
 */
_serializeAuditValue_: function(v) {
  if (v === undefined || v === null) return '';
  try {
    var s;
    if (v instanceof Date) s = v.toISOString();
    else if (typeof v === 'object') s = JSON.stringify(v);
    else s = String(v);
    if (s.length > 4000) s = s.substring(0, 4000) + '...[TRUNCATED]';
    return s;
  } catch(_) {
    return '[UNSERIALIZABLE]';
  }
},

B. シートスキーマの登録(100_config/101_sys_config.js

setupAllSchemas の 01_sys_config マッピング追記箇所(L424 付近)に以下を追加:

if (!existKeys.includes('LOG_AUDIT')) confSheet.appendRow(['LOG_AUDIT', '', '98_audit_log', '監査証跡ログ(操作追跡・WORM)']);

DDL の明示的な列定義は行わない(auditLog が自動でヘッダー作成するため)。CLAUDE.md の「DDL (setupAllSchemas) で管理されないタブ」一覧にも追加しない(動的生成のため)。

Step 2: 重要計装点への挿入

計装点一覧(MUST)

#対象ファイル関数operation挿入位置
1407_rpa_orchestrator.jsgenerateAllRpaInvoices() L32RUN完了ログ (Utils.logInfo '一括実行完了' L57) の直前
2410_subledger_engine.jsprocessInvoiceApprovals() L106 (Action A)CREATETRN 行を newTrnRows に push した直後(INV 単位で1件)
3410_subledger_engine.jsprocessSettlementClearings() (Action B)CONFIRMSTL 消込仕訳生成後(STL 単位で1件)— 本案件最重要計装点
4101_sys_config.jsonEdit(e) L387UPDATE33_wrk_bank / 32_wrk_invoice の「確認FLG」「承認ステータス」列が編集されたときのみ
5101_sys_config.jssetupAllSchemas() L412RUN正常完了のtoast直前
6800_ops/80*_migration_*.jsmigrationXxx() 各関数MIGRATE関数冒頭と完了時

コード例:Action B 消込確定(最重要)

410_subledger_engine.jsprocessSettlementClearings 内、STL 消込仕訳を newTrnRows に push した直後に以下を挿入:

Utils.auditLog(
  'CONFIRM',
  '33_wrk_bank',
  stlId,                                  // 対象STL ID
  FUNC,                                   // 'processSettlementClearings'
  { status: 'UNCONFIRMED' },              // beforeValue
  { status: 'CONFIRMED', jnlId: newTrnId, settleDate: settleDate }, // afterValue
  'Env=' + Env.name()
);

コード例:onEdit での承認チェックボックス監視

101_sys_config.js L408 (handleUxAssist(e) 呼び出し直前) に追加:

try {
  // N-03: 承認系列の編集を監査ログに記録
  if (sheetName === '33_wrk_bank' || sheetName === '32_wrk_invoice') {
    var hdr = e.range.getSheet().getRange(1, 1, 1, e.range.getSheet().getLastColumn()).getValues()[0];
    var iConfirm = hdr.indexOf('確認FLG');
    var iStatus  = hdr.indexOf('承認ステータス');
    if ((iConfirm >= 0 && col === iConfirm + 1) || (iStatus >= 0 && col === iStatus + 1)) {
      var iId = hdr.indexOf('決済ID(STL)') !== -1 ? hdr.indexOf('決済ID(STL)') : hdr.indexOf('請求ID(INV)');
      var targetId = (iId >= 0) ? String(e.range.getSheet().getRange(row, iId + 1).getValue()) : '';
      Utils.auditLog('UPDATE', sheetName, targetId, 'onEdit',
        e.oldValue !== undefined ? e.oldValue : '', val, '列=' + hdr[col - 1]);
    }
  }
} catch(err) { console.error('[AUDIT_ONEDIT_FAIL] ' + err.message); }

Step 3: 管理者メニュー + アーカイブ補助

A. 管理者メニュー追加(101_sys_config.js L346-350 付近)

🔧 開発・設定 メニューに追加:

.addItem('📋 監査ログを開く (98_audit_log)', 'openAuditLog')
.addItem('🗄️ 監査ログを月次アーカイブ', 'archiveAuditLogMonthly')

対応する関数を 101_sys_config.js 末尾に追加:

function openAuditLog() {
  var ss = getWebSpreadsheet_();
  var sheet = ss.getSheetByName('98_audit_log');
  if (!sheet) { SpreadsheetApp.getUi().alert('監査ログはまだ作成されていません'); return; }
  ss.setActiveSheet(sheet);
}

function archiveAuditLogMonthly() {
  var FUNC = 'archiveAuditLogMonthly';
  try {
    var ss = getWebSpreadsheet_();
    var src = ss.getSheetByName('98_audit_log');
    if (!src || src.getLastRow() <= 1) { SpreadsheetApp.getUi().alert('アーカイブ対象なし'); return; }
    var data = src.getDataRange().getValues();
    var header = data[0];
    var prevMonth = Utils.addMonths(Utils.parseDateToYm(new Date()), -1);  // 'YYYY-MM'
    var archiveName = '98_audit_log_' + prevMonth.replace('-', '');  // '98_audit_log_202604'
    var archived = ss.getSheetByName(archiveName) || ss.insertSheet(archiveName);
    if (archived.getLastRow() === 0) archived.appendRow(header);
    var keepRows = [header];
    var movedCount = 0;
    for (var i = 1; i < data.length; i++) {
      var ym = Utils.parseDateToYm(data[i][0]);
      if (ym && ym <= prevMonth) {
        archived.appendRow(data[i]);
        movedCount++;
      } else {
        keepRows.push(data[i]);
      }
    }
    src.clearContents();
    src.getRange(1, 1, keepRows.length, header.length).setValues(keepRows);
    Utils.auditLog('RUN', '98_audit_log', '', FUNC, '', { moved: movedCount, archiveSheet: archiveName }, '');
    SpreadsheetApp.getUi().alert('アーカイブ完了: ' + movedCount + '件を ' + archiveName + ' に移送');
  } catch(e) {
    Utils.logError(FUNC, e);
    SpreadsheetApp.getUi().alert('エラー', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
  }
}

B. テスト追加(900_test/901_test_runner.js

runAllTests に以下のテストケースを追加:

function testAuditLogBasic_() {
  var ss = getWebSpreadsheet_();
  Utils.auditLog('RUN', 'TEST', 'TST_0001', 'testAuditLogBasic_', null, { foo: 'bar' }, 'unit-test');
  var sheet = ss.getSheetByName('98_audit_log');
  if (!sheet) return { pass: false, message: '98_audit_log が作成されていない' };
  var last = sheet.getRange(sheet.getLastRow(), 1, 1, 9).getValues()[0];
  if (last[2] !== 'RUN' || last[5] !== 'testAuditLogBasic_') {
    return { pass: false, message: '最終行が期待値と不一致: ' + JSON.stringify(last) };
  }
  // テスト行は残すとゴミになるため削除
  sheet.deleteRow(sheet.getLastRow());
  return { pass: true, message: 'Utils.auditLog が 98_audit_log に正しく記録された' };
}

影響範囲

Stepファイル変更量既存動作への影響
1000_infra/004_utils.js約50行追加Utils 名前空間に auditLog / serializeAuditValue を追加。既存関数は変更なし
1100_config/101_sys_config.js1行追加 (L424付近)01_sys_config に LOG_AUDIT マッピング追加のみ
2400_domain/407_rpa_orchestrator.js1行追加 (L57付近)完了ログ直前に auditLog 呼び出し
2400_domain/410_subledger_engine.js約10行追加Action A/B の仕訳生成ループ内に auditLog 呼び出し
2100_config/101_sys_config.js約15行追加 (L408付近)onEdit に承認列監視ブロック追加
3100_config/101_sys_config.js約45行追加メニュー項目 + openAuditLog / archiveAuditLogMonthly 関数
3900_test/901_test_runner.js約15行追加testAuditLogBasic_ 追加

注意事項

  1. 循環参照の絶対防止: Utils.auditLog の catch ブロックから Utils.logError を呼ばないこと。logErrorpersistLog → (将来)auditLog の循環になる可能性があるため、console.error のみにフォールバック
  2. 802_audit.js とは別物: 「監査」の語が混同しやすいが、802 はデータ整合性チェッカー、MAS-179 は操作ログ。本案件で 802 は一切変更しない
  3. 99_error_log の再利用禁止: MAS-178 が定義する 99_error_log は ERROR/WARN 専用で 5 列スキーマ。用途・列数・保持期間が異なるため、98_audit_log を新設
  4. appendRow の使用: CLAUDE.md「MCP add_rows はシート先頭に追加されるため原則使わない」と矛盾しない。appendRow は末尾追加が保証される
  5. 有効フラグ列なし: 監査ログは追記専用(WORM: Write Once Read Many)。論理削除・編集を認めないことで改ざん耐性を確保
  6. onEdit の粒度制御: 33_wrk_bank / 32_wrk_invoice の承認列のみに絞り、それ以外の列編集は記録しない(ログ肥大化防止)
  7. e.oldValue の制限: onEdit の e.oldValue単一セル編集時のみ値を持つ。範囲貼り付けや複数セル選択では undefined。その場合は空文字を記録
  8. 大量行貼り付け対策: 100 行超の範囲編集は e.range.getNumRows() > 100 でガードし、範囲単位 1 レコードのサマリーのみ記録
  9. 新規シート追加時の .claspignore 確認: 98_audit_log は物理シートなので claspignore の影響はないが、念のため clasp push 後に GAS エディタで表示されることを確認 (MAS-096 失敗パターン回避)
  10. テストの SKIP_PATTERNS 更新: 新規シート追加時は 901_test_runner.js の SKIP_PATTERNS(もしあれば)に 98_audit_log を登録(T4-03 失敗パターン回避)
  11. .gs 拡張子変換の対象: .js.gs 変換は clasp 側で自動。追加する auditLog は Utils 名前空間配下の関数のため、既存のロード順(000_infra が最初)に従い全モジュールから参照可能

エッジケース

計算式は持たないが、運用上の境界動作をテーブルで明示する。

ケース期待動作理由
Session.getActiveUser().getEmail() が空文字ユーザー列に 'SYSTEM' を記録権限不足・共有ドライブ設定時のフォールバック
Session.getActiveUser() 自体が throw同様に 'SYSTEM' を記録(try-catch で吸収)権限なしアカウント実行時
beforeValue / afterValue が object / ArrayJSON.stringify() で文字列化セル値の型を統一し検索可能化
JSON.stringify が循環参照で throw_serializeAuditValue_ の try-catch で '[UNSERIALIZABLE]' を記録auditLog 自体のクラッシュ防止
記録文字列が 4000 文字超末尾切り捨て '...[TRUNCATED]'Google Sheets の 1セル 50000 文字制限より十分小さく、可読性確保
beforeValue / afterValue が Date 型.toISOString() で文字列化タイムゾーン依存の表示ズレを防ぐ
beforeValue / afterValue が undefined / null空文字を記録(スキップしない)「値なし」を明示的に残す
シート 98_audit_log が未作成自動作成してヘッダー書き込み + 凍結行 + フィルタ設定初回実行でも即利用可能(MAS-178 persistLog と同パターン)
auditLog 内で例外発生catch で握りつぶし console.error('[AUDIT_LOG_FAIL] ...') にフォールバック無限ループ防止(auditLog → logError → auditLog …)
onEdit の大量貼り付け(範囲 > 100 行)範囲単位で 1 レコードのサマリー記録(各行ごとに呼ばない)シート肥大化防止
e.oldValue が undefined(複数セル編集)beforeValue に空文字を記録GAS 仕様による制限を回避
dev / prod 両環境での運用どちらも同シート名 98_audit_log で記録。備考列に Env=dev/Env=prodclasp で push されるため構造統一
ログ総行数 > 100,000 行Utils.logInfo で WARN を出力し、手動アーカイブを案内appendRow の性能劣化(〜500ms/行)を防ぐ
archiveAuditLogMonthly 実行時に対象行が 0 件ダイアログで「アーカイブ対象なし」と通知、シートは作成しない無駄な空シート作成を回避
同一月を二度アーカイブ既存アーカイブシートに appendRow で追記(重複の可能性あり)冪等性は保証しない。警告扱いで運用ルールで回避

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

実装前に以下を MCP または GAS エディタで確認する:

  1. Session.getActiveUser().getEmail() の動作確認: dev / prod 両環境で GAS スクリプトエディタから console.log(Session.getActiveUser().getEmail()) を手動実行し、空文字が返らないか確認。共有ドライブ配下のプロジェクトでは空になるケースがある
  2. 既存シート名の衝突確認: MCP list_sheets98_audit_log / 98_audit_log_* が既に存在しないか確認。衝突する場合は 97_audit_log へリナンバリングを検討
  3. 99_error_log シートの存在確認: MAS-178 の実装順序により既存か未作成か。未作成でも MAS-179 は独立して動作可能だが、両者が並列運用される前提を明記
  4. 101_sys_config.js のメニュー構造: 現行 🔧 開発・設定 メニュー (L346) に 📋 監査ログを開く を追加する位置を実物で確認(既存項目との重複・競合なし)
  5. 計装点の既存ログ箇所: 407_rpa_orchestrator.js L57 / 410_subledger_engine.js 内の Utils.logInfo 呼び出し箇所を MCP grep で実物確認し、auditLog の挿入位置を最終確定
  6. e.oldValue の onEdit 挙動: dev 環境で 33_wrk_bank の確認FLG をチェックし、e.oldValue / e.value の実値を console で確認(チェックボックス true / false の文字列化挙動)
  7. 1秒あたりの appendRow 性能: appendRow 連続実行時の所要時間を計測(100 行で 5 秒以内が目安)。RPA 一括実行で数十件まとめて auditLog する場面で性能が出るか検証

関連ドキュメント

仕様書関連箇所
MAS-178 エラーハンドリングUtils.persistLog / 99_error_log と同一の失敗握りつぶしパターンを踏襲。シートは別
MAS-192 Repository モジュール分割98_audit_log は読み取り頻度低のため Repository を作成せず直書き(同じ判断)
MAS-196 Repo/Contracts テスト901_test_runner.js へのテスト追加パターン
MAS-199 prod→dev データ同期監査ログの dev 同期除外判定(個人情報混入防止のため同期対象外が妥当)
CLAUDE.md書き込み位置規約・DDL 管理外タブの扱い・clasp 認証

人間が検討すべき事項

  • 保持期間の決定: 選択肢は以下。推奨は 1 年(会計年度 + 翌期決算期)
    • 3ヶ月(最小)/6ヶ月/1年(推奨)/3年/7年(法定帳簿保存)/無期限
  • 容量制限の閾値: 9 列 × N 行 で Sheets の 500 万セル制限に到達するのは 約 55 万行。以下の閾値で運用警告を発する
    • 10 万行(警告)/ 50 万行(手動アーカイブ推奨)/ 100 万行(自動アーカイブ必須)
  • アーカイブ方式:
    1. 月次で 98_audit_log_YYYYMM シートに移送(本仕様書の Step 3 実装)
    2. CSV として Drive に書き出し、シートは削除(MAS-212 物理削除アーカイブと連携)
    3. BigQuery 等の外部ストレージにエクスポート(Phase 3-4 GCP 移行時)
  • 外部通知の要否: 高リスク操作(DELETE / MIGRATE / 大量 UPDATE)検知時に Slack / メール通知するか(MAS-184 通知連携と連動)
  • 閲覧権限の保護: 98_audit_log シートへの編集権限を管理者に限定するか。Apps Script の Protection API で以下を検討(→ 将来案件 MAS-213 として TODO 登録済
    • シート自体を保護し、編集者を管理者 Email に限定
    • ユーザーが auditLog シートを手動編集・削除しても、次回 auditLog 呼び出しで自動再作成される仕様とすべきか
  • 環境別運用: dev / prod で同じシート名を使う(本仕様書の方針)か、dev のみ別シート(98_audit_log_dev)にするか。推奨は統一(clasp push で構造統一、備考列に Env 記録)
  • 個人情報(Email)の扱い: ユーザー列に Email を平文保存することの社内規程整合(プライバシーポリシー・就業規則)。匿名化(ハッシュ化)の要否検討

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

Step 1: 監査ログ基盤(Utils.auditLog + 98_audit_log スキーマ)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-179「監査証跡の強化 (Audit Trail)」の Step 1(監査ログ基盤)を実装してください。

## 実行前タスク

以下のファイルを読み込み、各観点を把握してください:
1. `docs/dev/dev_mas-179_audit_trail.md` — 本仕様書全体。特に「修正方針 Step 1」と「エッジケース」を精読
2. `000_infra/004_utils.js` L227-257 — Utils.logInfo / logError / toastResult の既存パターン。auditLog の挿入位置
3. `docs/dev/dev_mas-178_error_handling.md` の Step 1 — persistLog の設計(失敗握りつぶしパターン)。本案件は同一パターンを踏襲するが別シート・別API
4. `100_config/101_sys_config.js` L420-445 — setupAllSchemas の 01_sys_config マッピング追記箇所
5. `CLAUDE.md` — シートキー命名規約、DDL管理外タブの扱い、書き込み位置ルール

## 修正対象ファイル

- `000_infra/004_utils.js` への追記のみ(Utils 名前空間に auditLog / _serializeAuditValue_ を追加)
- `100_config/101_sys_config.js` への追記のみ(setupAllSchemas の 01_sys_config に LOG_AUDIT マッピングを1行追加)

## 実装内容

### A. Utils.auditLog() の追加(`004_utils.js`)

Utils.toastResult (L257) の直下に、仕様書「修正方針 Step 1-A」のコードを一字一句そのまま追加してください。以下を必ず守ること:

- 第1引数の operation は ENUM: 'CREATE' / 'UPDATE' / 'DELETE' / 'CONFIRM' / 'CANCEL' / 'RUN' / 'MIGRATE'
- シート `98_audit_log` が未作成なら自動作成(ヘッダー + 凍結行 + フィルタ + 背景色 #434343 / 文字白)
- ユーザー取得は `Session.getActiveUser().getEmail()`。空文字または throw 時は 'SYSTEM' にフォールバック
- beforeValue / afterValue は Utils._serializeAuditValue_ で正規化(Date は ISO、Object は JSON、4000文字超は切り捨て)
- 関数内で発生した全例外は catch で握りつぶし、`console.error('[AUDIT_LOG_FAIL] ...')` にフォールバック
- **絶対に Utils.logError を呼ばない**(循環参照防止)

### B. setupAllSchemas への LOG_AUDIT マッピング追記(`101_sys_config.js`)

L424 付近の `if (!existKeys.includes('SYS_PARAM'))` の直後(論理的な挿入位置はシステム基盤系の末尾、TRN_JOUR 登録の後)に以下を追加:

    if (!existKeys.includes('LOG_AUDIT')) confSheet.appendRow(['LOG_AUDIT', '', '98_audit_log', '監査証跡ログ(操作追跡・WORM)']);

CLAUDE.md の「DDL (setupAllSchemas) で管理されないタブ」一覧には**追加しない**。

## 制約

- 既存の Utils.logInfo / logError / toastResult のシグネチャは変更しない
- `802_audit.js` は一切変更しない(データ整合性チェッカーで役割が別)
- `99_error_log` 関連コード(MAS-178 の資産)は一切触らない
- auditLog の catch から Utils.logError を呼ばない(循環参照防止)
- 98_audit_log の列定義 DDL は書かない(auditLog が自動生成)

## エッジケース(実装で必ずカバー)

| ケース | 期待動作 |
|--------|---------|
| Session.getActiveUser().getEmail() が空文字 | 'SYSTEM' を記録 |
| Session.getActiveUser() 自体が throw | try-catch で吸収し 'SYSTEM' |
| beforeValue/afterValue が object/Array | JSON.stringify で文字列化 |
| JSON.stringify が循環参照で throw | '[UNSERIALIZABLE]' を記録 |
| 文字列が 4000 文字超 | 末尾切り捨て '...[TRUNCATED]' |
| 値が undefined / null | 空文字を記録 |
| auditLog 内で例外発生 | console.error にフォールバック、throw しない |

## 実データ検証

実装後、GASエディタで以下を手動確認:
1. `Session.getActiveUser().getEmail()` を console.log で出力し、dev 環境で実値が取れるか確認(空なら共有ドライブ権限設定を要確認)
2. MCP `list_sheets` で `98_audit_log` / `98_audit_log_*` の既存衝突なし確認
3. 未作成状態から `Utils.auditLog('RUN', 'TEST', 'T001', 'manualTest', null, { x: 1 }, 'smoke')` を1回実行し、シート自動作成・ヘッダー・1レコード追記を目視確認

## 動作確認

1. `npm run push:dev` で dev 環境にデプロイ
2. GAS エディタで関数 `Utils.auditLog('RUN', 'TEST', 'T001', 'manualTest', null, { x: 1 }, 'smoke')` を手動実行
3. スプレッドシートを開き、`98_audit_log` シートが作成されていること、ヘッダー 9 列が正しいこと、1 行追記されていることを確認
4. `setupAllSchemas` を実行し、`01_sys_config` に `LOG_AUDIT | | 98_audit_log | 監査証跡ログ(操作追跡・WORM)` が追加されることを確認
5. 循環参照テスト: `var a = {}; a.self = a; Utils.auditLog('RUN','TEST','T002','t_', a, null, '')` → シートに `[UNSERIALIZABLE]` が記録されることを確認

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| A. Utils.auditLog 追加 | なし | appendRow + 自動シート作成の定型パターン(MAS-178 persistLog と同型) |
| B. LOG_AUDIT マッピング | なし | 1 行追加のみ |

Step 2: 重要計装点への挿入

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-179「監査証跡の強化 (Audit Trail)」の Step 2(重要計装点への挿入)を実装してください。

## 前提

Step 1 が完了し、`Utils.auditLog(operation, targetSheet, targetId, funcName, beforeValue, afterValue, note)` が利用可能であること。

## 実行前タスク

以下のファイルを読み込み、挿入位置を正確に特定してください:
1. `docs/dev/dev_mas-179_audit_trail.md` — 本仕様書「修正方針 Step 2」の計装点一覧表とコード例
2. `400_domain/407_rpa_orchestrator.js` L30-60 — generateAllRpaInvoices の完了ログ位置
3. `400_domain/410_subledger_engine.js` L100-200 (Action A) / L355付近-終端 (Action B) — 仕訳生成ループ内の TRN 行 push 箇所
4. `100_config/101_sys_config.js` L387-410 — onEdit 本体と handleUxAssist 呼び出し位置
5. `100_config/101_sys_config.js` L412 以降 — setupAllSchemas の正常完了位置
6. `800_ops/807_migration_i10.js` / `808_migration_i24.js` — 既存マイグレーションの定型パターン(冒頭ログ・完了ログ)
7. `000_infra/001_env.js` — Env.name() の戻り値('dev' / 'prod')

## 修正対象ファイル

- `400_domain/407_rpa_orchestrator.js` への追記のみ(1 行)
- `400_domain/410_subledger_engine.js` への追記のみ(Action A / Action B それぞれ数行)
- `100_config/101_sys_config.js` への追記のみ(onEdit 本体に承認列監視ブロック 15 行 + setupAllSchemas 完了時 1 行)
- `800_ops/80*_migration_*.js` 全マイグレーション関数への追記(冒頭・完了の 2 行ずつ)

## 実装内容

### A. 407_rpa_orchestrator.js の generateAllRpaInvoices 完了ログ直前

L57 の `Utils.logInfo(FUNC, '一括実行完了: 合計' + total + '件');` の直前に:

    Utils.auditLog('RUN', '', '', FUNC, '', { total: total }, 'Env=' + Env.name());

### B. 410_subledger_engine.js Action A(processInvoiceApprovals)

TRN 行を `newTrnRows.push(...)` した直後(INV 単位で 1 件):

    Utils.auditLog('CREATE', '42_trn_journal', trnId, FUNC,
      { invId: invId, status: '承認済' },
      { trnId: trnId, amount: amount, accountName: accountName },
      'Action A');

### C. 410_subledger_engine.js Action B(processSettlementClearings)—最重要

STL 消込仕訳を生成した直後:

    Utils.auditLog('CONFIRM', '33_wrk_bank', stlId, FUNC,
      { status: 'UNCONFIRMED' },
      { status: 'CONFIRMED', jnlId: newTrnId, settleDate: settleDate },
      'Action B / Env=' + Env.name());

### D. onEdit への承認列監視ブロック(101_sys_config.js L408 handleUxAssist 呼び出し直前)

仕様書「修正方針 Step 2 コード例:onEdit での承認チェックボックス監視」のコードを一字一句そのまま挿入。ガードは以下すべて:
- `sheetName === '33_wrk_bank' || sheetName === '32_wrk_invoice'` のみ
- 「確認FLG」または「承認ステータス」列のみ
- e.range.getNumRows() > 100 ならサマリー記録(行数のみ、値は記録しない)
- catch で握りつぶし、auditLog 失敗でも onEdit 本体を壊さない

### E. setupAllSchemas 完了時

正常完了の最後(ダイアログ通知の直前)に:

    try { Utils.auditLog('RUN', '', '', 'setupAllSchemas', '', '', 'DDL 最新化完了'); } catch(_){}

### F. マイグレーション関数の冒頭・完了

`800_ops/804_migration_d01_d03.js` 〜 `808_migration_i24.js` の各 migrationDNNDNN() 関数:
- 冒頭(try の直後): `Utils.auditLog('MIGRATE', '', '', FUNC, '', '', 'START');`
- 完了(alert 直前): `Utils.auditLog('MIGRATE', '', '', FUNC, '', { summary: summary }, 'END');`

## 制約

- 既存ロジック(仕訳生成・残高更新・冪等性チェック)は一切変更しない。追記のみ
- auditLog の呼び出しは**try-catch で囲まない**(auditLog 内部で握りつぶすため、二重ガードは不要)。例外は onEdit のブロック全体を囲むガードのみで十分
- 呼び出し位置は「既存の Utils.logInfo の直前 / 直後」を原則とし、ビジネスロジックの途中には挿入しない
- 100 行超の範囲編集では値の JSON.stringify を試みない(サマリーのみ)

## エッジケース

| ケース | 期待動作 |
|--------|---------|
| Action A/B で 0 件処理 | auditLog を呼ばない(空ループで終了) |
| onEdit の e.oldValue が undefined | 空文字を記録 |
| onEdit で 100 行超範囲編集 | `Utils.auditLog('UPDATE', sheetName, '', 'onEdit', '', '', 'bulk-edit rows=' + e.range.getNumRows())` のみ |
| マイグレーション関数が throw した場合 | 冒頭の START だけ記録され END は記録されない(catch で logError が呼ばれる)。これは許容 |

## 実データ検証

1. MCP `grep` で `Utils.logInfo` の呼び出し箇所を対象ファイルで全件列挙し、auditLog 挿入位置が既存ログと整合するか確認
2. dev 環境で Action B を実行し、98_audit_log にユーザー Email + CONFIRM + 対象STL ID が 1 件ずつ記録されることを確認
3. onEdit で 33_wrk_bank の確認FLG を true にし、UPDATE レコードが記録されるか確認(e.oldValue が false、e.value が true になることを併せて確認)
4. `e.value` がチェックボックスの場合、文字列 'true' / 'false' で記録されるか boolean のまま記録されるかを実データで確認し、エッジケース欄に反映

## 動作確認

1. `npm run push:dev` で dev 環境にデプロイ
2. Action B(消込済STLを仕訳台帳へ転記)を実行し、98_audit_log に CONFIRM レコードが記録されることを確認
3. 33_wrk_bank の確認FLG をチェック → 98_audit_log に UPDATE レコード(対象ID・列名・oldValue/newValue)が記録されることを確認
4. setupAllSchemas 実行時に RUN レコードが記録されることを確認
5. `migrationI10` を実行し、START と END の 2 レコードが記録されることを確認(既に略称が埋まっていればスキップでも END は記録される)
6. GAS の「コード検索」で `Utils.auditLog` を全件検索し、計装点一覧表の 6 種類がすべて挿入済みであることを grep 確認(MAS-192 失敗パターン回避)

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| A. RPA 一括起票計装 | なし | 1 行追加のみ |
| B. Action A 計装 | あり | ループ内での INV ↔ TRN 対応関係の特定が必要 |
| C. Action B 計装 | あり | STL ↔ 新 TRN の関連付けと、Env.name() 経由の環境記録 |
| D. onEdit 承認列監視 | あり | e.oldValue の undefined 対応・100 行超ガード・2シート対応の分岐 |
| E. setupAllSchemas 計装 | なし | 1 行追加のみ |
| F. マイグレーション一括計装 | なし | 冒頭・完了の定型パターンを各ファイルへ機械的に追加 |

Step 3: 管理者メニュー + アーカイブ補助 + テスト

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-179「監査証跡の強化 (Audit Trail)」の Step 3(管理者メニュー + アーカイブ補助 + テスト)を実装してください。

## 前提

Step 1, Step 2 が完了していること。

## 実行前タスク

1. `docs/dev/dev_mas-179_audit_trail.md` — 本仕様書「修正方針 Step 3」
2. `100_config/101_sys_config.js` L299-385 — onOpen の全メニュー構造。`🔧 開発・設定` メニューの位置
3. `100_config/101_sys_config.js` 末尾付近 — 既存ユーティリティ関数群(sortSheetsByName 等)
4. `000_infra/004_utils.js` — Utils.parseDateToYm / Utils.addMonths のシグネチャ
5. `900_test/901_test_runner.js` — runAllTests のテストケース構造(pass/message を返す関数のパターン)

## 修正対象ファイル

- `100_config/101_sys_config.js` への追記のみ(メニュー項目 2 行 + openAuditLog / archiveAuditLogMonthly 関数 約 45 行)
- `900_test/901_test_runner.js` への追記のみ(testAuditLogBasic_ 関数と runAllTests への登録)

## 実装内容

### A. 管理者メニュー追加(101_sys_config.js `🔧 開発・設定` L346-350)

`.addItem('タブを番号順に並べ替え', 'sortSheetsByName')` の直後に追加:

    .addItem('📋 監査ログを開く (98_audit_log)', 'openAuditLog')
    .addItem('🗄️ 監査ログを月次アーカイブ', 'archiveAuditLogMonthly')

### B. openAuditLog / archiveAuditLogMonthly 関数

仕様書「修正方針 Step 3-A」のコードを一字一句そのまま、`101_sys_config.js` 末尾(最後の function の後、閉じ `}` なし)に追加。以下を厳守:

- archiveAuditLogMonthly は前月分のみを対象(当月は現役データとして残す)
- 移送先シート名は `98_audit_log_YYYYMM` 形式
- 冪等性は保証しない(同一月を二度実行すると重複記録)→ 警告扱い、運用ルールで対処
- アーカイブ実行自体を `Utils.auditLog('RUN', '98_audit_log', '', FUNC, '', { moved: n, archiveSheet: name }, '')` で記録

### C. 901_test_runner.js への testAuditLogBasic_ 追加

仕様書「修正方針 Step 3-B」のコードを一字一句そのまま、901_test_runner.js の runAllTests の呼び出し対象配列に登録:

    { name: 'testAuditLogBasic_', fn: testAuditLogBasic_ }

登録位置は既存のテストケース配列の末尾。テスト関数自体はファイル末尾に配置。

テスト完了後、記録したテスト行は `sheet.deleteRow` で必ず削除し、98_audit_log にゴミを残さない。

### D. SKIP_PATTERNS 更新(あれば)

`901_test_runner.js` に SKIP_PATTERNS 配列が存在する場合、`98_audit_log` と `98_audit_log_*` を追加(T4-03 失敗パターン回避)。

## 制約

- openAuditLog 関数内で新規シート作成はしない(存在しない場合はアラートで案内のみ)
- archiveAuditLogMonthly は前月以前のみ対象。当月データを誤って移送しない
- アーカイブシートへの書き込みは appendRow ではなく setValues(性能・原子性)を検討してもよいが、本仕様では appendRow で統一
- テスト関数は 98_audit_log に残したテスト行を必ず deleteRow で削除する
- メニュー項目の位置は `🔧 開発・設定` メニュー内。`🔧 マイグレーション` には入れない

## エッジケース

| ケース | 期待動作 |
|--------|---------|
| openAuditLog 実行時にシート未作成 | アラート「監査ログはまだ作成されていません」表示 |
| archiveAuditLogMonthly 実行時に対象行 0 件 | ダイアログで「アーカイブ対象なし」表示、空シート作成しない |
| 同一月を二度アーカイブ | 既存アーカイブに appendRow で追記(警告なし。運用ルールで回避) |
| archiveAuditLogMonthly 実行中に auditLog が呼ばれる | 当月データとして保持(前月以前のみ対象のため影響なし) |
| testAuditLogBasic_ 実行後にテスト行を削除し忘れ | Step 1 の自動シート作成と同じく残る。deleteRow を必ず呼ぶ |

## 実データ検証

1. MCP で `98_audit_log` に複数月のデータが存在する状態を作り、archiveAuditLogMonthly 実行後に前月分のみがアーカイブシートに移送され、当月分は元シートに残ることを確認
2. 901_test_runner.js の既存テストが SKIP_PATTERNS で 98_audit_log をスキップしているか確認

## 動作確認

1. `npm run push:dev` で dev 環境にデプロイ
2. `🔧 開発・設定` メニューに 2 項目が追加されていることを確認
3. 監査ログを開く → 98_audit_log シートがアクティブ化されることを確認
4. runAllTests を実行し、testAuditLogBasic_ が pass すること、98_audit_log にテスト行が残っていないことを確認
5. 前月日付で auditLog を手動挿入 → archiveAuditLogMonthly 実行 → `98_audit_log_202603` 等の月次シートが作成され、前月行のみ移送されていることを確認

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| A. メニュー追加 | なし | 2 行の .addItem 追加のみ |
| B. openAuditLog / archiveAuditLogMonthly | あり | 月次判定(Utils.parseDateToYm + addMonths(-1))、setValues での原子的書き戻し |
| C. testAuditLogBasic_ | なし | pass/message 返却の定型パターン |
| D. SKIP_PATTERNS | なし | 配列追記のみ |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6複数ファイル横断の計装設計、セキュリティ設計の判断、MAS-178 との役割分離、アーキテクチャ(WORM / 循環参照防止)の判断
Step 1 実装(Utils.auditLog + スキーマ)Claude Haiku 4.5仕様書でコード完全定義済み。MAS-178 persistLog と同型の定型パターン
Step 2 実装(計装点への挿入)Claude Sonnet 4.6挿入位置の特定(Utils.logInfo の前後)、既存パターンの適用、Action A/B のループ内での対応関係把握
Step 3 実装(メニュー・アーカイブ・テスト)Claude Sonnet 4.6UI統合判断、月次判定ロジック、テスト関数の配置位置

変更履歴

日付変更内容
2026-04-17初版作成。98_audit_log による専用WORMシート + Utils.auditLog API + 6種類の計装点(RPA / Action A / Action B / onEdit 承認列 / setupAllSchemas / マイグレーション)+ 管理者メニュー + 月次アーカイブ + テストの3ステップ設計