MAS-042: 投資ハードルレートマスタ & Go/No-Go 判定(MAS-013 補完)
概要
| 項目 | 内容 |
|---|---|
| 案件 ID | MAS-042 |
| カテゴリ | FP&A・レポーティング |
| 対象ファイル(変更あり) | 100_config/101_sys_config.js(schemas に MST_HURDLE 追加)200_data/202_repository.js(末尾に HurdleRateRepository 追記)400_domain/431_investment_analyzer.js(新規・全判定ロジック集約)000_infra/002_constants.js(MENU_DEFINITION の 📊 マート更新 カテゴリに 1 項目追加) |
| 出力先シート | 68_report_investment_gonogo(v1.2 で実装時に変更 — 旧: MAS-013 既存出力 67_report_investment_analysis に判定ラベル列を追記する案。新: 判定結果専用シート 68_report_investment_gonogo を新設し MAS-013 出力との視認性を分離)22_bud_headcount(採用投資スクリーニング結果は別処理。HC シート自体への判定列追加は未実施・screenHiringInvestments で別アラート出力) |
| 前提案件 | MAS-013(投資回収シミュレーション・仕様書)/ MAS-017(WACC 計算・未実装) |
MAS-013 が算出する NPV / IRR / Payback / ROI に対し、投資カテゴリー別の閾値と照合して Go / グレー / No-Go の 3 段階ラベルを自動付与する判断補助エンジン。MAS-013 は計算エンジン、MAS-042 は判断エンジンという役割分担で、両者が揃うことで「いくらなら Go / いくらなら No-Go」を即断できる経営判断基盤が完成する。
ハードルレートはマスタ管理(29_mst_investment_hurdle 新設)することで、税制改正(経営強化税制 B 類型 5%→7%、2025 年改正)や業界相場変動をコード改修なしにマスタ更新だけで吸収できる設計。採用投資(離職リスク係数反映)と業務委託→正社員化アラート(月 50 万円 × 継続 12 ヶ月超の検知)の 2 つの特殊スクリーニングも同一エンジンに集約する。
目的
- MAS-013 計算結果の解釈を自動化: NPV / IRR / Payback / ROI を閾値と照合してラベル化することで、投資稟議の属人化・判断基準のバラツキを解消する
- 閾値変動のマスタ管理: ハードコード禁止原則(CLAUDE.md コーディング規約)に従い、税制改正・金利変動・業界相場の変化をマスタ更新だけで追随可能にする
- 採用投資の定量評価: 採用費 + オンボーディング工数 + 離職リスク係数から実効 Payback を算出し、職種別閾値で Go/No-Go を判定する(採用稟議を勘・感覚から脱却させる)
- 業務委託→正社員化の損益分岐アラート: 月 50 万円 × 継続 12 ヶ月超の業務委託を検知し、正社員化時のコスト差(20-30% 減)を自動試算することで、コスト最適化の機会を能動通知する
現在のコード
600_report/610_service_investment_analysis.js(MAS-013 計算エンジン)
MAS-013 で実装済みの計算エンジン。25_bud_investment_case(DDL 管理)を入力、67_report_investment_analysis(DDL 管理外・動的上書き)を出力とし、以下の 4 指標を計算する(MAS-013 仕様書 §出力シート 67_report_investment_analysis より):
- NPV(正味現在価値)
- IRR(内部収益率, %)
- 回収期間(Payback Period, 年)
- ROI(投資収益率, %)
MAS-042 はこの出力シートに 判定ラベル列(Go / グレー / No-Go) を追記する形で連携する。計算ロジックには手を入れない(MAS-013 の非侵襲拡張)。
200_data/202_repository.js(Repository パターン)
読み取り専用マスタの標準形として AccountRepository(L304-350)と PartnerRepository(L356-406)が実装済み。本案件の HurdleRateRepository は AccountRepository のキャッシュパターン(_cache: null / findAsMap() 内で有効フラグスキップ + キャッシュ返却 / resetCache())に完全準拠する。
readSheetAsDtos_(sheet)(L19): ファイルスコープのプライベート関数。戻り値{ headers: string[], dtos: Object[] }(rowsではなくdtos)。HurdleRateRepositoryを同ファイル末尾に追記すれば直接呼び出し可能。Utils.getSheetByKey(key, fallbackName)(000_infra/004_utils.jsL302): システムキーで GID 解決、フォールバック名で GID 未登録時に対応。
100_config/101_sys_config.js(DDL スキーマ)
setupAllSchemas() 関数内(L802 起点)の const schemas = {...}(L879 起点)でシート毎の headers / color / validations を宣言的に定義。MAS-042 ではここに 'MST_HURDLE': { headers: [...], color: "#666666" } を追加する。DDL 適用後、initConfigs()(L1596)で 01_sys_config にシートキーを登録する手順が標準フロー。
000_infra/002_constants.js(MENU_DEFINITION・COLORS・getParam)
MENU_DEFINITION(L206 起点):📋 サイドバー: 📊 マート更新カテゴリ(L231-241)のitems配列末尾に{ label, funcName, description }を追加する形式。Constants.COLORS(L36-55):HEADER_CONFIG(#d9ead3緑・Go 用)/PROFIT_BG(#fff2cc黄・グレー用)/WARN_RED_BG(#f4cccc赤・No-Go 用)が実在。条件付き背景色の引数として使用する。Constants.getParam(key, defaultVal)(L147-167):03_sys_paramsからパラメータを読み込むヘルパー。本案件では採用スクリーニング・業務委託アラートの閾値を03_sys_paramsで管理するために使用する。
400_domain/402_rpa_subscription.js(業務委託検知の入力元)
23_bud_subscription シートに 契約形態 / 税抜金額_計画 / 開始・契約年月 列が存在(BUD_SUBS schema headers L943 で実在確認済)。業務委託→正社員化アラートはこのシートの 契約形態 = '業務委託' 行を対象として、金額閾値 × 継続月数で検知する。
修正方針
全判定ロジックを新規 400_domain/431_investment_analyzer.js に集約する 4 機能統合設計。既存 MAS-013 のロジックは無変更、出力シートへの追記のみで連携する。
Step 1: DDL 追加 + HurdleRateRepository 新設
① 101_sys_config.js の schemas 配列に追加:
'MST_HURDLE': {
headers: ["有効フラグ", "カテゴリー名", "WACC閾値(%)", "Payback閾値(年)", "ROI閾値(%)", "年商比上限(%)", "備考"],
color: "#666666"
},
カラム構成は AccountRepository / PartnerRepository と同じく 有効フラグ を先頭に配置。カテゴリー名は 採用 / 設備 / SaaS / ERP / IT小物 / 一般CAPEX の 6 区分を初期投入予定。
② 01_sys_config へのシステムキー登録: サイドバー 🔧 開発・設定 → シートGID取得・リンク(initConfigs / L1596)を実行して MST_HURDLE キーを登録する(DDL 適用後 1 回のみ)。
③ 202_repository.js 末尾に HurdleRateRepository を追記:
// =====================================================================
// HurdleRateRepository — 29_mst_investment_hurdle (読み取り専用マスタ)
// =====================================================================
var HurdleRateRepository = {
_getSheet: function() {
return Utils.getSheetByKey('MST_HURDLE', '29_mst_investment_hurdle');
},
findAll: function() {
return readSheetAsDtos_(HurdleRateRepository._getSheet());
},
findAsMap: function() {
if (HurdleRateRepository._cache) return HurdleRateRepository._cache;
var result = HurdleRateRepository.findAll();
var map = {};
for (var i = 0; i < result.dtos.length; i++) {
var dto = result.dtos[i];
var flag = dto['有効フラグ'];
if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
var name = String(dto['カテゴリー名'] || '').trim();
if (!name || map[name]) continue; // 重複は先行行採用
map[name] = {
wacc: Number(dto['WACC閾値(%)']) || null,
payback: Number(dto['Payback閾値(年)']) || null,
roi: Number(dto['ROI閾値(%)']) || null,
revenueRatioCap: Number(dto['年商比上限(%)']) || null,
};
}
HurdleRateRepository._cache = map;
return map;
},
_cache: null,
resetCache: function() { HurdleRateRepository._cache = null; },
};
AccountRepository.findAsMap()(L323-341)のパターンと完全一致。有効フラグ FALSE / false の行をスキップし、同一カテゴリー名の重複行は先着行を採用(後行は findAsMap 内で検知 → Utils.persistLog('WARN', ...) 記録)。
Step 2: 400_domain/431_investment_analyzer.js 新設
グローバルエントリポイントは runInvestmentGoNoGoAnalysis()(MENU_DEFINITION の funcName と完全一致)。内部で プライベートサブ関数(末尾 _ で示される GAS 慣例の private 関数)applyGoNoGoToReport_() / screenHiringInvestments_() / detectOutsourcingAlerts_() を順次呼び出す設計。runInvestmentGoNoGoAnalysis() が冒頭で HurdleRateRepository.findAsMap() を 1 回だけ 呼び出し、閾値マップを引数として各サブ関数に渡す(シート読み込みの多重実行防止)。これら 3 サブ関数は外部からは呼び出されず、runInvestmentGoNoGoAnalysis() のみが公開エントリ。
applyGoNoGoToReport_(hurdleMap):67_report_investment_analysisの行を走査し、投資カテゴリー列(MAS-013 出力の既存列)でhurdleMap[category]を引き当てる。NPV ≥ 0 かつ IRR ≥ WACC 閾値 かつ Payback ≤ 閾値 かつ ROI ≥ 閾値 → Go、±10% の境界値は グレー、いずれか未達は No-Go。判定列(ヘッダーGo/No-Go 判定)に書き込み、条件付き背景色を設定。screenHiringInvestments_(hurdleMap):22_bud_headcountの採用予定行(入社年月が未来日付)を対象に、採用費 + オンボーディング工数(年収 ×HC_ONBOARDING_COST_RATE、既定 0.25)× 離職リスク係数(HC_TURNOVER_RATE、既定 0.527=厚労省小規模法人 3 年以内離職率)から実効 Payback を算出。職種別閾値(コンサル/営業 6-12 ヶ月、エンジニア 12-18 ヶ月、新卒 24-36 ヶ月)はマスタのカテゴリー名='採用-職種'行で個別管理(将来拡張余地)、MVP では採用カテゴリー共通閾値を参照。detectOutsourcingAlerts_(hurdleMap):23_bud_subscriptionの契約形態 = '業務委託'行を走査し、税抜金額_計画 ≥ OUTSOURCING_ALERT_AMOUNT(既定 500,000)かつ開始・契約年月からOUTSOURCING_ALERT_MONTHS(既定 12)ヶ月以上経過した行を抽出。正社員化時の年間コスト差(20-30% 減)を試算し、Utils.persistLog('WARN', 'detectOutsourcingAlerts_', msg, JSON.stringify(detail))で99_error_logに記録 +Utils.toastResult('detectOutsourcingAlerts_', summary, 8)で件数サマリーを Toast 通知する 2 段構成。
Step 3: MENU_DEFINITION への 1 行追記
000_infra/002_constants.js の MENU_DEFINITION(L206)内、📋 サイドバー: 📊 マート更新 カテゴリ(L231 起点)の items 配列末尾(buildFiveYearForecast / openWhatIfSidebar の直後、L240 相当)に以下を追加:
{ label: '📈 投資案件 Go/No-Go 判定', funcName: 'runInvestmentGoNoGoAnalysis', description: '29_mst_investment_hurdle の閾値で F-13・採用・業務委託の Go/No-Go を自動判定' },
funcName の値 'runInvestmentGoNoGoAnalysis' は Step 2 で定義するグローバル関数名と 完全一致 させること(failure_patterns #20 命名造語禁止)。
Step 4: テスト追加(900_test/901_test_runner.js)
calcEffectivePayback_(annualCost, annualSavings, turnoverRate)の単体テスト: 手計算との誤差 0.01% 未満を合格基準calcRegularizationSavings_(monthlyAmount, monthsContinued)の単体テスト: 同上HurdleRateRepository.findAsMap()のモックテスト:有効フラグ=FALSE行除外 / 重複カテゴリー時の先行行採用 / 空マスタ時の空オブジェクト返却
影響範囲
変更ファイル一覧
| ファイル | 変更種別 | 内容 |
|---|---|---|
100_config/101_sys_config.js | 追加のみ | schemas 配列に MST_HURDLE エントリ追加(約 4 行) |
200_data/202_repository.js | 追加のみ | 末尾に HurdleRateRepository 定義追記(約 35 行) |
400_domain/431_investment_analyzer.js | 新規 | エントリポイント + 3 サブ関数 + 計算ヘルパー(約 250 行) |
000_infra/002_constants.js | 追加のみ | MENU_DEFINITION に 1 エントリ追加(1 行) |
900_test/901_test_runner.js | 追加のみ | テストケース 3 件追加 |
docs/_config.json | 追加のみ | nav に仕様書エントリ追加 |
既存動作への影響
- MAS-013 計算エンジン (
610_service_investment_analysis.js): 変更なし。MAS-042 は67_report_investment_analysisに列追記するのみで、MAS-013 の書き込み処理と競合しない(MAS-013 はclearContents()→ 再書き込み方式、MAS-042 は MAS-013 実行後に 判定列のみ 上書き)。 22_bud_headcount(HC 予算): 判定ラベル列を追加。既存の HC RPA 起票(401_rpa_hc.js)はindexOfベースの列参照のため影響なし。01_sys_config:MST_HURDLEキーが 1 行追加。既存キー参照への影響なし。
運用・デプロイ手順
- dev 環境で
npm run push:dev→ サイドバー🔧 開発・設定 → DDL 全更新 (Full)(setupAllSchemas)を実行して29_mst_investment_hurdleを作成 - サイドバー
🔧 開発・設定 → シートGID取得・リンク(initConfigs)を実行してMST_HURDLEを01_sys_configに登録 29_mst_investment_hurdleに 6 カテゴリーの初期データを手動投入(採用 / 設備 / SaaS / ERP / IT小物 / 一般CAPEX)- サイドバー
📊 マート更新 → 📈 投資案件 Go/No-Go 判定を実行して動作確認 - prod 反映は
npm run push:prod後に同じ手順(DDL 更新 → initConfigs → 初期データ投入 → 実行)
注意事項
- ⚠️ failure_patterns #18-#20(コード未読による固有名詞誤記・命名造語禁止): DDL スキーマ名・システムキー
MST_HURDLE・メニューラベル・関数名等の固有名詞を仕様書に記述する前に、必ず101_sys_config.js/202_repository.js/002_constants.jsを Read して実在する文字列のみ引用すること。「〜という定数があるはず」「〜というメニューがあったはず」と推測した瞬間に手を止めて Read する(MAS-003 で 1 回のコード未読から同日 3 件の固有名詞誤記が発生した直接対策)。 - ⚠️ failure_patterns #25(並列実装対称性漏れ): カテゴリー別(採用/設備/SaaS/ERP/IT小物/一般CAPEX)の判定ロジックを実装する際、全カテゴリーで「①閾値取得 → ②計算 → ③ラベル付与 → ④条件付き書式設定」の処理ステップが対称になっているか、実装後にカテゴリーを並べて確認すること。片方だけエラーハンドリングが省略されていないか特に注意する。
- ⚠️ failure_patterns #26(
oauthScopes部分宣言): 本案件は外部 API 連携なし。appsscript.jsonのoauthScopesフィールドは 変更不要(該当なし)。理由:SpreadsheetApp/Utils内で完結し、UrlFetchApp/AdminDirectory等の外部スコープは使用しない。 - ⚠️ failure_patterns #27(Admin SDK API 変動): 外部 Google API 連携なし(該当なし)。
- ⚠️
Utils.auditLog(operation, ...)の誤用禁止: 第 1 引数operationは'CREATE' | 'UPDATE' | 'DELETE' | 'CONFIRM' | 'CANCEL' | 'RUN' | 'MIGRATE'のいずれかであり、ログレベルのパラメータは存在しない。業務委託アラートのような WARN レベル通知はUtils.persistLog('WARN', funcName, message, JSON.stringify(detail))+Utils.toastResult(funcName, summary, 8)の 2 段構成を使うこと。 - ⚠️ 列番号ハードコード禁止:
22_bud_headcount/67_report_investment_analysisへの判定ラベル書き込みは、ヘッダー配列をindexOfで検索して列インデックスを動的取得してからgetRange(row, colIdx + 1).setValue(val)で実行する(CLAUDE.md コーディング規約)。 - ⚠️
SpreadsheetApp直接呼び出し禁止:29_mst_investment_hurdleへのアクセスは必ずHurdleRateRepository経由。SpreadsheetApp.getActiveSpreadsheet().getSheetByName('29_mst_investment_hurdle')のような直接呼び出しは禁止(シート名変更時の影響波及を避けるため)。 - ⚠️ Human-in-the-Loop: 自動付与される Go/グレー/No-Go ラベルは 判断補助 であり、最終的な投資決定は人間が行う。仕様書末尾・実装の UI メッセージ・GAS 関数コメントに明記すること。
Human-in-the-Loop ポリシー
本案件は CLAUDE.md プロダクトポリシーの「AI/自動処理の結果は必ず人間がレビュー・承認してから確定する」に直接該当する。以下を遵守:
- 判定ラベル(Go / グレー / No-Go)はシートに書き込まれるが、投資実行の是非は人間が決定 する位置付けを
67_report_investment_analysisシート上部のメタ行に明記する - 条件付き背景色(Go=緑 / グレー=黄 / No-Go=赤)で視覚的に差分化し、人間の注意喚起を促す
- 業務委託アラートは Toast 通知のみで、業務委託契約の自動解除・変更は行わない(人間が個別に判断)
エッジケース
| # | 条件 | 検知方法 | 期待される挙動 | ログ出力 |
|---|---|---|---|---|
| 1 | 29_mst_investment_hurdle シート自体が未作成(DDL 未実行) | HurdleRateRepository._getSheet() が null | 「マスタ 29_mst_investment_hurdle が未作成です。setupAllSchemas を実行してください」ダイアログ表示して処理中断 | Utils.logError('runInvestmentGoNoGoAnalysis', new Error('MST_HURDLE 未作成')) |
| 2 | マスタが空(ヘッダー行のみ・データ 0 件) | HurdleRateRepository.findAsMap() が空オブジェクト {} | 「ハードルレートマスタにデータがありません」Toast 表示して処理中断 | Utils.persistLog('WARN', 'findAsMap', 'empty master', '') |
| 3 | 判定対象の投資カテゴリーがマスタ未登録 | findAsMap()[category] が undefined | ラベルを 「判定不能(マスタ未登録)」 として処理続行(他行の判定は継続) | Utils.persistLog('WARN', 'applyGoNoGoToReport_', '未登録カテゴリー: ' + category, rowId) |
| 4 | マスタの Payback / ROI 閾値が空欄または非数値文字列 | isNaN(Number(val)) または空文字判定 | 当該閾値をスキップし他の有効な閾値のみで判定。空欄閾値は 「—」 表示 | Utils.persistLog('WARN', 'findAsMap', '閾値欠損: ' + field, category) |
| 5 | マスタ閾値にマイナス値が設定されている | threshold < 0 | 絶対値で評価(保守的フォールバック)し WARN 記録 | Utils.persistLog('WARN', 'findAsMap', 'マイナス閾値: ' + threshold, category) |
| 6 | 同一カテゴリーがマスタに重複登録 | findAsMap 構築時に map[name] 既存検知 | 最初に検出した有効行を採用し後行を無視 | Utils.persistLog('WARN', 'findAsMap', '重複カテゴリー: ' + name, '先着採用') |
| 7 | 67_report_investment_analysis の計算結果が空欄またはシートエラー(#N/A / #REF! 等) | String(val).startsWith('#') または空文字判定 | 当該行の判定をスキップ。ラベルを 「計算エラー(要確認)」 とする | Utils.persistLog('WARN', 'applyGoNoGoToReport_', '計算エラー: ' + cellValue, rowId) |
| 8 | 22_bud_headcount の「月額給与・報酬」が 0 またはマイナス | Utils.parseAmt(val) <= 0 | 実効 Payback を Infinity(回収不能)として扱い No-Go 判定 | Utils.persistLog('WARN', 'screenHiringInvestments_', '給与 0 またはマイナス', empId) |
| 9 | 23_bud_subscription の「税抜金額_計画」が 0 またはマイナス | Utils.parseAmt(val) <= 0 | 業務委託アラート対象外として該行スキップ(正常系扱い) | ログ出力なし |
| 10 | 業務委託「開始・契約年月」が不正フォーマット | Utils.parseDateToYm(val) === '' | 継続月数計算をスキップし該行を対象外として処理続行 | Utils.persistLog('WARN', 'detectOutsourcingAlerts_', '契約年月が不正: ' + val, subId) |
| 11 | 年商比上限判定で参照年商データが 0(ゼロ除算) | annualRevenue === 0 | 年商比チェックをスキップし他の閾値のみで判定 | Utils.persistLog('WARN', 'applyGoNoGoToReport_', '年商 0 のため年商比判定スキップ', rowId) |
| 12 | Payback 閾値は登録済みだが ROI 閾値のみ空欄(片方欠損) | 各閾値の有無を独立チェック | 有効な Payback 閾値のみで判定。ROI 判定は 「—(閾値未設定)」 表示 | Utils.persistLog('WARN', 'applyGoNoGoToReport_', 'ROI 閾値未設定: ' + category, rowId) |
| 13 | グレー判定境界値(実績値が閾値の ±10% 以内) | Math.abs(actual - threshold) / threshold <= 0.1 | ラベルを 「グレー(境界値)」 と付記。人間によるレビューを促す(背景色 Constants.COLORS.PROFIT_BG 黄) | ログ出力なし(正常系扱い) |
| 14 | LockService.getScriptLock() の取得失敗(既に別実行中) | lock.tryLock(5000) が false | 「別の処理が実行中です。しばらく待ってから再実行してください」Toast | Utils.persistLog('WARN', 'runInvestmentGoNoGoAnalysis', 'lock acquisition failed', '') |
| 15 | Utils.getSheetByKey('MST_HURDLE', ...) の fallback 名 29_mst_investment_hurdle 自体が削除されている | getSheetByKey が null | エッジケース #1 と同一処理(ダイアログ → 中断) | エッジケース #1 と同一 |
冪等性・再実行の設計
判定処理はシートへの 上書きのみ で副作用なし(新規レコード生成・ID 発番なし)。メニューから再実行した場合、古い判定ラベル・計算値・条件付き書式が新しい結果で完全に上書きされる設計とする(「既存結果が残り混在する」バグを防ぐ)。具体的には:
applyGoNoGoToReport_は対象行の判定列をclearContent()してからsetValue()で書き込む- 条件付き背景色は
setBackground()を先にnull(既定色)にリセットしてから新しい色を設定 - 業務委託アラートの Toast 通知は毎回新規表示(過去の通知を復元しない)
Human-in-the-Loop(再掲・詳細)
CLAUDE.md プロダクトポリシー準拠の具体実装:
67_report_investment_analysisと22_bud_headcountの判定ラベル列に、GAS で条件付き背景色を設定:- Go:
Constants.COLORS.HEADER_CONFIG(#d9ead3緑) - グレー:
Constants.COLORS.PROFIT_BG(#fff2cc黄) - No-Go:
Constants.COLORS.WARN_RED_BG(#f4cccc赤)
- Go:
- すべて
002_constants.js実在定数。独自色定義禁止 - 業務委託アラートは「能動通知」にとどめ、契約の自動解除・金額変更は一切行わない
テスト要件(901_test_runner.js への追加必須)
| テスト関数 | 合格基準 | 備考 |
|---|---|---|
test_calcEffectivePayback_ | 手計算との誤差 0.01% 未満(5 パターン) | 採用費 100 万 / 月給 50 万 / 離職率 0.5 → Payback 3.33 ヶ月の検証等 |
test_calcRegularizationSavings_ | 手計算との誤差 0.01% 未満(3 パターン) | 月 50 万 × 12 ヶ月 → 正社員化で 120 万円/年削減の検証等 |
test_HurdleRateRepository_findAsMap_emptyMaster | 空マスタで空オブジェクト返却 | DDL 直後の状態を再現 |
test_HurdleRateRepository_findAsMap_disabledFlag | 有効フラグ=FALSE 行が除外される | 2 行中 1 行無効化 → 1 行のみマップに含まれる |
test_HurdleRateRepository_findAsMap_duplicateCategory | 重複カテゴリーで先行行が採用される | 同一カテゴリー 2 行 → 1 行目の値のみマップ登録 |
実データ検証
実装前に MCP で以下を確認:
29_mst_investment_hurdleシートの有無: DDL 実行前は存在しないことを確認(mcp__google-sheets__list_sheetsで名前検索)22_bud_headcountの実際のヘッダー行: 「月額給与・報酬」「採用エージェント費」「PC等初期費用」「雇用形態」の正確な列名をmcp__google-sheets__get_sheet_dataで確認(BUD_HCschema L930 と照合)67_report_investment_analysisの実際のヘッダー行: MAS-013 実装済みの場合は実シートの判定対象列名(投資カテゴリー・Payback・ROI 等)を確認。未実装時はdev_mas-013_investment_simulation.mdの §出力シート仕様から推定23_bud_subscriptionの列名:契約形態/税抜金額_計画/開始・契約年月がBUD_SUBSschema L943 の宣言と一致することを確認(402_rpa_subscription.jsの読み取りロジックとも突合)01_sys_configの既存キー一覧:MST_HURDLEが未登録であることを確認(衝突回避)03_sys_paramsの既存パラメータ:HC_ONBOARDING_COST_RATE/HC_TURNOVER_RATE/OUTSOURCING_ALERT_AMOUNT/OUTSOURCING_ALERT_MONTHS/WACC_RATEが未登録であることを確認Constants.TAX_RATESの累進ブラケット定義: MAS-013 で参照している 800 万円境界 / 実効税率の値を仕様書「現在のコード」と照合(MAS-042 側の計算には使わないが、MAS-013 との整合性確認)
関連ドキュメント
| 仕様書・ドキュメント | 関連箇所 |
|---|---|
| dev_mas-013_investment_simulation.md | 計算エンジン本体。本案件は MAS-013 出力 67_report_investment_analysis に判定ラベル列を追加する形で連携 |
| dev_mas-010_financial_modeling.md | MAS-042 判定の「年商比上限」算出の参照元として、MAS-010 出力 94_fs_longterm_forecast からの売上予測を使用する余地あり(Phase 2 検討) |
| dev_mas-018_financial_statement_linkage.md | CF 連動の設計思想参考 |
| dev_mas-017_funding_simulation.md | WACC 計算エンジン。MAS-017 完成後は本案件のマスタ WACC閾値(%) 列を MAS-017 算出値に切り替える |
| dev_mas-043_hybrid_track_profitability.md | 採用投資 Payback の単価前提(RICE/VALUE 単価)に相互影響 |
| CLAUDE.md | コーディング規約(ヘッダー名ベース列参照・有効フラグスキップ・Human-in-the-Loop ポリシー) |
| failure_patterns.md | #18-#20 命名造語禁止 / #25 並列実装対称性 / #26 oauthScopes / #27 Admin SDK |
人間が検討すべき事項
- 初期閾値の採用方針: 2026 年水準(設備 Payback 2 年 / SaaS 1.5 年 / ERP 3 年 / ROI 20% / 中小経営強化税制 B 類型 7%)で行くか、業界中央値(例: 中小企業白書のベンチマーク)を採用するか。どの数字を「基準」として主張するかは経営方針による
- 離職リスク係数の業種別テーブル化: 現状は全職種共通
HC_TURNOVER_RATE(0.527) を使う予定だが、厚労省の業種別離職率データ(IT 業 25%、サービス業 35% 等)をマスタ化する価値があるか検討。判断基準: 採用の職種分布が多様化した時点(概ね 5 職種超)で導入 - 採用投資の単価前提(MAS-043 連動): RICE/VALUE のどちらの単価で採用の Payback を計算するか。注意: 誤って VALUE 単価(高単価)で Payback を計算すると、あらゆる採用が「回収早い」と誤判定されるリスクあり。MAS-043 の案件種別との整合運用が必要
- MAS-017 完成までの暫定 WACC 値の持ち方: 案件ごと手入力(シートの「WACC閾値(%)」列に直接数値記入)とマスタで全社単一値を保持(
Constants.getParam('WACC_RATE', 0.07))のどちらをデフォルトにするか。初期案: マスタで単一値(7%)を保持し、特殊案件のみシート側で上書き - 業務委託アラートの通知手段: 画面 Toast だけで十分か、メール通知・Slack 通知を追加するか。依存関係: MAS-186(Slack 経費精算)実装時に通知基盤を共通化する余地あり
- Go / グレー / No-Go の境界共通化: 現状案は「ROI 20%↑ で Go、10〜20% でグレー、10%↓ で No-Go」を全カテゴリー共通としているが、採用案件のみグレー範囲を広く(±20%)取る等の個別調整が必要か
- MAS-010 (5 カ年予測) との連動: 年商比上限判定の「参照年商」を、過去 12 ヶ月実績にするか MAS-010 の将来予測値にするか。影響: 成長期の過小評価(過去年商ベース)vs 楽観バイアス(将来予測ベース)のトレードオフ
29_mst_investment_hurdleのバージョン管理: 閾値改定の履歴を残すか。案:04_mst_hurdle_history独立シートで日付付き履歴を保持、または MAS-002(予算バージョン管理)パターンをマスタにも適用- 職種別閾値の管理粒度: 現状案は
カテゴリー名='採用'で共通閾値だが、「採用-エンジニア」「採用-営業」「採用-新卒」等の細分化マスタ行に拡張するか。判断基準: 採用実績が年 5 名超になった時点 - 仕様書内「連動性」記載の精度: MAS-013 / MAS-017 / MAS-043 との依存関係を仕様書に明記するが、MAS-017 / MAS-043 未実装時の MVP での動作保証範囲を明文化する必要があるか
実装プロンプト(Claude Code 用)
あなたは GAS 会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-042「投資ハードルレートマスタ & Go/No-Go 判定(MAS-013 補完)」を以下の 4 Step に分けて実装してください。
## 実行前タスク(各 Step で Read してから着手すること)
1. `200_data/202_repository.js` の `AccountRepository`(L304-350)/ `PartnerRepository`(L356-406)を Read し、`HurdleRateRepository` の実装パターンを確認する
2. `100_config/101_sys_config.js` の `schemas` 配列(L879 起点)を Read し、DDL 追加フォーマット(既存エントリの `headers` / `color` / `validations` 構造)を確認する
3. `000_infra/002_constants.js` の `MENU_DEFINITION`(L206)内の `📋 サイドバー: 📊 マート更新` カテゴリ(L231-241)を Read し、`items` 末尾への追記フォーマットと追加位置を確認する
4. MCP で `29_mst_investment_hurdle` シートの有無を確認する(DDL 実行前は存在しないはず)
5. `docs/dev/dev_mas-013_investment_simulation.md` を Read し、`67_report_investment_analysis` の実際のヘッダー列名(NPV / IRR / Payback / ROI の正式表記)を確認する
## Step 1: DDL・Repository・初期データ投入(推奨モデル: Opus)
修正ファイル: `100_config/101_sys_config.js`(`schemas` 配列へ追記)/ `200_data/202_repository.js`(末尾に `HurdleRateRepository` 追記)
- `101_sys_config.js` の `schemas` 配列に `29_mst_investment_hurdle` の DDL 定義を追加:
```javascript
'MST_HURDLE': {
headers: ["有効フラグ", "カテゴリー名", "WACC閾値(%)", "Payback閾値(年)", "ROI閾値(%)", "年商比上限(%)", "備考"],
color: "#666666"
},
```
- `202_repository.js` 末尾に `HurdleRateRepository` を追加(`AccountRepository` パターン完全準拠):
- `_getSheet()`: `Utils.getSheetByKey('MST_HURDLE', '29_mst_investment_hurdle')` を呼ぶ
- `findAll()`: `readSheetAsDtos_(HurdleRateRepository._getSheet())` を呼ぶ
- `findAsMap()`: キャッシュ付き。カテゴリー名キー → `{ wacc, payback, roi, revenueRatioCap }` マップ。`有効フラグ=FALSE` 行スキップ必須。重複カテゴリーは先着行採用(後行は `Utils.persistLog('WARN', 'findAsMap', ...)` で記録)
- `_cache: null` / `resetCache()` を実装
- `npm run push:dev` → サイドバー `🔧 開発・設定 → DDL 全更新 (Full)`(`setupAllSchemas`)実行
- サイドバー `🔧 開発・設定 → シート GID 取得・リンク`(`initConfigs`)実行(`MST_HURDLE` を `01_sys_config` に登録)
- `29_mst_investment_hurdle` にカテゴリー別初期データを MCP(`mcp__google-sheets__add_rows`)で投入(採用 / 設備 / SaaS / ERP / IT小物 / 一般CAPEX の 6 行)。初期値は 2026 年水準(設備 Payback 2 年、SaaS 1.5 年、ERP 3 年、ROI 20%、中小経営強化税制 B 類型 7% 等)
## Step 2: `431_investment_analyzer.js` の新設(推奨モデル: Sonnet)
修正ファイル: `400_domain/431_investment_analyzer.js`(新規作成のみ)
- グローバル関数 `runInvestmentGoNoGoAnalysis()` を定義(`MENU_DEFINITION` の `funcName` と完全一致させること)
- `LockService.getScriptLock().tryLock(5000)` で排他制御、`try/finally` で確実に解放
- 内部サブ関数: `applyGoNoGoToReport_(hurdleMap)` / `screenHiringInvestments_(hurdleMap)` / `detectOutsourcingAlerts_(hurdleMap)`
- 各サブ関数の先頭で **呼び出し元から渡された閾値マップ** を使う(多重読み込み防止。`runInvestmentGoNoGoAnalysis()` が `HurdleRateRepository.findAsMap()` を 1 回だけ呼び出して引数で渡す)
- 変動パラメータは `Constants.getParam('HC_ONBOARDING_COST_RATE', 0.25)` / `Constants.getParam('HC_TURNOVER_RATE', 0.527)` / `Constants.getParam('OUTSOURCING_ALERT_AMOUNT', 500000)` / `Constants.getParam('OUTSOURCING_ALERT_MONTHS', 12)` / `Constants.getParam('WACC_RATE', 0.07)` で取得(`002_constants.js` 実在関数)
- 業務委託アラート通知: `Utils.persistLog('WARN', 'detectOutsourcingAlerts_', msg, JSON.stringify(detail))` + `Utils.toastResult('detectOutsourcingAlerts_', summary, 8)` の 2 段構成
- 判定結果書き込み: ヘッダー `indexOf` → 動的列インデックス取得 → `getRange(row, colIdx + 1).setValue(val)`(列番号ハードコード禁止)
- 条件付き書式: Go=`Constants.COLORS.HEADER_CONFIG`(緑)/ グレー=`Constants.COLORS.PROFIT_BG`(黄)/ No-Go=`Constants.COLORS.WARN_RED_BG`(赤)。`setBackground(null)` リセット → 新色設定の順
- エッジケース 15 件すべてを try/catch で防御的に実装。エラー発生時も処理を中断せず次の行へ続行する(冪等性を担保)
## Step 3: MENU_DEFINITION 追記(推奨モデル: Haiku)
修正ファイル: `000_infra/002_constants.js`(`MENU_DEFINITION` の 1 エントリ追記のみ)
- `📋 サイドバー: 📊 マート更新` カテゴリ(L231-241)の `items` 末尾(`buildFiveYearForecast` / `openWhatIfSidebar` の直後)に追記:
```javascript
{ label: '📈 投資案件 Go/No-Go 判定', funcName: 'runInvestmentGoNoGoAnalysis', description: '29_mst_investment_hurdle の閾値で F-13・採用・業務委託の Go/No-Go を自動判定' },
```
- `funcName` の文字列が Step 2 で定義したグローバル関数名と完全一致していることを確認してからコミット(failure_patterns #20 命名造語禁止)
## Step 4: テスト追加・動作確認(推奨モデル: Sonnet)
修正ファイル: `900_test/901_test_runner.js`(テストケース追記)
- `calcEffectivePayback_` の単体テスト(手計算との誤差 0.01% 未満を合格基準、5 パターン)
- `calcRegularizationSavings_` の単体テスト(同上、3 パターン)
- `HurdleRateRepository.findAsMap()` のモックテスト(空マスタ / 有効フラグ FALSE / 重複カテゴリーの 3 パターン)
- `npm run push:dev` → GAS エディタで `runInvestmentGoNoGoAnalysis()` を手動実行し動作確認
- `99_error_log` に不要な ERROR / WARN が記録されていないことを確認
- サイドバー `📊 マート更新 → 📈 投資案件 Go/No-Go 判定` から実行し、UI 表示・Toast 通知・条件付き背景色の全てが正常に動作することを確認
## 制約
- `appsscript.json` の `oauthScopes` を変更しない(failure_patterns #26)
- 列番号ハードコード禁止(CLAUDE.md コーディング規約)
- 架空の関数名・シート名・定数名を使わない(failure_patterns #18-#20)。記述前に必ず Read で実在を確認する
- `Utils.auditLog()` の `operation` 引数に `'WARN'` 等のログレベル文字列を渡さない(正しい `operation` 値: `CREATE` / `UPDATE` / `DELETE` / `CONFIRM` / `CANCEL` / `RUN` / `MIGRATE`)
- `29_mst_investment_hurdle` へのアクセスは必ず `HurdleRateRepository` 経由(`SpreadsheetApp` 直接呼び出し禁止)
- MAS-013 の既存計算エンジン(`610_service_investment_analysis.js`)は変更しない。`67_report_investment_analysis` への判定ラベル列追記のみで連携
## 実データ検証(実装前に MCP で確認)
- `29_mst_investment_hurdle` シートの有無(DDL 実行前は存在しない)
- `22_bud_headcount` の実際のヘッダー行(「月額給与・報酬」「採用エージェント費」「PC等初期費用」「雇用形態」の正確な列名)
- `67_report_investment_analysis` の実際のヘッダー行(MAS-013 未実装時は「要確認」、実装済みの場合は MAS-013 出力列と判定列の位置関係を確定)
- `23_bud_subscription` の「税抜金額_計画」「開始・契約年月」「契約形態」の実際の列名(`402_rpa_subscription.js` で確認済みの列名と一致させる)
- `01_sys_config` の既存キー一覧(`MST_HURDLE` 未登録を確認)
- `03_sys_params` の既存パラメータ(`HC_ONBOARDING_COST_RATE` / `HC_TURNOVER_RATE` / `OUTSOURCING_ALERT_AMOUNT` / `OUTSOURCING_ALERT_MONTHS` / `WACC_RATE` 未登録を確認)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| Phase 1 調査 | あり | `67_report_investment_analysis` 実ヘッダー・既存判定列有無の裏取り |
| Phase 2 実装 | なし | 仕様書で決定済みの内容を書き下すのみ |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| Step 1: DDL 設計 + HurdleRateRepository 実装 | Claude Opus 4.7 (1M context) | schemas フォーマット解読 + Repository 設計判断 + 初期データ設計が複合的に必要 |
Step 2: 431_investment_analyzer.js 新設 | Claude Sonnet 4.6 | 複数ファイル横断だが仕様書でロジック完全定義済み |
| Step 3: MENU_DEFINITION 追記 | Claude Haiku 4.5 | 既存パターンの 1 エントリ追記のみ、判断要素なし |
| Step 4: テスト追加 | Claude Sonnet 4.6 | テストケース設計の判断が必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-23 | 初版作成。Gemini Pro メタプロンプト + Claude Sonnet 添削(scripts/1_generate_prompts_gemini.js パイプライン)で生成された tasks/prompts/task_F-42.md をベースに執筆。MAS-013(投資回収シミュレーション)補完として、投資カテゴリー別閾値の Go/No-Go 判定・採用投資スクリーニング・業務委託→正社員化アラートの 4 機能を 400_domain/431_investment_analyzer.js に集約。新規マスタ 29_mst_investment_hurdle(HurdleRateRepository + DDL)で閾値を一元管理し、税制改正・業界相場変動をコード改修なしに吸収可能な設計。エッジケース 15 件・人間検討事項 10 件・推奨実行モデル 4 工程を定義 |
| 2026-04-23 | MAS-011 との番号衝突回避のため 430_investment_analyzer.js → 431_investment_analyzer.js にリネーム。MAS-011 MVP(PR #315)で 400_domain/430_what_if_simulator.js が実装されており同番号衝突のため。仕様書内 8 箇所 + tasks/prompts/task_F-42.md 6 箇所を一括置換 |
| 2026-04-28 | v1.2 (実装後の整合化・PR #336 反映) |
仕様書作成プロンプト
展開して表示(Gemini Pro + Claude Sonnet レビュー済み・`tasks/prompts/task_F-42.md`)
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全確定させる。Phase 2(清書)の各Step内では拡張思考を最小限に抑え、Phase 1で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等のtextのみでtool_useなしにturnを終了しない。説明は1文以内。直ちにtoolを呼ぶ。
3. **4-5分割のWrite/Edit実行**: Step 2-1(骨格 ~20行) / 2-2(概要〜注意事項 ~300行) / 2-3a(エッジケース〜人間検討事項 ~200行) / 2-3b(実装プロンプト〜変更履歴 ~250行) / 2-4(<details>プロンプト全文記録) の5Stepに分けて実行する。1回のWrite/Editは300行以内を目安。
4. **各Stepで何を書くかを具体指示**: Phase 2実行時に設計判断を持ち込まないよう、各Stepの内容をPhase 1で完全確定させる。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 F-42「投資ハードルレートマスタ & Go/No-Go 判定(F-13 補完)」の開発仕様書を作成してください。
開発仕様書を新規作成した場合は、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
(Phase 1 調査項目 1-6、Phase 2 Step 2-1〜2-4、Phase 3 登録・記録の全内容が記載される。
全文は `tasks/prompts/task_F-42.md` を参照)
</instruction>
※ Gemini Pro (gemini-2.5-pro) が docs/_internal/failure_patterns.md / docs/_internal/dev_spec_prompt_template.md / コアコード 4 ファイル(002_constants.js / 003_contracts.js / 004_utils.js / 202_repository.js)を読み込んで設計した後、Claude Sonnet 4.6 が実行者目線で添削した成果物。プロンプトパイプライン: scripts/1_generate_prompts_gemini.js → tasks/prompts/task_F-42.gemini.md(Gemini 原版)→ tasks/prompts/task_F-42.md(Sonnet 添削後・最終版)