概要

項目内容
案件IDMAS-133
案件名年度跨ぎアーカイブ機構(行積み上がり対策E)
カテゴリデータ基盤・運用
PhaseP2
優先度★★
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル800_ops/819_migration_s61_fiscal_year_archive.js(新規)
200_data/202_repository.js(4 つの findAll() 末尾追記)
000_infra/002_constants.jsMENU_DEFINITION に 1 行追加)
前提案件なし(独立実装可)
関連案件MAS-130(フィルタービュープリセット・行積み上がり対策A)/ MAS-131(条件付き書式・対策B)/ MAS-132(月次行グルーピング・対策D)/ MAS-167(STL 重複行削除)/ MAS-201(スプレッドシート定期バックアップ)
採番衝突回避800_ops/ 既存番号 = 801〜815 / 816-817 = MAS-234 セキュリティロードマップで予約済 / 818 = MAS-212 archive_records で予約済 → 本案件 (MAS-133) = 819
※ 2026-04-28 訂正: 旧 spec で 812 を予約していたが MAS-057 tier seed (812_migration_f57_tier_seed.js) が消費済のため 819 に変更。実装時はディレクトリを再確認すること

目的

トランザクション系 4 シート(31_wrk_order / 32_wrk_invoice / 33_wrk_bank / 42_trn_journal)の行数増大による Google スプレッドシートのパフォーマンス劣化(行追加遅延・式再計算遅延・編集レスポンス低下・10M セル制限への接近)を、会計年度単位でのアーカイブ退避により根本解決する。

具体的目標:

  1. 当年度シートを軽量に保つ:現行シート(31_wrk_order 等)に残るのは「当会計年度内のレコード」のみとし、行数を年間流入量(数百〜数千行)に抑える
  2. 過去データの参照可能性を維持する:マート集計・履歴照会などで過去年度データが必要なケースに対応するため、Repository.findAll() 経由でアーカイブシートも透過的にマージして返却する
  3. 冪等な年次バッチ運用:年度切替直後に「アーカイブメニュー → 年度入力 → プレビュー → 実行」の対話型 3 ステップで完結し、二重実行・コピー失敗時のデータロストを防止する
  4. マスタ・予算系シートは対象外:本案件のスコープはトランザクション系(イベントログ)のみとし、マスタ系(11/12/13/14/15)と予算系(21〜28)は据え置く

MAS-130〜MAS-132 が「視認性・体感速度の改善」(フィルター・条件付き書式・行グルーピング)であるのに対し、MAS-133 は データ量そのものの削減 によりプラットフォーム制約を回避する位置付けである。5 年・10 年運用時の必須基盤と整理する。

現在のコード(問題箇所)

200_data/202_repository.js の 4 つの findAll() は、いずれも単一の現行シートだけを readSheetAsDtos_ で読み込んでおり、アーカイブされた過去レコードを参照する経路を持たない。年度をまたいで集計する場合、現行シートに全データを残すしか選択肢がない(=肥大化必至)。

// 200_data/202_repository.js L107-120 (OrderRepository)
var OrderRepository = {
  /** @private */
  _getSheet: function() {
    return Utils.getSheetByKey('WRK_ORDR', '31_wrk_order');
  },

  /**
   * 全発注レコードを DTO 配列で取得する。
   * @returns {{ headers: string[], dtos: OrderDTO[] }}
   */
  findAll: function() {
    return readSheetAsDtos_(OrderRepository._getSheet());
  },
  // ... save / append
};

// 200_data/202_repository.js L162-165 (InvoiceRepository.findAll)
findAll: function() {
  return readSheetAsDtos_(InvoiceRepository._getSheet());
},

// 200_data/202_repository.js L224-227 (BankTxRepository.findAll)
findAll: function() {
  return readSheetAsDtos_(BankTxRepository._getSheet());
},

// 200_data/202_repository.js L269-272 (JournalRepository.findAll)
findAll: function() {
  return readSheetAsDtos_(JournalRepository._getSheet());
},

加えて、シート間で共有される内部ヘルパーは以下の 3 関数で、本案件もこれらを変更せずに再利用する:

  • readSheetAsDtos_(sheet)200_data/202_repository.js L19-29:シート全体を getDataRange().getValues() で取得 → 1 行目をヘッダー → 2 行目以降を Contracts.toDto() で DTO 化
  • appendDtosToSheet_(sheet, headers, dtos, lastRowCol) — L85-101:findLastRow_ で末尾行を求め、その下に DTO 配列を setValues で書き込み
  • writeDtosToSheet_(sheet, headers, dtos) — L38-59:ヘッダー行を残してデータ行を全置換

Constants.getParam(key, defaultVal)000_infra/002_constants.js L147-167)は 03_sys_params シートを 読み取り専用 で 1 度だけロードしてキャッシュするヘルパーであり、書き戻しメソッドは存在しない。アーカイブ実施履歴を 03_sys_params に保存する経路は現状なく、保存するなら別の方式が必要になる。

Utils.getIdPrefixConfig(sheetName)000_infra/004_utils.js L482-487)は for (entry of Constants.ID_PREFIX_MAP) { if (sheetName.includes(entry.pattern)) return entry; }String.prototype.includes による部分一致 を行う。handleUxAssist300_ui/301_ui_assist.js L75-76)も同様に validSheets.some(name => sName.includes(name)) で部分一致する。アーカイブシート命名はこの部分一致に巻き込まれない設計が必要。

修正方針

Step 1: 800_ops/819_migration_s61_fiscal_year_archive.js 新規作成

公開関数 archiveFiscalYear() を 1 本だけエクスポートする(メニュー・テストのエントリポイント)。CLAUDE.md の migrationDNNDNN() 命名規則は 一度限りのデータ移行用 であり、本機能は年次繰り返し操作のため archiveFiscalYear() を採用する(migrationS61 等の使い捨て命名は避ける)。

アーカイブシート命名規則(Phase 1 設計判断①)

arc_{ベースシート名}_{年度} 形式を採用する。具体例:

ベースシート2025 年度のアーカイブシート名
31_wrk_orderarc_31_wrk_order_2025
32_wrk_invoicearc_32_wrk_invoice_2025
33_wrk_bankarc_33_wrk_bank_2025
42_trn_journalarc_42_trn_journal_2025

採用理由:

  • arc_ プレフィックスにより 00_menu カタログや setupAllSchemas 反復対象(番号プレフィックス順並び)から視覚的・プログラム的に分離される
  • 年度サフィックス(_2025 等)により年度識別が一目で可能
  • Repository.findAll() 改修側で ^arc_{ベースシート名}_\d{4}$ の正規表現一致で対象アーカイブシートを安全に列挙できる

ただしUtils.getIdPrefixConfighandleUxAssist部分一致(includes)判定arc_31_wrk_order_202531_wrk_order のシートと誤認するため、後述「注意事項」で両関数への if (sheetName.startsWith('arc_')) return null; ガード追加を必須とする。

処理フロー(順序厳守)

  1. 会計年度入力ダイアログSpreadsheetApp.getUi().prompt('対象会計年度', '例: 2025(4月始まり 2025-04 〜 2026-03)', ButtonSet.OK_CANCEL)yyyy を取得。/^\d{4}$/ で形式検証
  2. 会計年度境界値計算var startMonth = Number(Constants.getParam('CFG_FISCAL_YEAR_START', '4'));(4 月始まり前提)。fyStartYmd = ${yyyy}-${zeroPad(startMonth)}-01fyEndYmd = ${yyyy+1}-${zeroPad(startMonth-1 || 12)}-{月末日}
  3. 対象行の抽出(プレビュー用):4 シートそれぞれを readSheetAsDtos_ で読み、各シートの 判定列(後述)を Utils.parseDateToYmd で文字列化し、fyStartYmd <= dateStr <= fyEndYmd の範囲に含まれる DTO を抽出。有効フラグ === false の行も対象に含める(廃棄ではなくアーカイブのため)
  4. プレビュー確認ダイアログui.alert('アーカイブ実行確認', '${yyyy}年度(${fyStartYmd}〜${fyEndYmd})\\n\\n${シート別件数リスト}\\n\\n⚠️ この操作は元に戻せません。\\n⚠️ 元シートの該当行は削除されます。\\n\\n実行しますか?', ButtonSet.OK_CANCEL)
  5. 排他ロック取得var lock = LockService.getScriptLock(); lock.waitLock(30000);。30 秒以内に取得できなければ lock.tryLock 例外をキャッチして「他ユーザー実行中」ダイアログ表示で中断
  6. 冪等性チェック(Phase 1 設計判断②):4 シート分のアーカイブシート名を構築し、ss.getSheetByName(name)いずれか 1 枚でも既に存在すれば${yyyy}年度は既にアーカイブ済みです(既存シート: ${name})」と通知してロック解放・処理終了。Constants.getParam には書き戻し手段がないため、シート存在自体を冪等性キー として用いる
  7. シート別アーカイブ処理ループ:4 シートを順に以下の手順で処理する
    • a. var arcSheet = ss.insertSheet(arcName); でアーカイブシートを生成
    • b. ベースシートのヘッダー行を appendDtosToSheet_ 用に保持(base.headers
    • c. ヘッダー行を arcSheet.getRange(1, 1, 1, headers.length).setValues([headers]) で書き込み
    • d. 抽出済み対象 DTO を appendDtosToSheet_(arcSheet, headers, targetDtos, 0) で書き込み(戻り値 = 書き込み行数)
    • e. コピー件数検証writtenRows === targetDtos.length でなければエラーログ出力、arcSheetss.deleteSheet(arcSheet) で削除し、当該シートのアーカイブをキャンセル(次シートへは進まず処理全体を中断)
    • f. 元シートの対象行削除Contracts.toDto 化前の 生データ行インデックス(1 始まり、ヘッダー行が 1 → データ 1 行目が 2) を抽出時に保持しておき、降順ソート してから sheet.deleteRow(rowIndex) を 1 行ずつ呼ぶ。sheet.deleteRows(start, n) は連続行のみ有効なため非連続行群への適用は誤削除を招くので使用禁止
    • g. シート別処理完了ログを Utils.logInfo で記録
  8. ロック解放finally ブロックで lock.releaseLock() を必ず実行
  9. 完了通知Utils.toastResult('archiveFiscalYear', '${yyyy}年度 計${total}件のアーカイブが完了しました。')ui.alert(...)(808_migration_i24.js のパターンに倣い両方を出力)

シート別の判定列(年度範囲フィルタに使用する列):

シート判定列根拠
31_wrk_order起票日時OrderDTO に発生日列なし。起票日時 を年度判定に使用(003_contracts.js L15-36)
32_wrk_invoice発生日(P/L計上日)InvoiceDTO L47。P/L 計上日 = 会計年度判定の正準キー
33_wrk_bank決済日_実績BankTxDTO L77。実決済日が空欄の場合は 決済日_計画 にフォールバック
42_trn_journal発生日(P/L計上日)JournalEntryDTO L99

監査ログ:開始時 Utils.auditLog('MIGRATE', '', '', '', 'archiveFiscalYear', '', { fiscalYear: yyyy }, 'START') / 完了時に件数サマリを END で出力(808_migration_i24.js L13-14, L52-53 のパターンに準拠)。

Step 2: 200_data/202_repository.jsfindAll() 4 関数を改修

findAll()末尾に数行追加するのみ。既存の readSheetAsDtos_ 呼び出しは変更しない。headers はベースシートのものを唯一の真として採用する(アーカイブシートとベースシートのスキーマは生成時点で同一であることを前提)。

擬似実装イメージ:

findAll: function() {
  var base = readSheetAsDtos_(OrderRepository._getSheet());
  // S-61: アーカイブシート(arc_31_wrk_order_YYYY)を末尾にマージ
  var ss = getWebSpreadsheet_();
  var pattern = /^arc_31_wrk_order_\d{4}$/;
  var allSheets = ss.getSheets();
  for (var i = 0; i < allSheets.length; i++) {
    var name = allSheets[i].getName();
    if (!pattern.test(name)) continue;
    var arc = readSheetAsDtos_(allSheets[i]);
    if (arc.dtos.length > 0) {
      base.dtos = base.dtos.concat(arc.dtos);
    }
  }
  return base;
},

findAll() の正規表現パターン:

Repositoryパターン
OrderRepository/^arc_31_wrk_order_\d{4}$/
InvoiceRepository/^arc_32_wrk_invoice_\d{4}$/
BankTxRepository/^arc_33_wrk_bank_\d{4}$/
JournalRepository/^arc_42_trn_journal_\d{4}$/

save() / append()改修しない。書き込み系は常に当年度シート(現行シート)にのみ作用する設計を維持し、過去年度データへの誤上書きを物理的に防ぐ。

Step 3: 000_infra/002_constants.jsMENU_DEFINITION に 1 行追加

MENU_DEFINITION(L206 開始)の 「📋 サイドバー: 🔧 マイグレーション」 カテゴリ(L298-309 範囲)の items 配列末尾に以下を 1 行追加する。100_config/101_sys_config.jsonOpen()(L323)は MENU_DEFINITION を動的にループしてメニュー生成するため、メニュー実体の編集先は 002_constants.js である(101_sys_config.js の編集は不要)。

{ label: '🗄️ S-61 年度跨ぎアーカイブ', funcName: 'archiveFiscalYear', description: '指定した会計年度のトランザクション系4シートをアーカイブシートに退避し、現行シートから削除' },

挿入位置:{ label: '🧹 全シート空行物理削除', funcName: 'cleanupEmptyRows', ... },直前直後(既存マイグレーション項目とクリーンアップ項目の間)。実装者が読みやすい位置を選ぶ。

影響範囲

区分対象影響内容
新規追加800_ops/819_migration_s61_fiscal_year_archive.js公開関数 archiveFiscalYear() 1 本のみ。約 200 行
機能拡張(後方互換)200_data/202_repository.jsfindAll() × 4(OrderRepository / InvoiceRepository / BankTxRepository / JournalRepository末尾に各 8〜10 行追記。アーカイブシート不在時は完全に従来動作
メニュー追加000_infra/002_constants.js MENU_DEFINITION「🔧 マイグレーション」カテゴリに 1 項目追加
ドキュメントdocs/_internal/TODO_future.mdMAS-133 ステータス更新(実装完了時)
波及確認必須600_report/ 配下のマート集計関数(601_datamart_ingest.js608_datamart_render.jsfindAll() 戻り値に過去アーカイブデータが加算されるため、集計側に発生日/決済日フィルタが適切に適用されているか 事前検証が必要(マート期間外データを誤集計しないか)
波及確認必須400_domain/410_subledger_engine.jsAction A / Action B)/ 400_domain/420_project_profitability.js同上
波及確認必須900_test/901_test_runner.jsアーカイブシート非存在環境でも従来テストがパスすることを実測確認
物理保護対象アーカイブシート(arc_*_YYYY監査・改ざん防止のため、生成時に arcSheet.protect().setDescription('S-61 アーカイブ(読み取り専用)') で保護を推奨。実装は将来拡張とし、初版では未実装でも可(人間検討事項に記載)

setupAllSchemasDDL)は本案件のスコープ外。アーカイブシートを DDL 管理対象に追加してはならない(追加すると DDL 実行のたびにアーカイブシートのヘッダー・バリデーションが上書きされ、過去データへの DDL 整合性圧力がかかる)。CLAUDE.md「DDL で管理されないタブ」方針に倣い、arc_*_YYYY動的生成・上書き不可・DDL 対象外 とする。

注意事項

  1. アーカイブシート命名と部分一致衝突への対策(必須):Utils.getIdPrefixConfig000_infra/004_utils.js L482-487)は sheetName.includes(entry.pattern) で判定するため、arc_32_wrk_invoice_2025'32_wrk_invoice' パターンにヒットする。同様に handleUxAssist300_ui/301_ui_assist.js L75-76)の validSheets.some(name => sName.includes(name)) も誤検出する。アーカイブシートは原則ユーザーが直接編集しない設計だが、Defense in Depth のため両関数の冒頭に if (String(sheetName).indexOf('arc_') === 0) return null;(あるいは同等のガード)を追加することを推奨。本案件の Step 4 として組み込むかは実装時に判断する(最小実装ならガードなしで運用ルールで担保/堅牢実装ならガード追加)。

  2. アーカイブシートを setupAllSchemas(DDL)の管理対象に追加しない:CLAUDE.md「DDL で管理されないタブ」セクションに arc_*_YYYY を追記し、DDL 実行時に confSheet.appendRow 等で誤登録されないこと、および setupAllSchemasIncremental のハッシュ計算対象から除外されていることを保証する。

  3. Constants.getParam() には書き戻し手段がない000_infra/002_constants.js L147-167 の実装は 読み取り専用キャッシュ で、書き戻し API は存在しない。冪等性ガードはアーカイブシートの存在チェック(ss.getSheetByName(arcName) !== null)で実装する(Phase 1 設計判断②)。PropertiesService.getScriptProperties() を直接呼ぶことは CLAUDE.md「PropertiesService を直接呼ばない(Env モジュール経由で使用する)」原則違反となるため禁止。

  4. 元シートの行削除は降順ループ厳守:行番号が大きい方から小さい方へ sheet.deleteRow(rowIndex) を 1 行ずつ呼ぶこと。昇順で deleteRow を呼ぶと行インデックスがずれて誤削除が発生する。sheet.deleteRows(startRow, numRows)連続する行範囲のみ有効 であり、年度フィルタで抽出された非連続行群には使用禁止。100_config/101_sys_config.jscleanupOrphanTrn(L297-301)が確立済の正パターン:deleteRows.sort(function(a, b) { return b - a; }); for (var d = 0; d < deleteRows.length; d++) { sheet.deleteRow(deleteRows[d]); }

  5. findAll() 改修後のテスト互換性900_test/901_test_runner.js の既存テストはアーカイブシート非存在環境を前提としている。改修後も Repository.findAll() の戻り値型 { headers: string[], dtos: Object[] } を変更しないこと、およびアーカイブシート不在時は従来の戻り値と完全一致することを保証する。テスト実行で失敗が出たら、改修側が原因か(過去データ混入による集計値変化)テスト側が原因か(型期待)を切り分けて対処する。

  6. コピー → 件数検証 → 削除の順序厳守:データロスト防止の最重要ルール。appendDtosToSheet_ の戻り値 = 書き込み行数を targetDtos.length と照合し、不一致時は当該シートの arc_*ss.deleteSheet でロールバック削除し、元シートには手を付けずに処理全体を中断する。「とりあえず削除を進めて後で復元」は 不可逆な操作 となるため絶対に行わない。

  7. LockService による排他制御LockService.getScriptLock().waitLock(30000) で 30 秒待機。タイムアウト例外を try-catch で受けて「別ユーザーが実行中のため 30 秒後に再試行してください」とダイアログ表示。finally ブロックで lock.releaseLock() を必ず呼ぶ(途中エラー時のロック残留防止)。

  8. アーカイブシートの保護(任意・推奨):監査用に arcSheet.protect().setDescription(...).removeEditors(arcSheet.protect().getEditors()) で読み取り専用化することを将来拡張として検討する。初版実装では運用ルール(「アーカイブシートは直接編集しない」)で担保し、技術的保護は人間検討事項として残す。

  9. マスタ系・予算系シートはアーカイブ対象外:本案件はトランザクション系(イベントログ)4 シート限定。マスタ系(11/12/13/14/15)・予算系(21〜28)はサイズが小さく増大しないため対象外。05_log_* 系(98_audit_log 等)は別案件(既存の archiveAuditLogMonthly 等)が責務を持つ。

エッジケース

#条件期待動作理由・備考
1対象年度のデータが全 4 シートで 0 件警告ダイアログ「${yyyy}年度の対象データはありません」を表示し、シート作成・削除いずれも行わず正常終了空アーカイブシートを作るのは無意味かつ冪等性ガードを誤発火させる
2アーカイブシートが既に 1 枚でも存在する(冪等性チェック)${yyyy}年度は既にアーカイブ済みです(既存シート: ${name}) と通知してロック解放・処理終了二重実行・二重削除防止(Phase 1 設計判断②)
3コピー件数 ≠ アーカイブシートに書き込まれた行数Utils.logError 出力 + 当該 arc_* シートを ss.deleteSheet でロールバック削除 + 元シート無変更 + ダイアログ「コピー失敗のため処理を中断しました。管理者に連絡してください」コピー失敗時のデータロスト防止(処理フロー e)
4CFG_FISCAL_YEAR_START03_sys_params に未登録Constants.getParam('CFG_FISCAL_YEAR_START', '4') のフォールバック値 '4'(4 月始まり)を使用Constants.getParam の第 2 引数デフォルト動作に準拠(002_constants.js L165-167)
5会計年度境界値(4 月始まり 2025 年度の末日 = 2026-03-31'2025-04-01' <= dateStr && dateStr <= '2026-03-31'<= 比較で境界日当日を含むUtils.parseDateToYmd 正規化文字列同士の辞書順比較で正しく動作
6LockService.waitLock(30000) タイムアウト例外を try-catch で受けてダイアログ「別のユーザーが実行中のため 30 秒後に再試行してください」表示し処理終了GAS 標準ロック機構の作法(finally で release は不要、取得失敗時はロック保持していない)
7findAll() 呼び出し時にアーカイブシートが 0 件ベースシートのみの戻り値(従来動作と完全一致)初回アーカイブ前の環境・テスト環境の互換性保証
8findAll() の正規表現が複数年度のアーカイブシートにマッチ全年度を concat で順次マージ。重複 ID は発生しない(年度切替時に元シートから削除済みのため)5 年運用時に arc_32_wrk_invoice_2025 / _2026 / _2027 ... が並存する想定
9マート集計で findAll() が過去アーカイブデータを返すマート側の 発生日 / 決済日 フィルタが既に適用済みなら影響なし。フィルタ未適用の集計が存在する場合は集計値が変わる影響範囲セクションで波及確認必須としてマーキング済み。事前確認手順を本セクション末尾「実データ検証」に記載
10判定列の値が空欄(例: 32_wrk_invoice発生日(P/L計上日) が未入力)アーカイブ対象外として残置(年度判定不能なため、過剰削除を避ける安全側倒し)Utils.parseDateToYmd が空欄に対し '' を返す前提で範囲外判定
11判定列の値が日付以外の文字列・不正フォーマットアーカイブ対象外として残置 + Utils.logError で警告ログ出力(処理は継続)ユーザーが手で誤入力した値を勝手に削除しない
12元シートに該当行が 0 行のまま削除ループに入る削除ループは空配列で何もしないfor ループの自然な動作
1333_wrk_bank決済日_実績 が空欄で 決済日_計画 を使う場合の年度ずれ仕様確定事項として「決済日_実績 優先 → 空欄なら 決済日_計画 フォールバック」を明記。両方空欄なら #10 に準じて残置キャッシュフロー実績の正準キーは 決済日_実績、未消込 STL は 決済日_計画 のみ持つため
14アーカイブシート命名衝突(手動で同名シートを既に作成済みのレアケース)#2 の冪等性チェックで検出 → 処理スキップgetSheetByName の戻り値を判定
15スプレッドシート 10M セル制限への接近アーカイブシート生成で更にセルを消費するため、事前に総セル数を試算しダイアログに表示。10M を超える見込みなら ui.alert(...) で警告し、別スプレッドシートへの分離を検討するよう人間検討事項に誘導5 年以上運用時に発生し得る。本案件初版は「同一スプレッドシート内のアーカイブ」までをスコープとし、別ファイル化は将来拡張
16アーカイブ完了後、過去アーカイブデータを修正したいケースアーカイブシートを直接編集(保護解除→編集→保護再設定)。仕訳整合性は実装者の責任で担保想定頻度は低いが運用ルールとして明文化が必要
1701_sys_config 等の Utils.getSheetByKey 経由で取得するキーが未登録(テスト環境)Utils.getSheetByKey('WRK_ORDR', '31_wrk_order') のフォールバック名で正常動作既存 _getSheet() 実装に準拠

実データ検証

実装着手前に以下を MCP(Google Sheets MCP / Apps Script Manifest)または GAS エディタ経由で必ず確認する。

#確認項目確認方法期待値・対応
103_sys_params シートの CFG_FISCAL_YEAR_START の存在と値03_sys_paramsgetDataRange().getValues() で取得 → 1 列目が CFG_FISCAL_YEAR_START の行を探す存在しない場合は新規追加パラメータとしてフォールバック値 '4'(4 月始まり)で動作。設定値がある場合は値を採用
2アーカイブシートが既に存在しないこと(命名衝突チェック)ss.getSheets().map(s => s.getName()) から ^arc_ で始まるシートを抽出0 件であることを確認。1 件でもあれば命名規則衝突の可能性を追加調査
331_wrk_order の現在の行数Utils.getTrueLastRow(sheet) または sheet.getLastRow()パフォーマンス基準値として記録(例: 850 行)。アーカイブ効果の見積もりに使用
432_wrk_invoice の現在の行数同上同上
533_wrk_bank の現在の行数同上同上
642_trn_journal の現在の行数同上同上
7600_report/ 配下の各マート集計関数が 発生日(P/L計上日) または 決済日_実績 で期間フィルタしているか601_datamart_ingest.js608_datamart_render.js を Read し、findAll() 戻り値の dtos を期間フィルタしている箇所を確認フィルタなしで全件集計している関数があれば、本案件と同時に期間フィルタ追加が必要(要追加 PR or スコープ拡張)
8400_domain/410_subledger_engine.js の Action A / Action B が findAll() を呼ぶ際に発生日フィルタを掛けているか同上フィルタ未適用なら過去アーカイブ INV/STL を Action A/B が再処理しないか確認(既処理判定が 自動仕訳JNL_ID 列の有無で正しく弾かれるなら問題なし)
9既存アーカイブ系機能との命名整合100_config/101_sys_config.jsarchiveAuditLogMonthly(98_audit_log → 99_audit_archive_YYYYMM)の命名規則を確認監査ログのアーカイブ命名(99_audit_archive_*)とは責務分離。MAS-133 のトランザクション系アーカイブ(arc_*_YYYY)は別系統
10LockService.getScriptLock() を使う既存コードのパターン100_config/101_sys_config.js 等で LockService 検索既存パターンに倣う。 waitLock(30000)try-finally で release

関連ドキュメント

人間が検討すべき事項

TODO_future.md(§3.1 の MAS-133 行)から転記された検討事項:

  1. アーカイブ粒度(年度単位 / 月単位):本案件初版は 年度単位 固定。月単位(より細粒度)にすればシートが増え、操作回数も増える。年度単位で 5〜10 年運用後に再評価
  2. 別スプレッドシート化の判断基準(セル数 / 行数):本案件初版は 同一スプレッドシート内 に限定。10M セル制限接近時に別スプレッドシート化の発動条件(例: 「総セル数 8M 超で警告、9M 超で別ファイル化必須」)を別案件として起票するか
  3. マート集計の横断読み込みコストfindAll() 改修により過去全アーカイブデータが常にメモリに読み込まれる。5 年経過後で何 MB / 何秒の遅延になるかを実測し、許容範囲を超えたら 遅延ロード方式(マート側で「過去年度マージが必要なら明示的に呼ぶ」設計に変更)への移行を検討
  4. アーカイブ後のデータ修正が必要なケースの運用:仕訳訂正・税務調査対応で過去年度データを修正する手順を明文化する(保護解除 → 編集 → 保護再設定 → 整合性チェック)

本仕様書執筆中に追加で識別された検討事項:

  1. アーカイブシート命名規則の最終決定(Phase 1 設計判断①の選択肢確認):
    • 採用案: arc_{ベースシート名}_{年度}(例: arc_32_wrk_invoice_2025
    • 不採用案: {ベースシート名}_archive_{年度}(例: 32_wrk_invoice_archive_2025)— getIdPrefixConfigincludes 判定にヒット度合いがより強く、誤動作リスクが高い
    • 注意: 採用案でも includes には部分一致するため、本仕様書「注意事項 #1」のガード追加が必須
  2. 冪等性保存方式の最終決定(Phase 1 設計判断②の選択肢確認):
    • 採用案: アーカイブシート存在チェック(追加インフラ不要)
    • 不採用案: PropertiesService ラッピング(CLAUDE.md Env モジュール経由原則への準拠が必要、追加実装コスト高)
  3. Utils.getIdPrefixConfig / handleUxAssist への arc_ プレフィックスガード追加の要否:本案件 Step 4 として組み込むか、別案件として切り出すか。最小実装では運用ルールで担保、堅牢実装では本案件に組み込み
  4. アーカイブシートへの保護設定Sheet.protect() で読み取り専用化するか。監査・改ざん防止には有効だが、過去データ修正運用との両立を要検討
  5. 削除前バックアップの自動実行:本案件実行前に MAS-201 バックアップを自動起動するか、運用ルール(「アーカイブ実行前に手動バックアップを取ること」)で担保するか
  6. findAll() 改修によるパフォーマンス影響の本番計測:dev 環境では行数が少ないため影響を体感しにくい。prod デプロイ後にマート再構築の所要時間を比較計測する手順を運用に組み込む
  7. 将来 Web UI 化(MAS-238)後のアーカイブ操作の扱い:現状はメニュー → ダイアログ駆動。Web UI 化時にアーカイブ操作を移植するか、運用者専用の GAS メニューに残すか

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-133「年度跨ぎアーカイブ機構(行積み上がり対策E)」を実装してください。
Step 1: アーカイブ実行スクリプト新規作成 / Step 2: Repository.findAll() 4 関数改修 /
Step 3: MENU_DEFINITION への 1 行追加 の 3 ステップ構成です。

## 実行前タスク(Read で裏取り)

- `200_data/202_repository.js` を Read し、`readSheetAsDtos_`(L19-29)/
  `appendDtosToSheet_`(L85-101)の引数と各 `findAll()` の現行実装行番号
  (L118-120 / L163-165 / L225-227 / L270-272)を確認する
- `000_infra/002_constants.js` を Read し、`SHEET_DEFAULTS`(L73-87)と
  `ID_PREFIX_MAP`(L93-112)の全 `pattern` 値、および `Constants.getParam`
  (L147-167)に書き戻し手段がないことを確認する
- `000_infra/004_utils.js` の `getIdPrefixConfig`(L482-487)と
  `300_ui/301_ui_assist.js` の `handleUxAssist`(L75-76)が `String.includes`
  ベースの部分一致を行うことを確認する(命名衝突対策のガード追加可否を判断)
- `100_config/101_sys_config.js` の `onOpen()`(L323)が
  `Constants.MENU_DEFINITION` を動的ループしてメニュー生成することを確認する
  → メニュー実体の編集先は `000_infra/002_constants.js` の `MENU_DEFINITION`
  (L298-309 の「📋 サイドバー: 🔧 マイグレーション」カテゴリ)であり、
  `101_sys_config.js` 側の編集は不要であることを把握する
- `800_ops/808_migration_i24.js` を Read し、冪等性チェック・`Utils.logInfo` /
  `Utils.auditLog` / `SpreadsheetApp.getUi().alert` の使い方パターンを確認する
- `100_config/101_sys_config.js` の `cleanupOrphanTrn`(L297-301)の
  「降順ソート → `deleteRow` 1 行ずつ」パターンを確認する(行削除の正パターン)
- `000_infra/003_contracts.js` の `OrderDTO`(L15-36)/ `InvoiceDTO`(L40-67)/
  `BankTxDTO`(L71-89)/ `JournalEntryDTO`(L97-110)の判定列名を確認する
  (`起票日時` / `発生日(P/L計上日)` / `決済日_実績` / `発生日(P/L計上日)`)
- MCP で `03_sys_params` シートを確認し、`CFG_FISCAL_YEAR_START` の存在と値を
  確認する(未登録ならフォールバック値 `'4'` で動作)
- MCP で 4 シートの現在の行数を確認し、アーカイブ効果の見積もりに使用する
- 800_ops/ ディレクトリを `ls` して既存最大番号を確認し、新規ファイル番号を
  確定する(2026-04-28 時点で 815 まで使用済 / 816-818 が予約済 → **819 が空き**)

## 修正対象ファイル

- `800_ops/819_migration_s61_fiscal_year_archive.js` — 新規作成。公開関数
  `archiveFiscalYear()` 1 本のみエクスポート(メニュー・テストのエントリポイント)
- `200_data/202_repository.js` — `OrderRepository.findAll`(L118-120)/
  `InvoiceRepository.findAll`(L163-165)/ `BankTxRepository.findAll`(L225-227)/
  `JournalRepository.findAll`(L270-272)の **末尾に各 8〜10 行追記するのみ**。
  既存コードの削除・変更禁止。`save()` / `append()` は改修しない
- `000_infra/002_constants.js` — `MENU_DEFINITION`(L206-)の
  「📋 サイドバー: 🔧 マイグレーション」カテゴリ(L298-309)の `items` 配列に
  1 項目追加(`{ label: '🗄️ S-61 年度跨ぎアーカイブ', funcName: 'archiveFiscalYear', description: '...' }`)

## 実装内容

仕様書 `docs/dev/dev_mas-133_fiscal_year_archive.md` の「## 修正方針」Step 1〜3 に
従うこと。仕様書に記載された設計判断(命名規則 = `arc_{ベース}_{年度}` /
冪等性方式 = シート存在チェック / 削除順序 = 降順 1 行ずつ / 処理フロー =
コピー → 件数検証 → 削除)を再調査・再考せず、そのまま実装に落とすこと。

## 制約

- `readSheetAsDtos_` / `appendDtosToSheet_` / `writeDtosToSheet_` /
  `findLastRow_` 等の内部ヘルパーは変更しない
- `findAll()` の戻り値の型 `{ headers: string[], dtos: Object[] }` を変更しない
- アーカイブシート(`arc_*_YYYY`)を `setupAllSchemas`(DDL)の管理対象に追加しない
- 元シートの行削除は **必ず降順インデックスループで `sheet.deleteRow(rowIndex)`**
  を 1 行ずつ実行(`sheet.deleteRows(start, n)` は連続行のみ有効、非連続行群への
  適用は誤削除を招くため使用禁止)
- `PropertiesService.getScriptProperties()` を直接呼ばない(CLAUDE.md 規約)
- 処理フローは「コピー → 件数検証 → 削除」の順序を厳守。検証失敗時は元シート
  に手を付けず、`arc_*` シートを `ss.deleteSheet` でロールバック削除して中断
- `LockService.getScriptLock().waitLock(30000)` で排他制御。`finally` で
  `lock.releaseLock()` を必ず呼ぶ
- 監査ログは開始 / 完了で `Utils.auditLog('MIGRATE', ..., 'START' / 'END')` を
  出力(808_migration_i24.js のパターンに準拠)

## 動作確認

1. `npm run push:dev` でデプロイ
2. スプレッドシートを再読み込みし「📋 サイドバー: 🔧 マイグレーション」サブメニュー
   に「🗄️ MAS-133 年度跨ぎアーカイブ」が表示されることを確認
3. テスト用年度(データが存在する年度)でメニュー実行 → 入力ダイアログに `2025` 等
   を入力 → プレビューダイアログに「対象年度・期間(例 2025-04-01〜2026-03-31)・
   4 シート別件数・元に戻せない旨の警告」が表示されることを確認
4. OK 押下後、4 シートそれぞれにアーカイブシート(`arc_31_wrk_order_2025` 等)が
   作成され、ヘッダー+対象データ行が書き込まれていることを確認
5. 同時に元シートから対象行が **降順削除** され残行数が減っていることを確認
   (ID 列・日付列を目視チェック)
6. 同じ年度で再実行し、冪等性ガードが働いて「既にアーカイブ済み」とスキップされる
   ことを確認
7. `Repository.findAll()` を呼び出すマート再構築(例: `buildBudgetTrendDataMart`)
   を実行し、過去年度データもマージされて集計値が正しいことを確認
8. `900_test/901_test_runner.js` を実行し、`findAll()` 関連テストが
   アーカイブシート存在環境・非存在環境の両方でパスすることを確認
9. dev 環境で問題なければ `npm run push:prod` で prod デプロイし、prod でも
   同手順で動作確認

## 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Step 1 実行前タスク(Read / MCP) | あり | 設計確定のため |
| Step 1 アーカイブスクリプト実装 | 最小限 | 仕様書の清書に徹する |
| Step 2 `findAll()` 改修 | 最小限 | 末尾追記のみ |
| Step 3 MENU_DEFINITION 追記 | なし | 1 行追加のみ |

推奨実行モデル

工程推奨モデル理由
Step 1 アーカイブスクリプト新規作成 (819_migration_s61_fiscal_year_archive.js)Claude Sonnet命名規則・冪等性チェック・LockService 排他・会計年度境界値計算・降順削除ループ・コピー件数検証など中程度の設計判断と複数 API の組み合わせが必要
Step 2 findAll() 4 関数改修 (202_repository.js)Claude Sonnet複数ファイル横断ではないが、4 関数への一貫した末尾追記と正規表現パターンの正確な書き分けが必要
Step 3 MENU_DEFINITION 1 行追加 (002_constants.js)Claude Haiku既存パターン(migrationD01D03 等)への倣いで判断要素なし
動作確認・テスト人間 / Claude Sonnetdev 実行 → テスト実行 → prod デプロイの判断は人間。テスト失敗時の原因切り分けは Sonnet

変更履歴

日付変更内容
2026-04-22初版作成。トランザクション系 4 シート(31_wrk_order / 32_wrk_invoice / 33_wrk_bank / 42_trn_journal)を会計年度単位でアーカイブシート(arc_*_YYYY)に退避し、現行シートから物理削除する仕組みを設計。冪等性はアーカイブシート存在チェックで担保(Constants.getParam に書き戻し手段がないため)。Repository.findAll() 4 関数の末尾追記により過去アーカイブデータを透過的にマージ。Utils.getIdPrefixConfig / handleUxAssistString.includes 部分一致による命名衝突リスクを「注意事項 #1」で明示し、両関数への arc_ プレフィックスガード追加を推奨事項として記載。新規ファイル番号は CLAUDE.md「次のマイグレーションは 810 から」記述のあと 810/811 が後発で消費済のため 812 を採用。メニュー実体は 100_config/101_sys_config.js ではなく 000_infra/002_constants.jsMENU_DEFINITION 動的ループであることを Phase 1 で確定し、編集先を後者に固定
2026-04-28番号衝突解消・812→819。実装監査の結果、初版で予約していた 800_ops/812_migration_s61_fiscal_year_archive.js812 番が MAS-057 tier seed (812_migration_f57_tier_seed.js) で既に消費済と判明したため、本案件の番号を 819 に変更800_ops/ 採番状況 (2026-04-28 時点): 801-815 既存 / 816-817 = MAS-234 セキュリティロードマップで予約済 / 818 = MAS-212 archive_records で予約済 / 819 = 本案件 (MAS-133・新規アサイン)。spec 内 4 箇所を一括置換 (実装プロンプト / 影響範囲 / 採番衝突回避 / 推奨実行モデル) + 採番セクション更新。ただし変更履歴・実装プロンプト末尾の判断根拠ログ (旧 prompt log セクション) には歴史的経緯保存のため 812 表記を残す。failure_patterns #31 (番号衝突チェック) の事後適用事例。docs-only 改訂で prod 自動デプロイへの影響なし。

仕様書作成プロンプト

展開して表示
【タイムアウト回避・実行原則(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(`<details>`にプロンプト全文記録)。1回のWrite/Editは約300行以内。
4. **各Stepで何を書くかを具体指示**: 設計判断をPhase 2実行時に持ち込まない。Phase 1で全固有名詞・行番号を確定させてから清書に入ること。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-61「年度跨ぎアーカイブ機構(行積み上がり対策E)」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の適切なセクションに必ず追記してください。

---

## Phase 1: 実行前タスク(テキスト報告禁止。各タスクを即座にTool実行)

### 1-A: 案件定義の把握
- `docs/_internal/TODO_future.md` を Read し、S-61 の案件名・概要・人間が検討すべき事項を取得する。

### 1-B: コアコードの読み込み(必須。Readで裏取りしてから仕様書に書くこと)
- `200_data/202_repository.js` を Read し、以下を確認する:
  - 内部ヘルパー `readSheetAsDtos_`・`appendDtosToSheet_`・`writeDtosToSheet_` の引数・戻り値・実装
  - `OrderRepository.findAll()`・`InvoiceRepository.findAll()`・`BankTxRepository.findAll()`・`JournalRepository.findAll()` の現行実装全文(改修対象のため行番号まで確定させる)
- `000_infra/002_constants.js` を Read し、以下を確認する:
  - `SHEET_DEFAULTS` 配列の全 `pattern` 値(例: `'32_wrk_invoice'`)— **アーカイブシート命名がこれらに `includes` でヒットしないか検証が必須**
  - `ID_PREFIX_MAP` 配列の全 `pattern` 値(同上の理由)
  - `Constants.getParam()` の実装全文 — `03_sys_params` への書き戻し手段が存在しないことを確認する
- `100_config/101_sys_config.js` を Read し、以下を確認する:
  - `onOpen()` 内の `ui.createMenu` を**そのまま引用し**、「🔧 マイグレーション」メニューの実在と現行エントリの行番号を確定させる
  - `setupAllSchemas` でDDL管理されているシート一覧(アーカイブシートをDDL管理対象に含めないための確認)
- `800_ops/808_migration_i24.js` を Read し、冪等性チェック・`Utils.logInfo`・`SpreadsheetApp.getUi().alert` の使い方パターンを確認する(本スクリプトの実装パターンに倣う)。

### 1-C: MCP でのデータ確認
- `03_sys_params` シートの全行を取得し、以下を確認する:
  - `CFG_FISCAL_YEAR_START` キーが既に存在するか(値は何月か)。存在しない場合は「新規追加パラメータ(デフォルト: `'4'`(4月始まり))」として扱う。
  - アーカイブ済み年度を記録するキーが既に存在するか。
- `31_wrk_order`・`32_wrk_invoice`・`33_wrk_bank`・`42_trn_journal` 各シートの現在の行数を確認する(パフォーマンス基準値として仕様書に記載)。

### 1-D: Phase 1中に以下の設計判断を確定させる(Phase 2で再考しない)

**【判断①】アーカイブシートの命名規則**
`'32_wrk_invoice_archive_2025'.includes('32_wrk_invoice')` は `true` になる。`SHEET_DEFAULTS` の `smartAddRow` や `ID_PREFIX_MAP` の `getIdPrefixConfig` がアーカイブシートに誤適用されるリスクがある。以下の選択肢から仕様書に明記する命名規則を選択すること:
- 案A: `arc_32_wrk_invoice_2025`(既存パターン非マッチ、推奨)
- 案B: `32_wrk_invoice_archive_2025`(Gemini原案。SHEET_DEFAULTS/ID_PREFIX_MAPの判定を呼び出す全関数でアーカイブシートへの適用をガードする必要あり)

**【判断②】冪等性の保存先**
`Constants.getParam()` は `03_sys_params` への書き戻し手段を持たない(Read時に確認済み)。冪等性ガードの実装方式を以下から選択し仕様書に明記する:
- **推奨**: アーカイブシートの存在チェック(シートが既に存在すればスキップ)— 追加インフラ不要
- 代替: `PropertiesService.getScriptProperties()` を `Env.setArchiveLastYear()` 等の新関数としてラップして使用(CLAUDE.md の「`PropertiesService` を直接呼ばない」原則への準拠が必要)

**【判断③】新規ファイル番号**
CLAUDE.md により 804-808 が使用済みで「次のマイグレーションは809から」と明記されている。`812_` ではなく `809_migration_s61_fiscal_year_archive.js` を使用すること。

**【判断④】公開関数名**
CLAUDE.md の `migrationDNNDNN()` 命名規則は一度限りのデータ移行用。本機能は年次繰り返し操作のため `archiveFiscalYear()` を公開関数名とすること(メニュー登録・テストのエントリポイント)。

---

## Phase 2: 仕様書の分割作成

出力先: `docs/dev/dev_mas-133_fiscal_year_archive.md`(Sは大文字、ケバブケース)

### Step 2-1: 骨格の作成(File Write / ~20行)
以下の見出しのみ記載。本文は空で可:
`# S-61: 年度跨ぎアーカイブ機構(行積み上がり対策E)`
`## 概要` / `## 目的` / `## 現在のコード(問題箇所)` / `## 修正方針` / `## 影響範囲` / `## 注意事項` / `## エッジケース` / `## 実データ検証` / `## 関連ドキュメント` / `## 人間が検討すべき事項` / `## 実装プロンプト(Claude Code 用)` / `## 推奨実行モデル` / `## 変更履歴` / `## 仕様書作成プロンプト`

### Step 2-2: 概要〜注意事項の追記(File Edit または Bash heredoc / ~300行)
(仕様書本文の Step 2-2 指示。本仕様書ではこれらを全て本文に反映済み)

### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash / ~200行)
(仕様書本文の Step 2-3a 指示。本仕様書ではこれらを全て本文に反映済み)

### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash / ~250行)
(仕様書本文の Step 2-3b 指示。本仕様書ではこれらを全て本文に反映済み)

### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)
末尾の `## 仕様書作成プロンプト` セクションに `<details><summary>展開して表示</summary>` ブロックを追加し、この `<instruction>` タグ内の全文を貼り付けること。

---

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

1. `docs/_config.json` の `nav` 配列 §E.6(パイプライン・RPA・外部連携)に追記:
   `{ "file": "dev/dev_mas-133_fiscal_year_archive.md", "title": "E.6.X S-61 年度跨ぎアーカイブ機構" }`
2. `docs/_internal/changelog.md` の先頭(ヘッダー直後)に追記:
   `| 2026-04-20 | [dev_mas-133_fiscal_year_archive.md](dev_mas-133_fiscal_year_archive.md) | 初版作成。トランザクション系4シートの行積み上がり対策アーカイブ機構の開発仕様書 |`
3. コミット&プッシュ:

git add docs/dev/dev_mas-133_fiscal_year_archive.md docs/_config.json docs/_internal/changelog.md git commit -m "docs: MAS-133 年度跨ぎアーカイブ機構の開発仕様書を作成

トランザクション系4シートの行積み上がり対策として年度単位アーカイブ機構を設計。 Repository.findAll()透過的マージ方式・LockService冪等性ガード・Human-in-the-Loop確認フローを採用。

https://claude.ai/code/session_XXXXX" git push -u origin {現在のブランチ}


仕様書作成中に発見した instruction との差分(実装上の注意)

  • 【判断③】instruction は「809_migration_s61_fiscal_year_archive.js を使用」と指示しているが、本仕様書執筆時点で 800_ops/809_backup_tool.js / 810_migration_partner_payment_terms.js / 811_audit_checker.js が既存。CLAUDE.md「次のマイグレーションは 810 から」の記述以降に 810/811 が消費されたため、空き番号は 812。本仕様書では 812 を採用した
  • メニュー追加先:instruction は「100_config/101_sys_config.js のメニュー1行追加」と表記しているが、onOpen()Constants.MENU_DEFINITION を動的ループする MAS-214 構造(101_sys_config.js L323-348)であり、メニュー実体の編集先は 000_infra/002_constants.jsMENU_DEFINITION101_sys_config.js 側の編集は不要)。本仕様書では編集先を後者に固定した
  • 命名規則 案A(arc_32_wrk_invoice_2025)も String.includes('32_wrk_invoice') には部分一致するため、「既存パターン非マッチ」とは厳密には言えない。本仕様書では案A を採用しつつ、getIdPrefixConfig / handleUxAssist への arc_ プレフィックスガード追加を「注意事項 #1」と「人間検討事項 #7」に明示した