概要

項目内容
案件 IDMAS-056
カテゴリシミュレーション・UX
優先度P2 ★★★(商用化後の最大差別化機能、MAS-005 吸収・再定義)
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル(変更あり・全 4 フェーズ合計)Phase 1 MVP: 400_domain/440_conversational_ui.js(新規)/ templates/conversational_ui.html(新規・React + Vite ビルド成果物)/ 000_infra/002_constants.jsMENU_DEFINITION に「🧭 意思決定対話(Web App)」追加)/ 100_config/101_sys_config.js03_sys_paramsF56_* キー)
Phase 2: 200_data/202_repository.jsScenarioRegistryRepository / ScenarioDriversRepository 追加 / 100_config/101_sys_config.jsMST_SCN_REG / TRN_SCN_DRV schema 追加 / templates/conversational_ui.html 拡張(カード並列 UI)
Phase 3: 400_domain/441_compensation_optimizer.js(新規・個人 B/S 統合)/ 400_domain/440_conversational_ui.js 拡張(Claude 3.7 Sonnet ルーティング / MAS-045 連動)
Phase 4: 既存ファイルの CacheService 組み込み・モバイル CSS・エラーリカバリー UX
出力先シート40_scenario_registry(親・新規・DDL 管理、空き確認済)
44_scenario_drivers(子・新規・DDL 管理、Deep Research 推奨の 41 は 41_trn_budget と衝突のため 44 に変更
会話履歴本体は Google Drive 上の JSON ファイルに保存、file_id を親テーブルに記録
前提案件MAS-232(Vite+React SPA 基盤・サイドバー統合)/ MAS-245GAS Web App 独立化)/ MAS-216(Vertex AI 移行・Claude 実装 + Gemini 移行)
吸収/再定義対象MAS-005(固定 3 シナリオ切替 UI → MAS-056 の動的マルチシナリオ管理に吸収)
後続連携MAS-011(What-if)/ MAS-013(投資分析)/ MAS-048(採用 TCO)/ MAS-045(予算転記)/ MAS-049(賃上げ税制)/ MAS-054(Monte Carlo 拡張点)

D.11 の 9 ステップ財務意思決定プロセスを「対話しながら複数シナリオを探索・確定する Web アプリ」で実現する主力機能。独立 Web アプリ(GAS doGet(e) ベース、MAS-245 の初のキラーユースケース)として提供、Gemini/Claude と会話しながら MAS-011/MAS-013/MAS-048 計算エンジンを Tool Use で横断呼び出し、確定したシナリオを 40_scenario_registry + 44_scenario_drivers に永続化して MAS-045 で予算転記する。

D.11 Step 9「個人 B/S + 法人 B/S 統合」 論点を単独案件化せず本機能に吸収し、calculateOptimalCompensation() で役員報酬の総当たり探索(2026 年福井県料率 + 軽減税率 + インボイス 70%控除)による意思決定を可能にする。

MAS-332(Gemini Deep Research 更新版 2026-04-24)で 8 設計論点すべての推奨が確定済。本仕様書はその結果 + Claude 検証 9 点を全反映した本格設計。

目的

  • 「経営方針と数字を紐付ける意思決定」を AI 対話で支援(D.11 設計 3 原則): 一人社長が自然言語で財務目標を入力 → AI がヒアリング → 計算エンジンを Tool Use で呼び出し → 結果を踏まえた対話で方針を確定
  • MAS-005 の固定 3 シナリオ UI を置き換え: 任意数のシナリオを会話から足し引きできる動的マルチシナリオ管理に進化
  • MAS-011 / MAS-013 / MAS-048 の「意思決定ハブ」化: 各計算エンジンをサイドバー入力フォームから解放し、対話経由で横断呼び出し
  • 個人 B/S + 法人 B/S 統合(D.11 Step 9 吸収): 役員報酬の最適化で手取り最大化 + 法人節税のスイートスポットを可視化
  • SaaS 化時の最大差別化機能: MAS-332 で確認「一人法人 × 個人 B/S 統合」は Runway / Causal 等グローバル FP&A SaaS の空白領域

現在のコード

既存計算エンジン(Tool Use 対象)

案件関数状態Tool 化時の引数
MAS-011 What-if MVPWhatIfSimulator.run(drivers, opts)実装済 PR #315/#320`driver: 'HC_ADD'
MAS-013 投資分析runInvestmentAnalysis()実装済 PR #32930_bud_investment_case 上の案件行を対象、NPV/IRR/Payback を返却
MAS-048 採用 TCOHiringTcoSimulator.runHiringTcoSimulation(input)仕様書完了 PR #334/#341input: {annualSalary, employmentType, ageGroup, recruitFeeRate, grossMarginRate}
MAS-045 What-if 予算転記WhatIfBudgetTransfer.*仕様書完了 PR #334MAS-011 結果を 22_bud_headcount / 23_bud_subscription に末尾追記(HitL 承認ダイアログ付き)

400_domain/ の採番状況(2026-04-24 時点)

実装済: 400/401/402/403/404/405/406/407/410/414/420/430(MAS-049 が 414 で PR #342 マージ、MAS-011 が 430 で確定)。MAS-056 は 440 台(440_conversational_ui.js / 441_compensation_optimizer.js)を新規カテゴリとして確保。440 台は全て空き。

doGet(e) Web App の現状

現時点で doGet ベースの Web App 公開はなし。既存は HtmlService.createHtmlOutputFromFile 経由のサイドバー表示のみ。MAS-056 Phase 1 で doGet ハンドラを新設し、スプレッドシート外 URL(https://script.google.com/macros/s/XXX/exec)から React SPA を配信する。MAS-245 仕様書で構想済の「段階 3: スプレッドシートを開かずに操作できる独立 Web アプリ」の具体化。

N-40 Vertex AI 基盤(仕様書完了 PR #313)

000_infra/004_utils.jscallClaudeApi_() / callGeminiForReceiptOnVertex_() の実装パターンが確立済(領収書 OCR で稼働中)。MAS-056 の AI 呼び出しは同パターンを流用し、Gemini 2.5 Flash(対話・ルーティング)と Claude 3.7 Sonnet(複雑な財務 JSON 抽出・考察)をハイブリッドで呼び分ける。

シート番号の空き状況(grep 確認済、2026-04-24)

  • 40_*: 空き(MAS-056 で 40_scenario_registry として確保)
  • 41_*: 41_trn_budget で使用中(予算仕訳P/L/B/S/CF マートのコア参照)→ MAS-056 では使用不可
  • 42_*: 42_trn_journal で使用中 → 使用不可
  • 43_*: 43_trn_timesheet で使用中 → 使用不可
  • 44_*: 空き(MAS-056 で 44_scenario_drivers として確保)
  • 45_* 以降: 全て空き(参考)

03_sys_params で管理する新規パラメータ

Constants.getParam(key, defaultVal) で一元取得。本案件で追加する F56_* キー:

キーデフォルト根拠・用途
F56_AI_PRIMARY_MODELgemini-2.5-flash対話・ルーティング用メインモデル
F56_AI_SECONDARY_MODELclaude-3-7-sonnet-20250224複雑抽出・考察用セカンダリモデル
F56_AI_ROUTING_MODEAUTOAUTO(Router 関数で自動切替)/ ALWAYS_PRIMARY / ALWAYS_SECONDARY
F56_POLLING_INTERVAL_MS3000クライアント → GAS のポーリング周期(ミリ秒)
F56_MAX_CONCURRENT_SCENARIOS51 セッションで並行探索可能なシナリオ数上限
F56_CHAT_HISTORY_DRIVE_FOLDER_ID(未設定)会話履歴 JSON 保存先 Drive フォルダ ID(セットアップ時に手動投入)
F56_ENCRYPTION_KEY(未設定)PropertiesService.getUserProperties() 側に保存。シートには書かない
F56_FUKUI_HEALTH_INSURANCE_RATE0.0971健保料率(福井県・2026 年 3 月分〜)
F56_FUKUI_NURSING_CARE_RATE0.0162介護保険料率(福井県・40 歳以上)
F56_FUKUI_CHILDCARE_RATE0.0023子ども子育て支援金率
F56_PENSION_RATE0.0915厚生年金料率(全国一律)
F56_CORPORATE_TAX_REDUCED_RATE0.15軽減税率(800 万以下)
F56_CORPORATE_TAX_STANDARD_RATE0.232標準税率(800 万超)
F56_INVOICE_TRANSITION_RATE0.70インボイス経過措置(2026 年 10 月〜 70%控除)
F56_OPTIMIZER_SALARY_STEP50000calculateOptimalCompensation 総当たり探索のステップ幅(5 万円)
F56_OPTIMIZER_SALARY_MIN300000役員報酬最低値(月額・生活費制約で動的上書き)
F56_OPTIMIZER_SALARY_MAX3000000役員報酬最高値(月額、厚生年金上限考慮で 65 万標準報酬月額 + 余裕)

000_infra/002_constants.jsMENU_DEFINITION に新規カテゴリ「🧭 意思決定対話」を追加。スプレッドシートメニューからは Web アプリ URL コピーのみを提供(起動自体は外部 URL からの直接アクセス):

{
  category: '🧭 意思決定対話(Web App)',
  items: [
    { label: '🌐 Web アプリ URL をコピー', funcName: 'copyF56WebAppUrl', description: 'F-56 意思決定対話 UI の公開 URL をクリップボードにコピー' },
    { label: '📋 過去シナリオ一覧', funcName: 'openF56ScenarioRegistrySheet', description: '40_scenario_registry シートを開く' },
  ]
}

会話履歴の保存先設計

  • シナリオメタデータ(シナリオ名・作成日・ステータス): 40_scenario_registry シート
  • シナリオドライバーパラメータ: 44_scenario_drivers シート
  • 会話履歴本体(各ターンのユーザー発話・AI 応答・Tool Use 結果): Google Drive 上の JSON ファイル(F-56_chat_{scenario_id}_{timestamp}.json)、file_id を親テーブルの chat_history_file_id 列に保存
  • 理由: スプレッドシートのセル文字数制限 50,000 字で長時間対話を保存できない。Drive なら無制限。DriveApp.getFolderById(Constants.getParam('F56_CHAT_HISTORY_DRIVE_FOLDER_ID')) で管理フォルダを参照

修正方針

実装は 4 フェーズに分割。各フェーズ完了時点で動作可能な増分デリバリー。

Phase 1: MVP(Vite+React 基盤 + Flash 連携 + MAS-048 のみ Tool 化・約 1.0 ヶ月)

ゴール: GAS 上で React が動き、AI が MAS-048 採用 TCO を Tool Use で呼び出してチャット返答できることの証明。シナリオ永続化なし(使い捨て)。

Step 1-1: React SPA 基盤構築(MAS-232 との統合 or 独立)

  • MAS-232(Vite+React SPA 基盤)が未着手の場合、本案件内で Vite+React+TypeScript プロジェクトを tools/webapp-client/ 配下に新設
  • vite-plugin-singlefile で単一 HTML にバンドル → npm run build:webapptemplates/conversational_ui.html を生成
  • ビルド成果物は .claspignore で除外せず、GAS 側にプッシュされる
  • MAS-232 が先行実装された場合はその成果物(vite-react-base/)を流用

Step 1-2: 400_domain/440_conversational_ui.js 新設

namespace ConversationalUI を IIFE で定義。公開 API:

var ConversationalUI = (function() {

  // Web App エントリポイント
  function doGet(e) {
    return HtmlService.createHtmlOutputFromFile('conversational_ui')
      .setTitle('🧭 一人社長の意思決定対話')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  }

  // ユーザー発話の処理開始(非同期ジョブ登録)
  // @param {Object} req - {sessionId, userMessage, scenarios}
  // @returns {{jobId: string}}
  function startChatJob(req) {
    var jobId = Utils.generateUuid();
    _persistJobState_(jobId, { status: 'PENDING', req: req, ts: Date.now() });
    // 時間差トリガーで重処理を起動(30/60 秒タイムアウト回避)
    ScriptApp.newTrigger('_F56_processChatJobTrigger')
      .timeBased().after(100).create();
    return { jobId: jobId };
  }

  // ポーリング: クライアント → GAS の定期問い合わせ
  // @param {string} jobId
  // @returns {{status: 'PENDING'|'RUNNING'|'DONE'|'ERROR', result?: Object, error?: string}}
  function checkStatus(jobId) {
    var state = _loadJobState_(jobId);
    return state || { status: 'ERROR', error: 'Job not found or expired' };
  }

  // 重処理本体(時間差トリガーから呼ばれる)
  function _processChatJob_(jobId) {
    var state = _loadJobState_(jobId);
    if (!state) return;
    _persistJobState_(jobId, Object.assign({}, state, { status: 'RUNNING' }));
    try {
      var req = state.req;
      var aiResponse = _callAIWithRouting_(req.userMessage, req.scenarios);
      if (aiResponse.toolCall) {
        var toolResult = _dispatchToolCall_(aiResponse.toolCall);
        _persistJobState_(jobId, { status: 'DONE', result: { toolCall: aiResponse.toolCall, toolResult: toolResult } });
      } else {
        _persistJobState_(jobId, { status: 'DONE', result: { aiText: aiResponse.text } });
      }
    } catch (err) {
      _persistJobState_(jobId, { status: 'ERROR', error: String(err) });
    }
  }

  // AI ルーティング(Flash / Claude / Pro の呼び分け)
  function _callAIWithRouting_(userMessage, scenarios) {
    var mode = Constants.getParam('F56_AI_ROUTING_MODE', 'AUTO');
    var model = (mode === 'ALWAYS_SECONDARY' || _isComplexQuery_(userMessage))
      ? Constants.getParam('F56_AI_SECONDARY_MODEL', 'claude-3-7-sonnet-20250224')
      : Constants.getParam('F56_AI_PRIMARY_MODEL', 'gemini-2.5-flash');
    // Tool 定義は Phase 1 では F-48 のみ、Phase 2 で F-11/F-13 を追加
    var tools = _buildToolDefinitions_();
    return Utils.callVertexAI(model, userMessage, { tools: tools, scenarios: scenarios });
  }

  // Tool 定義(Phase 1 は F-48 のみ、Phase 2 で拡張)
  function _buildToolDefinitions_() {
    return [
      {
        name: 'run_hiring_tco',
        description: '年収・雇用形態・年齢区分から採用 TCO と BEP を試算する',
        parameters: {
          type: 'object',
          properties: {
            annualSalary: { type: 'number', description: '想定年収(円)' },
            employmentType: { type: 'string', enum: ['SEISHAIN', 'KEIYAKU', 'GYOUMU_ITAKU', 'FUKUGYOU'] },
            ageGroup: { type: 'number', enum: [39, 40] },
            recruitFeeRate: { type: 'number' },
            grossMarginRate: { type: 'number' }
          },
          required: ['annualSalary', 'employmentType', 'ageGroup']
        }
      }
    ];
  }

  // Tool ディスパッチ
  function _dispatchToolCall_(toolCall) {
    switch (toolCall.name) {
      case 'run_hiring_tco':
        return HiringTcoSimulator.simulate(toolCall.arguments); // 副作用なし・シート書き込みしない
      // Phase 2 で追加:
      // case 'run_what_if': return WhatIfSimulator.run(toolCall.arguments);
      // case 'run_investment_analysis': return /* F-13 のメモリ内実行版 */;
      default:
        throw new Error('Unknown tool: ' + toolCall.name);
    }
  }

  // ジョブ状態の永続化(CacheService 優先、長期保持が必要なものは専用シート)
  function _persistJobState_(jobId, state) {
    var cache = CacheService.getUserCache();
    cache.put('F56_JOB_' + jobId, JSON.stringify(state), 3600); // 1 時間 TTL
  }
  function _loadJobState_(jobId) {
    var cache = CacheService.getUserCache();
    var raw = cache.get('F56_JOB_' + jobId);
    return raw ? JSON.parse(raw) : null;
  }

  function _isComplexQuery_(msg) {
    // Phase 1 は簡易判定: 「比較」「最適化」「ポートフォリオ」「個人 B/S」を含めば Complex
    var complexKeywords = ['比較', '最適化', 'ポートフォリオ', '個人B/S', '個人 B/S', 'シナリオ差分'];
    return complexKeywords.some(function(kw) { return msg.indexOf(kw) >= 0; });
  }

  return {
    doGet: doGet,
    startChatJob: startChatJob,
    checkStatus: checkStatus,
    _processChatJob_: _processChatJob_
  };
})();

// グローバル関数(Web App エントリ + トリガー)
function doGet(e) { return ConversationalUI.doGet(e); }
function _F56_processChatJobTrigger(e) {
  // 最新 PENDING ジョブを処理(トリガー起動時のジョブ特定ロジック)
  var props = PropertiesService.getScriptProperties();
  var pendingJobId = props.getProperty('F56_LATEST_PENDING_JOB');
  if (pendingJobId) ConversationalUI._processChatJob_(pendingJobId);
}

// メニュー用
function copyF56WebAppUrl() {
  var url = ScriptApp.getService().getUrl();
  SpreadsheetApp.getUi().alert('F-56 Web アプリ URL', url, SpreadsheetApp.getUi().ButtonSet.OK);
}
function openF56ScenarioRegistrySheet() {
  var ss = SpreadsheetApp.getActive();
  var sheet = ss.getSheetByName('40_scenario_registry');
  if (!sheet) {
    SpreadsheetApp.getUi().alert('シート未作成', 'Phase 2 以降で作成されます', SpreadsheetApp.getUi().ButtonSet.OK);
    return;
  }
  ss.setActiveSheet(sheet);
}

Step 1-3: conversational_ui.html(Vite+React ビルド成果物)

React + TypeScript で実装。主要コンポーネント:

src/
  App.tsx                   # ルートコンポーネント(左チャット + 右シナリオカード)
  components/
    ChatPane.tsx            # 左 30%: 発話履歴 + 入力欄 + 送信ボタン
    ScenarioCards.tsx       # 右 70%: カード並列 UI(モバイル時は横スワイプカルーセル)
    ScenarioCard.tsx        # 単一シナリオカード(P/L/B/S/CF/手取りサマリ)
  hooks/
    useChatPoller.ts        # 送信後にポーリング、完了/エラーで State 更新
    useScenarios.ts         # シナリオ状態管理(配列 + CRUD)
  lib/
    gasClient.ts            # google.script.run を Promise 化

ポーリングフロー:

// useChatPoller.ts 概要
async function sendMessage(msg: string, scenarios: Scenario[]) {
  setLoading(true);
  const { jobId } = await gasClient.call('startChatJob', { sessionId, userMessage: msg, scenarios });
  const intervalMs = 3000; // F56_POLLING_INTERVAL_MS の既定値
  const poll = async () => {
    const state = await gasClient.call('checkStatus', jobId);
    if (state.status === 'DONE') { setResult(state.result); setLoading(false); return; }
    if (state.status === 'ERROR') { setError(state.error); setLoading(false); return; }
    setTimeout(poll, intervalMs);
  };
  setTimeout(poll, intervalMs);
}

Phase 1 の MVP UI は:

  • チャットパネル(発話履歴 + 入力欄)
  • 結果表示エリア(AI の応答テキスト + Tool Use 呼び出し結果を 1 カードで表示)
  • シナリオカード並列 UI は Phase 2 で本格実装(Phase 1 は単一カードのみ)

Step 1-4: 03_sys_paramsF56_* キー初期投入

上記 17 キーを投入。Drive フォルダ ID は手動で作成 → ID をコピー投入。

Step 1-5: appsscript.json の Web App デプロイ設定

{
  "webapp": {
    "access": "DOMAIN",
    "executeAs": "USER_DEPLOYING"
  }
}
  • access: DOMAIN: @bizlp.co.jp Workspace ドメイン限定
  • executeAs: USER_DEPLOYING: 代表者権限でスプレッドシート参照

⚠️ failure_patterns #26(oauthScopes 部分宣言): Web App 公開で自動的に https://www.googleapis.com/auth/script.container.ui 等が追加される可能性。デプロイ後 appsscript.json を Read し、すべての追加スコープを明示宣言する。

Phase 2: マルチシナリオ UI + MAS-011/MAS-013 統合 + 親子テーブル永続化(約 1.5 ヶ月)

ゴール: 複数シナリオ比較 + ドラフトから「確定」ステータスで 40_scenario_registry に保存できる。

Step 2-1: 100_config/101_sys_config.js にスキーマ追加

var MST_SCN_REG = [
  { name: '有効フラグ', type: 'BOOLEAN', required: true },
  { name: 'scenario_id', type: 'STRING', required: true }, // UUID
  { name: 'title', type: 'STRING', required: true },
  { name: 'status', type: 'STRING', required: true }, // DRAFT / COMMITTED / ARCHIVED
  { name: 'created_at', type: 'DATETIME', required: true },
  { name: 'updated_at', type: 'DATETIME', required: false },
  { name: 'ai_summary', type: 'STRING', required: false },
  { name: 'chat_history_file_id', type: 'STRING', required: false },
  { name: '備考', type: 'STRING', required: false }
];

var TRN_SCN_DRV = [
  { name: '有効フラグ', type: 'BOOLEAN', required: true },
  { name: 'scenario_id', type: 'STRING', required: true }, // 親参照
  { name: 'engine_type', type: 'STRING', required: true }, // F-11 / F-13 / F-48 / F-49 / F-56 等
  { name: 'driver_key', type: 'STRING', required: true },
  { name: 'driver_value_json', type: 'STRING', required: true }, // パラメータ全体を JSON で保存
  { name: '備考', type: 'STRING', required: false }
];

// schemas オブジェクトに追加
schemas['40_scenario_registry'] = MST_SCN_REG;
schemas['44_scenario_drivers'] = TRN_SCN_DRV;

Step 2-2: 200_data/202_repository.js に Repository 追加

MAS-045 実装済の HeadcountRepository / SubscriptionRepository と同パターンで:

var ScenarioRegistryRepository = (function() {
  var SHEET_NAME = '40_scenario_registry';
  function findAll() { /* 全件取得 */ }
  function findById(scenarioId) { /* 単件取得 */ }
  function append(dto) { /* 末尾追記 */ }
  function updateStatus(scenarioId, newStatus) { /* ステータス更新 */ }
  return { findAll, findById, append, updateStatus };
})();

var ScenarioDriversRepository = (function() {
  var SHEET_NAME = '44_scenario_drivers';
  function findByScenarioId(scenarioId) { /* scenario_id で絞込 */ }
  function appendBatch(dtos) { /* 複数行一括追記 */ }
  function deleteByScenarioId(scenarioId) { /* 有効フラグ FALSE 更新で論理削除 */ }
  return { findByScenarioId, appendBatch, deleteByScenarioId };
})();

Step 2-3: 440_conversational_ui.js 拡張 — シナリオ管理 + MAS-011/MAS-013 Tool 追加

  • _buildToolDefinitions_()run_what_if / run_investment_analysis 追加
  • 新規関数 saveScenario(scenario): React から確定シナリオを受け取り、parent + children 2 テーブルに書き込み + 会話履歴を Drive JSON に保存
  • 新規関数 loadScenario(scenarioId): 過去シナリオをロードして React 側で復元
  • 新規関数 listScenarios(): 一覧取得(一覧ダイアログ用)
function saveScenario(scenario) {
  var lock = LockService.getScriptLock();
  if (!lock.tryLock(5000)) throw new Error('別処理実行中');
  try {
    var scenarioId = scenario.scenario_id || Utils.generateUuid();
    var fileId = _saveChatHistoryToDrive_(scenarioId, scenario.chatHistory);
    ScenarioRegistryRepository.append({
      '有効フラグ': true,
      scenario_id: scenarioId,
      title: scenario.title,
      status: scenario.status || 'DRAFT',
      created_at: new Date(),
      updated_at: new Date(),
      ai_summary: scenario.aiSummary || '',
      chat_history_file_id: fileId,
      '備考': ''
    });
    var driverRows = scenario.drivers.map(function(d) {
      return {
        '有効フラグ': true,
        scenario_id: scenarioId,
        engine_type: d.engineType,
        driver_key: d.key,
        driver_value_json: JSON.stringify(d.value),
        '備考': ''
      };
    });
    ScenarioDriversRepository.appendBatch(driverRows);
    Utils.auditLog({
      operation: 'CREATE',
      targetSheet: '40_scenario_registry',
      targetId: scenarioId,
      actor: Session.getActiveUser().getEmail(),
      summary: 'F-56 scenario saved: ' + scenario.title
    });
    return { scenarioId: scenarioId, fileId: fileId };
  } finally {
    lock.releaseLock();
  }
}

function _saveChatHistoryToDrive_(scenarioId, chatHistory) {
  var folderId = Constants.getParam('F56_CHAT_HISTORY_DRIVE_FOLDER_ID');
  if (!folderId) throw new Error('F56_CHAT_HISTORY_DRIVE_FOLDER_ID 未設定');
  var folder = DriveApp.getFolderById(folderId);
  var fileName = 'F-56_chat_' + scenarioId + '_' + Utilities.formatDate(new Date(), 'JST', 'yyyyMMdd_HHmmss') + '.json';
  var blob = Utilities.newBlob(JSON.stringify(chatHistory, null, 2), 'application/json', fileName);
  var file = folder.createFile(blob);
  return file.getId();
}

Step 2-4: React UI — カード並列型

  • ScenarioCards.tsx: CSS Grid で横並び、モバイル時 (window.innerWidth < 768) は overflow-x: scroll + スワイプ
  • 各カードに P/L(売上・営業利益)/ B/S(現預金残高)/ CF(月次 FCF)/ 個人手取り の 4 指標サマリ
  • AI に「比較結果は JSON フォーマットで出力」と system prompt で指示 → React でパース → カードにバインド
  • カード上の「このシナリオを保存」ボタン → google.script.run.saveScenario 呼出

Phase 3: 個人 B/S 統合 + Claude 3.7 ルーティング + MAS-045 連動(約 1.0 ヶ月)

ゴール: 役員報酬最適化で手取り最大化 + 法人節税のスイートスポットを対話から探索可能。確定シナリオを MAS-045 で 22_bud_headcount に転記。

Step 3-1: 400_domain/441_compensation_optimizer.js 新設

namespace CompensationOptimizercalculateOptimalCompensation(input) を提供:

var CompensationOptimizer = (function() {

  /**
   * 役員報酬の総当たり探索で合算キャッシュフロー最大化
   * @param {Object} input
   * @param {number} input.preTaxCorporateProfit - 法人税引前予測利益(年額)
   * @param {number} input.minMonthlySalary      - 最低役員報酬(生活費制約)
   * @param {number} input.monthlyLivingCost     - 月額生活費
   * @param {number} input.kyosaiMonthly         - 小規模企業共済月額
   * @param {number} input.idecoMonthly          - iDeCo 月額
   * @param {boolean} input.is40Plus             - 40 歳以上フラグ(介護保険料)
   * @returns {{optimalMonthlySalary, personalNetCash, corporateNetProfit, combinedCF, breakdown}}
   */
  function calculateOptimalCompensation(input) {
    var step = Constants.getParam('F56_OPTIMIZER_SALARY_STEP', 50000);
    var min = Math.max(input.minMonthlySalary, Constants.getParam('F56_OPTIMIZER_SALARY_MIN', 300000));
    var max = Constants.getParam('F56_OPTIMIZER_SALARY_MAX', 3000000);
    var best = null;
    for (var salary = min; salary <= max; salary += step) {
      var result = _simulateAtSalary_(salary, input);
      if (!best || result.combinedCF > best.combinedCF) best = result;
    }
    return best;
  }

  function _simulateAtSalary_(monthlySalary, input) {
    var annualSalary = monthlySalary * 12;
    // 標準報酬月額の上限(厚生年金 65 万円)
    var standardMonthlyRemuneration = Math.min(monthlySalary, 650000);

    // 社会保険料(福井県 2026 年料率)
    var healthInsurance = standardMonthlyRemuneration * Constants.getParam('F56_FUKUI_HEALTH_INSURANCE_RATE', 0.0971) * 12;
    var nursingCare = input.is40Plus
      ? standardMonthlyRemuneration * Constants.getParam('F56_FUKUI_NURSING_CARE_RATE', 0.0162) * 12
      : 0;
    var pension = standardMonthlyRemuneration * Constants.getParam('F56_PENSION_RATE', 0.0915) * 12;
    var childcare = standardMonthlyRemuneration * Constants.getParam('F56_FUKUI_CHILDCARE_RATE', 0.0023) * 12;
    var totalSocialInsurance = (healthInsurance + nursingCare + pension + childcare) / 2; // 労使折半なので個人負担は半額

    // 所得税・住民税の計算(簡略版)
    var incomeDeduction = totalSocialInsurance + (input.kyosaiMonthly + input.idecoMonthly) * 12 + 480000; // 基礎控除
    var taxableIncome = Math.max(annualSalary - incomeDeduction, 0);
    var incomeTax = _calcIncomeTax_(taxableIncome);
    var residentTax = taxableIncome * 0.10; // 住民税 10%

    var personalNetCash = annualSalary - incomeTax - residentTax - totalSocialInsurance;

    // 法人税計算(軽減税率 + 標準税率)
    var corporateTaxable = input.preTaxCorporateProfit - annualSalary - totalSocialInsurance; // 役員報酬 + 会社負担社保は損金
    var corporateTax = _calcCorporateTax_(corporateTaxable);
    var corporateNetProfit = corporateTaxable - corporateTax;

    return {
      monthlySalary: monthlySalary,
      personalNetCash: personalNetCash,
      corporateNetProfit: corporateNetProfit,
      combinedCF: personalNetCash + corporateNetProfit,
      breakdown: {
        healthInsurance, nursingCare, pension, childcare,
        incomeTax, residentTax, corporateTax
      }
    };
  }

  function _calcIncomeTax_(taxableIncome) {
    // 累進課税(2026 年度)簡略版
    if (taxableIncome <= 1950000) return taxableIncome * 0.05;
    if (taxableIncome <= 3300000) return taxableIncome * 0.10 - 97500;
    if (taxableIncome <= 6950000) return taxableIncome * 0.20 - 427500;
    if (taxableIncome <= 9000000) return taxableIncome * 0.23 - 636000;
    if (taxableIncome <= 18000000) return taxableIncome * 0.33 - 1536000;
    if (taxableIncome <= 40000000) return taxableIncome * 0.40 - 2796000;
    return taxableIncome * 0.45 - 4796000;
  }

  function _calcCorporateTax_(corporateTaxable) {
    if (corporateTaxable <= 0) return 0;
    var reducedRate = Constants.getParam('F56_CORPORATE_TAX_REDUCED_RATE', 0.15);
    var standardRate = Constants.getParam('F56_CORPORATE_TAX_STANDARD_RATE', 0.232);
    var threshold = 8000000;
    if (corporateTaxable <= threshold) return corporateTaxable * reducedRate;
    return threshold * reducedRate + (corporateTaxable - threshold) * standardRate;
  }

  return {
    calculateOptimalCompensation: calculateOptimalCompensation
  };
})();

Step 3-2: 440_conversational_ui.jsrun_optimal_compensation Tool 追加

// _buildToolDefinitions_() に追加
{
  name: 'run_optimal_compensation',
  description: '法人の税引前利益と生活費制約から、役員報酬の最適値(税引後手取り + 法人利益合算 CF を最大化)を総当たり探索で算出',
  parameters: {
    type: 'object',
    properties: {
      preTaxCorporateProfit: { type: 'number' },
      minMonthlySalary: { type: 'number' },
      monthlyLivingCost: { type: 'number' },
      kyosaiMonthly: { type: 'number' },
      idecoMonthly: { type: 'number' },
      is40Plus: { type: 'boolean' }
    },
    required: ['preTaxCorporateProfit', 'monthlyLivingCost']
  }
}

// _dispatchToolCall_() に追加
case 'run_optimal_compensation':
  return CompensationOptimizer.calculateOptimalCompensation(toolCall.arguments);

Step 3-3: MAS-045 連動 — 確定シナリオから予算転記

UI: シナリオカード上部に「📥 このシナリオを予算に反映」ボタン → MAS-045 の WhatIfBudgetTransfer.transferToBudgets(scenarioId) を呼ぶ。MAS-045 の Human-in-the-Loop 確認ダイアログをそのまま再利用。

Phase 4: 最適化(CacheService 高速化 + モバイル洗練 + エラーリカバリー UX・約 0.5 ヶ月)

  • マスタデータ(税率・MAS-044 テンプレート等)を CacheService.getScriptCache() に初回ロード時キャッシュ(6 時間 TTL)
  • 100KB/key 制限を超える場合はチャンク分割(F56_MASTER_CACHE_1 / F56_MASTER_CACHE_2
  • モバイル CSS: @media (max-width: 768px) で右ペインを overflow-x: scroll カルーセル化
  • エラー UX: Tool Use 失敗時に「AI に再試行させる」「前の発話に戻る」「ユーザーが手動入力する」の 3 択 UI
  • Flash 精度不足時のフォールバック: システムで _isComplexQuery_ が false と判定しても AI が「もっと高度なモデルで考えます」と言ってきた場合、React 側で「Claude Sonnet に切り替えて再実行」ボタンを表示

影響範囲

変更ファイル一覧

ファイル変更種別フェーズ内容
400_domain/440_conversational_ui.js新規Phase 1, 2, 3namespace ConversationalUI(約 400 行)、Phase 毎に拡張
400_domain/441_compensation_optimizer.js新規Phase 3namespace CompensationOptimizer(約 200 行)
templates/conversational_ui.html新規Phase 1-4Vite+React ビルド成果物(バンドルサイズ〜500KB)
tools/webapp-client/新規ディレクトリPhase 1Vite+React+TypeScript プロジェクト(MAS-232 独立の場合)
100_config/101_sys_config.js追加のみPhase 1, 203_sys_params への F56_* キー(17 件)、Phase 2 で MST_SCN_REG / TRN_SCN_DRV schema 追加
200_data/202_repository.js追加のみPhase 2ScenarioRegistryRepository + ScenarioDriversRepository(約 150 行)
000_infra/002_constants.js追加のみPhase 1MENU_DEFINITION に「🧭 意思決定対話(Web App)」カテゴリ追加
appsscript.json変更Phase 1webapp セクション追加(access: DOMAIN, executeAs: USER_DEPLOYING
CLAUDE.md追加のみPhase 1デプロイフロー節に「Web App URL 取得手順」追記
docs/_config.json追加のみPhase 1nav 登録

既存動作への影響

  • MAS-005 固定 3 シナリオ UI: 仕様書未着手のまま MAS-056 に吸収される。MAS-005 TODO エントリに「MAS-056 に吸収・廃止」明記(後続 PR で TODO_future.md 更新)
  • MAS-011 What-if サイドバー: 影響なし。MAS-056 は Tool Use で内部 API(WhatIfSimulator.run)を呼ぶのみ。サイドバー UI は並行稼働
  • MAS-013 投資分析メニュー: 影響なし(メニュー項目は維持)
  • MAS-048 採用 TCO サイドバー: 影響なし
  • MAS-045 予算転記: Phase 3 で Tool として呼び出すのみ。MAS-045 サイドバー UI は並行稼働
  • MAS-216 Vertex AI: 本案件が最初の大規模ユースケース。callClaudeApi_() / callGeminiApi_() の共通ユーティリティを 000_infra/004_utils.js に追加する必要あり(MAS-216 実装のスコープを微拡張)

運用・デプロイ手順

  1. MAS-232 先行確認: MAS-232 実装済 → 成果物を流用 / 未着手 → 本案件内で tools/webapp-client/ 新設
  2. Phase 1 デプロイ:
    • npm run build:webapptemplates/conversational_ui.html 生成
    • 03_sys_params に F56_* キーを手動投入(Drive フォルダ ID を含む)
    • PropertiesService.getUserProperties()F56_ENCRYPTION_KEY を手動投入(AES-256 キー)
    • npm run push:devappsscript.json 更新確認 → npm run deploy:dev で Web App URL 発行
    • 発行された URL をブラウザで開く → ドメイン認証 → Chat UI 動作確認
  3. Phase 2 以降: 各フェーズのスキーマ追加 → setupAllSchemas 実行 → UI 拡張 → npm run deploy:dev → 動作確認

注意事項

  • ⚠️ failure_patterns #18-#20(命名造語禁止): namespace ConversationalUI / CompensationOptimizer / ファイル名 440_conversational_ui.js / 441_compensation_optimizer.js / シート名 40_scenario_registry / 44_scenario_drivers / パラメータキー F56_* を記述前に再確認
  • ⚠️ failure_patterns #25(並列実装対称性漏れ): Tool 定義(_buildToolDefinitions_)と Tool ディスパッチ(_dispatchToolCall_)は必ず 1:1 対応。Tool 追加時に片方だけ更新しない
  • ⚠️ failure_patterns #26(oauthScopes 部分宣言): Web App 公開で userinfo.email / script.container.ui 等が追加される可能性。デプロイ後 appsscript.json を Read し、すべてのスコープを明示宣言する。appsscript-json-guard.yml(MAS-234 で予定)との連動要確認
  • ⚠️ failure_patterns #27(Admin SDK 仕様変動): 本案件では Admin SDK を使わない(該当なし)
  • ⚠️ Vertex AI 呼び出し: MAS-216 の callClaudeApi_() / callGeminiApi_() は仕様書時点で OCR 用途のみ。本案件で汎用チャット + Tool Use に拡張が必要000_infra/004_utils.js 側で callVertexAI(model, messages, opts) の汎用 API を新設し、MAS-056 から呼び出す。拡張の実装順序は MAS-216 汎用 API 先行 → MAS-056 Phase 1 着手
  • ⚠️ シート番号 41 衝突回避: Deep Research 推奨の 41_scenario_drivers41_trn_budget と衝突。必ず 44_scenario_drivers を使用
  • ⚠️ 会話履歴の機密性: 個人 B/S データ(生活費・個人資産)を含む会話履歴は Drive に保存される。Drive フォルダの共有権限は代表者のみに制限。DriveApp.getFolderById(folderId).setSharing(DriveApp.Access.PRIVATE, DriveApp.Permission.NONE) で明示設定
  • ⚠️ 個人資産の暗号化: シートに保存する個人 B/S 関連列(44_scenario_driversdriver_value_json に含まれる個人資産額等)は AES-256 で暗号化。暗号化キーは PropertiesService.getUserProperties() に保存、シートには絶対に書かない
  • ⚠️ GAS 時間差トリガーの制約: ScriptApp.newTrigger は 1 スクリプトあたり20 個の制限。ポーリングジョブが 20 個を超えると失敗する。毎回トリガーを作らず、既存トリガー再利用 or Utilities.sleep() 回避パターンを _F56_processChatJobTrigger 実装時に確定
  • ⚠️ CacheService の TTL: 最大 6 時間。1 時間以上の長時間セッションで TTL 切れが発生する可能性 → ジョブ状態はシートまたは PropertiesService に永続保存する fallback を Phase 4 で追加
  • ⚠️ Tool Use の並列呼び出し: Gemini 2.5 Flash / Claude 3.7 Sonnet は複数 Tool の並列呼び出しに対応するが、GAS 側で逐次実行する設計。並列実行で 6 分制限に抵触する場合は再設計
  • ⚠️ Human-in-the-Loop: AI の提案は判断補助であり、最終的なシナリオ確定・予算転記は人間が明示的にボタンクリックで承認。仕様書・UI に「AI 提案は参考情報」旨を明記
  • ⚠️ MAS-005 の公式廃止: Phase 1 完了時に MAS-005 TODO エントリを「MAS-056 に吸収・廃止」に更新。別 PR で実施
  • ⚠️ MAS-200 個人情報保護連携: 40_scenario_registry / 44_scenario_drivers / Drive 会話履歴は個人情報に該当するため、MAS-200 実装時にアクセス制御ルールを適用(監査ログ含む)

エッジケース

#条件検知方法期待される挙動ログ出力
1ジョブ ID が存在しない(期限切れ等)_loadJobState_(jobId) が null{status: 'ERROR', error: 'Job not found or expired'} を返却、クライアントで「再試行」UI 表示Utils.persistLog('WARN', 'checkStatus', 'job not found: ' + jobId)
2時間差トリガーの 20 個制限超過ScriptApp.newTrigger().timeBased().after(100).create() が throw既存トリガー再利用パスに fallback or ユーザーに「少し待ってから再送信」通知Utils.persistLog('WARN', 'startChatJob', 'trigger limit exceeded')
3Vertex AI 呼び出しが 60 秒タイムアウトUrlFetchApp.fetch が throwポーリング側に {status: 'ERROR', error: 'AI timeout, try simpler query'} を返却Utils.persistLog('WARN', '_callAIWithRouting_', 'AI timeout')
4AI が不正な Tool 名を呼び出し_dispatchToolCall_ の switch-default に到達AI に「存在しない tool: XXX、有効な tool は [...]」を返して再生成させるUtils.persistLog('WARN', '_dispatchToolCall_', 'unknown tool: ' + name)
5Tool 引数のバリデーションエラーMAS-048 / MAS-011 等の既存バリデーション throwエラー内容を AI に返し、AI がユーザーに追加情報を要求Utils.persistLog('WARN', '_dispatchToolCall_', 'tool arg error: ' + err)
6会話履歴の Drive フォルダ ID 未設定Constants.getParam('F56_CHAT_HISTORY_DRIVE_FOLDER_ID') が空saveScenario 実行時に throw、UI で「セットアップ未完了」警告Utils.persistLog('ERROR', '_saveChatHistoryToDrive_', 'folder ID not set')
7Drive フォルダへの書き込み権限なしfolder.createFile(blob) が throwエラー詳細を UI に表示 + ScriptOwner の DriveApp 権限確認を案内Utils.persistLog('ERROR', ...)
8シナリオ ID の重複(UUID 衝突の事実上ゼロだが保険)ScenarioRegistryRepository.findById(id) で既存検出新しい UUID を再生成して再試行(最大 3 回)Utils.persistLog('WARN', 'saveScenario', 'UUID collision: ' + id)
9LockService が 5 秒で取得失敗(同時実行)lock.tryLock(5000) が false「別処理実行中」Toast、クライアントで 10 秒後に自動再試行Utils.persistLog('WARN', 'saveScenario', 'lock failed')
10calculateOptimalCompensation で法人税引前利益が負値input.preTaxCorporateProfit < 0「法人が赤字のため最適化不可、役員報酬は最低値推奨」メッセージを AI 経由で返却Utils.logInfo('calculateOptimalCompensation', 'negative preTaxProfit')
11標準報酬月額の等級上限超過monthlySalary > 650000上限 650,000 円でクリップ(厚生年金実務)+ 上限超過である旨を結果に明記ログ出力なし(正常系)
12個人資産の暗号化キー未投入PropertiesService.getUserProperties().getProperty('F56_ENCRYPTION_KEY') が nullセットアップ未完了警告、個人 B/S 機能を無効化してチャット UI は継続利用可Utils.persistLog('WARN', 'Phase 3', 'encryption key missing')
13モバイル幅でのカード並列レイアウト崩れwindow.innerWidth < 768 で CSS Grid が崩れる横スワイプカルーセルに自動切替(.scenario-cardsoverflow-x: scroll
14Web App の同時接続 30 quota 超過GAS 側で throw(極めて稀、1 人利用なら起きない)クライアントに「サービス過負荷、1 分後に再試行」通知Utils.persistLog('ERROR', 'doGet', 'quota exceeded')
15AI 応答に Tool Use なし + 空テキストaiResponse.toolCall == null && aiResponse.text == ''「AI から応答を取得できませんでした、別の質問を試してください」UIUtils.persistLog('WARN', '_processChatJob_', 'empty AI response')
16Flash で処理を始めたが途中で複雑と判定_isComplexQuery_ が false だったが AI 応答に「不確実性」表現を検知「Claude Sonnet で再試行しますか?」UI ボタンを表示Utils.logInfo('_callAIWithRouting_', 'potential complexity detected')
17暗号化キーが 32 バイト未満(不正な AES-256 キー)起動時バリデーションセットアップエラー表示、暗号化機能を無効化Utils.persistLog('ERROR', '_validateEncryptionKey_', 'key too short')
1822_bud_headcount からのデフォルト値プリロード失敗(MAS-011 / MAS-048 依存)HeadcountRepository.findAll() が空デフォルト値(03_sys_params 定数)で計算継続 + UI で「実績データなし、初期値で試算」表示Utils.logInfo('loadDefaults', 'no headcount data')

冪等性・再実行の設計

  • シナリオ保存: MAS-045 / MAS-048 と同じ純粋追記方式。同一条件で 2 回 saveScenario 実行すれば 2 件のシナリオが作成される(UUID が毎回異なる)
  • 重複防止の UX: React 側で「確定」ボタン押下後に5 秒間クリック不可にするダブルクリック抑止を実装
  • チャット再開: シナリオ ID を URL ハッシュ(#scenario=<uuid>)に含めることで、同一シナリオに戻って会話再開可能

Human-in-the-Loop

  • AI 提案の最終確認: Tool Use 結果は AI が自動で次の Tool を呼ぶことも可能だが、金額ベースの提案 / シナリオ保存 / MAS-045 予算転記は必ずユーザーのボタンクリックで承認
  • 個人 B/S 入力の都度確認: 個人資産額・生活費等の機密情報入力時に「この情報は暗号化してシステムに保存されます」同意チェックボックスを必須

テスト要件(900_test/901_test_runner.js

テスト関数合格基準
test_F56_startChatJob_and_checkStatus_flowstartChatJob → checkStatus 3 回で DONE になること
test_F56_dispatchToolCall_runHiringTcoTool run_hiring_tco が MAS-048 の正しい API を呼び、戻り値が JSON シリアライズ可能
test_F56_saveScenario_idempotency同一データ 2 回保存で 2 行追加(UUID 異なる)
test_F56_CompensationOptimizer_knownCase年利益 1200 万 + 生活費 40 万 / 月 → 最適役員報酬 ~70 万 / 月(手計算一致)
test_F56_CompensationOptimizer_negativeProfit法人赤字 → {optimalMonthlySalary: min, reason: 'corporate loss'}
test_F56_CompensationOptimizer_standardRemunerationCap月 100 万指定 → 社保計算は 65 万基準でクリップ
test_F56_encryptionKeyMissing_gracefulDegrade暗号化キー未投入時にチャット UI は起動、個人 B/S 機能のみ無効化

実データ検証

実装前に MCP で以下を確認:

  1. 40_scenario_registry / 44_scenario_drivers シート未作成: setupAllSchemas で新規作成される想定
  2. 41_trn_budget / 42_trn_journal / 43_trn_timesheet が使用中: 衝突確認
  3. 03_sys_params の既存キー一覧: F56_* プレフィックスの 17 キーが未登録
  4. CLAUDE.md の DDL 管理外リスト: 40_scenario_registry / 44_scenario_driversDDL 管理対象なのでリスト追加不要
  5. appsscript.json の現状: webapp セクション未設定確認、oauthScopes 現状確認
  6. MAS-216 仕様書: callClaudeApi_() / callGeminiApi_() の実装完了済かを確認(未完なら MAS-216 先行実装)
  7. Google Drive にチャット履歴フォルダ作成可能か: 代表者アカウントで空フォルダ作成テスト、ID 取得

関連ドキュメント

仕様書・ドキュメント関連箇所
ref_financial_decision_9step_retrospective.mdD.11 9 ステップ財務意思決定プロセス振り返り(MAS-056 設計の原典)、特に Step 9 個人 B/S 統合論点
MAS-332 調査結果Gemini Deep Research 更新版(2026-04-24)の 8 設計論点推奨 + Claude 追記 9 点
dev_mas-216_vertex_ai_migration.mdVertex AI Claude / Gemini 呼び出しパターン。本案件で汎用 Chat + Tool Use に拡張
dev_mas-011_what_if_simulation.mdWhatIfSimulator.run(drivers, opts) を Tool 化
dev_mas-013_investment_simulation.mdrunInvestmentAnalysis() を Tool 化
dev_mas-048_hiring_tco_bep_simulator.mdHiringTcoSimulator.simulate(input) を Tool 化、MVP の最小スコープ
dev_mas-045_whatif_to_budget_transfer.mdPhase 3 で確定シナリオ → 予算転記を連携
dev_mas-049_wage_increase_tax_credit_simulator.md将来的に Tool として追加候補
CLAUDE.md環境分離・デプロイフロー・Human-in-the-Loop ポリシー

TODO_future.md 内の関連案件:

  • MAS-232(サイドバー SPA 化・Vite+React): Phase 1 の前提基盤、先行実装推奨
  • MAS-245(GAS Web App 独立化): 本案件が MAS-245 の初のキラーユースケース
  • MAS-005(動的シナリオプランニング UI): 本案件に吸収・廃止
  • MAS-054(Monte Carlo シミュレーションエンジン): 本案件の拡張点として併存
  • MAS-200(個人情報保護): 個人 B/S データのアクセス制御で連携

人間が検討すべき事項

MAS-332 チェックリスト 10 項目 + Claude 追記 9 項目のうち、本仕様書で方針提示済のものは [方針済]、仕様書に反映せず実装判断に委ねるものは [実装時決定] と分類:

  1. [方針済] フロントエンド基盤: Vite + React (TS) + vite-plugin-singlefile で進める
  2. [方針済] 非同期通信: google.script.run + クライアント側ポーリング(既定 3 秒間隔)+ GAS 側時間差トリガー
  3. [実装時決定] AI モデルルーティング閾値: _isComplexQuery_ の判定キーワード拡充タイミング / F56_AI_ROUTING_MODEAUTO / ALWAYS_PRIMARY / ALWAYS_SECONDARY の切替 UI をユーザーに公開するか
  4. [方針済] Tool Use オーケストレーション: 「AI → React → GAS → React → AI」クライアント仲介連鎖
  5. [方針済] チャット UI レイアウト: 左 30% チャット + 右 70% カード並列型(モバイル横スワイプ)
  6. [方針済] 永続化データスキーマ: 40_scenario_registry(親)+ 44_scenario_drivers(子、41 衝突回避で 44 へ変更済)
  7. [方針済] 会話履歴保存先: Drive JSON ファイル + file_id を親に記録
  8. [方針済] 機密情報: PropertiesService.getUserProperties() の AES-256 キーで暗号化
  9. [方針済] 財務モデル前提: 2026 年福井県料率(健保 9.71% / 介護 1.62% / 子ども 0.23%)+ インボイス 70%控除
  10. [方針済] MVP スコープ: MAS-048 採用 TCO のみ Tool 化、シナリオ永続化なし、単一カード表示のみ
  11. [実装時決定] MAS-005 の公式廃止: Phase 1 完了タイミングで TODO_future.md の MAS-005 エントリを「MAS-056 に吸収・廃止」に更新する別 PR を立てるか、本案件の Phase 1 PR 内で同時更新するか
  12. [実装時決定] MAS-232 先行実装 or 統合着手: MAS-232 がまだ未着手 → (A) MAS-232 を先行実装して成果物を流用、(B) 本案件 Phase 1 内で tools/webapp-client/ を新設。代表者のリソース配分で判断
  13. [実装時決定] MAS-045 実装順序: MAS-045 仕様書完成済・実装未着手。Phase 3 の直前に MAS-045 実装を割り込ませるか、Phase 3 内で同時実装するか
  14. [実装時決定] ポーリング周期既定値 3 秒: 既定を 2/5/10 秒のどれにするか。AI 応答時間(Flash 2-5 秒、Claude 10-15 秒)とのバランス。F56_POLLING_INTERVAL_MS で上書き可能な設計だが、初期値の選択
  15. [実装時決定] 時間差トリガーの運用: ScriptApp.newTrigger 20 個制限への対応方針。毎回作成 → 削除 vs 固定トリガーで常時待機(CacheService ポーリング)。後者は GAS 無料枠の使用量に影響
  16. [実装時決定] Flash 精度不足時のフォールバック UI: 「Claude Sonnet に切り替えますか?」を明示ボタンで提示するか、裏でサイレント再実行するか(UX 選好)
  17. [実装時決定] チャット履歴の保持期間: Drive 保存の JSON ファイルを自動削除するか(例: 90 日後)、永久保持するか。個人 B/S データ含むため MAS-200 連携時に確定
  18. [実装時決定] 多言語対応: 現案は日本語前提。英語 UI の優先度(将来の SaaS 化時)
  19. [実装時決定] D.11 Step 9 の個人 B/S 拡張: MVP は 3 項目ヒアリング(現預金 / 生活費 / 共済掛金)。将来的に配偶者資産・不動産等の粒度拡張を行うか
  20. [実装時決定] 税制改正追従の運用: F56_* パラメータは 03_sys_params で管理するが、毎年 3 月の料率改定の通知 / 監査責任を誰が担うか(顧問社労士へのレビュー依頼運用)

実装プロンプト(Claude Code 用・Phase 1 MVP 版)

本仕様書は 4 フェーズ構成のため、実装プロンプトは Phase 1 MVP 版のみ記載。Phase 2-4 は Phase 1 完了後に別 PR で個別に実装プロンプトを追加する方針。

あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-056「一人社長の意思決定対話 UI」の **Phase 1 MVP**(Vite+React 基盤 + Gemini Flash 連携 + MAS-048 のみ Tool 化)を実装してください。

## 実行前タスク(必須・7 件)
1. `docs/_internal/research_prompts/RQ-034_conversational_scenario_ui_result.md` を Read(Deep Research 結果)
2. `docs/dev/dev_mas-216_vertex_ai_migration.md` を Read(Vertex AI 呼び出しパターン)
3. `docs/dev/dev_mas-048_hiring_tco_bep_simulator.md` を Read(Tool 化対象の API)
4. `000_infra/002_constants.js` の `Constants.getParam`(L147)と `MENU_DEFINITION`(L206 起点)を Read
5. `000_infra/004_utils.js` の既存 Vertex AI 呼び出しヘルパー確認。汎用 `callVertexAI` が未実装なら MAS-216 先行実装を別 PR で発行
6. MCP で `03_sys_params` に `F56_*` キー未登録、`appsscript.json` に `webapp` 未設定を確認
7. MAS-232 進捗確認: 実装済なら成果物を流用、未着手なら本案件内で `tools/webapp-client/` 新設判断

## Step 1: Vite+React プロジェクト構築(推奨モデル: Sonnet)
- `tools/webapp-client/` に Vite+React+TypeScript プロジェクトを `npm create vite@latest` で初期化
- `vite-plugin-singlefile` を追加(`npm install -D vite-plugin-singlefile`)
- `vite.config.ts` で singlefile プラグイン有効化
- `src/App.tsx` にチャット UI のスケルトン(左チャット + 右結果表示)
- `src/lib/gasClient.ts` で `google.script.run` を Promise 化
- `src/hooks/useChatPoller.ts` でポーリングロジック
- `npm run build` で `dist/index.html` 生成 → これを `templates/conversational_ui.html` にコピーする npm-script 追加

## Step 2: `03_sys_params` パラメータ投入(推奨モデル: Haiku)
- 17 キー(F56_*)を MCP `update_cells` で投入
- `F56_CHAT_HISTORY_DRIVE_FOLDER_ID` は手動で Drive フォルダ作成 → ID 取得 → 投入
- `F56_ENCRYPTION_KEY` は `PropertiesService.getUserProperties()` に投入(スクリプト経由)

## Step 3: `400_domain/440_conversational_ui.js` 新設(推奨モデル: Opus)
- namespace `ConversationalUI` を IIFE で定義
- `doGet(e)` / `startChatJob(req)` / `checkStatus(jobId)` の 3 公開関数
- 内部関数: `_processChatJob_` / `_callAIWithRouting_` / `_buildToolDefinitions_` / `_dispatchToolCall_` / `_persistJobState_` / `_loadJobState_` / `_isComplexQuery_`
- グローバル関数: `doGet(e)` / `_F56_processChatJobTrigger` / `copyF56WebAppUrl` / `openF56ScenarioRegistrySheet`
- Tool 定義は **MAS-048 の `run_hiring_tco` のみ**(Phase 2 で拡張)
- ジョブ状態は `CacheService.getUserCache()` に 1 時間 TTL で保存
- 時間差トリガー `ScriptApp.newTrigger('_F56_processChatJobTrigger').timeBased().after(100).create()` で重処理を分離

## Step 4: `000_infra/004_utils.js` に `callVertexAI` 汎用 API 追加(推奨モデル: Sonnet)
- MAS-216 の `callClaudeApi_` / `callGeminiApi_` を汎用 Chat + Tool Use 対応に拡張
- 引数: `(model, messages, opts: {tools, scenarios})`
- 戻り値: `{text?: string, toolCall?: {name, arguments}}`
- Gemini 2.5 Flash / Claude 3.7 Sonnet の両対応(model プレフィックスで分岐)

## Step 5: `MENU_DEFINITION` 追加 + `appsscript.json` 更新(推奨モデル: Haiku)
- `000_infra/002_constants.js` に「🧭 意思決定対話(Web App)」カテゴリ追加
- `appsscript.json` に `webapp: {access: 'DOMAIN', executeAs: 'USER_DEPLOYING'}` 追加
- Web App デプロイ → URL 取得 → テスト

## 制約
- **41 は使わない**(`41_trn_budget` 衝突)。本 Phase ではシート作成しないが Phase 2 で `40` / `44` を使用
- **`appsscript-json-guard.yml`** との整合(failure_patterns #26): Web App デプロイ後に追加スコープを明示宣言
- 列番号ハードコード禁止(Phase 1 ではシート操作なしだが原則明記)
- 架空の関数名・シート名を使わない(failure_patterns #18-#20)
- MAS-232 未着手時は Vite+React 構成を本案件内で確立(独立して動く)

## 動作確認
1. `npm run build:webapp` → `templates/conversational_ui.html` 生成
2. `npm run deploy:dev` → Web アプリ URL 発行
3. URL をブラウザで開く → Workspace 認証 → Chat UI 表示
4. 「年収 600 万で正社員採用の TCO 教えて」と入力
5. 数秒のローディング後に MAS-048 の TCO 計算結果が表示される
6. 会話履歴が React State に保持される(リロードで消える、Phase 2 で永続化)

推奨実行モデル

工程推奨モデル理由
Phase 1 Step 1: Vite+React 初期化Claude Sonnet 4.6モダンフロント実装判断、MAS-232 流用判断
Phase 1 Step 2: 03_sys_params 投入Claude Haiku 4.5定型データ投入、判断なし
Phase 1 Step 3: 440_conversational_ui.js 新設Claude Opus 4.7最も複雑、非同期ポーリング + Tool Use ディスパッチ + エラーハンドリング + failure_patterns 複数対応
Phase 1 Step 4: callVertexAI 汎用 APIClaude Sonnet 4.6MAS-216 の既存 API を拡張、両モデル対応
Phase 1 Step 5: MENU_DEFINITION + appsscript.jsonClaude Haiku 4.5定型追記
Phase 2 Step 2-1: スキーマ追加Claude Sonnet 4.6既存 101_sys_config.js パターン準拠
Phase 2 Step 2-2: Repository 追加Claude Sonnet 4.6HeadcountRepository パターン準拠
Phase 2 Step 2-3: 440_ 拡張 + MAS-011/MAS-013 ToolClaude Opus 4.7複数 Tool + Drive 保存 + Lock + AuditLog の複合実装
Phase 2 Step 2-4: React カード並列 UIClaude Sonnet 4.6CSS Grid + モバイル CSS + JSON バインディング
Phase 3 Step 3-1: 441_compensation_optimizer.jsClaude Opus 4.7税制ロジック + 総当たり探索 + 社保計算の精度
Phase 3 Step 3-2: Tool 追加 + MAS-045 連動Claude Sonnet 4.6既存パターン活用
Phase 4 最適化Claude Sonnet 4.6CacheService + モバイル + エラー UX の組み合わせ

変更履歴

日付変更内容
2026-04-24初版作成。D.11 9 ステップ財務意思決定プロセス + MAS-332 Gemini Deep Research(更新版 2026-04-24)の成果を全反映した本格設計仕様書。MAS-056 を 4 フェーズ × 計 4 ヶ月 で実装する計画を確定。Phase 1 MVP (1.0 ヶ月): Vite+React + vite-plugin-singlefile + GAS doGet + クライアント側ポーリング + Gemini Flash 連携 + MAS-048 採用 TCO のみ Tool 化(永続化なし)。Phase 2 (1.5 ヶ月): 40_scenario_registry + 44_scenario_drivers(41 衝突回避)の親子テーブル + Drive JSON 会話履歴 + MAS-011/MAS-013 統合 + カード並列 UI。Phase 3 (1.0 ヶ月): calculateOptimalCompensation() 総当たり探索による個人 B/S 統合(福井県 2026 年料率 健保 9.71%/介護 1.62%/子ども 0.23% + 軽減税率 15% + インボイス 70%控除) + Claude 3.7 Sonnet ハイブリッドルーティング + MAS-045 予算転記連動。Phase 4 (0.5 ヶ月): CacheService 高速化 + モバイル洗練 + エラーリカバリー UX。MAS-005 を吸収・再定義、MAS-054 Monte Carlo は拡張点として併存。エッジケース 18 件・人間検討事項 20 件(方針済 10 + 実装時決定 10)・推奨実行モデル 12 工程(Opus×3 / Sonnet×7 / Haiku×2)を定義。関連案件 MAS-232(Vite+React SPA 基盤・Phase 1 前提基盤)/ MAS-245(Web App 独立化・本案件が初のキラーユースケース)/ MAS-216(Vertex AI・callVertexAI 汎用 API を本案件で拡張)

仕様書作成プロンプト(再現性・監査性のため記録)

Claude Opus 4.7 に与えた指示の要約(2026-04-24)

本仕様書は、以下の成果物を前提に Claude Opus 4.7 が直接執筆した:

  1. MAS-332 Gemini Deep Research 結果(更新版 2026-04-24): 8 設計論点の具体的推奨案(RQ-034_conversational_scenario_ui_result.md
  2. D.11 9 ステップ財務意思決定プロセス振り返り: 一人法人の個人 B/S + 法人 B/S 統合の原典(ref_financial_decision_9step_retrospective.md
  3. MAS-011 / MAS-013 / MAS-048 / MAS-045 既存仕様書: Tool Use 対象の既存 API
  4. MAS-216 Vertex AI 仕様書: AI 呼び出し基盤の前提
  5. Claude 側検証 9 点: Deep Research 結果に対する Claude 追記(41 衝突検出・MAS-232 先行実装・MAS-005 吸収・ポーリング周期・AI ルーティング閾値・MAS-200 連携・Flash フォールバック UI・トリガー結果保存先・暗号化運用)

指示の骨子:

  • 14 セクションテンプレート(dev_spec_prompt_template.md v1.7)厳守
  • failure_patterns(#18-#20 命名造語 / #25 並列実装対称性 / #26 oauthScopes 部分宣言)を注意事項に明示
  • シート番号 41 衝突を記述前に再確認し、44 に統一(Deep Research の 41 推奨を Claude 側で 44 に修正)
  • MAS-005 / MAS-232 / MAS-245 / MAS-216 / MAS-045 との関係を明記
  • 4 フェーズ分離を明示、Phase 1 MVP のみ実装プロンプトを記載(Phase 2-4 は別 PR で追加)