MAS-045: What-if 結果を 22_bud_headcount / 23_bud_subscription へ転記する機能
概要
| 項目 | 内容 |
|---|---|
| 案件 ID | MAS-045 |
| カテゴリ | FP&A |
| 優先度 | P3 ★★ |
| 対象ファイル(変更あり) | 400_domain/415_whatif_budget_transfer.js(新規・namespace WhatifBudgetTransfer)200_data/202_repository.js(末尾に HeadcountRepository / SubscriptionRepository を新設 — InvoiceRepository.append パターン踏襲で層分離原則を維持)400_domain/430_what_if_simulator.js(結果返却時に transferPayload 構築)templates/what_if_sidebar.html(結果表示後に「📥 この条件で予算に反映」ボタン + 確認ダイアログ + クライアント setTimeout ハイライト) |
| 出力先 | 22_bud_headcount / 23_bud_subscription(末尾行追加)、98_audit_log(転記ログ記録) |
| 前提案件 | MAS-011 MVP(PR #315 + #320 polish、完了済)、MAS-044(テンプレートマスタ・テンプレ ID 記録列追加検討) |
| 関連案件 | MAS-010(5 カ年モード確定時の複数年転記設計)、MAS-048(採用 TCO 後の予算化動線) |
MAS-011 What-if シミュレーションで設定した値を、実際の予算シート(22_bud_headcount / 23_bud_subscription)に新規行として転記 する機能。「検討 → 実行」を 1 サイクルで完結させ、What-if で最終確定した採用計画・SaaS 導入計画を即時に予算へ反映できるようにする。
従来は「What-if で何度もシミュレーション → 最終案を手動で 22/23 タブに転記」という二重作業が発生していた。MAS-045 はこの転記を 1 クリックで実行し、Human-in-the-Loop(確認ダイアログ + 監査ログ)で安全性を担保する。
目的
- What-if → 予算化の 1 アクション完結: 意思決定の流速向上、二重作業解消
- MAS-010 連携ベースラインへの自動反映: 5 カ年モードで確定した採用計画をそのまま MAS-010 連携に取り込める
- 監査性の担保:
98_audit_logに全転記をoperation='CREATE'で記録、誰がいつどの条件で予算化したかをトレース可能に - MAS-044 テンプレ連携: 転記行に
template_id列を追加する拡張により、採用予算が「どのテンプレから派生したか」を追跡可能にする(オプション)
現在のコード
400_domain/430_what_if_simulator.js(MAS-011 MVP 実装、708 行)
MAS-011 MVP で以下の構造を持つ:
WhatIfSimulator.run(params)/getWhatIfDefaults()/getTemplateList()(MAS-044 で追加予定)- HC_ADD / SAAS_ADD ドライバー対応
- ANNUAL / FIVE_YEAR モード
- 戻り値:
{ baseline, scenario, delta, meta }(ANNUAL)/{ baseline, scenario, meta }(FIVE_YEAR)
本案件では戻り値に transferPayload を追加し、転記に必要な全フィールドを構造化して返すよう拡張する:
// WhatIfSimulator.run の戻り値に追加
transferPayload: {
driver: 'HC_ADD' | 'SAAS_ADD',
targetSheet: '22_bud_headcount' | '23_bud_subscription',
rowData: {
// HC_ADD の場合(BUD_HC スキーマ準拠)
'管理ID': '', // append 時に ID_PREFIX_MAP 参照で採番
'氏名・ポジション': '新規採用(テンプレ: XXX)',
'雇用形態': '正社員',
'科目名': '給料手当',
'月額給与・報酬': 500000,
'開始年月': '2026-10',
'採用エージェント費': 1000000,
'PC等初期費用': 300000,
// ... その他列
},
templateId: 'POS_001', // F-44 連携時
scenarioMeta: { /* 元シナリオの情報 */ }
}
200_data/202_repository.js の既存パターン
InvoiceRepository.save()/InvoiceRepository.append()(L155 起点):writeDtosToSheet_/appendDtosToSheet_を使用appendDtosToSheet_(sheet, headers, dtos, lastRowCol)(L85): シート末尾に DTO を追記- ID 発番は
Utils.generateId(prefix, digit, isDate)(002_constants.jsID_PREFIX_MAP)
22_bud_headcount(BUD_HC schema L943、41 列)
管理ID列:EMP_0001等(ID_PREFIX_MAPのEMP_、4 桁、日付なし)有効フラグ/雇用形態/科目名/月額給与・報酬/開始年月/終了年月/採用エージェント費/PC等初期費用等を転記対象に含む- デフォルト値:
SHEET_DEFAULTS(002_constants.jsL76)で雇用形態='正社員'/月額給与・報酬=0等がプリセット
23_bud_subscription(BUD_SUBS schema L956、24 列)
管理ID列:SUB_0001等(ID_PREFIX_MAPのSUB_、4 桁、日付なし)サービス・ツール名/費用科目/取引先名/契約形態/開始・契約年月/税抜金額_計画/決済ラグ(月)等- デフォルト値:
契約形態='月額'/税区分='対象外'/決済ラグ(月)=1/利用ステータス='利用中'
98_audit_log(LOG_AUDIT schema L962、10 列)
- ヘッダー:
日時 / ユーザー / 操作種別 / 対象シート / 対象ID / 対象列 / 関数名 / 変更前値 / 変更後値 / 備考 Utils.auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)で記録operation許容値:'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'
templates/what_if_sidebar.html(MAS-011 MVP 実装、432 行)
- 結果表示テーブル(
<table class="result">)の下部に追加 UI を挿入 - ボタン
.btn-transfer(新規)を「📥 この条件で予算に反映」として配置 - クリック時に
google.script.run.transferWhatIfToBudget(transferPayload)を呼び出し
修正方針
Step 1: 計算エンジン側の拡張(430_what_if_simulator.js)
WhatIfSimulator.run の戻り値に transferPayload を追加。HC_ADD / SAAS_ADD それぞれで、ユーザー入力(params.payload)から 22 / 23 タブのスキーマにマッピングした rowData を生成。
function _buildTransferPayload_(params) {
if (params.driver === 'HC_ADD') {
return {
driver: 'HC_ADD',
targetSheet: '22_bud_headcount',
systemKey: 'BUD_HC',
rowData: {
'氏名・ポジション': '[What-if転記] ' + (params.payload.positionName || '新規採用'),
'雇用形態': params.payload.employmentType || '正社員',
'科目名': params.payload.accountName || '給料手当',
'月額給与・報酬': Number(params.payload.monthlySalary) || 0,
'開始年月': params.payload.startYm || Utils.nextYm(),
'採用エージェント費': Number(params.payload.recruitFee) || 0,
'PC等初期費用': Number(params.payload.pcCost) || 0,
'有効フラグ': false, // ★ デフォルト FALSE = 下書き扱い(人間検討事項 #1 準拠)
'備考': 'F-45 転記(' + params.meta.runAtJst + '、シナリオ: ' + (params.payload.scenarioNote || '') + ')',
// その他は 22_bud_headcount の SHEET_DEFAULTS で補完
},
templateId: params.payload.templateId || null,
};
} else if (params.driver === 'SAAS_ADD') {
return {
driver: 'SAAS_ADD',
targetSheet: '23_bud_subscription',
systemKey: 'BUD_SUBS',
rowData: {
'サービス・ツール名': '[What-if転記] ' + (params.payload.serviceName || '新規 SaaS'),
'費用科目': params.payload.accountName || '通信費',
'契約形態': params.payload.contractType || '月額',
'開始・契約年月': params.payload.startYm || Utils.nextYm(),
'税抜金額_計画': Number(params.payload.monthlyAmount) || 0,
'決済ラグ(月)': Number(params.payload.paymentLagMonths) || 1,
'有効フラグ': false,
'備考': 'F-45 転記(' + params.meta.runAtJst + ')',
},
templateId: params.payload.templateId || null,
};
}
return null;
}
Step 2: Repository 新設(202_repository.js 末尾)
Gemini レビュー指摘(🔴 Critical)を反映: Domain 層(415_whatif_budget_transfer.js)から直接シートに書き込む方針を撤回し、200_data/202_repository.js に HeadcountRepository / SubscriptionRepository を新設 して層分離原則を維持する。既存 InvoiceRepository.append(L155 起点)のパターンに完全準拠。
// =====================================================================
// HeadcountRepository — 22_bud_headcount (F-45 追記専用)
// =====================================================================
var HeadcountRepository = {
_getSheet: function() {
return Utils.getSheetByKey('BUD_HC', '22_bud_headcount');
},
findAll: function() {
return readSheetAsDtos_(HeadcountRepository._getSheet());
},
/**
* 末尾行に DTO を追記。ID は呼出側で採番済の想定(Utils.generateId)
* @param {Object[]} dtos DTO 配列(各要素はヘッダー名キー → 値)
* @returns {{ appended: number }}
*/
append: function(dtos) {
var sheet = HeadcountRepository._getSheet();
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]
.map(function(h) { return String(h).trim(); });
return appendDtosToSheet_(sheet, headers, dtos, 1); // 列 B = ID 列で最終行判定
},
};
// =====================================================================
// SubscriptionRepository — 23_bud_subscription (F-45 追記専用)
// =====================================================================
var SubscriptionRepository = {
_getSheet: function() {
return Utils.getSheetByKey('BUD_SUBS', '23_bud_subscription');
},
findAll: function() {
return readSheetAsDtos_(SubscriptionRepository._getSheet());
},
append: function(dtos) {
var sheet = SubscriptionRepository._getSheet();
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]
.map(function(h) { return String(h).trim(); });
return appendDtosToSheet_(sheet, headers, dtos, 1);
},
};
設計原則:
appendDtosToSheet_(sheet, headers, dtos, lastRowCol)は既存202_repository.jsL85 の private ヘルパで、第 4 引数lastRowCol=1は列 B(0-indexed)で最終行判定する意味(CLAUDE.md コーディング規約「列 A のチェックボックス回避」準拠)- DTO はヘッダー名キー → 値のオブジェクト。
appendDtosToSheet_内でheaders順に配列化 - ID 採番は呼出側(
WhatifBudgetTransfer)でUtils.generateId使用、DTO の'管理ID'キーに格納済の前提
Step 3: 転記エンジン(415_whatif_budget_transfer.js 新規)
var WhatifBudgetTransfer = (function() {
/**
* 公開 API: サイドバーから呼ばれる転記関数
* @param {Object} transferPayload WhatIfSimulator.run の戻り値の transferPayload
* @returns {{ success, transferId, targetSheet, rowNumber } | { error }}
*/
function transferWhatIfToBudget(transferPayload) {
var FUNC = 'transferWhatIfToBudget';
var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
return { error: '別の処理が実行中です。しばらく待ってから再実行してください。' };
}
try {
// バリデーション
if (!transferPayload || !transferPayload.targetSheet || !transferPayload.rowData) {
return { error: '転記データが不正です。What-if シミュレーションを再実行してください。' };
}
// Repository 選択(driver に応じて HeadcountRepository / SubscriptionRepository)
var repo;
if (transferPayload.driver === 'HC_ADD') {
repo = HeadcountRepository;
} else if (transferPayload.driver === 'SAAS_ADD') {
repo = SubscriptionRepository;
} else {
return { error: '未対応の driver: ' + transferPayload.driver };
}
// ID 採番(ID_PREFIX_MAP から prefix を取得)
var prefixConfig = Utils.getIdPrefixConfig(transferPayload.targetSheet);
var newId = Utils.generateId(
prefixConfig.prefix,
prefixConfig.digit,
prefixConfig.isDate,
repo._getSheet()
);
// DTO 構築: rowData + 採番 ID + SHEET_DEFAULTS フォールバック
var dto = {};
dto['管理ID'] = newId;
Object.keys(transferPayload.rowData).forEach(function(key) {
dto[key] = transferPayload.rowData[key];
});
// Repository 経由で末尾追記(層分離原則維持)
var result = repo.append([dto]);
// 監査ログ記録
Utils.auditLog(
'CREATE',
transferPayload.targetSheet,
newId,
'',
FUNC,
'',
JSON.stringify(transferPayload.rowData).slice(0, 500),
'F-45 What-if 転記(driver=' + transferPayload.driver + ', templateId=' + (transferPayload.templateId || 'none') + ')'
);
// Toast 通知(Utils.toastResult は 004_utils.js L520 に実在)
Utils.toastResult(FUNC, '転記完了: ' + newId + ' を ' + transferPayload.targetSheet + ' に追加しました', 5);
return {
success: true,
transferId: newId,
targetSheet: transferPayload.targetSheet,
rowNumber: result.lastRow || null, // Repository から取得、なければ null
};
} catch (e) {
Utils.logError(FUNC, e, 'payload=' + JSON.stringify(transferPayload).slice(0, 500));
return { error: e.message || String(e) };
} finally {
try { lock.releaseLock(); } catch (_) {}
}
}
return {
transferWhatIfToBudget: transferWhatIfToBudget,
};
})();
// グローバル公開(サイドバーから google.script.run で呼べるように)
function transferWhatIfToBudget(transferPayload) {
return WhatifBudgetTransfer.transferWhatIfToBudget(transferPayload);
}
Gemini レビュー対応:
- ❌ 削除:
Utils.findLastRowByCol(sheet, 2)(実在しない架空関数) → Repository のappend経由でappendDtosToSheet_が内部で処理 - ❌ 削除:
Utilities.sleep(3000)サーバー側ハイライト(UI フリーズの原因) → Step 4 でクライアントsetTimeout方式に変更 - ✅ 維持:
Utils.toastResult(004_utils.js L520 に実在、Gemini 誤指摘) - ✅ 追加: Repository 選択分岐(driver 対称性、#25 準拠)
Step 4: サイドバー UI 拡張(what_if_sidebar.html)
結果表示後に「📥 この条件で予算に反映」ボタンを追加。ハイライトはクライアント setTimeout で実装(サーバー側 Utilities.sleep による UI フリーズを回避、Gemini レビュー 🟡 Major 指摘を反映):
<!-- 結果表示テーブルの下 -->
<div id="transfer-section" style="display: none; margin-top: 14px; padding: 10px; background: #fafafa; border: 1px solid #eee; border-radius: 4px;">
<div style="font-size: 11px; font-weight: 700; margin-bottom: 6px;">📥 この条件で予算に反映</div>
<div class="hint" style="margin-bottom: 8px;">
⚠️ 転記後の有効フラグは <strong>FALSE(下書き)</strong> になります。確認後に手動で TRUE にしてください。
</div>
<button class="btn-run" id="btn-transfer" style="background: #38761d;">📥 {{driver_label}} として予算に反映</button>
<div id="transfer-result" style="margin-top: 8px; font-size: 11px;"></div>
</div>
<script>
document.getElementById('btn-transfer').addEventListener('click', function() {
var btn = this;
if (!confirm('シミュレーション条件を ' + lastResult.transferPayload.targetSheet + ' に転記します。よろしいですか?\n\n転記後の有効フラグは FALSE(下書き)です。')) return;
btn.disabled = true;
setTimeout(function() { btn.disabled = false; }, 5000); // 5 秒無効化で連続クリック防止
google.script.run
.withSuccessHandler(function(result) {
if (result.error) {
document.getElementById('transfer-result').innerHTML = '<span style="color:#cc0000;">❌ ' + result.error + '</span>';
return;
}
// クライアントサイド成功通知(サーバー側 Utilities.sleep は使わない)
var html = '<span style="color:#274e13; background:#d9ead3; padding:4px 8px; border-radius:3px;">✅ 転記完了: ' + result.transferId + ' → ' + result.targetSheet + '</span>';
var resultDiv = document.getElementById('transfer-result');
resultDiv.innerHTML = html;
// 一時ハイライト (3 秒) をクライアント setTimeout で実装
setTimeout(function() {
resultDiv.style.transition = 'opacity 1s';
resultDiv.style.opacity = '0.5';
}, 3000);
})
.withFailureHandler(function(err) {
document.getElementById('transfer-result').innerHTML = '<span style="color:#cc0000;">❌ ' + err.message + '</span>';
})
.transferWhatIfToBudget(lastResult.transferPayload);
});
</script>
シート上のハイライト(転記行の黄色表示)は GAS 側では実装せず、ユーザーが 22_bud_headcount / 23_bud_subscription を開いた際に条件付き書式で「有効フラグ=FALSE 行を黄色表示」を別途設定する運用(setupAllSchemas の条件付き書式として追加する拡張余地あり、Phase 2 検討事項 #3 参照)。
シミュレーション結果表示後に #transfer-section を display: block に変更。lastResult.transferPayload は WhatIfSimulator.run の戻り値から取得。
Step 5: 5 カ年モード対応(Phase 2 検討事項、MVP では見送り)
FIVE_YEAR モードで確定した採用計画(例: FY1 に 1 名採用 + FY2 にもう 1 名採用)を複数行転記する場合、MVP では「FY1 の 1 名のみ転記」に限定し、残りは手動追加する運用にする。
将来拡張として:
transferPayload.multiRowData: [row1, row2, ...]で配列化- ユーザー確認時に「3 行転記します。OK?」と表示
影響範囲
| ファイル | 変更種別 | 内容 |
|---|---|---|
400_domain/415_whatif_budget_transfer.js | 新規 | namespace WhatifBudgetTransfer(約 150 行、Repository 経由化で軽量化) |
200_data/202_repository.js | 追加のみ | 末尾に HeadcountRepository / SubscriptionRepository 新設(約 60 行、Gemini レビュー指摘対応) |
400_domain/430_what_if_simulator.js | 変更 | _buildTransferPayload_ 追加 + run 戻り値拡張(約 40 行) |
templates/what_if_sidebar.html | 変更 | 転記ボタン + 確認ダイアログ + クライアント setTimeout ハイライト(約 60 行) |
docs/_config.json | 追加のみ | nav 登録 |
既存動作への影響
- MAS-011 MVP: 完全後方互換(
transferPayload未使用時は転記ボタン非表示) - MAS-044 テンプレートマスタ: 本案件の
transferPayload.templateIdで連携 98_audit_log:operation='CREATE'/targetSheet='22_bud_headcount'等で既存ログ体系に合流- MAS-048 採用 TCO: 将来 MAS-048 サイドバーにも転記ボタンを追加する拡張余地(MAS-048 完成後)
運用・デプロイ手順
npm run push:dev→ MAS-011 What-if サイドバー起動 → シミュレーション実行- 結果表示後に「📥 この条件で予算に反映」ボタンが表示されることを確認
- クリック → 確認ダイアログ → OK →
22_bud_headcount末尾行に有効フラグ=FALSEで追加 - 追加行が黄色ハイライト表示(3 秒)→ 元色に戻る
98_audit_logにoperation=CREATEログが記録される- 転記行の有効フラグを手動で TRUE に変更 → 予算に反映
npm run push:prod→ 同手順
注意事項
- ⚠️ failure_patterns #18-#20(命名造語禁止):
transferWhatIfToBudget/WhatifBudgetTransfer/ シート名22_bud_headcount/23_bud_subscription/ システムキーBUD_HC/BUD_SUBSを記述前に再確認 - ⚠️ failure_patterns #24(ラベル解決脆弱性): 転記時に
headers.indexOf('管理ID')で列位置を動的取得。MATCH 数式は使わない - ⚠️ CLAUDE.md コーディング規約: 列 B(ID 列)で最終行判定(既存
202_repository.jsL68 の private 関数findLastRow_を Repository 内部で使用。appendDtosToSheet_の第 4 引数lastRowCol=1で指定)。列 A のチェックボックス回避 - ⚠️ Human-in-the-Loop: 転記行の
有効フラグ=FALSE(下書き)で追加 → ユーザーが確認後に手動で TRUE に変更する 2 段承認フロー - ⚠️ MCP
add_rowsは使用禁止: シート先頭追加になるため、getRange().setValues()で末尾追加(CLAUDE.md 準拠) - ⚠️ 5 カ年モードの多行転記: MVP は FY1 の 1 名のみ。複数年は手動追加 or Phase 2 拡張
- ⚠️ ID 採番の衝突:
Utils.generateId(prefix, digit, isDate, sheet)で現行最大 + 1 を採番。LockServiceで排他制御必須 - ⚠️
SHEET_DEFAULTSとの整合: 転記時にデフォルト値適用を忘れると必須列欠損になる。_getDefaultValueで補完 - ⚠️ 監査ログのペイロード長:
afterValueにJSON.stringify(rowData)を入れるが、500 文字超の場合は truncate(98_audit_logの列幅考慮)
エッジケース
| # | 条件 | 検知方法 | 期待される挙動 | ログ出力 |
|---|---|---|---|---|
| 1 | transferPayload が null | 引数バリデーション | エラー返却「転記データが不正」 | Utils.persistLog('WARN', FUNC, 'null payload') |
| 2 | transferPayload.driver が未対応(例: HC_REMOVE) | driver バリデーション | エラー返却「未対応ドライバー」 | Utils.persistLog('WARN', FUNC, 'unsupported driver: ' + driver) |
| 3 | 対象シートが存在しない | Utils.getSheetByKey が null | エラー返却「シートが見つかりません」 | Utils.logError(FUNC, new Error('sheet not found')) |
| 4 | ID 採番衝突(LockService.tryLock 失敗) | 5 秒タイムアウト | 「別の処理が実行中」返却 | Utils.persistLog('WARN', ...) |
| 5 | ID 採番時の最大値計算失敗(シート空) | Utils.generateId が初回 ID EMP_0001 を返却 | 正常系として処理続行 | ログ出力なし |
| 6 | 必須列(例: 月額給与・報酬)が空欄 | rowData に未含有 | SHEET_DEFAULTS 経由で 0 等デフォルト値補完 | Utils.logInfo(FUNC, 'default applied for ' + key) |
| 7 | 開始年月 が過去日付 | バリデーション | 警告表示(「過去日付です」)+ 転記続行(ユーザー判断) | Utils.persistLog('WARN', ...) |
| 8 | 金額がマイナス値 | バリデーション | 警告表示 + 転記続行 | Utils.persistLog('WARN', ...) |
| 9 | MAS-044 未実装時に templateId 指定 | templateId 参照 | 無視して転記続行(監査ログには templateId=null 記録) | ログ出力なし |
| 10 | 監査ログ記録失敗 | Utils.auditLog が throw | 転記は成功扱いとするが警告表示「監査ログ記録に失敗(要確認)」 | Utils.logError(FUNC, e, 'auditLog failed') |
| 11 | 同一シナリオを連続 2 回転記 | UI ボタンダブルクリック | 2 行が末尾に追加される(重複検知しない MVP 仕様、人間検討事項 #3) | ログ出力なし |
| 12 | 転記後の視覚フィードバック | クライアント setTimeout で実装(サーバー側 Utilities.sleep は使わない) | 「✅ 転記完了」メッセージ表示 → 3 秒後に opacity 0.5 でフェードアウト。UI フリーズなし | — |
| 13 | 大量転記(5 カ年で 5 行) | MVP はサポートせず | UI で「1 行のみ転記可能」警告 | Utils.persistLog('WARN', ...) |
| 14 | Web アプリから実行 | SpreadsheetApp.getUi() 不要(サイドバー経由のみ) | 問題なし(サイドバー起動自体が SpreadsheetApp コンテキスト必要) | — |
| 15 | 転記した行を手動削除した後に再転記 | ID 採番は常に最大値 + 1 | 新規 ID が採番される(元 ID は欠番として残る) | ログ出力なし(正常系) |
冪等性・再実行の設計
転記は追記方式で冪等性は担保しない(同じ操作を 2 回やれば 2 行追加される)。ユーザーが誤って 2 回押さないよう、ボタンクリック後は 5 秒間無効化する UX を実装。
Human-in-the-Loop(詳細)
- 転記前: 確認ダイアログ(「このシナリオを 22_bud_headcount に転記します」)
- 転記後:
有効フラグ=FALSE(下書き)で追加 → ユーザーが確認後に手動でTRUEへ変更 - 黄色ハイライト(3 秒)で視覚的に転記位置を明示
98_audit_logにoperation=CREATEログ記録 → 後日の監査追跡可能- 取り消しメニュー: MVP では実装せず、手動で
有効フラグ=FALSEのまま放置 or 手動削除(Phase 2 で「取消」メニュー検討)
テスト要件
| テスト関数 | 合格基準 |
|---|---|
test_transferWhatIfToBudget_HC_ADD | HC_ADD → 22_bud_headcount 末尾行追加、ID が EMP_XXXX 形式、有効フラグ FALSE |
test_transferWhatIfToBudget_SAAS_ADD | SAAS_ADD → 23_bud_subscription 末尾行追加 |
test_transferWhatIfToBudget_nullPayload | null 入力 → { error } 返却 |
test_transferWhatIfToBudget_unsupportedDriver | HC_REMOVE → エラー返却 |
test_transferWhatIfToBudget_auditLog | 転記後 98_audit_log に CREATE 行記録 |
test_transferWhatIfToBudget_defaultValues | rowData 欠損列が SHEET_DEFAULTS で補完される |
test_transferWhatIfToBudget_idGeneration | 連続 3 回転記で ID が連番 EMP_0001 / 0002 / 0003 |
実データ検証
22_bud_headcount/23_bud_subscriptionの実ヘッダー行:BUD_HC/BUD_SUBSschemas との一致確認ID_PREFIX_MAPのEMP_/SUB_エントリ:002_constants.jsL93 起点で存在確認98_audit_logのLOG_AUDITschema: L962 / 10 列の確認- MAS-011 MVP サイドバー UI: 結果テーブル下部に要素を追加する位置の確定
Utils.generateIdの動作: 既存22_bud_headcountにEMP_0001〜EMP_0050が存在する場合、次 ID がEMP_0051になるLockServiceの排他制御: 同時実行で片方エラー返却の動作確認
関連ドキュメント
| 仕様書・ドキュメント | 関連箇所 |
|---|---|
| dev_mas-011_what_if_simulation.md | MAS-011 MVP 完成済。本案件は MAS-011 の結果を予算へ転記する拡張 |
| dev_mas-044_hc_template_master.md | テンプレート ID を転記行に記録する連携。MAS-044 完成後に transferPayload.templateId が有効化 |
| dev_mas-048_hiring_tco_bep_simulator.md | 採用 TCO 試算後の予算化動線。MAS-048 サイドバーから MAS-045 への連携は Phase 2 |
| dev_mas-010_financial_modeling.md | 5 カ年モード確定時の複数年転記設計。MVP は FY1 のみ、Phase 2 で多行転記拡張 |
| dev_mas-179_audit_trail.md | 監査証跡。operation='CREATE' で統一ログ記録 |
| CLAUDE.md | コーディング規約(ヘッダー名ベース列参照・列 B で最終行判定・MCP add_rows 使用禁止) |
人間が検討すべき事項
- 転記後の
有効フラグ: MVP はFALSE(下書き)。ユーザー判断でTRUE即反映も選択肢 → UI オプション追加検討 - 監査ログのフォーマット:
afterValueにJSON.stringify(rowData)を全量入れるか、主要列のみに絞るか。500 文字上限対応 - 連続クリック防止: 5 秒無効化 UX で十分か、サーバー側の冪等性チェック(同一シナリオ 1 時間以内は拒否等)まで実装するか
- 5 カ年モードの複数年転記: MVP は FY1 のみ。Phase 2 で
multiRowData: []実装時の UX(「3 行転記します」確認) - MAS-044 テンプレ ID 記録列:
22_bud_headcount/23_bud_subscriptionにtemplate_id列を追加するか、備考列に埋め込むか。DDL 変更の影響範囲検討 - 取消機能:
有効フラグ=FALSE運用で事実上の取消。専用「取消」メニュー新設 vs 現状維持の判断 - MAS-048 からの転記連携: MAS-048 採用 TCO 試算結果から直接 MAS-045 に渡す動線。UI 統合度の判断
- MAS-010 連携ベースラインへの自動反映タイミング: 転記後すぐ MAS-010 更新 or 次回 MAS-010 実行時に反映
- エラー時のロールバック: 転記途中でエラーが発生(例: 監査ログ失敗)した場合、末尾行を削除する自動ロールバック vs 手動修正運用
- 多言語対応: 英語 UI 対応時の「下書き」「転記」の訳語(一般 UI コピー規約との整合)
実装プロンプト(Claude Code 用)
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-045「What-if 結果を 22_bud_headcount / 23_bud_subscription へ転記する機能」を以下の 4 Step で実装してください。
## 実行前タスク(必須・5 件)
1. `400_domain/430_what_if_simulator.js`(MAS-011 MVP 708 行)を Read し `run` 関数の戻り値構造を確認
2. `200_data/202_repository.js` の `appendDtosToSheet_`(L85)と `InvoiceRepository.append`(L155 起点)を Read
3. `100_config/101_sys_config.js` の `BUD_HC`(L943)/ `BUD_SUBS`(L956)/ `LOG_AUDIT`(L962)schemas を確認
4. `000_infra/002_constants.js` の `ID_PREFIX_MAP`(L93 起点)/ `SHEET_DEFAULTS`(L73 起点)を確認
5. `templates/what_if_sidebar.html`(432 行)を Read し結果表示セクション直後への挿入位置を確定
## Step 1: `430_what_if_simulator.js` 拡張(推奨モデル: Sonnet)
- `_buildTransferPayload_(params)` 新設(HC_ADD / SAAS_ADD 別に rowData 構造化)
- `run` 戻り値に `transferPayload` フィールド追加(後方互換:ANNUAL / FIVE_YEAR 両対応)
- `有効フラグ=FALSE` デフォルト、`氏名・ポジション` に `[What-if転記]` プレフィックス
## Step 2: `202_repository.js` 末尾に `HeadcountRepository` / `SubscriptionRepository` 新設(推奨モデル: Haiku)
- 既存 `InvoiceRepository.append`(L155 起点)パターン完全準拠
- `_getSheet()` / `findAll()` / `append(dtos)` の 3 メソッド
- `append` は `appendDtosToSheet_(sheet, headers, dtos, 1)` を呼ぶ(列番号 1 = 列 B = ID 列)
- システムキー: `BUD_HC` / `BUD_SUBS`(既存定義)
## Step 3: `415_whatif_budget_transfer.js` 新設(推奨モデル: Sonnet)
- namespace `WhatifBudgetTransfer` を IIFE で定義
- `transferWhatIfToBudget(transferPayload)` 公開関数
- `LockService.tryLock(5000)` で排他制御、`try/finally` で解放
- ID 採番: `Utils.generateId` + `ID_PREFIX_MAP` 参照
- driver 分岐で `HeadcountRepository` or `SubscriptionRepository` を選択(Step 2 で新設)
- Repository 経由で末尾追記(層分離維持、`Utils.findLastRowByCol` 等の架空関数は使わない)
- `Utils.toastResult(FUNC, msg, 5)` で Toast 通知(`004_utils.js` L520 実在)
- `Utils.auditLog('CREATE', ...)` で監査記録
- グローバル関数 `transferWhatIfToBudget(payload)` を公開(サイドバー `google.script.run` 経由)
## Step 4: サイドバー UI 拡張(`what_if_sidebar.html`、推奨モデル: Sonnet)
- 結果表示テーブル直後に `<div id="transfer-section">` 追加
- ボタン `📥 この条件で予算に反映`(`.btn-run` スタイル、緑背景)
- `<div id="transfer-result">` クライアント側の結果表示領域
- クリック時の確認ダイアログ: `'このシナリオを ' + targetSheet + ' に転記します。よろしいですか?\n\n転記後の有効フラグは FALSE(下書き)です。'`
- `google.script.run.transferWhatIfToBudget(lastResult.transferPayload)` 呼出
- 成功時: `#transfer-result` に緑バック ✅ メッセージ → 3 秒後にクライアント `setTimeout` で opacity 0.5 にフェード(サーバー側 `Utilities.sleep` 不使用、UI フリーズ回避)
- エラー時: `#transfer-result` に赤テキスト ❌ メッセージ
- ボタン連続クリック防止: クリック後 5 秒間 `disabled = true`、5 秒後にクライアント `setTimeout` で解除
## Step 5: テスト追加(`900_test/901_test_runner.js`、推奨モデル: Sonnet)
- 7 テスト関数(HC_ADD / SAAS_ADD / null payload / 未対応 driver / auditLog / デフォルト補完 / ID 連番)
- 合格基準: 転記後に `22_bud_headcount` 末尾行追加、`98_audit_log` に CREATE 行、有効フラグ FALSE
## 制約
- `appsscript.json` の `oauthScopes` を変更しない
- 列番号ハードコード禁止(`headers.indexOf` で動的取得)
- MCP `add_rows` 使用禁止(シート先頭追加になる、CLAUDE.md 準拠)
- 列 B(ID 列)で最終行判定(列 A のチェックボックス回避)
- MAS-011 MVP の既存動作を破壊しない(`transferPayload` 未使用時は転記ボタン非表示)
- `有効フラグ=FALSE` デフォルトで転記(Human-in-the-Loop 準拠)
- 5 カ年モードは FY1 のみ転記(MVP 仕様、Phase 2 で多行拡張)
## 動作確認
1. What-if サイドバー起動 → HC_ADD シミュレーション実行
2. 結果表示後に「📥 この条件で予算に反映」ボタン表示確認
3. クリック → 確認ダイアログ → OK
4. `22_bud_headcount` 末尾行に `EMP_XXXX` で追加される
5. 黄色ハイライト 3 秒 → 元色復帰
6. `98_audit_log` に CREATE ログ記録確認
7. SAAS_ADD でも同様の動作確認
8. `23_bud_subscription` 末尾行に `SUB_XXXX` で追加される
9. 有効フラグ FALSE で追加されていることを確認
10. 5 カ年モードでは FY1 のみ転記される(Phase 2 で複数年対応予定のメッセージ表示)
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
Step 1: 430_what_if_simulator.js 拡張 | Claude Sonnet 4.6 | 後方互換維持の判断 + スキーマ転換ロジック |
Step 2: HeadcountRepository / SubscriptionRepository 新設 | Claude Haiku 4.5 | InvoiceRepository.append パターンの定型的な複製、判断要素なし |
Step 3: 415_whatif_budget_transfer.js 新設 | Claude Sonnet 4.6 | Repository 経由の書込 + ID 採番 + 監査ログ + driver 分岐 |
| Step 4: サイドバー UI 拡張 | Claude Sonnet 4.6 | HTML + JS + google.script.run + クライアント setTimeout ハイライト |
| Step 5: テスト追加 | Claude Sonnet 4.6 | 合格基準の統合検証(追記・監査ログ・ID 連番) |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-23 | 初版作成。MAS-011 MVP(PR #315 + #320 polish)の後継として、What-if シミュレーション結果を 22_bud_headcount / 23_bud_subscription に末尾行追加する仕様。namespace WhatifBudgetTransfer(400_domain/415_whatif_budget_transfer.js 新規)で transferWhatIfToBudget(payload) 公開関数を提供。430_what_if_simulator.js の run 戻り値に transferPayload を追加。Human-in-the-Loop 準拠: 確認ダイアログ + 有効フラグ=FALSE(下書き)デフォルト + 98_audit_log に operation='CREATE' 記録。ID 採番は Utils.generateId + ID_PREFIX_MAP、LockService.tryLock(5000) で排他制御。5 カ年モードは FY1 のみ転記(MVP 仕様)。MAS-044 完成後は transferPayload.templateId 連携でテンプレ派生追跡可能。エッジケース 15 件・人間検討事項 10 件 |
| 2026-04-23 | Gemini レビュー(PR #335)指摘を反映した改訂。🔴 Critical 対応: (1) 架空関数 Utils.findLastRowByCol を削除し、200_data/202_repository.js 末尾に HeadcountRepository / SubscriptionRepository を新設(InvoiceRepository.append パターン準拠・appendDtosToSheet_(sheet, headers, dtos, 1) 使用)で層分離原則を維持。(2) Domain 層から直接シート書き込みする方針を撤回し、Repository 経由に変更。🟡 Major 対応: サーバー側 Utilities.sleep(3000) ハイライトを削除し、クライアント setTimeout フェードに変更(UI フリーズ回避)。Step 構成を 4 → 5 Step に再編(Repository 新設を独立 Step 2 に)。推奨実行モデルも 5 工程に更新(Step 2 Haiku / 他 4 工程 Sonnet)。誤指摘(Utils.toastResult を架空と判定 → L520 に実在)は訂正せず現状維持 |