MAS-011: ボトムアップ型What-ifシミュレーション
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-011 |
| カテゴリ | シミュレーション (FP&A) |
| Phase | P3 |
| 優先度 | ★ |
| 所要時間 | 4〜6時間 |
| 対象ファイル | 400_domain/ に新設予定の 430_what_if_simulator.js(メインロジック)templates/what_if_sidebar.html(新規UIテンプレート)100_config/101_sys_config.js(openWhatIfSidebar エントリ追加)000_infra/002_constants.js(MENU_DEFINITION の「📋 サイドバー: 📊 マート更新」カテゴリ末尾に項目追加)000_infra/003_contracts.js(HeadcountDTO JSDoc 追加)600_report/ の既存関数は読み取り専用で再利用(変更なし) |
| 前提案件 | MAS-024 (BEP計算・固変区分), MAS-001 (予実差異分析)。いずれも既存実装済みで ctx.martPl / sectionTotalsPl を経由して結果が返る仕組みのため、本案件はその ctx パイプラインに仮想イベントを注入するだけで済む |
目的
「営業を 1 人増やしたら」「SaaS 契約を 1 件追加したら」「スポット売上を ◯ 百万円上積みしたら」など、ドライバー単位の操作を行った場合の P/L・CF インパクトをインメモリで即時試算する。シミュレーション結果はDB(スプレッドシート)には一切永続化しない(シナリオ保存は後続案件で別途検討)。22_bud_headcount や 41_trn_budget の実データは書き換えず、結果は専用サイドバーの結果テーブルに表示するのみ。
現在のコード
600_report/602_datamart_main.js — ctx 経由の計算パイプライン(buildBudgetTrendDataMart)
実績・予算の両方が、シート読み込み → ctx.finalUnionData 構築 → dmProcessAllEvents_(ctx) → dmCalcPl_(ctx) / dmCalcBs_(ctx) / dmCalcCf_(ctx) の一方向パイプライン。計画側は実際に「別 ctx (planCtx) を組み立てて同じ関数群に再投入する」というパターンで動いている (L256〜L284):
const planCtx = {
targetMonths: ctx.targetMonths,
obMonthStr: ctx.obMonthStr,
acctMap: ctx.acctMap,
TAX_RATES: ctx.TAX_RATES,
PL_SECTIONS: ctx.PL_SECTIONS,
boundaryMonthStr: ctx.boundaryMonthStr,
uiHeader: uiHeaderPlan,
targetMonthsWithActBgt: ctx.targetMonthsWithActBgt,
isActualOnly: false
};
var planData = dmIngestPlanData_(ctx, sheetInv);
var sheetPipe = ss.getSheetByName('21_bud_pipeline');
var pipeResult = dmIngestPipelinePlanData_(ctx, sheetPipe);
planData = planData.concat(pipeResult.planData);
planCtx.finalUnionData = planData;
dmProcessAllEvents_(planCtx);
dmCalcPl_(planCtx);
dmCalcBs_(planCtx);
dmCalcCf_(planCtx);
この構造が What-if シミュレーションにそのまま流用可能。つまり 600_report/ 側の大規模リファクタリングは不要で、planCtx と同じ要領で「baseline ctx + 仮想イベントを concat した simCtx」を構築して同じ関数群を再実行すればシミュレーション結果が得られる。
600_report/601_datamart_ingest.js — 予算の取込関数
dmIngestPlanData_(ctx, sheetInv)… 32_wrk_invoice の全 INV から planData を返す (L277)dmIngestPipelinePlanData_(ctx, sheetPipe)… 21_bud_pipeline の確度加重済みイベントを返す (L363)
どちらも戻り値が配列 (planData / events) になっており、DTO 相当のインメモリオブジェクト配列に仮想行を差し込むことが容易。
600_report/603_datamart_pl.js / 604_datamart_bs.js / 605_datamart_cf.js
いずれも dmCalcPl_(ctx) / dmCalcBs_(ctx) / dmCalcCf_(ctx) の形で ctx のみを引数にとり、計算結果を ctx に書き戻す設計。シート I/O は一切行わない(書き込みは dmBuildPlOutput_(ctx) + dmApplyDwhFormat_() 側に分離)。本案件では出力系(dmBuildPlOutput_ / dmApplyDwhFormat_)は呼ばず、ctx.sectionTotalsPl 等の集計結果だけをサイドバーに返す。
000_infra/002_constants.js L76 — SHEET_DEFAULTS の 22_bud_headcount エントリ
HC 予算シートの未入力フィールドを補完するためのデフォルト値。仮想 HC レコード生成時に必ずここから転記する(ハードコード禁止):
{ pattern: '22_bud_headcount', prefix: 'EMP_', defaults: {
'雇用形態': '正社員',
'決済ラグ(月)': 1,
'健保料率': 0.05,
'介護保険料率': 0,
'厚年料率': 0.0915,
'雇用保険料率': 0.0065,
'子ども・子育て拠出金率': 0.0036,
'源泉所得税額': 0,
'源泉消費税額': 0,
'免税フラグ': false,
'住民税額': 0,
'月額給与・報酬': 0,
'採用エージェント費': 0,
'PC等初期費用': 0,
_dynamic: { '適用年度': 'fiscalYear', '開始年月': 'nextYm' }
}}
000_infra/003_contracts.js — DTO 定義状況
| エンティティ | DTO 定義 | 備考 |
|---|---|---|
31_wrk_order | OrderDTO | 定義済み |
32_wrk_invoice | InvoiceDTO | 定義済み |
33_wrk_bank | BankTxDTO | 定義済み |
42_trn_journal | JournalEntryDTO | 定義済み |
41_trn_budget | BudgetDTO | 定義済み |
22_bud_headcount | 未定義 | 本案件 Step 2 で HeadcountDTO (JSDoc のみ) を追加する |
12_mst_partner | PartnerDTO | 定義済み |
200_data/202_repository.js — Repository 定義状況
| Repository | 対象シート | 備考 |
|---|---|---|
OrderRepository | 31_wrk_order | 実在 |
InvoiceRepository | 32_wrk_invoice | 実在 |
BankTxRepository | 33_wrk_bank | 実在 |
JournalRepository | 42_trn_journal | 実在 |
AccountRepository | 11_mst_account | 実在 |
PartnerRepository | 12_mst_partner | 実在 |
HeadcountRepository | — | 未定義。本案件では新設せず、Utils.getSheetByKey('BUD_HC', '22_bud_headcount') + Contracts.toDtoList(data) でインメモリ DTO を生成する |
BudgetRepository | — | 未定義。同様に Utils.getSheetByKey('TRN_BUDG', '41_trn_budget') を直接読む |
100_config/101_sys_config.js L323 — onOpen() のメニュー構成
onOpen() は Constants.MENU_DEFINITION をループしてメニューを動的生成する設計 (MAS-214)。個別メニューへの手動 addItem 呼び出しは存在しない。本案件の MAS-011 エントリは 002_constants.js の MENU_DEFINITION 配列(L206〜L324)に追記する。具体的には L230〜L239 の '📋 サイドバー: 📊 マート更新' カテゴリ配下の items 配列末尾に追加する(実在するカテゴリ名なので造語禁止):
{
category: '📋 サイドバー: 📊 マート更新',
source: 'sidebar',
items: [
{ label: '財務3表の更新', funcName: 'buildBudgetTrendDataMart', ... },
{ label: '📅 基準年月指定で更新', funcName: 'buildDataMartWithCustomBoundary', ... },
{ label: 'プロジェクト別 採算', funcName: 'buildProjectProfitability', ... },
{ label: '📊 KPIダッシュボード再描画', funcName: 'buildKpiDashboard', ... },
{ label: '📸 前年度P/Lスナップショット', funcName: 'savePlSnapshot', ... },
// ← ここに MAS-011 の項目を追加
]
}
100_config/101_sys_config.js L356 — 既存サイドバーの作り方
openOperationsSidebar() が HtmlService.createTemplateFromFile('templates/operations_sidebar') で HTML を評価してサイドバーを表示している。本案件の openWhatIfSidebar() も同一パターンを踏襲する(createHtmlOutputFromFile ではなく createTemplateFromFile を使用。Env.isDev() 等の変数をテンプレートに注入できるため)。
300_ui/301_ui_assist.js のロック利用
LockService.getScriptLock() パターンが既存。getUserLock() は本リポジトリでは未使用のため、本案件でも getScriptLock() で二重実行を防止する(指示原文の getUserLock() は一般記述のため既存パターンに合わせる)。
修正方針
Step 1: バックエンド計算エンジンの整備(ctx ベース再利用の確立)
方針: 600_report/ の大規模リファクタリングは行わない。現状の dmProcessAllEvents_(ctx) / dmCalcPl_(ctx) / dmCalcBs_(ctx) / dmCalcCf_(ctx) がすでに「ctx 受け取り → ctx 書き戻し」の純粋計算関数として構成されているため、上位で planCtx と同じ要領の simCtx を組み立てれば再利用できる(buildBudgetTrendDataMart 内で planCtx が構築されている L256-L284 と同パターン)。
薄いヘルパー _buildBaselineCtx_() を 430_what_if_simulator.js 内の private 関数として新設し、以下を行う:
buildBudgetTrendDataMart()冒頭と同様にシート参照(WRK_INVC / WRK_BANK / TRN_BUDG / MST_ACCT)を取得targetMonths/obMonthStr/startYear/TAX_RATESを同じロジックで算出dmIngestData_(ctx, sheetInv, sheetBank, sheetAcct)で実績を取込dmIngestPlanData_(ctx, sheetInv)で計画 INV を取込dmIngestPipelinePlanData_(ctx, sheetPipe)でパイプラインを取込- 予算(TRN_BUDG)を直接読み込んで
BudgetDTO相当の仮想イベントに変換 - 以上を concat した
baselineData配列をctx.finalUnionDataにセットして ctx を返す
既存関数は一切変更しない。ラッパーとしての _buildBaselineCtx_() は新規ファイルに閉じ、呼び出し元は runWhatIfSimulation_ のみ。
Step 2: シミュレーション関数 runWhatIfSimulation_(params) の実装
400_domain/430_what_if_simulator.js(新規)に以下を実装。サイドバーから google.script.run.runWhatIfSimulation_(params) で呼び出される公開関数。
2-1. シグネチャ
/**
* @param {Object} params - UIから受け取るドライバー設定
* @param {string} params.driver - 'HC_ADD' | 'HC_REMOVE' | 'SAAS_ADD' | 'PIPELINE_SPOT_ADD'
* @param {string} params.startYm - 'YYYY-MM' 開始年月
* @param {Object} params.payload - ドライバー別パラメータ
* @returns {{ baseline: Object, scenario: Object, delta: Object, meta: Object }}
*/
function runWhatIfSimulation_(params) { ... }
2-2. 処理フロー
- 二重実行防止:
LockService.getScriptLock()をtryLock(5000)で取得。取れなければ'別の処理が実行中です。しばらく待ってから再実行してください。'を throw - バリデーション:
_validateParams_(params)で以下を検証し、失敗時は{ error: '…' }を返すparams.driverが許可リストに含まれることparams.startYmがUtils.parseDateToYm()でパース可能かつ、当期のtargetMonths範囲内params.payloadの数値フィールドがUtils.parseAmt()で 0 以外かつ正(後述エッジケースで分岐)
- baseline ctx 構築:
baselineCtx = _buildBaselineCtx_()→dmProcessAllEvents_→dmCalcPl_→dmCalcBs_→dmCalcCf_を実行 - シナリオ ctx 構築:
- baseline の
ctx.finalUnionDataをディープコピー(JSON.parse(JSON.stringify(...))相当。Date はなくなるので注意 →pYm/sYmは既に文字列のため問題なし) - ドライバー別のファクトリ関数で仮想イベント配列を生成:
_buildHcAddEvents_(payload, startYm, ctx)→HeadcountDTOを 1 体インメモリ生成し、401_rpa_hc.jsのロジックを参考に給与・社保・源泉の月次 INV 相当イベント ({pYm, sYm, acc, amt, booked: false}) を 12 ヶ月分展開_buildHcRemoveEvents_(empId, removeYm, ctx)→ 既存 HC レコード起源のイベントをbaseline.finalUnionDataから抽出し、removeYm以降分を金額符号反転して打ち消すイベントを生成_buildSaasAddEvents_(payload, startYm, ctx)→ 月額 × 残月数ぶんの費用イベントを生成_buildPipelineSpotEvents_(payload, startYm, ctx)→ startYm 月に売上 + lag 月後に売掛金/現金化イベントを生成
scenarioCtx.finalUnionData = baseline.finalUnionData.concat(virtualEvents)
- baseline の
- シナリオ計算:
dmProcessAllEvents_(scenarioCtx)→dmCalcPl_(scenarioCtx)→dmCalcBs_(scenarioCtx)→dmCalcCf_(scenarioCtx) - 結果抽出: baseline / scenario それぞれから以下の KPI を取り出し、delta を計算
- 売上高合計(年次 Total):
sectionTotalsPl['sales'][0] - 営業利益(年次 Total):
sectionTotalsPl['opProfit'][0](実際のキー名はctx.PL_SECTIONSのtype:'profit'行を参照して特定する) - 営業CF合計:
ctx.cfOpの Total - 期末現預金:
martBs.asset_ca['現金及び預金'][12]相当 - BEP 売上高 (MAS-024):
ctx.bepSales[0](MAS-024 が格納している場合)
- 売上高合計(年次 Total):
- 返り値:
{ baseline: {...}, scenario: {...}, delta: {...}, meta: { driver, startYm, runAtJst } } - クリーンアップ:
finallyでlock.releaseLock()
2-3. HC 用 DTO の新規定義 (000_infra/003_contracts.js に JSDoc 追記)
/**
* 22_bud_headcount — 人件費予算レコード
* @typedef {Object} HeadcountDTO
* @property {boolean} 有効フラグ
* @property {string} 管理ID - "EMP_NNNN"
* @property {string} 氏名・ポジション
* @property {string} 雇用形態 - "正社員" | "業務委託" 等
* @property {string} 科目名 - "給料手当" | "役員報酬" 等
* @property {string} 取引先名
* @property {number} 適用年度
* @property {string} 開始年月 - "YYYY-MM"
* @property {string} 終了年月 - "YYYY-MM"
* @property {number} 月額給与・報酬
* @property {string} 決済手段
* @property {number} 決済ラグ(月)
* @property {number} 健保料率
* @property {number} 介護保険料率
* @property {number} 厚年料率
* @property {number} 雇用保険料率
* @property {number} 子ども・子育て拠出金率
* @property {number} 源泉所得税額
* @property {number} 住民税額
* @property {number} 採用エージェント費
* @property {number} PC等初期費用
* @property {string} 組織名
*/
Contracts.toDto / toDtoList は既存の汎用実装がそのまま使えるため、追加のファクトリは不要。JSDoc 追加のみで IDE 補完と型ドキュメントの整合を取る。
Step 3: サイドバーUI(HtmlService)
templates/what_if_sidebar.html(新規)を createTemplateFromFile パターンで作成。100_config/101_sys_config.js に openWhatIfSidebar() を新設:
function openWhatIfSidebar() {
var template = HtmlService.createTemplateFromFile('templates/what_if_sidebar');
template.envName = (function() { try { return Env.name().toUpperCase(); } catch (e) { return 'UNKNOWN'; } })();
var html = template.evaluate().setTitle('🧪 What-if シミュレーション').setWidth(360);
SpreadsheetApp.getUi().showSidebar(html);
}
UI 構成:
- ドライバー選択プルダウン(初期対象: 「人員追加 (HC_ADD)」「人員削減 (HC_REMOVE)」「SaaS契約追加 (SAAS_ADD)」「スポット売上追加 (PIPELINE_SPOT_ADD)」の 4 種)
- ドライバー別パラメータ入力フォーム(例: HC_ADD → 氏名、月額給与、開始年月、雇用形態、科目名; SaaS → 月額費用、開始年月、費用科目)
- 「シミュレーション実行」ボタン
- クリック時: ボタンを
disabledにしてローディング表示(spinner) google.script.run.withSuccessHandler(onResult).withFailureHandler(onError).runWhatIfSimulation_(params)
- クリック時: ボタンを
- 結果テーブル(baseline / scenario / delta の 3 列)
- 表示 KPI: 売上高、営業利益、営業CF、期末現預金、BEP売上高(Total 年次の差分のみ表示。月次は V2 で検討)
- 数値は
Constants.NUMBER_FORMATS.CURRENCY相当の書式(JS 側でtoLocaleString('ja-JP')+ マイナス時に△プレフィックス)
- 固定注意書き:
⚠️ これはインメモリでのシミュレーションです。結果は保存されません。実際の予算へ反映する場合は、各予算シート(22_bud_headcount / 23_bud_subscription / 21_bud_pipeline 等)を直接編集してください。
Step 4: メニューへの登録
000_infra/002_constants.js の MENU_DEFINITION 配列内、L230〜L239 の '📋 サイドバー: 📊 マート更新' カテゴリの items 配列末尾に以下を追加:
{ label: '🧪 What-if シミュレーション', funcName: 'openWhatIfSidebar', description: 'ドライバー操作によるP/L・CFインパクトをインメモリで即時試算 (MAS-011)' },
onOpen() 側の変更は不要(MENU_DEFINITION のループで自動的にメニュー項目が生成される)。
影響範囲
| 変更区分 | ファイル | 変更量 |
|---|---|---|
| 新設 | 400_domain/430_what_if_simulator.js | 約 350 行(public runWhatIfSimulation_ + private ヘルパー群) |
| 新設 | templates/what_if_sidebar.html | 約 200 行(HTML + inline JS。operations_sidebar.html を参考に) |
| 追記 | 100_config/101_sys_config.js | openWhatIfSidebar() 1 関数 約 10 行 |
| 追記 | 000_infra/002_constants.js | MENU_DEFINITION 内 1 行追加 |
| 追記 | 000_infra/003_contracts.js | HeadcountDTO JSDoc ブロック 約 25 行 |
| 不変 | 600_report/ 配下全ファイル | 変更なし(既に ctx ベースのため読み取り専用で再利用) |
| 不変 | 200_data/202_repository.js | 変更なし(HeadcountRepository 等は新設しない。シート直読みで対応) |
既存テストへの影響
900_test/901_test_runner.js の現状のテストケース(T1-01〜T1-14 系の HC RPA テスト、整合性チェック系)は 600_report/ の関数や RPA 実装を変更しないため影響なし。新規のテスト T7-01〜T7-XX(What-if 系)の追加は本案件スコープ外(後続 PR で検討)。
デプロイ時の .claspignore
templates/what_if_sidebar.html は .claspignore の pattern 次第で push 対象に含める必要がある。既存の templates/operations_sidebar.html が既に push されている(= .claspignore で templates/ は除外されていない)ため、追加作業は不要。
注意事項
HeadcountRepository/BudgetRepositoryは実在しない。200_data/202_repository.jsを勝手に追記しない。HC/予算データの取得はUtils.getSheetByKey('BUD_HC', '22_bud_headcount')+Contracts.toDtoList(sheet.getDataRange().getValues())でインメモリ DTO を生成する。将来これらの Repository が必要になるか否かは別案件(N-NN)で検討する。600_report/ の既存関数は読み取り専用で利用する。
dmIngestData_/dmProcessAllEvents_/dmCalcPl_/dmCalcBs_/dmCalcCf_のシグネチャは変更禁止。内部実装にも手を入れない(他の呼び出し元=buildBudgetTrendDataMartの planCtx 構築箇所への副作用を絶対に起こさないため)。変更したくなった場合は必ず grep で呼び出し元を全件確認してから別 PR を切る。シミュレーション結果を永続化しない。
sheet.appendRow()/Repository.save()/Repository.append()/setValues()は本案件のコードパスで一切呼ばない。結果はサイドバーへの戻り値としてのみ返す。永続化が必要になった場合は「シナリオ保存シート」を別案件で設計する。日付比較は
Utils.parseDateToYm()で正規化する。Dateオブジェクトを直接<>比較したりString(date)でソートしたりしない(失敗パターン #17)。pYm/sYmは常に'YYYY-MM'文字列で扱う。数値パースは
Utils.parseAmt()を使う。JS のNumber()は空文字列を0として返すためバリデーションがすり抜ける。parseAmtで parse →Number.isFinite+ 正値チェックでバリデーションする。数値フォーマットは
Constants.NUMBER_FORMATS.CURRENCYの定義に合わせる('#,##0;[Red]△ #,##0;"-"'パターン)。UI 側では JS のtoLocaleString('ja-JP')+amt < 0 ? '△ ' + ...で再現する。MENU_DEFINITIONの変更は002_constants.jsL230〜L239 のカテゴリ配列内に限定する。onOpen()本体(L323)や他のカテゴリには手を入れない。カテゴリ名'📋 サイドバー: 📊 マート更新'は既存実在文字列をそのまま引用(造語禁止、失敗パターン #20 の再発防止)。LockService は
getScriptLock()を使う。getUserLock()は本リポジトリ既存コードで未使用。301_ui_assist.jsや811_audit_checker.jsの既存getScriptLock()パターンに合わせる。HtmlServiceはcreateTemplateFromFileを使う。createHtmlOutputFromFileは環境変数を注入できないため、既存operations_sidebarと同じcreateTemplateFromFile+.evaluate()パターンに揃える。科目マスタ未登録の科目名はエラーにする(CLAUDE.md 会計ロジック規約)。仮想 HC レコードで
科目名に「給料手当」や「役員報酬」等を入れる際は、AccountRepository.findAsMap()に実在することを確認してからイベント生成する。未登録科目はシミュレーション対象外として UI 側で弾く。
エッジケース
| 条件 | 表示値 / 振る舞い | 理由 |
|---|---|---|
費用・給与入力にマイナス値(例: 月額給与 -500000) | UI 側でバリデーションエラー '金額は正の整数で入力してください。人員を減らす場合は「人員削減」ドライバーを使用してください'。runWhatIfSimulation_ は呼ばない | 「人員を減らす」シナリオは HC_REMOVE ドライバーで明示的に扱う。金額符号反転を汎用許可すると誤入力の検知が困難になる(失敗パターン #2 予防) |
| 費用・給与入力が空文字 / 文字列 / NaN | Utils.parseAmt() でパースして結果が 0 または !Number.isFinite なら UI 側でバリデーションエラー | Number('') が 0 になる JS 仕様をすり抜けないよう、空文字は明示拒否 |
params.startYm が当期 targetMonths 範囲外(過去・未来) | UI で警告表示 + 計算範囲は当期会計年度(targetMonths[0] 〜 targetMonths[11])に限定。範囲外月のイベントは targetMonths.indexOf(pYm) === -1 により dmProcessAllEvents_ 内部で OB(期首残高)扱いになるため、P/L 影響はゼロ、B/S 期首残高のみ変動する | 会計年度をまたぐシミュレーションは V2 で別途要件化。本案件では当期のみ |
| シミュレーション結果で売上ゼロ月の KPI(利益率・BEP売上高等) | "-" または "N/A" を表示。ゼロ除算フォールバック(varRate = 0 等)は使わない | 失敗パターン #2(MAS-024 BEP 売上ゼロ月問題)の教訓。sales === 0 を明示判定して N/A を返す |
| scenario の営業利益や CF が負値 | マイナス金額として △ プレフィックス付きで表示(△ 1,234,567)。赤字/黒字の判定閾値は置かない | Constants.NUMBER_FORMATS.CURRENCY の書式 '#,##0;[Red]△ #,##0;"-"' に合わせる |
| 月別値を年次 Total として合計するケース(売上金額等の加算可能指標) | filterWithRecalcTotal を使用(既存 MAS-024 と同パターン) | 加算可能指標は Total = sum(M1..M12) で再計算 |
| 月別値を年次 Total として合計できないケース(利益率・BEP 到達率等の非加算指標) | filterValues + Total 独自計算(既存 MAS-024 と同パターン) | 非加算指標を filterWithRecalcTotal に通すと Total が壊れる(失敗パターン #1) |
| 同一ユーザーが連打して 2 回同時実行 | 2 回目は lock.tryLock(5000) で失敗し、サイドバー側で '別の処理が実行中です' エラー表示。ボタンの disabled 制御もあるため通常は発生しない | getScriptLock() で冪等性を保証 |
シート 22_bud_headcount / 41_trn_budget が DDL 未実行で存在しない | Utils.getSheetByKey() が null を返す → ベースライン読み込み時に警告ログ + 空配列として続行(baseline 計算のみ実行、scenario も計算可能) | 開発環境での検証を阻害しないため。prod ではシートが必ず存在する想定 |
| シナリオが baseline と完全一致(delta = 0 の行のみ) | 結果テーブルの delta 列に 0(または空欄)を表示。エラーではなく正常完了扱い | 入力ミスで 0 金額のイベントを追加した場合でも、単に「影響なし」として表示する |
_buildHcAddEvents_ で生成される月次 INV 相当イベントの社保計算 | SHEET_DEFAULTS['22_bud_headcount'] のデフォルト料率(健保 0.05、厚年 0.0915 等)と、ユーザー入力の月額給与を掛けて月額社保額を算出。介護保険(40 歳以上のみ)はデフォルト 0 だがユーザー入力で上書き可能 | 正確な保険料計算は 401_rpa_hc.js の既存ロジックを参考。ただしシミュレーション用途なので厳密な労使折半比率は簡易版で OK(全額を法定福利費科目に計上する近似) |
実データ検証
実装前に MCP(Sheets API)または GAS エディタから以下を必ず確認する:
22_bud_headcountの列ヘッダー一覧(GAS エディタでsheet.getRange(1,1,1,sheet.getLastColumn()).getValues()[0]を実行)- 期待列:
有効フラグ / 管理ID / 氏名・ポジション / 雇用形態 / 科目名 / 取引先名 / 適用年度 / 入社年月 / 退職年月 / 開始年月 / 終了年月 / 月額給与・報酬 / 決済手段 / 決済ラグ(月) / 支払基準日 / 休日調整 / CF計上 / 免税フラグ / 源泉所得税額 / 源泉消費税額 / 住民税額 / 健保料率 / 健保額 / 介護保険料率 / 介護保険額 / 厚年料率 / 厚年額 / 雇用保険料率 / 雇用保険額 / 子ども・子育て拠出金率 / 子ども拠出金額 / 法定福利費合計 / 社保預り金合計 / 社保控除後支給額 / 差引支給額 / 採用エージェント費 / PC等初期費用 / 組織名 / 起票ターゲット月 / 最終起票年月日 / 備考 - DDL 定義(101_sys_config.js)のヘッダーと実シートが 100% 一致することを確認。乖離があれば DDL を先に修正する
- 期待列:
Constants.SHEET_DEFAULTSの22_bud_headcountエントリの全フィールド名が実シートヘッダーに存在するか- 特に
健保料率/介護保険料率/厚年料率/雇用保険料率/子ども・子育て拠出金率が実際の列名と一致しているか
- 特に
600_report/602_datamart_main.jsのbuildBudgetTrendDataMartがどのシートキー (Utils.getSheetByKeyのキー) を参照しているかを確認- WRK_INVC / WRK_BANK / TRN_BUDG / MST_ACCT / PL_M_ACT 等
dmIngestPlanData_(ctx, sheetInv)/dmIngestPipelinePlanData_(ctx, sheetPipe)の戻り値構造が仕様どおりArray<{pYm, sYm, acc, amt, booked, isBsForce}>になっているか- 科目マスタに仮想 HC レコードで使う科目名(「給料手当」「役員報酬」「法定福利費」「預り金」等)が登録されているか(
AccountRepository.findAsMap()に存在すること)
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| CLAUDE.md | コーディング規約、科目マスタ未登録禁止、ヘッダーベース列参照、変更時テスト対象の確認 |
| TODO_future.md L245 | MAS-011 案件定義 |
| dev_mas-024_bep_analysis.md | BEP 計算の既存実装。シミュレーション結果の BEP 差分表示で参照 |
| dev_mas-001_variance_analysis.md | 予実差異分析の実装。planCtx 構築パターンの参考 |
| dev_mas-003_kpi_dashboard.md | KPI ダッシュボード。本案件の KPI 抽出ロジックの参照 |
| dev_mas-214_menu_catalog.md | MENU_DEFINITION 配列への追加パターン |
| dev_mas-217_sidebar_catalog_and_readability.md | operations_sidebar.html のサイドバー統合方針 |
| dev_mas-119_headcount_rate_master.md | SHEET_DEFAULTS['22_bud_headcount'] の保険料率マスタ化 |
| spec/spec_rpa_hc.md | HC RPA の既存仕訳ロジック。仮想 HC イベント生成の参考 |
人間が検討すべき事項
- シミュレーション対象のドライバー一覧の決定(TODO_future.md 原文の「人間が検討すべき事項」より転記)
- 初期対象: HC_ADD / HC_REMOVE / SAAS_ADD / PIPELINE_SPOT_ADD の 4 種を提案
- V2 で検討: CAPEX 追加(24_bud_capex)/ 継続売上 MRR 増減 / 為替・原価率変動 / 組織別人員シフト
- 結果 KPI の粒度(Total 年次のみ? 月次も?)
- 本仕様は Total 年次のみを提案(UI が複雑になりすぎるのを避ける)。月次ドリルダウンは V2 で「シミュレーション結果を一時シートに書き出す」設計とセットで検討
- シナリオ保存機能の是非
- 本仕様では非対応(インメモリのみ)。将来的に「シナリオA / B の並列比較」が必要になれば別案件として設計
- HC_REMOVE の正確なモデル化
- 簡易案: 既存 HC レコード起源の月次イベントを
removeYm以降で金額符号反転して打ち消す - 退職金・未消化有給・社保資格喪失タイミング等の細目はスコープ外(必要なら別案件)
- 簡易案: 既存 HC レコード起源の月次イベントを
- 社会保険料の労使折半の扱い
- 本仕様は簡易版(全額を法定福利費科目として計上、個人負担分と会社負担分を分離しない)
- 厳密な仕訳(会社負担分 → 法定福利費、個人負担分 → 預り金)は既存 RPA_HC ロジックに委ね、シミュレーションでは近似で OK
- 600_report/ リファクタリングスコープの最終判断(Phase 1 調査結果)
- 調査結論: 既存の
dmCalcPl_(ctx)等はすでに ctx 受け取り型で、リファクタリング不要(planCtx 構築パターンがそのまま使える) - したがって「ラッパー関数を追加する」Step 1 も最小スコープ(
_buildBaselineCtx_()ヘルパー 1 つ)に縮小可能 - 将来
600_report/を大幅に変更する別案件(例: データマート関数のファイル分割)と競合しないよう、本案件の変更は新規ファイル430_what_if_simulator.jsと102_constants.js等への最小追記に限定する
- 調査結論: 既存の
- E2E テスト方針
- UI を含むためユニットテストが書きにくい。当面は手動動作確認(サイドバーを開く → HC_ADD で 1 名追加 → 期待通りの delta 表示 → シートが書き変わっていないこと確認)で検証
- バックエンドロジック(
runWhatIfSimulation_+ 仮想イベント生成ヘルパー)単体のテストケースを901_test_runner.jsに追加するかは後続 PR で判断
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-011「ボトムアップ型What-ifシミュレーション」を実装してください。
## 実行前タスク
以下のファイルを Read し、実在するクラス名・関数名・シート名・列名を確認してから実装に着手してください。記憶や推測で固有名詞を書かないこと(失敗パターン #18-#20 予防)。
- `200_data/202_repository.js` : 実在する Repository は `OrderRepository` / `InvoiceRepository` / `BankTxRepository` / `JournalRepository` / `AccountRepository` / `PartnerRepository` の 6 種のみ。`HeadcountRepository` / `BudgetRepository` は**実在しない**ことを確認すること。
- `000_infra/003_contracts.js` : 実在する DTO は `OrderDTO` / `InvoiceDTO` / `BankTxDTO` / `JournalEntryDTO` / `BudgetDTO` / `PartnerDTO`。`HeadcountDTO` は未定義 → 本実装で追加する。`Contracts.toDto` / `toDtoList` / `toRow` / `toRows` の既存シグネチャをそのまま流用する。
- `000_infra/002_constants.js` : L73〜L87 の `SHEET_DEFAULTS`。特に L76 の `22_bud_headcount` エントリの全フィールド(健保料率 0.05、厚年料率 0.0915 等)を確認。L206〜L324 の `MENU_DEFINITION` 配列、L230〜L239 の `'📋 サイドバー: 📊 マート更新'` カテゴリを確認。
- `000_infra/004_utils.js` : `parseDateToYm()` / `addMonths()` / `parseAmt()` / `getSheetByKey()` の引数・戻り値。
- `600_report/601_datamart_ingest.js` : `dmIngestData_(ctx, sheetInv, sheetBank, sheetAcct)` / `dmIngestPlanData_(ctx, sheetInv)` / `dmIngestPipelinePlanData_(ctx, sheetPipe)` のシグネチャと戻り値形式。
- `600_report/602_datamart_main.js` : `buildBudgetTrendDataMart()` L159〜L380 の全体構造と、L256〜L284 の planCtx 構築パターン(これを What-if simCtx のテンプレートとする)。
- `600_report/603_datamart_pl.js` / `604_datamart_bs.js` / `605_datamart_cf.js` : `dmCalcPl_(ctx)` / `dmCalcBs_(ctx)` / `dmCalcCf_(ctx)` のシグネチャ。これらは ctx を in-out 引数として受け取る純粋関数(シート I/O なし)であること。
- `100_config/101_sys_config.js` : L323 の `onOpen()` と L356 の `openOperationsSidebar()` を Read し、`HtmlService.createTemplateFromFile('templates/operations_sidebar')` のパターンを踏襲する。メニュー登録は `MENU_DEFINITION` 経由の動的生成のため、本ファイルへの `addItem` 追加は不要(`002_constants.js` 側のみ修正)。
- `templates/operations_sidebar.html` : 既存サイドバーの HTML 構造・CSS・`google.script.run` 呼び出しパターン。
- `900_test/901_test_runner.js` : 既存の T1-XX(HC RPA)テストが破壊されないことを Read で確認(本案件は 600_report/ を**変更しない**ため影響なし想定)。
## 修正対象ファイル
1. `400_domain/430_what_if_simulator.js` — **新規作成**
2. `templates/what_if_sidebar.html` — **新規作成**
3. `100_config/101_sys_config.js` — **追記のみ**(`openWhatIfSidebar()` 約 10 行)
4. `000_infra/002_constants.js` — **追記のみ**(`MENU_DEFINITION` 内 1 行)
5. `000_infra/003_contracts.js` — **追記のみ**(`HeadcountDTO` JSDoc ブロック)
6. `600_report/` 配下 — **変更禁止**(読み取り専用で再利用)
7. `200_data/202_repository.js` — **変更禁止**(新規 Repository を追加しない)
## 実装内容
### Step 1: `000_infra/003_contracts.js` に `HeadcountDTO` の JSDoc を追加
`BudgetDTO` の直後(既存 `PartnerDTO` の前)に、仕様書「Step 2-3. HC 用 DTO の新規定義」のテーブル内容を JSDoc `@typedef` として追加する。ファクトリ関数は追加不要(既存 `Contracts.toDto` / `toDtoList` で代替)。
### Step 2: `400_domain/430_what_if_simulator.js` を新規作成
以下の構成で実装する:
- `runWhatIfSimulation_(params)` — public 公開関数。仕様書「Step 2-2 処理フロー」の 1〜8 をそのまま実装。
- `_validateParams_(params)` — private。仕様書「エッジケーステーブル」の条件を順に検証し、`{ ok: false, error: '...' }` もしくは `{ ok: true }` を返す。
- `_buildBaselineCtx_()` — private。`buildBudgetTrendDataMart()` 冒頭と同じロジック(シート取得、targetMonths/obMonthStr/TAX_RATES 算出、`dmIngestData_` + `dmIngestPlanData_` + `dmIngestPipelinePlanData_`)でベースライン ctx を構築する。**既存 buildBudgetTrendDataMart のコードを変更せず**、必要箇所を新ファイル内に再実装する(L159〜L280 の相当箇所をコピーして独立関数化)。
- `_buildHcAddEvents_(payload, startYm, ctx)` — private。`SHEET_DEFAULTS['22_bud_headcount']` の料率を使って月額社保を計算し、当期 12 ヶ月ぶんの `{pYm, sYm, acc, amt, booked:false, isBsForce:null}` 配列を返す。`科目名` は `AccountRepository.findAsMap()` で実在確認(未登録はエラー throw)。
- `_buildHcRemoveEvents_(empId, removeYm, ctx)` — private。baseline `ctx.finalUnionData` の中から該当 HC 由来のイベント(管理ID が `empId` のもの)を `removeYm` 以降で抽出し、`amt` を符号反転した打ち消しイベント配列を返す。
- `_buildSaasAddEvents_(payload, startYm, ctx)` — private。月額費用 × 残月数ぶんの費用イベントを生成。`決済ラグ(月)` は `SHEET_DEFAULTS['23_bud_subscription']` のデフォルト値を使う。
- `_buildPipelineSpotEvents_(payload, startYm, ctx)` — private。startYm 月に売上計上イベント、入金ラグ分後に売掛金解消イベント(fromSTL 相当)を生成。
- `_extractKpiFromCtx_(ctx)` — private。ctx から `{ sales, opProfit, cfOp, endCash, bep }` を抽出する。キー名は実際の `ctx.PL_SECTIONS` / `ctx.sectionTotalsPl` / `ctx.cfOp` / `ctx.martBs` の構造を**事前 Read で確認**してから使う。
### Step 3: `templates/what_if_sidebar.html` を新規作成
`operations_sidebar.html` の `<style>` セクションと `google.script.run` 呼び出しパターンを参考に作成する。仕様書「Step 3 UI 構成」の 1〜5 を HTML + inline JavaScript で実装する。外部 CDN 参照は禁止(GAS のサンドボックス制約)。
### Step 4: `100_config/101_sys_config.js` に `openWhatIfSidebar()` を追記
`openOperationsSidebar()` 関数定義(L356)の直後に追記する。既存関数は変更しない。
### Step 5: `000_infra/002_constants.js` の `MENU_DEFINITION` を更新
L230〜L239 の `'📋 サイドバー: 📊 マート更新'` カテゴリ内 `items` 配列の末尾(L237 の `savePlSnapshot` 行の直後)に MAS-011 エントリを 1 行追加する。他のカテゴリ・他の行には**一切触れない**。
## 制約
- **`HeadcountRepository` / `BudgetRepository` は実在しない**。`202_repository.js` を追記しないこと。HC/予算データの取得は `Utils.getSheetByKey('BUD_HC', '22_bud_headcount')` + `Contracts.toDtoList()` のみ使用する。
- **シミュレーション結果を `Repository.save()` / `Repository.append()` / `sheet.appendRow()` / `sheet.getRange(...).setValues(...)` で書き込まない**(永続化禁止)。結果は `runWhatIfSimulation_` の戻り値として返すのみ。
- **`600_report/` 配下の既存関数の呼び出し元を変更しない**。変更したくなった場合は `grep -r 'dmCalcPl_\|dmCalcBs_\|dmCalcCf_\|dmProcessAllEvents_' .` で全件確認し、**別 PR** で対応する。
- メニュー名・関数名・シート名・列名は、該当コード(`onOpen()` / `MENU_DEFINITION` / `SHEET_DEFAULTS` / 22_bud_headcount のヘッダー)を Read して**実在する文字列のみ引用**する(造語禁止、失敗パターン #20 の再発防止)。
- `MENU_DEFINITION` のカテゴリ名は `'📋 サイドバー: 📊 マート更新'`(絵文字込みの完全一致)を使う。
- 日付比較は必ず `Utils.parseDateToYm()` で `'YYYY-MM'` に正規化してから文字列比較する(失敗パターン #17 予防)。
- 数値バリデーションは `Utils.parseAmt()` → `Number.isFinite()` + 正値チェックの 2 段階で行う(`Number('')` が `0` になる JS 仕様を回避)。
- `LockService.getScriptLock()` を使う(`getUserLock()` は既存未使用のため合わせない)。
## エッジケース
仕様書「エッジケース」テーブルの全 10 行をそのまま実装・検証すること:
- マイナス金額入力 → `HC_REMOVE` ドライバー誘導のエラー
- 空文字 / NaN 入力 → `parseAmt` で弾く
- 当期範囲外の `startYm` → 警告 + 範囲外月はゼロ寄与
- 売上ゼロ月の KPI → `'-'` / `'N/A'` 表示(ゼロ除算フォールバック禁止)
- 負値金額 → `△` プレフィックス表示
- 加算可能指標 → `filterWithRecalcTotal`
- 非加算指標 → `filterValues + Total 独自計算`
- 連打による二重実行 → `tryLock(5000)` 失敗でエラー
- シート未作成 → 警告ログ + 空配列で続行
- delta = 0 → 正常完了扱い
- HC 社保計算 → `SHEET_DEFAULTS['22_bud_headcount']` の料率を使用
## 実データ検証
実装前に MCP(Sheets API)または GAS エディタから以下を確認:
1. `22_bud_headcount` の列ヘッダー 41 列が、仕様書「実データ検証」の期待列リストと 100% 一致するか
2. `Constants.SHEET_DEFAULTS` の `22_bud_headcount` エントリのフィールド名(健保料率等)が実シート列名と一致するか
3. `buildBudgetTrendDataMart` の参照シートキー(WRK_INVC / WRK_BANK / TRN_BUDG / MST_ACCT 等)が 01_sys_config に登録済みか
4. 科目マスタ(11_mst_account)に「給料手当」「役員報酬」「法定福利費」「預り金」「売上高」が登録されているか
5. `ctx.sectionTotalsPl` / `ctx.cfOp` / `ctx.martBs` の実際のキー構造(`sales` / `opProfit` / `asset_ca` 等)を確認
## 動作確認
1. `npm run push:dev` で開発用 GAS にデプロイする。
2. GAS エディタで `onOpen()` を再実行してメニューを再生成し、**🚀 BizLP → 操作パネルを開く** からサイドバー経由、または **📋 サイドバー: 📊 マート更新 → 🧪 What-if シミュレーション** から直接、`openWhatIfSidebar()` を呼び出す。
3. ドライバー「人員追加 (HC_ADD)」を選択し、氏名「シミュレ太郎」、月額給与「500000」、開始年月「当期の任意月」、科目名「給料手当」を入力して「シミュレーション実行」をクリック。
4. 結果テーブルに baseline / scenario / delta の 3 列が表示され、売上高は変わらず、営業利益と営業 CF が人件費相当額(月額給与 × 残月数 + 社保概算)減少していることを確認する。
5. **`22_bud_headcount` シート・`41_trn_budget` シートが一切書き変わっていないこと**をシートエディタで確認する(これが本案件の最重要動作確認)。
6. 月額給与に `-500000` を入力 → UI でバリデーションエラー「金額は正の整数で入力してください。人員を減らす場合は「人員削減」ドライバーを使用してください」が表示されることを確認。
7. 月額給与に空文字または `abc` を入力 → UI でバリデーションエラーが表示されることを確認。
8. ボタン連打(2 秒以内に 3 回クリック)→ 2 回目以降は `'別の処理が実行中です'` エラーが表示されるか、ボタンの disabled 制御で発火しないことを確認。
9. 売上ゼロ月のシナリオ(例: 期首月を startYm にして SaaS 費用のみ追加)で、BEP 到達率等の非加算 KPI が `'-'` / `'N/A'` で表示されることを確認。
10. 既存のデータマート更新(**📊 マート更新 → 財務3表の更新**)を実行し、結果が本案件導入前と変わらないことを確認する(`600_report/` を変更していないため当然変わらないはず)。
11. `runAllTests()` を実行し、既存のテスト T1-XX が全て PASS することを確認する。
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| 実行前タスク(Read調査) | あり | ファイル構造・関数名・ctx 構造の確定 |
| Step 1: HeadcountDTO 追加 | なし | JSDoc 転記のみ |
| Step 2: simulator 実装 | あり | planCtx パターンの simCtx への移植、HC 月次イベント生成ロジックの設計 |
| Step 3: サイドバーUI | なし | 既存 operations_sidebar.html パターンの踏襲 |
| Step 4-5: メニュー/エントリ追記 | なし | 1〜2 行追記のみ |
| 動作確認(手動) | なし | 確定手順の実行 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 複数ファイル横断の設計判断、600_report/ の ctx 構造の理解、HC 仕訳ロジックの理解が必要 |
| Step 1: HeadcountDTO JSDoc 追加 | Claude Haiku 4.5 | 定義済みテーブルの JSDoc 転記のみ。判断要素なし |
| Step 2: simulator 実装 (runWhatIfSimulation_ + private ヘルパー群) | Claude Opus 4.6 | 複数ファイル横断(600_report/ の読み取り、SHEET_DEFAULTS 参照、HC 社保計算)、会計ロジックの理解が必要 |
| Step 3: サイドバーUI (what_if_sidebar.html) | Claude Sonnet 4.6 | 既存 operations_sidebar.html パターンの適用 + google.script.run 連携 |
| Step 4: openWhatIfSidebar() 追記 | Claude Haiku 4.5 | 既存 openOperationsSidebar() の直後に約 10 行追加。判断要素なし |
| Step 5: MENU_DEFINITION 1 行追記 | Claude Haiku 4.5 | カテゴリ配列内の指定位置に 1 行追加。判断要素なし |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-21 | 初版作成 |
| 2026-04-22 | MVP 完了 (PR #315)。新規 400_domain/430_what_if_simulator.js(WhatIfSimulator.run() / getWhatIfDefaults())+ templates/what_if_sidebar.html。ドライバーは HC_ADD / SAAS_ADD の 2 種のみ実装、評価期間 2 モード(mode='ANNUAL' = マートエンジン 12 ヶ月 Total / mode='FIVE_YEAR' = MAS-010 ベースラインに overlay 重ね合わせ)。MAS-010 側に FinancialModelingService.simulateWithOverlay(overlay, opts) 公開 API を新設し既存関数は無変更で再利用。結果はサイドバー戻り値のみで永続化なし(Repository.save() / appendRow / setValues を本コードパスで呼ばない)。実績平均プリロード(月額給与 / 一人当たり月次売上 / 初期費用)を 22_bud_headcount + 32_wrk_invoice 過去実績から自動算出。HC_REMOVE / PIPELINE_SPOT_ADD は ALLOWED_DRIVERS = ['HC_ADD','SAAS_ADD'] バリデーションで明示的に reject し、V2 残件とする |
仕様書作成プロンプト(再現性・監査性のため必ず記録)
展開して表示
<instruction>
【タイムアウト回避・実行原則(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(骨格)/2-2(概要〜注意事項)/2-3a(エッジケース〜人間検討事項)/2-3b(実装プロンプト〜変更履歴)/2-4(<details>記録) に分割する。1回のWrite/Editは約300行以内。
4. 各Stepで何を書くかを具体指示: Phase 2実行時に設計判断を持ち込まない。Phase 1で確定した固有名詞・行番号・関数名をそのまま書き出す。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 MAS-011「ボトムアップ型What-ifシミュレーション」の開発仕様書を作成してください。
作成後、`docs/_config.json` の `nav` 配列(§E.5 FP&A・レポーティング)に必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
以下を順番に Read/Grep し、Phase 2で設計判断を再考しないよう全項目を確定させる。
**Grepは「どこにあるか」の発見まで。「どう書くか」の判断は必ずReadで行う。**
### 1-A: 案件要件の把握
- `docs/_internal/TODO_future.md` を検索し、MAS-011の案件名・概要・期待される効果・人間が検討すべき事項を取得する。
### 1-B: 仕様書フォーマットの確認
- `docs/_internal/dev_spec_prompt_template.md` の「Phase 2: 仕様書の作成」セクション構成と「実装プロンプトのフォーマット」を読み込み、出力する仕様書が完全準拠するよう構成を確定させる。
- `docs/dev/dev_mas-024_bep_analysis.md`(FP&A系の近傍事例)を1件読み込み、フォーマットの実例を把握する。
### 1-C: データアクセス層・DTO・定数の把握
以下をすべてReadで開き、**実在するクラス名・メソッド名・型定義・フィールド名を確認する**(記憶や名前からの推測禁止)。
- `000_infra/003_contracts.js`: 定義済みDTOの一覧と各フィールドを確認する。実在するDTOは `OrderDTO` / `InvoiceDTO` / `BankTxDTO` / `JournalEntryDTO` / `BudgetDTO` の5種。HC(人員)シート用のDTO(`22_bud_headcount`)は**定義されていない**ことを確認し、新規定義が必要かどうかを判断する。
- `200_data/202_repository.js`: 実在するRepositoryクラスと `findAll()` / `save()` / `append()` の引数・戻り値を確認する。実在するのは `OrderRepository` / `InvoiceRepository` / `BankTxRepository` / `JournalRepository` / `AccountRepository` の5種。**`HeadcountRepository`・`BudgetRepository` は実在しない。** `22_bud_headcount`(HC予算)・`41_trn_budget`(予算)のシートへのアクセス方法を確認し、新規Repositoryが必要か、あるいは他の手段(`Utils.getSheetByKey` 経由のシート直読み)を採用するかを確定させる。
- `000_infra/002_constants.js`: `SHEET_DEFAULTS` の `22_bud_headcount` エントリ(`健保料率: 0.05`・`厚年料率: 0.0915` 等)と `ID_PREFIX_MAP` を確認し、仮想レコード生成時に補完できるデフォルト値の全フィールドを把握する。
- `000_infra/004_utils.js`: `parseDateToYm()` / `addMonths()` / `parseAmt()` の引数・戻り値を確認する。
- `000_infra/001_env.js`: `Env` モジュールのAPIを確認する(スプレッドシートID取得等)。
### 1-D: 600_report/ データマートの把握
以下のファイルをすべてReadし、**各ファイルの公開関数名・引数・内部でsheetを直接読む箇所**を特定する。「DTO配列を引数として受け取れるようにリファクタリングできるか」を評価するために必要。
- `600_report/601_datamart_ingest.js`
- `600_report/602_datamart_pl.js`
- `600_report/603_datamart_bs.js`
- `600_report/604_datamart_cf.js`
- `600_report/605_datamart_kpi.js`(存在する場合)
- `600_report/606_datamart_render.js`〜`608_datamart_render.js`(存在するファイルのみ)
上記を読んで以下を確定させる:
1. シミュレーション計算に再利用できる関数の正確な名前と引数シグネチャ
2. 現在シート直読みしている箇所(リファクタリングスコープ)
3. リファクタリングが現実的かどうか(関数が大きすぎる場合は「計算ロジックを別関数に抽出する」方針に切り替える)
### 1-E: UI・メニュー構造の確認
- `100_config/101_sys_config.js` の `onOpen()` 関数を Read し、**実在するメニュー名を確認**する。MAS-011のUIエントリポイントをどのメニューに追加するかを確定させる(記憶・造語による「〜メニューがあるはず」は禁止)。
- `300_ui/301_ui_assist.js` を Read し、`HtmlService` / `LockService` の既存利用パターンを確認する。
### 1-F: テスト・影響範囲の確認
- `900_test/901_test_runner.js` を Grep し、`600_report/` 関連のテストケースを特定する。リファクタリングで壊れるテストを事前に把握する。
---
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-011_what_if_simulation.md`
**絶対に1回のツール呼び出しで全内容を出力せず、以下のStepに分割して実行すること。**
### Step 2-1: 骨格の作成 (File Write, ~20行)
`dev_spec_prompt_template.md` の必須セクション構成に従い、全セクションの見出しのみを持つ骨格ファイルを作成する。本文は空で可。
### Step 2-2: 概要〜注意事項の追記 (File Edit または Bash heredoc, ~300行)
以下の内容を書く(Phase 1で確定した固有名詞・行番号を使用。再調査・再考禁止):
- **概要テーブル**: 案件ID, カテゴリ, Phase, 優先度, 所要時間, 対象ファイル(Phase 1で特定したファイル名を列挙), 前提案件
- **目的**: 「『営業を1人増やしたら』等のドライバー操作によるP/L・CFインパクトをインメモリで即時試算する。結果はDBに永続化しない」旨を簡潔に記述。
- **現在のコード**: Phase 1で特定した `600_report/` の関連関数(実名を引用)と、それらが現在シートを直接読んでいる箇所を引用する。
- **修正方針**:
- **Step 1: バックエンド計算エンジンの整備**: Phase 1で特定した `600_report/` の計算関数を、シート直読みからDTO配列受け取りに切り替えるリファクタリング(またはラッパー関数の追加)。リファクタリングが過大な場合は「DTO配列を受け取る薄いラッパー関数を追加し、内部でシート読みを委譲する」方針に切り替えて明記する。
- **Step 2: シミュレーション関数の実装**: GAS側の `runWhatIfSimulation_(params)` 関数。
1. UIから受け取った `params`(ドライバー種別・パラメータ)を検証する。
2. ドライバー種別に対応するRepositoryまたはシートから現在の予算データをDTOとして取得する(Phase 1で特定した取得方法を明記。`findAll()` が使えるか、`Utils.getSheetByKey` 経由の直読みになるかを確定させる)。
3. `Constants.SHEET_DEFAULTS` の `22_bud_headcount` エントリ(`健保料率: 0.05`・`介護保険料率`・`厚年料率: 0.0915` 等)を参照し、ユーザー入力パラメータと組み合わせてインメモリで仮想DTOを生成する。HC用DTOが未定義の場合は本Stepで定義する。
4. 仮想DTOを既存DTOリストに追加し、Step 1で整備した計算エンジンに渡す。
5. 計算結果(主要KPIのシミュレーション前後の値と差分)を返す。
6. `LockService.getUserLock()` で二重実行を防止する。
- **Step 3: サイドバーUI (`HtmlService`)**: ドライバー選択プルダウン(初期対象: 人員・SaaS契約・スポット売上)、パラメータ入力フォーム(人数・金額・開始年月)、計算実行ボタン(処理中は無効化+ローディング表示)、結果テーブル表示、「これはシミュレーション結果です。実際の予算への反映は各予算シートを直接編集してください」の注意書き。
- **Step 4: メニューへの登録**: Phase 1で確認した `onOpen()` の実在するメニュー構造に、MAS-011のエントリポイントを追加する(メニュー名は `onOpen()` から引用した実在する文字列のみ使用)。
- **影響範囲**: 変更ファイル・変更量・既存テストへの影響(Phase 1-Fで特定したテストケース名を列挙)。
- **注意事項**:
1. `HeadcountRepository`・`BudgetRepository` は `202_repository.js` に実在しない。HC/予算データの取得方法はPhase 1で確定した方法を使用すること。
2. `600_report/` の関数をリファクタリングする場合、既存のデータマート更新フロー(シートからの読み込み経路)を壊さないこと。既存の呼び出し元を `grep` で全件確認してから変更する。
3. インメモリ計算の結果を `spreadsheet.appendRow()` や `Repository.save()` で書き込まないこと(永続化禁止)。
4. `Utils.parseDateToYm()` で日付比較を行うこと(`String(Date)` のソートは失敗パターン #17 参照)。
5. 数値フォーマットは `Constants.NUMBER_FORMATS.CURRENCY` を使用する。
### Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash, ~200行)
- **エッジケーステーブル**(テーブル形式: 条件 | 表示値 | 理由):
- 費用にマイナス値が入力された場合: バリデーションエラーを返す(人員を減らすシナリオは専用の「人員削減」ドライバーとして別定義し、既存DTOの仮想無効化として実装する)。
- 費用入力に文字列・空文字が入力された場合: バリデーションエラーを返す(`Utils.parseAmt()` でパースしNaN/0なら拒否)。
- 開始年月が会計年度の範囲外・過去日付の場合: UIで警告を表示し、計算範囲は現在の会計年度内のみに限定する(範囲外月はゼロ寄与として扱う)。
- シミュレーション結果で売上ゼロ/分母ゼロの月のKPI(利益率・BEP売上高等): `"-"` または `N/A` を表示する(失敗パターン #2 の教訓を適用。`varRate=0` のフォールバックは不可)。
- 加算可能指標(金額合計)の月別合算: `filterWithRecalcTotal` を使用。非加算指標(比率・BEP等)は `filterValues + Total独自計算` を使用(失敗パターン #1 参照)。
- **実データ検証**(実装前にMCPまたはGASエディタで確認すべき項目):
- `22_bud_headcount` の列ヘッダー一覧(DTOフィールド名との整合確認)。
- `Constants.SHEET_DEFAULTS` の `22_bud_headcount` エントリの全フィールドが実シートヘッダーと一致しているか。
- `600_report/` のデータマート関数が現在どのシートキー(`Utils.getSheetByKey` のキー)を参照しているか。
- **関連ドキュメント**: テーブル形式で関連仕様書リンクを列挙。
- **人間が検討すべき事項**: TODO_future.mdから転記した「シミュレーション対象のドライバー一覧の決定」に加え、Phase 1調査で判明した追加事項(600_report/ リファクタリングスコープの最終判断等)を列挙する。
### Step 2-3b: 実装プロンプト〜変更履歴の追記 (File Edit または Bash, ~250行)
実装プロンプトは行頭4スペースインデントで出力すること(バッククォートで囲まない)。
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-011「ボトムアップ型What-ifシミュレーション」を実装してください。
## 実行前タスク
- `200_data/202_repository.js`: 実在するRepository一覧と `findAll()` の戻り値型を確認する。
- `000_infra/003_contracts.js`: 定義済みDTO(OrderDTO / InvoiceDTO / BankTxDTO / JournalEntryDTO / BudgetDTO)のフィールドを確認する。HeadcountシートのDTOが未定義であれば本実装で追加する。
- `000_infra/002_constants.js`: `SHEET_DEFAULTS` の `22_bud_headcount` エントリのフィールドを確認し、仮想DTO生成時に補完する値を把握する。
- `600_report/601_datamart_ingest.js` 〜 `608_datamart_render.js`: 計算エンジンとして再利用する対象関数の実名と引数を確認する。
- `100_config/101_sys_config.js`: `onOpen()` を Read し、実在するメニュー名を確認してからエントリポイントを追加する。
## 修正対象ファイル
(Phase 1で特定した実在ファイルのみ列挙すること。存在しないファイル名を書かない)
## 実装内容
(上記仕様書の修正方針 Step 1〜4 の順番で実装する。各Stepで変更するファイル・関数・行番号を明記する)
## 制約
- `HeadcountRepository`・`BudgetRepository` は実在しない。`202_repository.js` を勝手に追記しない。HC/予算データの取得は仕様書で確定した方法のみ使用する。
- シミュレーション計算結果を `Repository.save()` や `sheet.appendRow()` で書き込まない(永続化禁止)。
- `600_report/` の既存関数の呼び出し元を変更する場合は `grep` で全件確認してから実施する。
- メニュー名・関数名・シート名は `onOpen()` および対象ファイルを Read して実在する文字列のみ引用する(造語禁止)。
## エッジケース
(仕様書のエッジケーステーブルの全行を転記する)
## 動作確認
1. `npm run push:dev` で開発用GASにデプロイする。
2. GASエディタから `onOpen()` で表示されるメニュー(実在する名称)経由でサイドバーを開く。
3. ドライバー「人員追加」を選択し、人数1名・開始年月を入力して計算を実行する。シミュレーション結果がUIに表示され、シート(`22_bud_headcount` 等)が書き変わっていないことを確認する。
4. 費用欄にマイナス値・文字列を入力し、バリデーションエラーが返ることを確認する。
5. 売上ゼロ月のKPIが `"-"` または `N/A` で表示されることを確認する。
6. 既存のデータマート更新(`600_report/` の既存フロー)が壊れていないことをテストランナーで確認する。
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read調査) | あり | ファイル構造・関数名の確定 |
| 実装(コード変更) | なし | 確定済み仕様の書き下し |
- **推奨実行モデルテーブル**:
| 工程 | 推奨モデル | 理由 |
|------|----------|------|
| Step 1: 600_report/ リファクタリング | Claude Opus 4.6 | 複数ファイル横断の設計判断・会計ロジック理解が必要 |
| Step 2: シミュレーション関数実装 | Claude Sonnet 4.6 | 既存パターン適用・挿入位置の特定が必要 |
| Step 3: サイドバーUI実装 | Claude Sonnet 4.6 | HTMLテンプレート・GAS doGet 既存パターンの適用 |
| Step 4: メニュー登録 | Claude Haiku 4.5 | `onOpen()` への1〜2行追記。判断要素なし |
- **変更履歴テーブル**: `| 2026-04-20 | 初版作成 |`
### Step 2-4: 仕様書作成プロンプトの記録 (File Edit または Bash)
末尾に以下の形式で追記する(最重量工程のため必ず独立Stepで実行すること):
仕様書作成プロンプト(再現性・監査性のため必ず記録)
展開して表示
(このPhase 3: _config.json への追記と構文チェック
仕様書作成後、docs/_config.json を Read し、nav 配列の §E.5 FP&A・レポーティング セクションの末尾に以下を追記する:
{ "file": "dev/dev_mas-011_what_if_simulation.md", "title": "E.5.X MAS-011 ボトムアップ型What-ifシミュレーション" }
追記後、JSONとして構文が壊れていないことを確認する(Bashで node -e "JSON.parse(require('fs').readFileSync('docs/_config.json','utf8'))" 等)。
</details>