案件位置づけ

  • 案件 ID: MAS-002 (TODO_future.md §1 P2・🧠 Domain・バージョン管理 35% 進行中の残課題)
  • 連携先:
    • MAS-057 cockpit (PR #400/#454 で localStorage 系完了 → 本案件で確定値保存)
    • MAS-067 マルチイヤー (Y0 着地見込み PR #459)
    • MAS-177 多年度データ基盤 (並行起票・本案件は MAS-177 利用例)

Section 1. 概要

項目内容
案件 IDMAS-002
カテゴリ🧠 Domain (バージョン管理・確定スナップショット)
PhaseP2
優先度★★
所要時間1-2 週間
対象ファイル200_data/204_snapshot_repository.js 新設 / 400_domain/470_snapshot_engine.js 新設 / 100_config/101_sys_config.js schemas に 99_snapshot_periods + 99_snapshot_data 追加 / webapp_client/src/cockpit/SnapshotPanel.tsx 新設
前提案件MAS-057 (PR #400/#454 シナリオ保存) / MAS-077 (settlement_date 同期) / MAS-085 (整合性チェック)
後続案件MAS-021 (PDF/Excel エクスポート) / MAS-067 (期首 BS 安定化) / MAS-177 (多年度データ基盤) / MAS-251-253 (第三者監査)

Section 2. 目的

bizlp の財務諸表 (91_fs_bs / 92_fs_pl / 81_cf_actual) は 動的に再計算される 構造になっている。datamart 再構築のたびに INV データから再生成され、過去の確定値が「上書き」されて消えてしまう。

本案件は、特定時点の財務諸表値を 不変スナップショット として永続化する仕組みを導入する。

達成目標

  1. 期末確定 B/SP/L・CF の 不変保存 (動的再計算の影響を受けない)
  2. 過去 N 期分の財務諸表を 確定値として再現可能
  3. スナップショット間 差分比較 で年次変動の可視化
  4. 改竄不可性 (SHA-256 ハッシュ) で第三者監査適合性確保
  5. MAS-067 多年シミュレーションの 期首 B/S 安定化 基盤
  6. MAS-021 PDF/Excel エクスポートの データソース 提供

非目標 (本案件のスコープ外)

  • PDF/Excel 形式の出力 (MAS-021 で実装)
  • 多年度データ基盤への自動配信 (MAS-177 で連携)
  • 第三者監査ワークフロー全体 (MAS-251-253 で実装)
  • 旧バージョンへのロールバック (参照モードのみ提供)

Section 3. 現在のコード

3.1 既存パターン

ファイル役割本案件での参照点
100_config/101_sys_config.jsDDL setupAllSchemas() でスキーマシート一括生成99_snapshot_periods + 99_snapshot_data 追加
200_data/202_repository.jsRepository パターン (cache + findAll + save + append)SnapshotRepository は同パターンで新設
200_data/201_data_validator.js入力 DTO バリデーションスナップショット保存前の整合性チェック
600_report/601_datamart_ingest.js財務諸表生成 (dmIngestPlanData_ 等)期末確定処理で datamart 再構築を先行実行
600_report/608_datamart_render.js91_fs_bs / 92_fs_pl レンダリング主要 5 タブから値取得する起点
webapp_client/src/cockpit/CockpitApp.tsxStep 7 SoloFinancialStatementsPanelSnapshotPanel を Step 7 隣接配置
000_infra/002_constants.jsConstants.ID_PREFIXESSNP プレフィックス追加
000_infra/004_utils.jsUtils.lockUntil / Utils.auditLog / Utils.computeHash排他制御・監査ログ・改竄検知

3.2 主要 5 タブ (スナップショット対象)

タブキー内容取得粒度
61_pl_monthly月次 P/L (科目 × 月)全行
71_bsB/S 残高 (科目 × YearMonth)期末月の列
81_cf_actualCF (実績)期末月までの累計
91_fs_bs財務諸表 B/S (確定形式)全行
92_fs_pl財務諸表 P/L (確定形式)全行

3.3 既存の「バージョン管理」機能 (35% 進行中)

  • PR #400/#454: cockpit 38_f57_assumptions / 39_f57_scenarios (シナリオ前提保存)
  • 上記は 入力前提 のバージョン管理であり、出力確定値 は未対応
  • 本案件は「出力確定値」の永続化に特化 (役割の明確な分担)

Section 4. 修正方針

4.1 シート設計

4.1.1 99_snapshot_periods (DDL 管理・メタ情報)

列名データ型内容
A(チェックボックス)bool削除/参照用
Bスナップショット IDstringSNP_YYYYMM_NNNN
Cスナップショット種別string期末B/S / 期末P/L / 期末CF / カスタム
D対象年月stringYYYY-MM
E保存日時Dateスナップショット作成タイムスタンプ
F保存者string'手動' or 'AUTO_PERIOD_END' or ユーザー email
G備考stringフリーテキスト (期末確定理由・暫定/確定区別)
HpayloadJSONstringサマリ JSON 文字列 (タブメタ + 主要指標)
I承認FLGbooltrue=確定, false=暫定
JハッシュstringSHA-256 hex (改竄不可性・MAS-251 連携)
K有効フラグbool削除時 false 化 (物理削除しない)

4.1.2 99_snapshot_data (DDL 管理・大規模行データ用)

列名データ型内容
A(チェックボックス)bool削除/参照用
Bスナップショット IDstring外部キー (99_snapshot_periods.B)
Cタブキーstring61_pl_monthly / 71_bs / 81_cf_actual / 91_fs_bs / 92_fs_pl
D行番号int元タブ内の行番号 (再現用)
E行ヘッダーstring元タブのキー列値 (科目名・月度等)
F行データJSONstring元タブ 1 行を JSON.stringify
G有効フラグbool削除時 false 化

設計意図: 単一 payloadJSON では GAS の 50MB 上限に近づくため、行単位正規化で 任意行参照大容量対応 を両立 (Q2 案 Z ハイブリッド採用)。

4.2 SnapshotRepository (200_data/204_snapshot_repository.js)

const SnapshotRepository = (() => {
  /**
   * @param {string} targetYm - YYYY-MM
   * @param {{type: string, approver: string, note: string, isApproved: boolean}} options
   * @returns {string} snapId
   */
  function savePeriodEnd(targetYm, options) { /* ... */ }

  /**
   * @param {string} targetYm
   * @param {string} [type] - 期末B/S 等。省略時は最新承認済
   * @returns {Contracts.Snapshot|null}
   */
  function findPeriodEnd(targetYm, type) { /* ... */ }

  /**
   * @returns {Array<Contracts.Snapshot>}
   */
  function listSnapshots() { /* ... */ }

  /**
   * @param {string} snapId
   */
  function deleteSnapshot(snapId) { /* 有効フラグ false 化 */ }

  /**
   * @param {string} snapIdA
   * @param {string} snapIdB
   * @returns {Array<Contracts.SnapshotDiff>}
   */
  function compareSnapshots(snapIdA, snapIdB) { /* ... */ }

  return { savePeriodEnd, findPeriodEnd, listSnapshots, deleteSnapshot, compareSnapshots };
})();

4.3 SnapshotEngine (400_domain/470_snapshot_engine.js)

const SnapshotEngine = (() => {
  /**
   * 期末確定処理: datamart 再構築 → 主要 5 タブから値取得 → 保存
   * @param {string} targetYm
   * @param {{forceRebuild: boolean, isApproved: boolean, note: string}} options
   * @returns {{snapId: string, hash: string, rowCount: number}}
   */
  function executePeriodEnd(targetYm, options) {
    return Utils.lockUntil(60000, () => {
      // 1. datamart 再構築 (forceRebuild === true 時)
      // 2. 主要 5 タブから値取得 (61/71/81/91/92)
      // 3. payloadJSON 生成 + SHA-256 ハッシュ計算
      // 4. SnapshotRepository.savePeriodEnd 呼出
      // 5. Utils.auditLog('SNAPSHOT_SAVED', { snapId, targetYm, hash })
    });
  }

  /**
   * 参照モードでの復元 (91_fs_bs 等は上書きしない)
   * @param {string} snapId
   * @returns {Contracts.SnapshotPayload}
   */
  function restoreFromSnapshot(snapId) { /* ... */ }

  /**
   * @returns {Array<{tabKey, rowKey, valueA, valueB, delta}>}
   */
  function diffSnapshots(snapIdA, snapIdB) { /* ... */ }

  return { executePeriodEnd, restoreFromSnapshot, diffSnapshots };
})();

4.4 UI (cockpit パネル)

webapp_client/src/cockpit/SnapshotPanel.tsx:

export const SnapshotPanel: React.FC = () => {
  // セクション 1: 期末確定実行
  //   - 対象年月入力 (デフォルトは現在年月)
  //   - 種別選択 (期末B/S/P/L/CF/カスタム)
  //   - 承認FLG チェックボックス
  //   - 「期末確定実行」ボタン (Apps Script google.script.run 呼出)

  // セクション 2: 過去スナップショット一覧
  //   - 年月降順テーブル (ID/種別/対象年月/保存日時/承認/ハッシュ末尾4桁)
  //   - 削除ボタン (有効フラグ false 化のみ)

  // セクション 3: スナップショット間差分比較
  //   - A/B 2 つのドロップダウン (snapId 選択)
  //   - 「差分計算」ボタン → 結果テーブル表示
  //   - 主要科目 (経常利益・現預金・売上) を上位表示

  // セクション 4: 復元 (参照モード)
  //   - snapId 選択 → 別ダイアログでスナップショット内容を読み取り表示
  //   - 「91_fs_bs に書き戻す」ボタンは提供しない (誤上書き防止)
};

4.5 メニュー (100_config/101_sys_config.js)

// MENU_DEFINITION への追加項目 (抜粋)
{
  name: '📸 スナップショット',
  items: [
    { name: '期末確定実行', functionName: 'menuExecutePeriodEnd' },
    { name: '一覧表示', functionName: 'menuListSnapshots' },
    { name: '差分比較', functionName: 'menuDiffSnapshots' },
  ],
}

4.6 Constants 拡張 (000_infra/002_constants.js)

Constants.ID_PREFIXES.SNP = 'SNP'; // SNP_YYYYMM_NNNN

Constants.SNAPSHOT_TYPES = {
  PERIOD_END_BS: '期末B/S',
  PERIOD_END_PL: '期末P/L',
  PERIOD_END_CF: '期末CF',
  CUSTOM: 'カスタム',
};

Constants.SNAPSHOT_TARGET_TABS = [
  '61_pl_monthly',
  '71_bs',
  '81_cf_actual',
  '91_fs_bs',
  '92_fs_pl',
];

4.7 Contracts 拡張 (000_infra/003_contracts.js)

/**
 * @typedef {Object} Contracts.Snapshot
 * @property {string} snapId
 * @property {string} type
 * @property {string} targetYm
 * @property {Date} savedAt
 * @property {string} savedBy
 * @property {string} note
 * @property {Object} payload
 * @property {boolean} isApproved
 * @property {string} hash
 * @property {boolean} isActive
 */

/**
 * @typedef {Object} Contracts.SnapshotDiff
 * @property {string} tabKey
 * @property {string} rowKey
 * @property {number|string} valueA
 * @property {number|string} valueB
 * @property {number} delta
 */

Section 5. 影響範囲

5.1 新規ファイル

ファイル概算行数役割
200_data/204_snapshot_repository.js~150 行Repository 層
400_domain/470_snapshot_engine.js~200 行ドメインサービス
webapp_client/src/cockpit/SnapshotPanel.tsx~250 行UI パネル
8XX_migration_snapshot_seed.js~80 行初期データ投入 (任意)

8XX 番号採番: 822 まで使用済み + MAS-336/337/338 予約 = 824 以降 で採番。実装着手時に最終確定。

5.2 既存ファイル変更

ファイル変更内容
100_config/101_sys_config.jsschemas に 99_snapshot_periods + 99_snapshot_data 追加 / MENU_DEFINITION 拡張
000_infra/002_constants.jsID_PREFIXES.SNP + SNAPSHOT_TYPES + SNAPSHOT_TARGET_TABS 追加
000_infra/003_contracts.jsSnapshot / SnapshotDiff typedef 追加
000_infra/004_utils.js(既存の場合は不要) computeHash / auditLog 利用
webapp_client/src/cockpit/CockpitApp.tsxStep 7 隣接で SnapshotPanel をインポート・配置

5.3 既存ロジックへの影響

  • datamart 再構築: 不変 (本案件は読取専用追加機能)
  • 91_fs_bs / 92_fs_pl: 不変 (上書きしない・別シート保存)
  • MAS-057 cockpit: 不変 (シナリオ保存と確定値保存は役割分離)
  • Action A/B 仕訳エンジン: 不変

5.4 後続案件への波及

  • MAS-067: 期首 BS として SnapshotEngine.findPeriodEnd(prevYm, '期末B/S') を参照する v2 設計余地
  • MAS-021: snapId からの PDF/Excel 出力 API
  • MAS-177: 多年スナップショット履歴の集約・JOIN
  • MAS-251-253: ハッシュ列を監査ログとして外部へエクスポート

Section 6. 注意事項

  1. 不変性の保証: payloadJSON で全データ保存し、元データ (61/71/81/91/92) を変更してもスナップショット内容は影響しない。スナップショット書込後に元シートが変わっても、スナップショット側は独立保持される。

  2. スナップショットの種類: 期末確定/暫定/監査用の用途別タグ (SNAPSHOT_TYPES) を必ず付与する。検索時の絞込で用途識別ができないと、暫定値を誤って税理士提出してしまうリスクあり。

  3. 承認フラグ: 検討中の暫定スナップショットを複数試作 → 承認済 1 件で他は参照保持。承認済み 2 件以上の同種別 + 同年月は警告ダイアログ表示する。

  4. 大規模データ正規化: 99_snapshot_data に行単位で保持。1 ID あたり数千行を想定するが、SpreadsheetApp.appendRow のループは遅いため setValues で一括書込必須。

  5. payloadJSON サイズ上限: GAS の JSON.stringify 結果は 50MB 上限。99_snapshot_periods.payloadJSON はサマリのみ・全行は 99_snapshot_data 側に逃がすことで実質無制限化 (Q2 ハイブリッド採用理由)。

  6. ID 採番: SNP_YYYYMM_NNNNConstants.ID_PREFIXES.SNP を使用。同一年月・同一日内の連番は Utils.lockUntil で発番ロック必須 (エッジケース 7 参照)。

  7. GAS 権限スコープ: シート読み書きのみで OAuth 追加スコープ不要。既存 appsscript.jsonoauthScopes 拡張なし。

  8. 冪等性: 同 targetYm + 種別の重複時のルールは Q3 で議論。並列保存 (案 Y) を推奨するため、ID で完全分離する。

  9. failure_patterns #34 整合性: PHASE 1 仕訳振替 INV (MAS-336) との整合性確保。スナップショット作成時点で dmIsAccruedExpense_ が正しく動作している前提。期末確定実行前に MAS-085 整合性チェックを呼ぶフックを Step 5 で検討。

  10. MAS-057 既存 localStorage 系との関係: 38_f57_assumptions / 39_f57_scenarios は 入力前提 の保存、MAS-002 は 出力確定値 の保存。役割を仕様書 + UI 上で明示し混同を防ぐ。

  11. 多年運用: MAS-177 多年度データ基盤と相互利用。MAS-177 は本案件のスナップショットを履歴ソースとして集約する設計余地あり (将来 v2)。

  12. MAS-021 連携: snapId からの PDF 生成 API 設計余地。本案件では payload 構造を MAS-021 が読みやすい形 (タブキー + 行データ JSON) に揃えておく。


Section 7. エッジケース

  1. 月初実行で当該月データ空: 期末確定処理を月初 (例 4/1) に実行すると当該月 (4 月) の INV データが空 → datamart 再構築結果も空 → フォールバック: 直近確定月までで保存し、保存ダイアログに「対象年月のデータが空です。直近確定月で保存しますか?」と確認表示。

  2. 過去年月のスナップショット取得でデータ削除済: 99_snapshot_data の有効フラグ FALSE 化済の場合 → findPeriodEnd は null 返却 + UI で「該当スナップショットは削除されています」表示。

  3. 同一年月複数スナップショット (検討中複数試作): ID 別に並列保存 (Q3 案 Y)。承認済 2 件以上は UI で警告 + 一覧で「承認」列ハイライト。

  4. payloadJSON 50MB 上限超過: 行単位正規化 (99_snapshot_data) で回避。99_snapshot_periods.payloadJSON はサマリのみで上限到達リスクなし。

  5. 保存中に他ユーザーが datamart 再構築: Utils.lockUntil(60000, ...) で排他制御。タイムアウト時は「他処理実行中です。1 分後に再試行ください」ダイアログ。

  6. 比較で大量行差分: 主要 5 タブ全行 × 2 スナップショットで数千行の差分が出る場合 → ページネーション (50 行/ページ) + 上位 N 件 (絶対値降順) のサマリビュー両方提供。

  7. 同時実行で ID 重複: SNP_YYYYMM_NNNN の発番は Utils.lockUntil 内で 99_snapshot_periods の最新 ID + 1 を計算。並列実行時は後行が待機。

  8. データ修正で過去スナップショット元 INV 削除済: スナップショットは payloadJSON で独立保持されるため、元 INV 削除でも参照可能。元 INV 復元時の整合性検証は MAS-085 側で扱う (本案件はスナップショット側を信頼)。

  9. 月次決算と期末決算の使い分け: スナップショット種別 (SNAPSHOT_TYPES) で区別。月次は カスタム + 備考に「月次」記載、期末は 期末B/S 等。UI のフィルタで切替表示。

  10. MAS-067 多年シミュレーションで期首 BS = MAS-002: findPeriodEnd(prevYm, '期末B/S') で取得。取得失敗時のフォールバックは MAS-067 側で「期首 BS 未確定です。手動入力に切替」ダイアログ。

  11. スナップショット復元 = 91_fs_bs 上書きせず別ダイアログ表示 (Q4 案 X): 復元ボタンクリック → モーダルで snapshot 内容を読み取りビュー表示。「91_fs_bs に書き戻す」機能は本案件では提供せず、誤上書き防止。

  12. MAS-251 第三者監査でハッシュ値 (SHA-256) (Q5 案 X): payloadJSON に対する SHA-256 を J 列に保存。監査時は payloadJSON を再ハッシュして J 列と照合 → 一致で改竄なし証明。


Section 8. 実データ検証 (5 シナリオ)

Scenario 1: bizlp 設立年度 Y0 (2026FY) の期末確定

  • 前提: 2026-04 〜 2027-03 の Y0 期末
  • 手順: 2027-03-31 時点で executePeriodEnd('2027-03', { isApproved: true, note: 'Y0 期末確定' })
  • 期待: SNP_202703_0001 で 5 タブの確定値が保存・ハッシュ列に SHA-256 記録
  • 検証: 翌期で datamart 再構築実行後、findPeriodEnd('2027-03', '期末B/S') で当時の確定値再現

Scenario 2: 2 期目 Y1 で過去 1 年スナップショット取得

  • 前提: Y0 (2027-03) 期末スナップショット保存済 + Y1 期中 (2027-09) で過去 1 年参照要求
  • 手順: SnapshotRepository.findPeriodEnd('2027-03') 呼出
  • 期待: Y0 確定値が即時取得 (datamart の現状に左右されない)
  • 検証: 銀行融資審査向け「過去 3 期 B/S」要請を Scenario 1 + 同様処理 × 3 回で再現

Scenario 3: 同一年月複数スナップショット

  • 前提: 2027-03 期末を決算前 2 回 (暫定) + 決算後 1 回 (確定) の 3 段階で保存
  • 手順:
    1. executePeriodEnd('2027-03', { isApproved: false, note: '暫定 v1' })SNP_202703_0001
    2. executePeriodEnd('2027-03', { isApproved: false, note: '暫定 v2 (修正)' })SNP_202703_0002
    3. executePeriodEnd('2027-03', { isApproved: true, note: '確定' })SNP_202703_0003
  • 期待: 3 件並列保存・承認FLG で確定 1 件のみ識別可能
  • 検証: listSnapshots() で 3 件すべて返却・UI で承認済 1 件強調表示

Scenario 4: スナップショット間差分比較

  • 前提: A = SNP_202703_0003 (Y0 期末) + B = SNP_202803_0001 (Y1 期末)
  • 手順: diffSnapshots('SNP_202703_0003', 'SNP_202803_0001') 呼出
  • 期待: 経常利益・現預金・売上の年次変動が delta 列で算出
  • 検証: UI で上位 N 件 (絶対値降順) ハイライト・年次成長率の概観把握

Scenario 5: スナップショット復元 (参照モード起動)

  • 前提: SNP_202703_0003 の内容確認要求
  • 手順: restoreFromSnapshot('SNP_202703_0003') 呼出 → モーダル表示
  • 期待: payloadJSON + 99_snapshot_data 行データを統合表示・91_fs_bs は上書きされない
  • 検証: 復元後に 91_fs_bs のハッシュが復元前と同一 (誤上書き防止確認)

Section 9. 関連ドキュメント

案件 ID / ドキュメント関係
MAS-057cockpit・SnapshotPanel 配置先・シナリオ保存基盤 (PR #400/#454)
MAS-067期首 BS として参照 (将来 v2)
MAS-177並行起票・本案件は MAS-177 の利用例
MAS-021PDF/Excel エクスポート・将来連携
MAS-251-253第三者監査・データ主権・将来連携
MAS-085整合性チェック・スナップショット保存時の整合性検証
MAS-077settlement_date 同期・前提案件
MAS-336PHASE 1 仕訳振替 INV・failure_patterns #34 整合性
failure_patterns #34計画 B/S 構造的乖離・PHASE 1 仕訳振替 INV 整合性
docs/_internal/dev_spec_prompt_template.md仕様書テンプレート
docs/prd.mdプロダクトポリシー Human-in-the-Loop

Section 10. 人間が検討すべき事項 (Q1-Q5)

Q1. UI 配置

内容メリットデメリット
X (推奨)cockpit パネル (SnapshotPanel.tsx)経営判断後の確定アクションとして自然・Step 7 隣接で視認性高cockpit 全体の表示量増
Y独立サイドバー (MAS-232 配下)サイドバーに集約・運用ツール扱いcockpit との文脈分離
Zメニューのみ実装最小一覧・差分 UI を提供できない

推奨理由: 期末確定は「経営判断 → 承認 → 確定」の HIL フロー (PRD ポリシー) に合致。cockpit Step 7 の財務諸表閲覧直後に確定アクションがあると操作動線が自然。

Q2. payloadJSON の格納方針

内容メリットデメリット
X99_snapshot_periods.payloadJSON 1 列に全 JSONスキーマ単純50MB 上限到達リスク
Y99_snapshot_data に行単位正規化任意行参照容易・上限なしサマリ取得が遅い
Z (推奨)ハイブリッド (メタ + サマリは X・主要 5 タブ全行は Y)サマリ高速 + 大容量対応スキーマ 2 シート

推奨理由: 99_snapshot_periods 一覧表示時はサマリ JSON のみで高速描画、詳細閲覧 / 差分比較時のみ 99_snapshot_data を JOIN。GAS の制約 (50MB 上限・appendRow パフォーマンス) を両立。

Q3. 期末確定の冪等性

内容メリットデメリット
X既存スナップショットがあればスキップ重複防止検討中複数試作不可
Y (推奨)並列保存 (履歴保持)検討中/確定/監査用の複数バージョン保持可一覧で複数件表示

推奨理由: Solo-CEO の期末決算では暫定値を複数回試算する運用が想定される。承認FLG で確定 1 件を識別する設計のほうが実運用に合致。

Q4. スナップショット復元時の動作

内容メリットデメリット
X (推奨)91_fs_bs を上書きせず参照モード元データ破壊リスクなし「過去値で再計算」は手動コピペ
Y一時上書き + バックアップ + リバートワンクリック切替バックアップ漏れで上書き事故

推奨理由: bizlp の単一 SoT (Single Source of Truth) 原則に合致。スナップショットは「読み取り専用の過去断面」であり、現行データへの書き戻しは別 UX (将来要求があれば MAS-021 PDF 生成等) で扱う。

Q5. 改竄不可性 (第三者監査向け)

内容メリットデメリット
X (推奨)payloadJSON SHA-256 ハッシュ記録MAS-251 連携を見据えて当初実装・実装コスト低ハッシュ列管理必要
Y要求しない (社内利用前提)実装最小後付けで全スナップショット再ハッシュ困難

推奨理由: MAS-251-253 第三者監査・データ主権で改竄検知は必須要件。SHA-256 計算は GAS の Utilities.computeDigest で 1 行・実装コスト最小。後付けで全件再ハッシュは不可能なため当初から仕込む。


Section 11. 実装プロンプト (Step 1-5)

Step 1: DDL シート追加 + Repository (Sonnet)

目的: スキーマ + Repository 層の基盤実装

作業:

  • 200_data/204_snapshot_repository.js 新設 (~150 行)
    • savePeriodEnd / findPeriodEnd / listSnapshots / deleteSnapshot / compareSnapshots
    • cache + findAll + save + append の Repository パターン適用
  • 100_config/101_sys_config.js の schemas に 99_snapshot_periods + 99_snapshot_data 追加
  • 000_infra/002_constants.jsID_PREFIXES.SNP + SNAPSHOT_TYPES + SNAPSHOT_TARGET_TABS 追加
  • 000_infra/003_contracts.jsSnapshot / SnapshotDiff typedef 追加

完了基準:

  • setupAllSchemas() 実行で 99_snapshot_periods + 99_snapshot_data が生成される
  • 単体テスト F002-01 〜 F002-05 (Repository CRUD + 一覧 + 差分基本動作) が通る

テストケース:

  • F002-01: savePeriodEnd → findPeriodEnd で同一データ取得
  • F002-02: listSnapshots で全件取得 (有効フラグ true のみ)
  • F002-03: deleteSnapshot で有効フラグ false 化
  • F002-04: 同一年月 2 件保存 → ID 重複なし
  • F002-05: compareSnapshots で 2 件の delta 配列取得

Step 2: SnapshotEngine (Opus)

目的: 期末確定 + 復元 + 差分比較のドメインロジック

作業:

  • 400_domain/470_snapshot_engine.js 新設 (~200 行)
    • executePeriodEnd(targetYm, options): datamart 再構築 → 5 タブ取得 → SnapshotRepository.savePeriodEnd
    • restoreFromSnapshot(snapId): 参照モード読取 (91_fs_bs 上書きしない)
    • diffSnapshots(snapIdA, snapIdB): 行レベル差分計算
  • Utils.lockUntil で排他制御
  • Utils.computeHash (SHA-256) で payloadJSON ハッシュ計算
  • 既存 datamart 再構築 (dmIngestPlanData_ 等) との整合性確保

完了基準:

  • 単体テスト F002-06 〜 F002-10 が通る

テストケース:

  • F002-06: executePeriodEnd 実行 → datamart 再構築 + 5 タブ取得 + 保存
  • F002-07: 同一実行で ID 重複なし (lockUntil 動作)
  • F002-08: SHA-256 ハッシュが payloadJSON と一致
  • F002-09: restoreFromSnapshot で 91_fs_bs が変更されないこと確認
  • F002-10: diffSnapshots で経常利益等の delta が正しく計算

Step 3: cockpit パネル + メニュー (Sonnet)

目的: UI 提供

作業:

  • webapp_client/src/cockpit/SnapshotPanel.tsx 新設 (~250 行)
    • 期末確定実行セクション (年月入力 + 種別 + 承認FLG + 実行ボタン)
    • 過去スナップショット一覧 (年月降順テーブル)
    • スナップショット間差分比較 (A/B ドロップダウン + 結果テーブル)
    • 復元 (参照モード) (snapId 選択 → モーダル表示)
  • 100_config/101_sys_config.js MENU_DEFINITION に「📸 スナップショット」メニュー追加
  • webapp_client/src/cockpit/CockpitApp.tsx の Step 7 隣接で SnapshotPanel 配置

完了基準:

  • cockpit Step 7 隣接に SnapshotPanel 表示
  • 期末確定ボタンで google.script.run.executePeriodEnd 呼出成功
  • 単体テスト F002-11 〜 F002-12 が通る

テストケース:

  • F002-11: 期末確定ボタン → google.script.run 呼出 → snapId 返却
  • F002-12: 一覧表示で listSnapshots 結果が年月降順表示

Step 4: バージョン間比較ビュー (Sonnet)

目的: 差分比較 UI の機能拡充

作業:

  • SnapshotPanel に差分比較 UI 拡張
    • A/B ドロップダウン (snapId 選択)
    • 「差分計算」ボタン → 結果テーブル (上位 N 件 + ページネーション)
    • 主要科目 (経常利益・現預金・売上) を上位ハイライト
  • SnapshotEngine.diffSnapshots 戻り値構造設計
    • { tabKey, rowKey, valueA, valueB, delta, deltaPct }[]
    • 主要科目フラグ isHighlight を付与

完了基準:

  • 2 件の snapshot 選択 → 差分テーブル表示・主要科目ハイライト
  • ページネーション動作 (50 行/ページ)

Step 5: 改竄不可性 + 監査ログ (Haiku)

目的: 第三者監査適合性の担保

作業:

  • Utils.computeHash (SHA-256) を Utilities.computeDigest(SHA_256, ...) で実装 (既存なら流用)
  • payloadJSON 保存時に SHA-256 計算 + ハッシュ列保存
  • Utils.auditLog('SNAPSHOT_SAVED', { snapId, targetYm, hash, savedBy }) 呼出
  • スナップショット読取時にハッシュ照合 (改竄検知) を restoreFromSnapshot 内で実施
  • 不一致時は警告ダイアログ「このスナップショットは改竄されている可能性があります」

完了基準:

  • スナップショット保存時にハッシュ列が記録される
  • 監査ログテーブル (既存の Utils.auditLog 出力先) に SNAPSHOT_SAVED イベントが記録
  • ハッシュ不一致シミュレーション (J 列を手動編集) で警告ダイアログ表示

Section 12. 推奨実行モデル (Step ごと)

Stepモデル理由
Step 1SonnetDDL + Repository パターン応用 (中程度の判断)
Step 2Opus期末確定 + 復元 + 差分比較ロジック設計 (複数ファイル横断・会計ロジック理解必要)
Step 3Sonnetcockpit パネルパターン応用 (中程度の判断)
Step 4Sonnet既存 SnapshotPanel への拡張 (中程度の判断)
Step 5Haikuハッシュ計算 + 監査ログ呼出 (機械的書込)

CLAUDE.md「実装時のモデル選択ルール」に準拠。


Section 13. 変更履歴

日付変更内容
2026-05-01v0.1 起票 (main 側起票依頼。バージョン管理進捗 35% の残課題「期末スナップショット保存」を仕様化。Q1-Q5 推奨判断 + Step 1-5 段階リリース計画。MAS-057 cockpit シナリオ保存 (PR #400/#454) と役割を区別 (シナリオ vs 確定値)・MAS-067 多年シミュレーション期首 BS 安定化として後続活用想定・MAS-177 多年度データ基盤と並行起票で相互利用。バージョン管理 35% → 残機能の主要部 (期末スナップショット保存) を仕様化)

Section 14. 仕様書作成プロンプト

展開して表示
# MAS-002: 期末スナップショット保存 + バージョン管理 (Period-End Snapshot + Version Management)

## frontmatter (先頭・必須)
---
id: MAS-002
aliases: ["F-02", "PERIOD-END-SNAPSHOT", "VERSION-MGMT"]
type: Story
status: Open
---

## 案件位置づけ
- 案件 ID: MAS-002 (TODO_future.md §1 P2・🧠 Domain・バージョン管理 35% 進行中の残課題)
- 連携先: MAS-057 cockpit (PR #400/#454 で localStorage 系完了→本案件で確定値保存)・MAS-067 マルチイヤー (Y0 着地見込み PR #459)・MAS-177 多年度データ基盤 (並行起票・本案件は MAS-177 利用例)

## 背景・問題意識
bizlp の財務諸表 (`91_fs_bs` / `92_fs_pl` / `81_cf_actual`) は**動的に再計算される**: datamart 再構築のたびに INV データから再生成・過去の確定値が「上書き」されて消える。期末スナップショット (BSE: B/S Ending) を**確定値として保存**する仕組みがない。

### 顕在化している不足
- 税理士向け過去 3 期分財務諸表エクスポートで各期の確定値が再現できない
- 銀行融資審査での「過去 3 期分財務諸表」要請に確定スナップショット必須
- MAS-067 マルチイヤー計画で各年度の期首 B/S が動くと多年シミュレーション再現性失われる
- MAS-057 cockpit シナリオ保存 (38_f57_assumptions / 39_f57_scenarios) は前提条件・シナリオ保存だが**確定値**保存していない
- MAS-251-253 第三者監査・データ主権で過去確定値の証跡必要

### User Story
- Solo-CEO: 「2026-03 期末確定 B/S を期末確定後に修正しても当時の値が残る」
- 税理士: 「過去 3 期の財務諸表を bizlp から確定値で取得」
- 銀行融資審査: 「過去 3 期確定 B/S を即提出」
- MAS-067 多年シミュレーション: 「各年度の期首 B/S が安定して再現可能」

## 仕様書本体 (14 セクション必須)

### Section 1. 概要
(対象ファイル / 前提案件 / 後続案件 表)

### Section 2. 目的
動的再計算される財務諸表に対し、特定時点の確定値を**不変スナップショット**として永続化する仕組み。

### Section 3. 現在のコード
- 100_config/101_sys_config.js の schemas DDL パターン
- 200_data/202_repository.js の Repository パターン
- 600_report/601_datamart_ingest.js の財務諸表生成プロセス
- webapp_client/src/cockpit/CockpitApp.tsx の Step 7 SoloFinancialStatementsPanel

### Section 4. 修正方針
4.1 シート設計 (99_snapshot_periods + 99_snapshot_data)
4.2 SnapshotRepository (204_snapshot_repository.js)
4.3 SnapshotEngine (470_snapshot_engine.js)
4.4 UI (SnapshotPanel.tsx)
4.5 メニュー

### Section 5. 影響範囲
新規ファイル / DDL 拡張 / 既存ロジック / MAS-057 cockpit / MAS-067 / MAS-021 / MAS-177

### Section 6. 注意事項 (10-12 件)

### Section 7. エッジケース (10-12 件)

### Section 8. 実データ検証 (5 シナリオ)

### Section 9. 関連ドキュメント

### Section 10. 人間が検討すべき事項 (Q1-Q5)
Q1. UI 配置 / Q2. payloadJSON 格納 / Q3. 冪等性 / Q4. 復元動作 / Q5. 改竄不可性

### Section 11. 実装プロンプト (Step 1-5)

### Section 12. 推奨実行モデル (Step ごと)

### Section 13. 変更履歴

### Section 14. 仕様書作成プロンプト