概要

項目内容
案件IDMAS-212
案件名論理削除の理由記録+物理削除アーカイブ
カテゴリデータ管理 / 基盤改善
優先度P2
想定工数★★
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル100_config/101_sys_config.jsDDL・メニュー・トリガー登録)
300_ui/301_ui_assist.js(onEdit ハンドラ追加)
000_infra/004_utils.jsUtils.auditLog JSDoc 拡張)
800_ops/818_archive_records.js(新規作成)
関連案件MAS-179(監査証跡基盤)、MAS-148(ワンクリック月次締め:アーカイブ組込み候補)、MAS-201(バックアップ / 809_backup_tool.js)

目的

本案件は次の 2 つの課題を同時に解決する。

  1. 削除理由の説明責任 現状、有効フラグを FALSE にする論理削除は誰でも無言で実行できる。税務調査時等に「この行をなぜ無効化したか」を説明する手段がない。削除時に理由・操作者・日時を必須で記録することで、MAS-179 監査証跡と連携してトレーサビリティを確保する。

  2. 論理削除データの蓄積によるシート肥大化防止 論理削除した行は永遠に 31/32/33 タブに残り続け、行数が増えるとマート再構築や findAll() キャッシュ更新コストが膨らむ。一定期間(デフォルト 6 ヶ月)経過した無効行を専用アーカイブシート(89_arch_*)に移動させ、元シートから物理削除することで、ワーキングタブを実稼働データのみに保つ。

現在のコード

  • 31_wrk_order / 32_wrk_invoice / 33_wrk_bank は A 列に「有効フラグ」を持ち、FALSE の行は dmIsActive_ 相当のフィルタで集計・マート対象外となる(論理削除パターン)。
  • 論理削除時のアテンションは既存の onEdit100_config/101_sys_config.js:409 付近)に仕訳発行後の UPDATE 監査ロジックはあるが、削除理由を記録する仕組みは存在しない
  • 物理削除はメニューからの cleanupEmptyRows / cleanupDuplicateRows 等の限定的な運用ツールしかなく、「古くなった論理削除行を物理削除する」汎用アーカイブ機能は未実装。
  • Repository 層(200_data/202_repository.js)に存在するのは OrderRepository / InvoiceRepository / BankTxRepository / JournalRepository / AccountRepository / PartnerRepository の 6 クラス。ExpenseRepository / FinanceRepository は存在しない。

修正方針

実装は以下の 2 ステップに分ける。各ステップ完了後に動作確認を行ってから次へ進む。

Step 1: DDL 変更と論理削除理由記録

1-1. setupAllSchemas() への 3 列追加(100_config/101_sys_config.js

対象 3 シート(WRK_ORDR / WRK_INVC / WRK_BANK)の schemas 定義 headers 末尾に以下 3 列を追加する。

列名想定型用途
無効化理由テキストonEditLogicalDelete_ がユーザー入力を書き込む
無効化日時日付時刻new Date() を書き込む。アーカイブ判定の基準
無効化者テキストSession.getActiveUser().getEmail() を書き込む

冪等性: setupAllSchemas() 既存パターン(confSheet.getDataRange().getValues().map(r => r[0]); if (!existKeys.includes('…')) …)はシート登録で使われており、列追加は schemas[key].headers 配列そのものをマスターソースとして setValues で上書きするパターン。既存の列の右側に 3 列を付け足す分には、既にデータが入っている下段行は影響を受けない(ヘッダー行のみ更新)。

setupAllSchemas 実行前に既存シートに値が入っている行があっても、新設 3 列は「空」のまま維持されるため安全に追加可能。

1-2. onEditLogicalDelete_ ハンドラの新設(300_ui/301_ui_assist.js

既存 handleUxAssist(e) と同じファイルに新規関数を追加する。呼び出しはシンプルトリガー onEdit(e)101_sys_config.js:409)から handleUxAssist(e) 同様に try { if (typeof onEditLogicalDelete_ === 'function') onEditLogicalDelete_(e); } catch(err) { console.error(...); } で受け渡す、または下記 1-3 でインストーラブルトリガーとして独立登録する(後者を採用)。

重要制約: SpreadsheetApp.getUi().prompt() / .alert() はシンプルトリガー(function onEdit(e))からは呼び出せない(Exception: このサービスを呼び出すには認証が必要です になる)。そのため インストーラブルトリガーとして登録が必須

ハンドラの処理フロー:

  1. e.range の対象シート名が対象 3 シート(WRK_ORDR / WRK_INVC / WRK_BANK に対応する物理名)であることを確認。それ以外は即 return
  2. 編集された列が A 列(col === 1。ヘッダーから indexOf('有効フラグ') で動的取得するのが厳密だが、全シート共通で A 列固定の運用前提)であることを確認。
  3. 編集後の値が false / FALSE であることを確認(truefalse の遷移のみを対象にする。既に false の行への再編集は無視)。
  4. SpreadsheetApp.getUi().prompt('無効化理由', '…', ui.ButtonSet.OK_CANCEL) で理由入力ダイアログを表示。
  5. キャンセル時: e.range.setValue(e.oldValue) で元の値(true)にリバートし処理中断。
  6. OK 時: 入力された理由、new Date()Session.getActiveUser().getEmail() を同行の 無効化理由 / 無効化日時 / 無効化者 列に書き込む(列インデックスはヘッダーから indexOf で動的取得)。
  7. Utils.auditLog('DELETE_LOGICAL', sheetName, recordId, '有効フラグ', 'onEditLogicalDelete_', true, false, reason) を呼び出す(recordId は B 列の ID 値)。

'DELETE_LOGICAL' は新設 operation コード000_infra/004_utils.js:429Utils.auditLog JSDoc 列挙値 'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE''DELETE_LOGICAL' を追記する(ロジック変更なし、JSDoc のみ)。

1-3. インストーラブルトリガー登録関数(100_config/101_sys_config.js

installAutoOpenSidebarTrigger の直下に類似パターンで installOnEditLogicalDeleteTrigger を追加。

function installOnEditLogicalDeleteTrigger() {
  const ui = SpreadsheetApp.getUi();
  var existing = ScriptApp.getProjectTriggers().filter(function(t) {
    return t.getHandlerFunction() === 'onEditLogicalDelete_';
  });
  existing.forEach(function(t) { ScriptApp.deleteTrigger(t); });
  ScriptApp.newTrigger('onEditLogicalDelete_')
    .forSpreadsheet(SpreadsheetApp.getActive())
    .onEdit()
    .create();
  Utils.auditLog('RUN', '', '', '', 'installOnEditLogicalDeleteTrigger', '', { removed: existing.length, added: 1 }, 'N-36 計装');
  ui.alert('✅ 論理削除理由ダイアログを有効化', '有効フラグを FALSE にしたとき、理由入力ダイアログが表示されます。', ui.ButtonSet.OK);
}

uninstallOnEditLogicalDeleteTrigger も対で用意する。

メニュー登録は次節参照。

Step 2: 物理削除アーカイブ機能

2-1. アーカイブ先シートの DDL 追加

setupAllSchemas()schemas オブジェクトに ARCH_WRK_ORDR / ARCH_WRK_INVC / ARCH_WRK_BANK を追加(対応物理名: 89_arch_31_wrk_order / 89_arch_32_wrk_invoice / 89_arch_33_wrk_bank)。ヘッダーは元シートと完全一致させる(3 列追加込みの定義を共有)。

01_sys_config の登録キーも ARCH_ORDR / ARCH_INVC / ARCH_BANK で追加。

2-2. archiveInvalidRecords() 本体(新規ファイル 800_ops/818_archive_records.js

処理フロー:

  1. 権限チェック: isPrivilegedUser_() で false なら中断(setupAllSchemas と同パターン)。
  2. 保持期間取得: const retentionMonths = Constants.getParam('ARCHIVE_RETENTION_MONTHS', 6);
    • 03_sys_paramsARCHIVE_RETENTION_MONTHS が未登録でも 6 がフォールバック。初期設定として手動追記を推奨する旨を動作確認手順に含める。
  3. 閾値日時計算: const threshold = new Date(); threshold.setMonth(threshold.getMonth() - retentionMonths);
  4. シートごとのアーカイブ対象抽出(対象 3 シート分ループ):
    • 該当 Repository の findAll(){ headers, dtos } を取得(OrderRepository / InvoiceRepository / BankTxRepository)。
    • dtos.filter(d => d.isActive === false && d.invalidatedAt instanceof Date && d.invalidatedAt < threshold) でアーカイブ対象を抽出。
    • dto.invalidatedAtnull / undefined / 空の行は保護対象(アーカイブしない)。旧データの誤削除を防ぐため。
  5. 件数集計と確認ダイアログ:
    • 全シートの合計件数を算出。
    • 0 件ならアラートを出して正常終了。
    • 1 件以上なら SpreadsheetApp.getUi().alert('アーカイブ確認', '{シート名}: N件, {シート名}: M件\n合計 X 件をアーカイブします。よろしいですか?', ButtonSet.OK_CANCEL) を表示。キャンセル時は中断。
  6. アーカイブ実行(確認 OK 後、シート単位ループ):
    • 対象シートの全 DTO を「アーカイブ対象」と「保持レコード」に分割。
    • アーカイブ対象を 89_arch_{シート名} シートに sheet.getRange(lastRow+1, 1, n, headers.length).setValues(rowsToArchive) で追記(Contracts.dtoToRow 経由で DTO を行形式に変換)。
    • 保持レコードを元シートに Repository.save(dtos)全件置換する。
    • Utils.auditLog('ARCHIVE', sheetName, '', '', 'archiveInvalidRecords', null, null, ${count}件アーカイブ (retention=${retentionMonths}ヶ月)) を呼び出す。
  7. 完了通知: Utils.toastResult('archiveInvalidRecords', '{総件数}件をアーカイブしました') + ui.alert で完了表示。

'ARCHIVE' も新設 operation コードUtils.auditLog JSDoc に追記する。

sheet.deleteRows() 使用禁止: Repository.save() による全件置換パターンを厳守。deleteRows() は例外時のロールバックが難しく、データ損失リスクがあるため。

2-3. onOpen メニュー追加(000_infra/002_constants.js

Constants.MENU_DEFINITION「📋 サイドバー: 🔧 開発・設定」 カテゴリ(002_constants.js:241)に以下 3 項目を追加:

{ label: '🔒 論理削除ダイアログを有効化', funcName: 'installOnEditLogicalDeleteTrigger', description: '有効フラグ FALSE 時に理由入力ダイアログを表示するトリガーを設置' },
{ label: '🔓 論理削除ダイアログを無効化', funcName: 'uninstallOnEditLogicalDeleteTrigger', description: '論理削除理由ダイアログのトリガーを削除' },
{ label: '🗄️ 無効レコードをアーカイブ', funcName: 'archiveInvalidRecords', description: 'ARCHIVE_RETENTION_MONTHS 以上経過した論理削除行を 89_arch_* に移動' },

Contracts / Repository での DTO 拡張は Step 1-1 の DDL で追加した列を contracts.js の toDto/toRow にも反映する必要がある(isActive と同様に invalidationReason / invalidatedAt / invalidatedBy をマップ)。

影響範囲

領域影響
setupAllSchemas()スキーマハッシュ変更により Incremental 実行時も Full 適用が走る(1 回のみ)
Contracts.toDto / toRow31_wrk_order / 32_wrk_invoice / 33_wrk_bank の DTO に 3 プロパティ追加。既存の変換コードは後方互換(新プロパティは undefined → 空文字扱い)
Repository キャッシュ列追加後の初回 findAll() で再構築。大量データ時は数秒の遅延
マート再構築(600_report)ヘッダー追加だけなので dmIsActive_有効フラグ 列名ベースの参照のため影響なし
901_test_runner対象 3 シートの Contracts 往復テストは列追加を検知するため更新が必要
MAS-179 監査ログ'DELETE_LOGICAL' / 'ARCHIVE' の 2 種類の operation が追加記録される

注意事項

  1. シンプルトリガーの制約: SpreadsheetApp.getUi().prompt()function onEdit(e) からは呼べない。onEditLogicalDelete_ は必ずインストーラブルトリガー経由で呼び出すこと。installOnEditLogicalDeleteTrigger をユーザーがメニューから一度実行する必要がある(初回は認可ダイアログが表示される)。
  2. 複数行同時変更: ユーザーが A 列を複数行まとめて FALSE に変更した(コピペ等)場合、e.range.getNumRows() > 1 となる。ダイアログを行数分表示するのは UX が悪いため、1 回だけ表示し、入力した理由を全対象行に適用する。
  3. deleteRows() 禁止: アーカイブ処理の「元シートからの削除」は必ず Repository.save(保持レコード) による全件置換で実現する。deleteRows() は処理途中の例外でデータ損失リスクがある。
  4. 新設 operation コードの JSDoc 同期: 'DELETE_LOGICAL' / 'ARCHIVE' は既存の Utils.auditLog JSDoc 列挙値に存在しない。実装時に必ず 000_infra/004_utils.js の JSDoc を拡張すること(ロジック変更不要だが、型推論・ドキュメント整合性のため)。
  5. ARCHIVE_RETENTION_MONTHS 初期値: Constants.getParam のデフォルト値 6 が未登録時のフォールバックだが、運用開始前に 03_sys_params へ明示的に登録することを推奨(監査上、設定値の存在証明になる)。
  6. Repository 未整備シートの扱い: TODO_future.md 原文では「31/32/33/42 等の主要タブ」とあったが、42_trn_journal には「有効フラグ」列が存在せず論理削除の概念が適用されない。本仕様では 42_trn_journal を対象外とする(人間検討事項に記載)。
  7. 32_wrk_expense / 33_wrk_finance: これらのシートは現状のプロジェクトに存在しない(32_wrk_invoice / 33_wrk_bank のみ存在)。対象から除外する。
  8. Contracts 層の同期: DDL で 3 列追加する際、000_infra/003_contracts.jsorderToDto / invoiceToDto / bankTxToDto と逆変換関数にも新プロパティを追加する。

エッジケース

条件想定動作理由
onEditLogicalDelete_ でキャンセルボタンが押されたe.range.setValue(e.oldValue) でセル値を true に戻し、無効化 3 列への書き込みを行わず return誤操作保護。理由未記録のまま論理削除されることを防ぐ
複数行が一括で FALSE に変更された(コピペ・範囲選択)ダイアログを 1 回だけ表示し、入力した理由を e.range 内の全対象行に適用UX と一貫性のバランス。各行ごとにダイアログは煩雑
無効化理由が空文字のまま OK が押された初期実装では空文字を許容(理由未記録で論理削除)。ダイアログのデフォルトボタンは OK_CANCEL とし、空入力時の再プロンプトは行わない人間検討事項参照。厳格運用が必要なら後続案件で再プロンプト or 空文字禁止に変更
アーカイブ対象レコードが 0 件(全シート合計)確認ダイアログ前に「アーカイブ対象レコードが見つかりませんでした」を SpreadsheetApp.getUi().alert() で表示し正常終了空実行での誤動作防止
有効フラグ=FALSE だが 無効化日時 が空(旧データ)retentionMonths 判定でアーカイブ対象から除外(保護対象)無効化日時 未記録の旧論理削除データを誤って消さない。MAS-212 適用前のレコードは人間が個別に判断すべき
アーカイブ実行中に例外発生Repository.save() 方式のため元シートは変更前のまま(deleteRows() 非使用)。例外は Utils.logError に記録し、ユーザーに通知するデータ損失リスク回避
有効フラグを truefalsetrue に戻す(誤操作取消)truefalse の時点で理由ダイアログが起動するため、その時点でキャンセルすれば回避可能。falsetrue への戻しでは何も起きない(onEditLogicalDelete_true への遷移を監視しない)論理削除取消の監査は MAS-179 の通常 UPDATE ログで別途記録される
アーカイブ対象行に自動仕訳 JNL_ID が紐付いていたアーカイブ実行を止めない(保持期間を超えた無効行のみ対象のため、既に会計処理は確定済みの想定)。ただし JNL_ID を持つ無効行のアーカイブは監査ログの note にその旨を記録する(任意、人間検討事項参照)会計台帳との参照整合性は「アーカイブシート内に完全なコピーがある」ため運用上問題なし
03_sys_paramsARCHIVE_RETENTION_MONTHS が未登録Constants.getParam がデフォルト値 6 を返し処理続行初回運用時のフェイルセーフ
インストーラブルトリガー未登録のまま有効フラグを FALSE にシンプルトリガー onEdit は発火するが onEditLogicalDelete_ は呼ばれない(無効化 3 列は空のまま)。従来通りの「無言の論理削除」となる初回セットアップの手順として「🔒 論理削除ダイアログを有効化」をメニューから実行する案内を README / 動作確認手順に含める
アーカイブ先シート 89_arch_* が未作成の状態で archiveInvalidRecords 実行setupAllSchemas 未実行 → アーカイブ先シートが無く insertSheet 相当の自動生成でフォールバック(既存 98_audit_log の再作成パターンを参考に)。ただし原則は先に setupAllSchemas を実行する運用ユーザビリティ優先のフォールバック

実データ検証

実装着手前〜実装途中で、以下を MCP または GAS エディタで確認する。

  1. 03_sys_paramsARCHIVE_RETENTION_MONTHS 登録状況確認
    • 未登録の場合: dev / prod いずれも手動で 1 行追加する(キー=ARCHIVE_RETENTION_MONTHS、値=6)。
    • 登録済みの場合: 値が妥当か(1〜24 程度の整数)を確認する。
  2. 対象 3 シートのヘッダー最終列確認
    • 31_wrk_order / 32_wrk_invoice / 33_wrk_bank の現在のヘッダー末尾を MCP で取得し、無効化理由 / 無効化日時 / 無効化者 の 3 列が既に存在していないことを確認する。存在する場合は既に別案件で追加された可能性を調査する。
  3. Repository 実在確認
    • 200_data/202_repository.js を Grep で ExpenseRepository / FinanceRepository を検索し、存在しないことを確認。存在した場合は対象シートを再検討する。
  4. アーカイブシート命名衝突確認
    • 01_sys_config シートで 89_* / 89_arch_* というキーが既に登録されていないことを確認。
  5. 既存有効フラグ=FALSE の件数確認
    • 対象 3 シートで 有効フラグ=FALSE の行数を事前にカウントしておき、初回アーカイブ実行時の期待件数と突合する(ただし 無効化日時 が空の旧データはアーカイブされない点に注意)。
  6. isPrivilegedUser_() の動作確認
    • archiveInvalidRecords は特権操作とする想定。03_sys_params.PRIVILEGED_USERS に実行ユーザーが登録されているか確認する。

関連ドキュメント

人間が検討すべき事項

  1. アーカイブからの復元手順 当面は手動運用(アーカイブシート 89_arch_* から元シートへのコピペ)とする。復元専用ツール(例: restoreArchivedRecord(recordId))は将来案件として TODO_future.md に別エントリを起こすかを検討する。
  2. アーカイブ実行タイミング
    • 現行案: メニューからの手動実行のみ。
    • 将来案: MAS-148「ワンクリック月次締め」のフローへ組み込み(月次締め成功時に自動でアーカイブ走行)。自動化する場合、予期せぬデータ移動に備えて実行直前に MAS-201 バックアップを強制取得する運用を併設する。
  3. 無効化理由の必須/任意
    • 現行案: 任意(空文字許容)。
    • 厳格運用案: 空文字時にダイアログ再表示、または FALSE へのリバート。
    • どちらを採用するかは UX(入力負担)と内部統制(説明責任の厳格化)のトレードオフ。
  4. 旧データ(無効化日時 未記録の FALSE 行)の扱い
    • 現行案: アーカイブ保護対象(ずっと元シートに残る)。
    • 代替案 A: 別途マイグレーションスクリプト(810_migration_n36_backfill_invalidated_at.js 相当)で 無効化日時 = 案件リリース日 を一括付与し、保持期間経過後に自動アーカイブ対象化する。
    • 代替案 B: 人間が 1 行ずつ無効化日時を手動入力する(推奨しない)。
    • 対象行数次第でどちらが現実的かを決める。
  5. アーカイブ先シートのアクセス権限
    • 89_arch_* シートを一般ユーザーから隠す(hideSheet())、読み取り専用にする(protect() + 管理者のみ編集可)、またはそのまま参照可能にするかを運用で決定。税務調査対応の観点では「保持+閲覧可能」が安全。
  6. 保持期間の基準
    • 現行案: ARCHIVE_RETENTION_MONTHS = 6(無効化日時から 6 ヶ月)。
    • 代替案: 「年度末まで保持」「決算確定後N日」等の会計期準拠。後者の場合、判定ロジックが複雑化するため本案件ではシンプルな月数閾値を採用。会計期準拠が必要なら別案件化する。
  7. アーカイブシートの構造
    • 現行案: タブ別(89_arch_31_wrk_order / 89_arch_32_wrk_invoice / 89_arch_33_wrk_bank)。元シートとヘッダー構造が一致するため復元が容易。
    • 代替案: 全タブ統合(89_arch_unified)にして「元シート名」列を追加。件数が少ないときはシート数を抑えられるが、ヘッダー構造の違いを吸収する実装コストが高い。
  8. 自動仕訳 JNL_ID を持つ無効行のアーカイブ可否
    • JNL_ID が紐付いた行の論理削除は、既に 42_trn_journal 側の対応仕訳も無効化されている前提(現在の processInvoiceApprovals / processSettlementClearings の逆操作フロー)。本案件では「JNL_ID の有無でアーカイブ除外しない」方針だが、運用実態とのギャップがあれば要見直し。

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

あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-212「論理削除の理由記録+物理削除アーカイブ」を実装してください。

## 実行前タスク(即座にツール実行。テキスト報告禁止)
1. `100_config/101_sys_config.js` を Read し、以下を確認する:
   - `setupAllSchemas()` の `schemas` オブジェクト定義パターン(`WRK_ORDR` / `WRK_INVC` / `WRK_BANK` の headers)
   - `01_sys_config` シート登録パターン (`if (!existKeys.includes('…')) confSheet.appendRow(…);`)
   - `onOpen()` の実装(Constants.MENU_DEFINITION をループして構築している)
   - `installAutoOpenSidebarTrigger` / `uninstallAutoOpenSidebarTrigger` のインストーラブルトリガー登録パターン
2. `000_infra/002_constants.js` の `Constants.MENU_DEFINITION` を Read し、「📋 サイドバー: 🔧 開発・設定」カテゴリの位置を確認する。
3. `000_infra/003_contracts.js` を Read し、`orderToDto` / `invoiceToDto` / `bankTxToDto` および逆変換関数のパターンを確認する。
4. `200_data/202_repository.js` を Read し、`OrderRepository.findAll()` / `save()` のシグネチャ、`InvoiceRepository` / `BankTxRepository` の構造を確認する。
5. `300_ui/301_ui_assist.js` を Read し、既存 `handleUxAssist(e)` の `e.range` / `e.oldValue` 参照パターンを確認する。
6. `000_infra/004_utils.js` の `Utils.auditLog` の JSDoc 列挙値 (`'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'`) を確認する。
7. `ls 800_ops/` で使用済み番号を確認し、`818_archive_records.js` が未使用であることを確認する (旧 spec で 812 を予約していたが MAS-057 tier seed が 812 を消費済のため 818 に変更済)。
8. MCP で対象 3 シート (`31_wrk_order` / `32_wrk_invoice` / `33_wrk_bank`) のヘッダー末尾列を取得し、`無効化理由` / `無効化日時` / `無効化者` の 3 列が存在しないことを確認する。
9. MCP で `03_sys_params` に `ARCHIVE_RETENTION_MONTHS` が登録済みかを確認する。未登録なら実装完了後の動作確認時に手動追加する。

## 修正対象ファイル
- `100_config/101_sys_config.js`
  - `schemas.WRK_ORDR` / `WRK_INVC` / `WRK_BANK` の headers 末尾に 3 列追加
  - `schemas.ARCH_WRK_ORDR` / `ARCH_WRK_INVC` / `ARCH_WRK_BANK` を新規追加(headers は元シート + 3 列と同一)
  - `01_sys_config` のシート登録エントリ 3 つを追加(`ARCH_ORDR` / `ARCH_INVC` / `ARCH_BANK`)
  - `installOnEditLogicalDeleteTrigger` / `uninstallOnEditLogicalDeleteTrigger` 関数を新設
- `000_infra/002_constants.js`
  - `MENU_DEFINITION` の「📋 サイドバー: 🔧 開発・設定」カテゴリに 3 メニュー項目を追加
- `000_infra/003_contracts.js`
  - `orderToDto` / `invoiceToDto` / `bankTxToDto` に `invalidationReason` / `invalidatedAt` / `invalidatedBy` を追加
  - 逆変換関数 (`dtoToOrderRow` 等) にも対応追加
- `000_infra/004_utils.js`
  - `Utils.auditLog` JSDoc の `@param operation` 列挙値に `'DELETE_LOGICAL'` と `'ARCHIVE'` を追記(ロジック変更なし)
- `300_ui/301_ui_assist.js`
  - `onEditLogicalDelete_(e)` 関数を新規追加
- `800_ops/818_archive_records.js`
  - 新規作成。`archiveInvalidRecords()` を実装

## 実装内容

### [1] setupAllSchemas の schemas 定義拡張(101_sys_config.js)
対象 3 シート(WRK_ORDR / WRK_INVC / WRK_BANK)の headers 末尾に次を追加する:
`"無効化理由", "無効化日時", "無効化者"`

アーカイブ先シートの schemas を新規定義:
- `ARCH_WRK_ORDR`: headers は WRK_ORDR と完全一致(3 列追加込み)、color は `"#434343"`(ログ系グレー)
- `ARCH_WRK_INVC`: 同上
- `ARCH_WRK_BANK`: 同上

`01_sys_config` 登録エントリ:
- `ARCH_ORDR` → `89_arch_31_wrk_order` (論理名: `アーカイブ_受発注台帳`)
- `ARCH_INVC` → `89_arch_32_wrk_invoice`(論理名: `アーカイブ_請求・債権債務台帳`)
- `ARCH_BANK` → `89_arch_33_wrk_bank` (論理名: `アーカイブ_入出金・消込台帳`)

### [2] Contracts 拡張(003_contracts.js)
`OrderDTO` / `InvoiceDTO` / `BankTxDTO` の @typedef に次のプロパティを追加:
- `invalidationReason: string`
- `invalidatedAt: Date|null`
- `invalidatedBy: string`

`orderToDto` / `invoiceToDto` / `bankTxToDto` で対応するヘッダー (`無効化理由` / `無効化日時` / `無効化者`) から読み取り。
逆変換関数でも同様に書き戻し。

### [3] Utils.auditLog JSDoc 拡張(004_utils.js)
`@param operation` 行の列挙値を次に置換(実装ロジックは変更しない):
`'CREATE' | 'UPDATE' | 'DELETE' | 'DELETE_LOGICAL' | 'ARCHIVE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'`

### [4] onEditLogicalDelete_ ハンドラ(301_ui_assist.js)
- 対象シート: `31_wrk_order` / `32_wrk_invoice` / `33_wrk_bank` のみ。それ以外は即 return。
- 編集列: A 列(col === 1)のみ。ヘッダー `有効フラグ` と一致することも確認。
- 編集後の値が `false` / `FALSE` であることを確認。それ以外は return。
- `e.range.getNumRows()` が複数なら複数行対応として処理(ダイアログは 1 回表示)。
- `SpreadsheetApp.getUi().prompt('無効化理由を入力してください', 'この論理削除の理由を記録します', ButtonSet.OK_CANCEL)` を表示。
- キャンセル時: `e.range.setValue(e.oldValue)`(複数行なら `setValues` で `e.oldValue` 相当を復元)して return。
- OK 時: `e.range.getSheet()` のヘッダーから `無効化理由` / `無効化日時` / `無効化者` の列インデックスを `indexOf` で取得。
- 対象全行の 3 列に次を書き込む(`Range#setValue` を行ごとにループ or `getRangeList` で一括):
  - 無効化理由: prompt 入力値
  - 無効化日時: `new Date()`
  - 無効化者: `Session.getActiveUser().getEmail()`
- 各行に対して次を呼び出す:
  `Utils.auditLog('DELETE_LOGICAL', sheetName, recordId, '有効フラグ', 'onEditLogicalDelete_', true, false, reason)`
  (recordId は B 列の ID 値。`indexOf` でヘッダー `発注ID(ORD)` / `請求ID(INV)` / `決済ID(STL)` のいずれか該当するものを動的に取得)

### [5] installOnEditLogicalDeleteTrigger(101_sys_config.js)
既存 `installAutoOpenSidebarTrigger` と同じパターンで実装:
- 既存の同名ハンドラトリガーを削除
- `ScriptApp.newTrigger('onEditLogicalDelete_').forSpreadsheet(SpreadsheetApp.getActive()).onEdit().create();`
- `Utils.auditLog('RUN', ..., 'N-36 計装')` を呼び出す
- `ui.alert` で完了通知

`uninstallOnEditLogicalDeleteTrigger` も同様に実装。

### [6] MENU_DEFINITION への追加(002_constants.js)
「📋 サイドバー: 🔧 開発・設定」カテゴリに以下を追加:

```javascript
{ label: '🔒 論理削除ダイアログを有効化', funcName: 'installOnEditLogicalDeleteTrigger', description: '有効フラグ FALSE 時に理由入力ダイアログを表示するトリガーを設置' },
{ label: '🔓 論理削除ダイアログを無効化', funcName: 'uninstallOnEditLogicalDeleteTrigger', description: '論理削除理由ダイアログのトリガーを削除' },
{ label: '🗄️ 無効レコードをアーカイブ', funcName: 'archiveInvalidRecords', description: 'ARCHIVE_RETENTION_MONTHS 以上経過した論理削除行を 89_arch_* に移動' },
```

### [7] archiveInvalidRecords(新規 812_archive_records.js)
```
function archiveInvalidRecords() { ... }
```
処理順序:
1. `isPrivilegedUser_()` で false → アラート表示して return。auditLog に試行を記録。
2. `const retentionMonths = Constants.getParam('ARCHIVE_RETENTION_MONTHS', 6);`
3. `const threshold = new Date(); threshold.setMonth(threshold.getMonth() - retentionMonths);`
4. 対象シートごとのループ (WRK_ORDR / WRK_INVC / WRK_BANK):
   a. 該当 Repository の `findAll()` で `{ headers, dtos }` を取得。
   b. `filter()` で「アーカイブ対象」と「保持レコード」に分割。
      - アーカイブ対象: `dto.isActive === false && dto.invalidatedAt instanceof Date && dto.invalidatedAt < threshold`
      - `invalidatedAt` が null / undefined / 空は保護対象(保持レコード側へ)
   c. アーカイブ対象が 0 件ならスキップ。
   d. 集計に加える: `{ sheetName, archiveCount, retainDtos, archiveDtos }`
5. 総件数 0 件なら `ui.alert('アーカイブ対象レコードが見つかりませんでした')` で正常終了。
6. 確認ダイアログ表示:
   ```
   const response = ui.alert('アーカイブ確認', '31_wrk_order: N件\n32_wrk_invoice: N件\n33_wrk_bank: N件\n\n合計 X 件をアーカイブします。よろしいですか?', ui.ButtonSet.OK_CANCEL);
   if (response !== ui.Button.OK) return;
   ```
7. 各シートのアーカイブ実行:
   a. `89_arch_{シート名}` シートを取得(`setupAllSchemas` 実行済み前提)。未作成なら `insertSheet` + ヘッダー付与。
   b. アーカイブ対象 DTO を Contracts.toRow 相当で 2 次元配列化。
   c. `archSheet.getRange(lastRow+1, 1, n, headers.length).setValues(rows)` で追記。
   d. 元シートへ `Repository.save(retainDtos)` で全件置換(**`deleteRows()` 使用禁止**)。
   e. `Utils.auditLog('ARCHIVE', sheetName, '', '', 'archiveInvalidRecords', null, null, \`${count}件アーカイブ (retention=${retentionMonths}ヶ月)\`)` を呼び出す。
8. 完了通知: `Utils.toastResult('archiveInvalidRecords', '合計 {N} 件をアーカイブしました')` + `ui.alert`。

## 制約
- `sheet.deleteRows()` の使用を絶対に禁止する。全件置換(`Repository.save()`)パターンを厳守する。
- `onEditLogicalDelete_` はシンプルトリガーではなくインストーラブルトリガー経由で呼び出すこと。
- 列参照はヘッダー名ベース(`indexOf` / `buildHeaderIndex_`)で行う。列番号ハードコード禁止。
- メニュー名・関数名・シート名はすべて Read で実在する文字列のみ使用する。推測で書かない。
- `42_trn_journal` は有効フラグを持たないため対象外。
- `32_wrk_expense` / `33_wrk_finance` は存在しないため対象外。

## 動作確認(dev 環境)
1. `npm run push:dev` でデプロイ。
2. GAS エディタ (dev) で `setupAllSchemas` を実行し、対象 3 シートに 3 列が追加され、`89_arch_*` シートが新規作成されることを確認。
3. `03_sys_params` に `ARCHIVE_RETENTION_MONTHS = 3` を手動登録(テスト用に短期間)。
4. `Constants._paramsCache` をクリアするため一度 setupAllSchemas を再実行するか、テスト用に新しいセッションを開く。
5. サイドバー「🔒 論理削除ダイアログを有効化」を実行し、GAS「トリガー」画面に `onEditLogicalDelete_` が登録されることを確認。
6. `32_wrk_invoice` で任意行の有効フラグ (A 列) を FALSE に変更 → 理由入力ダイアログが表示されることを確認。
7. 理由を入力して OK → 同行の `無効化理由` / `無効化日時` / `無効化者` が埋まることを確認。
8. `98_audit_log` に `DELETE_LOGICAL` エントリが記録されることを確認。
9. 別の行で FALSE → ダイアログでキャンセル → A 列が `true` に戻り、無効化 3 列が空のままであることを確認。
10. 複数行まとめて FALSE(範囲選択→削除キー等)→ ダイアログ 1 回表示 → 全対象行に同じ理由が書き込まれることを確認。
11. テストデータとして、`無効化日時` が 4 ヶ月前の FALSE 行を手動で用意(`ARCHIVE_RETENTION_MONTHS = 3` なので対象化)。
12. サイドバー「🗄️ 無効レコードをアーカイブ」を実行 → 確認ダイアログ → OK。
13. 対象行が `89_arch_{元シート名}` に移動し、元シートから削除されていることを確認。
14. `98_audit_log` に `ARCHIVE` エントリが記録されていることを確認。
15. 「無効化日時が空の FALSE 行」を用意して 12 を再実行 → 0 件扱いでアラート表示、その行は保持されることを確認(旧データ保護の検証)。
16. テスト完了後、`ARCHIVE_RETENTION_MONTHS` を運用値 `6` に戻す。

## 本番反映
- dev で全テスト合格後、`npm run push:prod` で本番デプロイ。
- 本番の `03_sys_params` に `ARCHIVE_RETENTION_MONTHS = 6` を登録。
- 本番で `setupAllSchemas` 実行(Incremental モードでも Full 適用が走る。ハッシュ差分で検知される)。
- 特権ユーザーが「🔒 論理削除ダイアログを有効化」を実行(各スプレッドシートごとに 1 回必要)。

推奨実行モデル

工程推奨モデル理由
Step 1-1 (schemas headers 3 列追加)Claude Haiku既存パターンへの単純な配列末尾追加。判断要素なし
Step 1-2 (onEditLogicalDelete_ 実装)Claude Sonnet既存 handleUxAssist パターンの踏襲 + 複数行対応・キャンセル処理の分岐判断あり
Step 1-3 (インストーラブルトリガー登録関数)Claude HaikuinstallAutoOpenSidebarTrigger のコピー改変。判断要素なし
Step 2-1 (アーカイブ先 schemas / 01_sys_config 登録)Claude Haiku既存パターンの機械的拡張
Step 2-2 (archiveInvalidRecords 本体)Claude Sonnet複数 Repository 横断、DTO フィルタ分岐、全件置換パターン適用の判断あり
Step 2-3 (MENU_DEFINITION 追加)Claude Haiku既存配列への追記のみ
Contracts 拡張(003_contracts.js)Claude SonnetDTO プロパティ追加は機械的だが、複数クラス横断の整合性確認が必要

変更履歴

日付内容
2026-04-21初版作成(MAS-212 仕様書)
2026-04-28番号衝突解消・812→818。実装監査の結果、初版で予約していた 800_ops/812_archive_records.js812 番が MAS-057 tier seed (812_migration_f57_tier_seed.js) で既に消費済と判明したため、archiveRecords の番号を 818 に変更800_ops/ 採番状況 (2026-04-28 時点): 801-815 既存 / 816-817 = MAS-234 セキュリティロードマップで予約済 / 818 = 本案件 (新規アサイン)。spec 内 4 箇所を一括置換 + 採番セクション更新。failure_patterns #31 (番号衝突チェック) の事後適用事例。docs-only 改訂で prod 自動デプロイへの影響なし。

仕様書作成プロンプト

展開して表示

本仕様書は tasks/prompts/task_N-36.md<instruction> タグ内プロンプトを Phase 1(調査)→ Phase 2(分割清書)→ Phase 3(nav 登録)の順に自律実行して作成された。

主な指示ポイント:

  • 対象案件: MAS-212「論理削除の理由記録+物理削除アーカイブ」(docs/_internal/TODO_future.md L292)
  • 作成対象: docs/dev/dev_mas-212_data_archiving.md
  • 追加タスク: docs/_config.jsonnav 配列(§E.1 基盤・DevOps セクション)に追記

Phase 1 調査で確定した事項(プロンプトテンプレートの記述と実リポジトリ状態の乖離を補正):

  1. 800_ops/ 次の空き番号は 818(2026-04-28 訂正・旧 spec で 812 を予約していたが MAS-057 tier seed 812_migration_f57_tier_seed.js が消費済 → MAS-212 は 818 を使用)。採番状況: 801-815 既存、816/817 = MAS-234 セキュリティロードマップで予約済 → 818 が次の空き番号。
  2. Repository 実在クラスは OrderRepository / InvoiceRepository / BankTxRepository / JournalRepository / AccountRepository / PartnerRepositoryExpenseRepository / FinanceRepository は存在しない。
  3. 対象シート 32_wrk_expense / 33_wrk_finance実在しないため対象から除外。
  4. 42_trn_journal は「有効フラグ」列を持たないため論理削除の概念が適用されず、対象から除外。
  5. 最終的な対象シートは 31_wrk_order / 32_wrk_invoice / 33_wrk_bank3 シート
  6. onOpenConstants.MENU_DEFINITION000_infra/002_constants.js)をループする宣言的定義方式。メニュー項目の追加は MENU_DEFINITION への配列要素追加で行う。
  7. 「🔧 開発・設定」メニューの実在名は「📋 サイドバー: 🔧 開発・設定」。
  8. Utils.auditLog の既存 operation 列挙値は 'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'。本案件で使用する 'DELETE_LOGICAL' / 'ARCHIVE' は新設。
  9. アーカイブシート命名帯 89_arch_* は未使用で衝突なし(既存の 89_* / 99_* 帯は 98_audit_log / 99_error_log / 99_audit_archive_YYYYMM 等)。

Phase 2 は 5 分割(2-1 骨格 / 2-2 概要〜注意事項 / 2-3a エッジケース〜人間検討事項 / 2-3b 実装プロンプト〜変更履歴 / 2-4 本セクション)で書き下した。