最終更新: 2026/06/22 18:56
MAS-179: 監査証跡の強化 (Audit Trail)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-179 |
| カテゴリ | セキュリティ |
| Phase | P1 |
| 優先度 | ★★ |
| 所要時間 | 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.js(RPA 起票時の CREATE 記録)400_domain/410_subledger_engine.js(Action 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 との関係 |
|---|---|---|---|
| 1 | 800_ops/802_audit.js | データ整合性チェッカー(資本金推移・予算整合性・マスタ参照) | 別物。本案件では触らない |
| 2 | Utils.logInfo / Utils.logError (004_utils.js L232-246) | console 出力のみ(7日で揮発、検索不可) | 維持。操作ログは別API |
| 3 | Utils.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 | 挿入位置 |
|---|---|---|---|---|
| 1 | 407_rpa_orchestrator.js | generateAllRpaInvoices() L32 | RUN | 完了ログ (Utils.logInfo '一括実行完了' L57) の直前 |
| 2 | 410_subledger_engine.js | processInvoiceApprovals() L106 (Action A) | CREATE | 各 TRN 行を newTrnRows に push した直後(INV 単位で1件) |
| 3 | 410_subledger_engine.js | processSettlementClearings() (Action B) | CONFIRM | STL 消込仕訳生成後(STL 単位で1件)— 本案件最重要計装点 |
| 4 | 101_sys_config.js | onEdit(e) L387 | UPDATE | 33_wrk_bank / 32_wrk_invoice の「確認FLG」「承認ステータス」列が編集されたときのみ |
| 5 | 101_sys_config.js | setupAllSchemas() L412 | RUN | 正常完了のtoast直前 |
| 6 | 800_ops/80*_migration_*.js | migrationXxx() 各関数 | MIGRATE | 関数冒頭と完了時 |
コード例:Action B 消込確定(最重要)
410_subledger_engine.js の processSettlementClearings 内、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 | ファイル | 変更量 | 既存動作への影響 |
|---|---|---|---|
| 1 | 000_infra/004_utils.js | 約50行追加 | Utils 名前空間に auditLog / serializeAuditValue を追加。既存関数は変更なし |
| 1 | 100_config/101_sys_config.js | 1行追加 (L424付近) | 01_sys_config に LOG_AUDIT マッピング追加のみ |
| 2 | 400_domain/407_rpa_orchestrator.js | 1行追加 (L57付近) | 完了ログ直前に auditLog 呼び出し |
| 2 | 400_domain/410_subledger_engine.js | 約10行追加 | Action A/B の仕訳生成ループ内に auditLog 呼び出し |
| 2 | 100_config/101_sys_config.js | 約15行追加 (L408付近) | onEdit に承認列監視ブロック追加 |
| 3 | 100_config/101_sys_config.js | 約45行追加 | メニュー項目 + openAuditLog / archiveAuditLogMonthly 関数 |
| 3 | 900_test/901_test_runner.js | 約15行追加 | testAuditLogBasic_ 追加 |
注意事項
- 循環参照の絶対防止:
Utils.auditLogの catch ブロックからUtils.logErrorを呼ばないこと。logError→persistLog→ (将来)auditLogの循環になる可能性があるため、console.errorのみにフォールバック 802_audit.jsとは別物: 「監査」の語が混同しやすいが、802 はデータ整合性チェッカー、MAS-179 は操作ログ。本案件で 802 は一切変更しない99_error_logの再利用禁止: MAS-178 が定義する 99_error_log は ERROR/WARN 専用で 5 列スキーマ。用途・列数・保持期間が異なるため、98_audit_log を新設- appendRow の使用: CLAUDE.md「MCP
add_rowsはシート先頭に追加されるため原則使わない」と矛盾しない。appendRow は末尾追加が保証される - 有効フラグ列なし: 監査ログは追記専用(WORM: Write Once Read Many)。論理削除・編集を認めないことで改ざん耐性を確保
- onEdit の粒度制御: 33_wrk_bank / 32_wrk_invoice の承認列のみに絞り、それ以外の列編集は記録しない(ログ肥大化防止)
e.oldValueの制限: onEdit のe.oldValueは単一セル編集時のみ値を持つ。範囲貼り付けや複数セル選択では undefined。その場合は空文字を記録- 大量行貼り付け対策: 100 行超の範囲編集は
e.range.getNumRows() > 100でガードし、範囲単位 1 レコードのサマリーのみ記録 - 新規シート追加時の
.claspignore確認: 98_audit_log は物理シートなので claspignore の影響はないが、念のためclasp push後に GAS エディタで表示されることを確認 (MAS-096 失敗パターン回避) - テストの SKIP_PATTERNS 更新: 新規シート追加時は
901_test_runner.jsの SKIP_PATTERNS(もしあれば)に 98_audit_log を登録(T4-03 失敗パターン回避) .gs拡張子変換の対象:.js→.gs変換は clasp 側で自動。追加する auditLog は Utils 名前空間配下の関数のため、既存のロード順(000_infra が最初)に従い全モジュールから参照可能
エッジケース
計算式は持たないが、運用上の境界動作をテーブルで明示する。
| ケース | 期待動作 | 理由 |
|---|---|---|
Session.getActiveUser().getEmail() が空文字 | ユーザー列に 'SYSTEM' を記録 | 権限不足・共有ドライブ設定時のフォールバック |
Session.getActiveUser() 自体が throw | 同様に 'SYSTEM' を記録(try-catch で吸収) | 権限なしアカウント実行時 |
beforeValue / afterValue が object / Array | JSON.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=prod | clasp で push されるため構造統一 |
| ログ総行数 > 100,000 行 | Utils.logInfo で WARN を出力し、手動アーカイブを案内 | appendRow の性能劣化(〜500ms/行)を防ぐ |
archiveAuditLogMonthly 実行時に対象行が 0 件 | ダイアログで「アーカイブ対象なし」と通知、シートは作成しない | 無駄な空シート作成を回避 |
| 同一月を二度アーカイブ | 既存アーカイブシートに appendRow で追記(重複の可能性あり) | 冪等性は保証しない。警告扱いで運用ルールで回避 |
実データ検証(MCP 事前確認項目)
実装前に以下を MCP または GAS エディタで確認する:
Session.getActiveUser().getEmail()の動作確認: dev / prod 両環境で GAS スクリプトエディタからconsole.log(Session.getActiveUser().getEmail())を手動実行し、空文字が返らないか確認。共有ドライブ配下のプロジェクトでは空になるケースがある- 既存シート名の衝突確認: MCP
list_sheetsで98_audit_log/98_audit_log_*が既に存在しないか確認。衝突する場合は 97_audit_log へリナンバリングを検討 99_error_logシートの存在確認: MAS-178 の実装順序により既存か未作成か。未作成でも MAS-179 は独立して動作可能だが、両者が並列運用される前提を明記101_sys_config.jsのメニュー構造: 現行🔧 開発・設定メニュー (L346) に📋 監査ログを開くを追加する位置を実物で確認(既存項目との重複・競合なし)- 計装点の既存ログ箇所:
407_rpa_orchestrator.jsL57 /410_subledger_engine.js内のUtils.logInfo呼び出し箇所を MCPgrepで実物確認し、auditLog の挿入位置を最終確定 e.oldValueの onEdit 挙動: dev 環境で 33_wrk_bank の確認FLG をチェックし、e.oldValue/e.valueの実値を console で確認(チェックボックス true / false の文字列化挙動)- 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 万行(自動アーカイブ必須)
- アーカイブ方式:
- 月次で
98_audit_log_YYYYMMシートに移送(本仕様書の Step 3 実装) - CSV として Drive に書き出し、シートは削除(MAS-212 物理削除アーカイブと連携)
- 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.6 | UI統合判断、月次判定ロジック、テスト関数の配置位置 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-17 | 初版作成。98_audit_log による専用WORMシート + Utils.auditLog API + 6種類の計装点(RPA / Action A / Action B / onEdit 承認列 / setupAllSchemas / マイグレーション)+ 管理者メニュー + 月次アーカイブ + テストの3ステップ設計 |